From f81d382230c77b9d5cd53a64d29f203972166d08 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 20 Jul 2026 22:06:13 +0800 Subject: [PATCH] fix(tui): make color-scheme detection fully covered and race-free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The color-scheme detection block left packages/ui/tui/src/index.ts below the 100% per-file coverage gate on three counts: the .then callback's `scheme === undefined` branch was reachable only via the 2s query timeout, the .catch only via a query-write failure, and the `editor.borderColor` assignment inside applyColorScheme was dead code — the next line's setStatus() immediately reassigns editor.borderColor. Register the scheme listener before firing the startup query so the query's own reply is delivered through the listener (the same path as later theme switches), which removes the redundant .then re-application and its uncoverable undefined branch, and closes the theoretical window where a synchronous reply lands before the listener exists. Drop the dead editor.borderColor line. Cover the rest: a same-scheme report (early return) and a terminal that throws on the query write (the swallowed .catch). --- packages/ui/tui/src/index.ts | 21 ++++++++++----------- packages/ui/tui/tests/tui.spec.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) 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) + }) })