Skip to content

fix: terminal theme color switching (codex)#2640

Open
janburzinski wants to merge 5 commits into
mainfrom
emdash/codex-theme-switch-fix
Open

fix: terminal theme color switching (codex)#2640
janburzinski wants to merge 5 commits into
mainfrom
emdash/codex-theme-switch-fix

Conversation

@janburzinski

Copy link
Copy Markdown
Collaborator

Description

  • fixes terminal theme switching
  • normalize theme colros safely

Screenshot/Recording (if applicable)

https://streamable.com/j2c97o

Checklist
  • I kept this PR small and focused
  • I ran a self-review before opening this PR
  • I ran the relevant local checks or explained why not
  • I updated docs when behavior or setup changed
  • I added or updated tests when behavior changed, or explained why not
  • I only added comments where the logic is not obvious
  • I used Conventional Commits for commit
    messages and, when possible, the PR title

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes terminal theme color switching by adding proper CSS variable resolution before xterm receives theme colors. Instead of eagerly resolving --background via cssVar() at React render time, it now passes the literal 'var(--background)' string so the new resolveCssVarColor chain can handle it during theme application — enabling chained design-token aliasing and deferred resolution that stays in sync with the active theme.

  • cssVars.ts: Adds resolveCssVarColor (recursive CSS variable traversal with cycle detection) and isValidCSSColor (guards against painting invalid colors to canvas), replacing the old sentinel #010203 approach.
  • pty.ts: Adds normalizeThemeColors to run all theme color strings (including overrides) through cssColorToHex; wraps the theme mutation in applyTheme which calls terminal.refresh() to force a full repaint after each color change; and moves minimumContrastRatio into the Terminal constructor options.
  • xterm-host.test.ts: Adds two browser tests covering the contrast ratio setting and the fallback-to-original behavior for unresolvable CSS variable overrides.

Confidence Score: 5/5

Safe 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.

Important Files Changed

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)"]
Loading
%%{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)"]
Loading

Reviews (2): Last reviewed commit: "fix(renderer): resolve xterm theme color..." | Re-trigger Greptile

Comment thread apps/emdash-desktop/src/renderer/utils/cssVars.ts Outdated
Comment thread apps/emdash-desktop/src/renderer/utils/cssVars.ts Outdated
Comment thread apps/emdash-desktop/src/renderer/lib/pty/pty.ts
@janburzinski

Copy link
Copy Markdown
Collaborator Author

@greptileai

@arnestrickmann

Copy link
Copy Markdown
Contributor

Thanks!

When is the toast being rendered? Everytime the user opens the app?

@janburzinski

Copy link
Copy Markdown
Collaborator Author

which toast? @arnestrickmann

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants