feat(desktop): theme xterm with active light/dark mode

The right-sidebar terminal hardcoded a light palette, which read poorly
on the dark glass surface. Subscribe to `useTheme().resolvedMode` and
hot-swap `term.options.theme` so Shift+X (and any other mode change)
updates the terminal in place without tearing down the PTY session.

Dark mode uses xterm's built-in defaults (white fg/cursor + vivid ANSI
16) with just a transparent background so the glass shows through;
light mode keeps the existing hand-tuned overrides for legibility on a
bright surface.
This commit is contained in:
Brooklyn Nicholson
2026-05-16 20:40:55 -05:00
parent f9908af1a0
commit eb68d66ff9
2 changed files with 25 additions and 11 deletions
@@ -1,14 +1,21 @@
import type { Terminal } from '@xterm/xterm'
import type { ITheme, Terminal } from '@xterm/xterm'
import type { CSSProperties } from 'react'
export const TERMINAL_THEME = {
background: '#00000000',
// xterm's defaults (white fg/cursor + vivid ANSI 16) already match a dark
// terminal — we just punch the background transparent so the glass shows.
// Light mode has no built-in, so hand-tune fg/cursor for a bright surface.
const TRANSPARENT_BG: ITheme = { background: '#00000000' }
const LIGHT_OVERRIDES: ITheme = {
cursor: '#6f6f6f',
cursorAccent: '#f7f7f7',
foreground: '#4d4d4d',
selectionBackground: '#8c8c8c33'
}
export const terminalTheme = (mode: 'light' | 'dark'): ITheme =>
mode === 'dark' ? TRANSPARENT_BG : { ...TRANSPARENT_BG, ...LIGHT_OVERRIDES }
export const isMacPlatform = () => navigator.platform.toLowerCase().includes('mac')
export const addSelectionShortcutLabel = () => (isMacPlatform() ? '⌘L' : 'Ctrl+L')
@@ -6,13 +6,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import type { CSSProperties } from 'react'
import { triggerHaptic } from '@/lib/haptics'
import { useTheme } from '@/themes/context'
import {
isAddSelectionShortcut,
TERMINAL_THEME,
terminalSelectionAnchor,
terminalSelectionLabel
} from './selection'
import { isAddSelectionShortcut, terminalSelectionAnchor, terminalSelectionLabel, terminalTheme } from './selection'
type TerminalStatus = 'closed' | 'open' | 'starting'
@@ -29,6 +25,8 @@ export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSes
const selectionLabelRef = useRef('')
const selectionRef = useRef('')
const onAddSelectionToChatRef = useRef(onAddSelectionToChat)
const { resolvedMode } = useTheme()
const resolvedModeRef = useRef(resolvedMode)
const [status, setStatus] = useState<TerminalStatus>('starting')
const [selection, setSelection] = useState('')
const [selectionStyle, setSelectionStyle] = useState<CSSProperties | null>(null)
@@ -38,11 +36,20 @@ export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSes
onAddSelectionToChatRef.current = onAddSelectionToChat
}, [onAddSelectionToChat])
useEffect(() => {
resolvedModeRef.current = resolvedMode
if (termRef.current) {
termRef.current.options.theme = terminalTheme(resolvedMode)
}
}, [resolvedMode])
const addSelectionToChat = useCallback(() => {
const selectedText = selectionRef.current || termRef.current?.getSelection() || ''
const label =
selectionLabelRef.current || (termRef.current ? terminalSelectionLabel(termRef.current, shellNameRef.current, selectedText) : 'selection')
selectionLabelRef.current ||
(termRef.current ? terminalSelectionLabel(termRef.current, shellNameRef.current, selectedText) : 'selection')
const trimmed = selectedText.trim()
@@ -102,7 +109,7 @@ export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSes
lineHeight: 1.12,
macOptionIsMeta: true,
scrollback: 1000,
theme: TERMINAL_THEME
theme: terminalTheme(resolvedModeRef.current)
})
const fit = new FitAddon()