fix: terminal theme color switching (codex)#2640
Conversation
Greptile SummaryThis PR fixes terminal theme color switching by adding proper CSS variable resolution before xterm receives theme colors. Instead of eagerly resolving
Confidence Score: 5/5Safe to merge — all four changed files are narrowly scoped to theme color resolution and terminal repaint, with no data persistence or auth paths affected. The logic changes are well-contained: recursive CSS variable resolution with cycle detection, a validity guard before canvas painting, and an explicit terminal.refresh after theme mutation. Both new tests exercise the precise behaviors changed. No regressions are likely because readXtermCssVars still pre-resolves its own values and normalizeThemeColors only runs cssColorToHex on strings, passing non-strings through unchanged. No files require special attention.
|
| Filename | Overview |
|---|---|
| apps/emdash-desktop/src/renderer/utils/cssVars.ts | Adds recursive CSS variable resolution with cycle detection and CSS.supports validation; the seen set is shared by reference across the call chain (correct for cycle detection) and the isValidCSSColor fallback to true when CSS is undefined means canvas is still attempted in non-browser environments, but early context guard handles that path. |
| apps/emdash-desktop/src/renderer/lib/pty/pty.ts | Adds normalizeThemeColors, applyTheme (with terminal.refresh), and minimumContrastRatio in constructor; already-resolved hex values from readXtermCssVars are double-processed by normalizeThemeColors but this is harmless. |
| apps/emdash-desktop/src/renderer/features/tasks/terminals/terminal-pty-content.tsx | Switches from cssVar('--background') (eager resolution at render time) to 'var(--background)' (deferred resolution at theme application), which is the correct approach for keeping the terminal in sync with the active theme. |
| apps/emdash-desktop/src/renderer/tests/browser/xterm-host.test.ts | Adds two focused tests: one verifying minimumContrastRatio is set at construction and one verifying that unresolvable var() overrides are preserved rather than coerced to black. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["themeOverride: { background: 'var(--background)' }"] --> B["buildTheme(theme)"]
B --> C["readXtermCssVars()"]
C --> D["cssColorToHex(cssVar('--xterm-bg')) etc."]
B --> E["normalizeThemeColors(merged)"]
E --> F["cssColorToHex(value) for each string entry"]
F --> G["resolveCssVarColor(cssColor)"]
G --> H{parseCssVarReference?}
H -- "not a var()" --> I["return color as-is"]
H -- "is var(--name)" --> J["cssVar('--name')"]
J --> K{value empty?}
K -- "no" --> L["recurse with resolved value"]
K -- "yes / fallback" --> M["recurse with fallback or original"]
L --> G
M --> G
I --> N["isValidCSSColor(resolved)"]
N -- "invalid" --> O["return original cssColor string"]
N -- "valid" --> P["paint 1×1 canvas pixel"]
P --> Q["read back sRGB bytes → hex"]
Q --> R["terminal.options.theme = hex theme"]
R --> S["terminal.refresh(0, rows-1)"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["themeOverride: { background: 'var(--background)' }"] --> B["buildTheme(theme)"]
B --> C["readXtermCssVars()"]
C --> D["cssColorToHex(cssVar('--xterm-bg')) etc."]
B --> E["normalizeThemeColors(merged)"]
E --> F["cssColorToHex(value) for each string entry"]
F --> G["resolveCssVarColor(cssColor)"]
G --> H{parseCssVarReference?}
H -- "not a var()" --> I["return color as-is"]
H -- "is var(--name)" --> J["cssVar('--name')"]
J --> K{value empty?}
K -- "no" --> L["recurse with resolved value"]
K -- "yes / fallback" --> M["recurse with fallback or original"]
L --> G
M --> G
I --> N["isValidCSSColor(resolved)"]
N -- "invalid" --> O["return original cssColor string"]
N -- "valid" --> P["paint 1×1 canvas pixel"]
P --> Q["read back sRGB bytes → hex"]
Q --> R["terminal.options.theme = hex theme"]
R --> S["terminal.refresh(0, rows-1)"]
Reviews (2): Last reviewed commit: "fix(renderer): resolve xterm theme color..." | Re-trigger Greptile
|
Thanks! When is the toast being rendered? Everytime the user opens the app? |
|
which toast? @arnestrickmann |
Description
Screenshot/Recording (if applicable)
https://streamable.com/j2c97o
Checklist
messages and, when possible, the PR title