Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions ui/src/components/workspace/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
type ClientControlMessage,
} from './protocol';
import { attachWebglRenderer } from './renderer';
import { darkTheme } from './theme';
import { darkTheme, lightTheme } from './theme';
import { useEffectiveTheme } from '../../theme/useEffectiveTheme';
// Lazy-import so the demo subtree (transcripts, fixtures, handlers) is
// dynamic-imported only when demo mode is actually on. With a static import,
// Rollup is conservative about module side-effects (the transcript file
Expand Down Expand Up @@ -99,6 +100,19 @@ export function TerminalView(props: TerminalViewProps): ReactElement {
const onSessionLostRef = useRef<TerminalViewProps['onSessionLost']>(props.onSessionLost);
onSessionLostRef.current = props.onSessionLost;

// Terminal palette follows the app theme (auto resolves via the OS). Read the
// current value through a ref so the connect effect doesn't recreate the
// terminal on a theme flip — a separate effect re-skins the live instance.
const effectiveTheme = useEffectiveTheme();
const xtermTheme = effectiveTheme === 'light' ? lightTheme : darkTheme;
const themeRef = useRef(xtermTheme);
themeRef.current = xtermTheme;
const termRef = useRef<Xterm | null>(null);

useEffect(() => {
if (termRef.current) termRef.current.options.theme = xtermTheme;
}, [xtermTheme]);

useEffect(() => {
const container = containerRef.current;
if (!container) return undefined;
Expand All @@ -110,7 +124,7 @@ export function TerminalView(props: TerminalViewProps): ReactElement {
setChildExited(false);

const term = new Xterm({
theme: darkTheme,
theme: themeRef.current,
fontFamily:
'ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", "DejaVu Sans Mono", monospace',
fontSize: 13,
Expand All @@ -121,6 +135,7 @@ export function TerminalView(props: TerminalViewProps): ReactElement {
macOptionIsMeta: true,
convertEol: false,
});
termRef.current = term;

const fit = new FitAddon();
term.loadAddon(fit);
Expand Down Expand Up @@ -313,6 +328,7 @@ export function TerminalView(props: TerminalViewProps): ReactElement {
}
webgl?.dispose();
term.dispose();
termRef.current = null;
};
}, [wsId, sessionId, wsUrl]);

Expand Down
31 changes: 31 additions & 0 deletions ui/src/components/workspace/theme.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import type { ITheme } from '@xterm/xterm';

/**
* Light xterm palette (GitHub-light-ish, warmed to sit on Daybreak's cream).
* ANSI `white` maps to a mid-grey and `brightWhite` to near-black, the way
* every light terminal theme does, so CLI output that prints as bright-white
* on a dark terminal becomes dark + legible here instead of vanishing.
* Picked by the active theme in Terminal.tsx (`auto` resolves via the OS).
*/
export const lightTheme: ITheme = {
background: '#faf8f1',
foreground: '#1c2a41',
cursor: '#2f62b0',
cursorAccent: '#faf8f1',
selectionBackground: 'rgba(47, 98, 176, 0.22)',
black: '#1c2a41',
red: '#cf222e',
green: '#116329',
yellow: '#7a4d05',
blue: '#0969da',
magenta: '#8250df',
cyan: '#1b7c83',
white: '#6e7781',
brightBlack: '#57606a',
brightRed: '#a40e26',
brightGreen: '#1a7f37',
brightYellow: '#633c01',
brightBlue: '#218bff',
brightMagenta: '#a475f9',
brightCyan: '#3192aa',
brightWhite: '#24292f',
};

export const darkTheme: ITheme = {
background: '#0b0d10',
foreground: '#e6edf3',
Expand Down
47 changes: 26 additions & 21 deletions ui/src/components/workspace/workspaces.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
.workspaces-root {
color-scheme: dark;
--bg: #0b0d10;
--panel: #11151a;
--sidebar: #0d1014;
--border: #21262d;
--fg-dim: #8b949e;
--fg: #e6edf3;
--accent: #79c0ff;
--danger: #ff7b72;
/* Alias the local palette to the global theme tokens so the whole
Workspaces surface (session view + sidebar) follows light / dark.
`color-scheme` is inherited from :root (per-theme), not pinned dark.
The terminal itself is themed separately (xterm ITheme in Terminal.tsx).
Remaining hardcoded semantic chips (agent badges, status pills) are
tracked in ANG-110. */
--bg: var(--color-bg);
--panel: var(--color-bg-secondary);
--sidebar: var(--color-bg-secondary);
--border: var(--color-border);
--fg-dim: var(--color-text-muted);
--fg: var(--color-text);
--accent: var(--color-accent);
--danger: var(--color-red);
}

.app {
Expand Down Expand Up @@ -247,7 +252,7 @@
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.sidebar-overview-btn:hover {
background: rgba(255, 255, 255, 0.04);
background: var(--color-overlay);
color: var(--fg);
}
.sidebar-overview-btn.is-active {
Expand Down Expand Up @@ -290,7 +295,7 @@
font: inherit;
}
.sidebar-row-main:hover {
background: rgba(255, 255, 255, 0.04);
background: var(--color-overlay);
}

.sidebar-status-dot {
Expand Down Expand Up @@ -370,7 +375,7 @@
transition: background 80ms;
}
.sidebar-session:hover {
background: rgba(255, 255, 255, 0.03);
background: var(--color-overlay);
}
.sidebar-session.is-active {
background: rgba(121, 192, 255, 0.10);
Expand Down Expand Up @@ -610,7 +615,7 @@
transition: background 80ms;
}
.sidebar-headless-item:hover {
background: rgba(255, 255, 255, 0.03);
background: var(--color-overlay);
}
.sidebar-headless-item .sidebar-status-dot {
width: 5px;
Expand Down Expand Up @@ -700,7 +705,7 @@
margin: 0 0 14px;
border: 1px solid var(--border);
border-radius: 6px;
background: rgba(255, 255, 255, 0.02);
background: var(--color-overlay);
}
.resume-tui-meta-strong {
font-weight: 600;
Expand All @@ -712,7 +717,7 @@
.resume-tui-prompt {
display: flex;
gap: 10px;
background: rgba(255, 255, 255, 0.04);
background: var(--color-overlay);
padding: 8px 14px;
margin: 6px 0;
border-radius: 4px;
Expand Down Expand Up @@ -995,7 +1000,7 @@
}
.workspace-cta-hint kbd {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
background: rgba(255, 255, 255, 0.06);
background: var(--color-overlay-strong);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0 4px;
Expand Down Expand Up @@ -1149,7 +1154,7 @@
align-items: center;
gap: 8px;
padding: 8px 12px;
background: linear-gradient(180deg, #161b22, #11151a);
background: linear-gradient(180deg, var(--color-bg-tertiary), var(--color-bg-secondary));
border-bottom: 1px solid var(--border);
font-size: 11px;
color: var(--fg-dim);
Expand Down Expand Up @@ -1179,7 +1184,7 @@
transition: background 0.12s, color 0.12s;
}
.panel-collapse:hover {
background: rgba(255, 255, 255, 0.06);
background: var(--color-overlay-strong);
color: var(--fg);
}

Expand Down Expand Up @@ -1326,7 +1331,7 @@
align-items: baseline;
}
.git-log-row:hover {
background: rgba(255, 255, 255, 0.03);
background: var(--color-overlay);
}

.git-log-hash {
Expand Down Expand Up @@ -1396,7 +1401,7 @@
}
.files-row.files-dir:hover,
.files-row.files-symlink:hover {
background: rgba(255, 255, 255, 0.03);
background: var(--color-overlay);
}

.files-icon {
Expand Down Expand Up @@ -1439,7 +1444,7 @@
align-items: center;
gap: 10px;
padding: 8px 12px;
background: linear-gradient(180deg, #161b22, #11151a);
background: linear-gradient(180deg, var(--color-bg-tertiary), var(--color-bg-secondary));
border-bottom: 1px solid var(--border);
font-size: 12px;
color: var(--fg-dim);
Expand Down
29 changes: 29 additions & 0 deletions ui/src/theme/useEffectiveTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useSyncExternalStore } from 'react'

import { useThemeStore } from './store'

const mq = window.matchMedia('(prefers-color-scheme: dark)')

function subscribeSystem(cb: () => void): () => void {
mq.addEventListener('change', cb)
return () => mq.removeEventListener('change', cb)
}

/**
* Resolve the preference (`light | dark | auto`) to a concrete `'light' | 'dark'`
* — `auto` follows the OS. Use this for the rare surfaces that need the actual
* value in JS rather than via CSS: notably the xterm terminal, whose palette is
* a JS object, not a CSS variable. CSS-driven surfaces should just read the
* `--color-*` tokens (which already resolve `data-theme` + prefers-color-scheme).
*/
export function useEffectiveTheme(): 'light' | 'dark' {
const theme = useThemeStore((s) => s.theme)
const systemDark = useSyncExternalStore(
subscribeSystem,
() => mq.matches,
() => true,
)
if (theme === 'light') return 'light'
if (theme === 'dark') return 'dark'
return systemDark ? 'dark' : 'light'
}
Loading