diff --git a/packages/ui/tui/src/index.ts b/packages/ui/tui/src/index.ts index 9544d13cb0..17fef4c1e6 100644 --- a/packages/ui/tui/src/index.ts +++ b/packages/ui/tui/src/index.ts @@ -1134,25 +1134,24 @@ export function createTuiChat( currentScheme = scheme Object.assign(palette, createPalette(resolved.color, scheme)) Object.assign(mdTheme, markdownTheme(palette)) - editor.borderColor = text => palette.dim(text) rebuildTranscript(false) setStatus(agent.status) requestRender() } let currentScheme: TerminalColorScheme = 'dark' - // Detect the terminal's color scheme via device-status report. Most terminals - // do not respond, so the promise settles with `undefined` and we keep the - // dark-optimised palette. - ui.queryTerminalColorScheme({ timeoutMs: 2000 }).then((scheme) => { - if (scheme !== undefined) applyColorScheme(scheme) - }).catch(() => { - // Timeout or query failure — keep dark default. - }) - - // Live-update when the user switches their terminal theme behind us. + // Apply any color scheme the terminal reports. Registering before the query + // below means even a synchronous reply reaches `applyColorScheme`; in practice + // the startup query's reply is the only report, since dsh-tui leaves + // unsolicited color-scheme notifications disabled. const disposeSchemeListener = ui.onTerminalColorSchemeChange(applyColorScheme) + // Ask the terminal for its color scheme via device-status report; the reply, + // if any, arrives through the listener above. Most terminals do not respond, + // so we keep the dark-optimised palette. Swallow a query-write failure for the + // same reason. + ui.queryTerminalColorScheme({ timeoutMs: 2000 }).catch(() => {}) + const toggleTools = (): void => { toolsExpanded = !toolsExpanded for (const card of allToolCards) card.setExpanded(toolsExpanded) diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index 51a3ee4ec0..3eacfa4fcf 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -942,6 +942,13 @@ describe('terminal mounting', () => { // Initial render uses dark-optimised palette: SGR 2 (dim) for dim text. expect(result.terminal.output).toContain('\x1b[2mdeepseek-v4-flash') + // A report matching the current scheme is a no-op: no palette rebuild or + // re-render (ESC [?997;1n = dark, the startup default). + const beforeSameScheme = result.terminal.output.length + result.terminal.send('\x1b[?997;1n') + await tick() + expect(result.terminal.output.length).toBe(beforeSameScheme) + // Simulate the terminal responding with a light color scheme report // (ESC [?997;2n = light, ESC [?997;1n = dark). result.terminal.send('\x1b[?997;2n') @@ -963,4 +970,23 @@ describe('terminal mounting', () => { expect(result.terminal.output).toContain('\x1b[2mdeepseek-v4-flash') await dispose(result) }) + + it('keeps the dark palette when the terminal rejects the color-scheme query', async () => { + class QueryFailTerminal extends FakeTerminal { + override write(data: string): void { + // The device-status query is the only write that fails; the promise + // rejects and the swallowed `.catch` leaves the dark palette in place. + if (data === '\x1b[?996n') throw new Error('query write failed') + super.write(data) + } + } + const terminal = new QueryFailTerminal() + const result = await createTuiTestHarness(terminal, vi.fn(), { + config: { color: true }, + cwd: process.cwd(), + }) + await tick() + expect(terminal.output).toContain('\x1b[2mdeepseek-v4-flash') + await disposeTuiTestHarness(result) + }) })