Add appearance settings for theme, fonts, and syntax highlighting (#1236)
* Add appearance settings for theme, fonts, and syntax highlighting New Appearance section in Settings to configure the app theme, UI and mono font family + size, and a syntax-highlighting theme chosen independently of light/dark. Font family fields default to empty and show the active system stack as the placeholder; a live split-diff preview reflects the choices as they change. The theme picker moves here from General. Preferences are client-only (AsyncStorage) and applied at runtime by patching every registered Unistyles theme via UnistylesRuntime.updateTheme, so existing StyleSheet consumers repaint with no per-component preference reads and no useUnistyles. No daemon/protocol change. * Expand syntax themes and move the preview under the chooser Replace the small fixed syntax-theme set with eight popular themes — GitHub, Catppuccin, Dracula, Tokyo Night, One, Nord, Gruvbox, Solarized — built from a compact per-theme role palette. The app theme and syntax theme stay separate; the only coupling is the light/dark axis: a theme with both variants uses its light palette on a light app and its dark palette on a dark app, while the code frame (gutter, line numbers, background) keeps following the app theme. Default is now GitHub (GitHub keeps its existing hand-tuned maps for an exact match). Move the live preview directly under the syntax-theme chooser, drop the "preview.ts" filename header, and use a clearer code snippet for it. * Update settings e2e for the moved theme picker and Appearance section The theme picker moved from General to the new Appearance section, so the General-content assertion no longer finds "Theme" — point it at "Default send" (which stays in General) and add Appearance to the e2e section map plus an expectAppearanceContent check, and exercise the Appearance section in the sidebar-navigation test. * Render the appearance preview as a unified diff Switch the live preview from side-by-side columns to a single unified diff: unchanged context lines plus "-"/"+" rows for the change, matching how diffs read elsewhere in the app. Same snippet, mono font, syntax colors, and live code-font draft behavior. * Fix code font size change shrinking all UI text applyAppearance scaled the live (already-patched) theme ramp instead of the authored FONT_SIZE ramp, so it was not idempotent: every appearance change re-scaled the current sizes, compounding the whole-UI font scale. With any non-default UI size, changing the code size — or any setting — shrank (or grew) all app text, and it persisted into the theme. Build the ramp from the canonical FONT_SIZE ramp each apply (code size still set absolutely), so applies are idempotent and a code-size change never touches the UI ramp. * Apply the interface font across the app on web react-native-web stamps a default font onto every text element, so the interface font only reached the few components that set a font explicitly. Inject one rule that points all text at a CSS variable (updated live as the setting changes), with high specificity (1,2,0) so it deterministically beats RN-web's base font and Unistyles' generated classes — no stylesheet-order fragility. Code, diff, and monospace surfaces carry a `data-pmono` marker (and their subtrees are excluded), so they keep their code font. Web-only: React Native has no global font cascade, so native still applies the UI font only where components opt in. Inline `code` within chat prose is a known minor gap (its render paths aren't tagged yet). * Keep preview code in the code font * Apply code font settings to terminals * Fix settings host picker import after rebase * Format terminal webview bundle
This commit is contained in:
@@ -8,13 +8,13 @@ import {
|
||||
} from "react-native";
|
||||
import { ScrollView as GHScrollView } from "react-native-gesture-handler";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { Fonts } from "@/constants/theme";
|
||||
import type { ToolCallDetail } from "@getpaseo/protocol/agent-types";
|
||||
import { buildLineDiff, parseUnifiedDiff, type DiffLine } from "@/utils/tool-call-parsers";
|
||||
import { highlightDiffLines } from "@/utils/diff-highlight";
|
||||
import { hasMeaningfulToolCallDetail } from "@/utils/tool-call-detail-state";
|
||||
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
|
||||
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
||||
import { CODE_SURFACE_DATASET } from "@/styles/code-surface";
|
||||
import { extensionFromPath, highlightToKeyedLines } from "@/utils/highlight-cache";
|
||||
import { HighlightedLines } from "./highlighted-content";
|
||||
import { DiffViewer } from "./diff-viewer";
|
||||
@@ -179,7 +179,7 @@ function ShellDetailSection({ command, output, ds }: ShellDetailProps) {
|
||||
style={ds.webScrollbarStyle}
|
||||
contentContainerStyle={styles.codeHorizontalContent}
|
||||
>
|
||||
<View style={styles.codeLine}>
|
||||
<View style={styles.codeLine} dataSet={CODE_SURFACE_DATASET}>
|
||||
<Text selectable style={styles.scrollText}>
|
||||
<Text style={styles.shellPrompt}>$ </Text>
|
||||
{normalizedCommand}
|
||||
@@ -224,7 +224,7 @@ function WorktreeSetupDetailSection({
|
||||
style={ds.webScrollbarStyle}
|
||||
contentContainerStyle={styles.codeHorizontalContent}
|
||||
>
|
||||
<View style={styles.codeLine}>
|
||||
<View style={styles.codeLine} dataSet={CODE_SURFACE_DATASET}>
|
||||
<Text selectable style={styles.scrollText}>
|
||||
{hasLog ? setupLog : `Preparing worktree ${branchName} at ${worktreePath}`}
|
||||
</Text>
|
||||
@@ -383,7 +383,7 @@ function SubAgentDetailSection({
|
||||
style={ds.webScrollbarStyle}
|
||||
contentContainerStyle={styles.codeHorizontalContent}
|
||||
>
|
||||
<View style={styles.codeLine}>
|
||||
<View style={styles.codeLine} dataSet={CODE_SURFACE_DATASET}>
|
||||
{childSessionId ? (
|
||||
<Text selectable style={styles.subAgentSessionText}>
|
||||
session {childSessionId}
|
||||
@@ -466,7 +466,7 @@ function ScrollableTextSection({
|
||||
{keyedLines ? (
|
||||
<HighlightedLines lines={keyedLines} startLine={startLine} />
|
||||
) : (
|
||||
<Text selectable style={styles.scrollText}>
|
||||
<Text selectable style={styles.scrollText} dataSet={CODE_SURFACE_DATASET}>
|
||||
{content}
|
||||
</Text>
|
||||
)}
|
||||
@@ -498,7 +498,7 @@ function FetchDetailSection({ url, result, ds }: FetchDetailProps) {
|
||||
showsHorizontalScrollIndicator
|
||||
style={ds.webScrollbarStyle}
|
||||
>
|
||||
<Text selectable style={styles.scrollText}>
|
||||
<Text selectable style={styles.scrollText} dataSet={CODE_SURFACE_DATASET}>
|
||||
{result ? `${url}\n\n${result}` : url}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
@@ -542,7 +542,7 @@ function buildSearchSections(detail: SearchDetail, ds: DetailStyles): ReactNode[
|
||||
showsHorizontalScrollIndicator
|
||||
style={ds.webScrollbarStyle}
|
||||
>
|
||||
<Text selectable style={styles.scrollText}>
|
||||
<Text selectable style={styles.scrollText} dataSet={CODE_SURFACE_DATASET}>
|
||||
{detail.content}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
@@ -553,7 +553,7 @@ function buildSearchSections(detail: SearchDetail, ds: DetailStyles): ReactNode[
|
||||
if (detail.filePaths && detail.filePaths.length > 0) {
|
||||
out.push(
|
||||
<View key="search-files" style={styles.section}>
|
||||
<Text selectable style={styles.scrollText}>
|
||||
<Text selectable style={styles.scrollText} dataSet={CODE_SURFACE_DATASET}>
|
||||
{detail.filePaths.join("\n")}
|
||||
</Text>
|
||||
</View>,
|
||||
@@ -562,7 +562,7 @@ function buildSearchSections(detail: SearchDetail, ds: DetailStyles): ReactNode[
|
||||
if (detail.webResults && detail.webResults.length > 0) {
|
||||
out.push(
|
||||
<View key="search-web-results" style={styles.section}>
|
||||
<Text selectable style={styles.scrollText}>
|
||||
<Text selectable style={styles.scrollText} dataSet={CODE_SURFACE_DATASET}>
|
||||
{detail.webResults.map((entry) => `${entry.title}\n${entry.url}`).join("\n\n")}
|
||||
</Text>
|
||||
</View>,
|
||||
@@ -571,7 +571,7 @@ function buildSearchSections(detail: SearchDetail, ds: DetailStyles): ReactNode[
|
||||
if (detail.annotations && detail.annotations.length > 0) {
|
||||
out.push(
|
||||
<View key="search-annotations" style={styles.section}>
|
||||
<Text selectable style={styles.scrollText}>
|
||||
<Text selectable style={styles.scrollText} dataSet={CODE_SURFACE_DATASET}>
|
||||
{detail.annotations.join("\n\n")}
|
||||
</Text>
|
||||
</View>,
|
||||
@@ -638,7 +638,7 @@ function buildUnknownSections(detail: UnknownDetail, ds: DetailStyles): ReactNod
|
||||
contentContainerStyle={styles.jsonContent}
|
||||
showsHorizontalScrollIndicator={true}
|
||||
>
|
||||
<Text selectable style={styles.scrollText}>
|
||||
<Text selectable style={styles.scrollText} dataSet={CODE_SURFACE_DATASET}>
|
||||
{value}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
@@ -738,7 +738,7 @@ function ErrorSection({ errorText, ds }: { errorText: string; ds: DetailStyles }
|
||||
contentContainerStyle={styles.jsonContent}
|
||||
showsHorizontalScrollIndicator={true}
|
||||
>
|
||||
<Text selectable style={SCROLL_TEXT_ERROR_STYLE}>
|
||||
<Text selectable style={SCROLL_TEXT_ERROR_STYLE} dataSet={CODE_SURFACE_DATASET}>
|
||||
{errorText}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
@@ -823,7 +823,7 @@ const styles = StyleSheet.create((theme) => {
|
||||
padding: theme.spacing[3],
|
||||
},
|
||||
plainText: {
|
||||
fontFamily: Fonts.sans,
|
||||
fontFamily: theme.fontFamily.ui,
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.foreground,
|
||||
lineHeight: 22,
|
||||
@@ -876,7 +876,7 @@ const styles = StyleSheet.create((theme) => {
|
||||
padding: insets.padding,
|
||||
},
|
||||
scrollText: {
|
||||
fontFamily: Fonts.mono,
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
fontSize: theme.fontSize.code,
|
||||
color: theme.colors.foreground,
|
||||
lineHeight: 18,
|
||||
@@ -891,7 +891,7 @@ const styles = StyleSheet.create((theme) => {
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
subAgentSessionText: {
|
||||
fontFamily: Fonts.mono,
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
fontSize: theme.fontSize.code,
|
||||
color: theme.colors.foregroundMuted,
|
||||
lineHeight: 18,
|
||||
@@ -907,13 +907,13 @@ const styles = StyleSheet.create((theme) => {
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
subAgentActionTool: {
|
||||
fontFamily: Fonts.mono,
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
fontSize: theme.fontSize.code,
|
||||
color: theme.colors.foregroundMuted,
|
||||
lineHeight: 18,
|
||||
},
|
||||
subAgentActionSummary: {
|
||||
fontFamily: Fonts.mono,
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
fontSize: theme.fontSize.code,
|
||||
color: theme.colors.foreground,
|
||||
lineHeight: 18,
|
||||
|
||||
Reference in New Issue
Block a user