Merge pull request #431 from deepseek-harness/fix/tui-color-scheme-v2
fix(tui): detect terminal color scheme; harden the flaky sandbox probe-timeout test
This commit is contained in:
@@ -325,13 +325,19 @@ describe('probeTimeoutMs config', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('bounds the default probes: a launcher slower than the configured timeout reads as unusable', async () => {
|
it('bounds the default probes: a launcher slower than the configured timeout reads as unusable', async () => {
|
||||||
// The same sleeping launcher passes under the default 5000ms budget and
|
// The same 1s launcher reads usable under a generous budget and unusable
|
||||||
// fails under a 250ms one — the config demonstrably reaches spawnSync.
|
// under a 250ms one — the config demonstrably reaches spawnSync. Both bounds
|
||||||
|
// keep a wide margin from the launcher's 1s runtime so a loaded host (where
|
||||||
|
// spawnSync blocks the worker and fork/exec latency inflates wall-clock)
|
||||||
|
// cannot flip either verdict; the vitest timeout clears the patient budget.
|
||||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-slow-landlock-'))
|
const dir = mkdtempSync(join(tmpdir(), 'dsh-slow-landlock-'))
|
||||||
const launcher = join(dir, 'landlock-run')
|
const launcher = join(dir, 'landlock-run')
|
||||||
writeFileSync(launcher, '#!/bin/sh\nsleep 1\necho "landlock: fully enforced"\nexit 0\n', { mode: 0o755 })
|
writeFileSync(launcher, '#!/bin/sh\nsleep 1\necho "landlock: fully enforced"\nexit 0\n', { mode: 0o755 })
|
||||||
|
|
||||||
const patient = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
|
const patient = await setup(
|
||||||
|
{ probeTimeoutMs: 15_000 },
|
||||||
|
{ platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher },
|
||||||
|
)
|
||||||
expect(patient.sandbox.confine(['true'], RO).enforcement).toBe('full')
|
expect(patient.sandbox.confine(['true'], RO).enforcement).toBe('full')
|
||||||
|
|
||||||
const impatient = await setup(
|
const impatient = await setup(
|
||||||
@@ -339,7 +345,7 @@ describe('probeTimeoutMs config', () => {
|
|||||||
{ platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher },
|
{ platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher },
|
||||||
)
|
)
|
||||||
expect(() => impatient.sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
|
expect(() => impatient.sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
|
||||||
})
|
}, 30_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('the default seatbelt probe (sandbox-exec contract)', () => {
|
describe('the default seatbelt probe (sandbox-exec contract)', () => {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
type OverlayHandle,
|
type OverlayHandle,
|
||||||
type SelectListTheme,
|
type SelectListTheme,
|
||||||
type Terminal,
|
type Terminal,
|
||||||
|
type TerminalColorScheme,
|
||||||
} from '@earendil-works/pi-tui'
|
} from '@earendil-works/pi-tui'
|
||||||
import type { Context } from 'cordis'
|
import type { Context } from 'cordis'
|
||||||
import z from 'schemastery'
|
import z from 'schemastery'
|
||||||
@@ -237,17 +238,21 @@ function displayText(text: string): string {
|
|||||||
* backgrounds alike; grouping uses foreground-only gutter bars and reverse
|
* backgrounds alike; grouping uses foreground-only gutter bars and reverse
|
||||||
* video rather than fixed background fills.
|
* video rather than fixed background fills.
|
||||||
*/
|
*/
|
||||||
function createPalette(enabled: boolean): Palette {
|
function createPalette(enabled: boolean, scheme: TerminalColorScheme = 'dark'): Palette {
|
||||||
return {
|
return {
|
||||||
accent: ansi('94', '39', enabled),
|
accent: ansi('94', '39', enabled),
|
||||||
accent2: ansi('95', '39', enabled),
|
accent2: ansi('95', '39', enabled),
|
||||||
text: text => text,
|
text: text => text,
|
||||||
muted: ansi('90', '39', enabled),
|
muted: ansi('90', '39', enabled),
|
||||||
dim: ansi('2', '22', enabled),
|
// SGR 2 (dim) lightens text on a light background — substitute ANSI 90
|
||||||
|
// (bright black / gray) which renders as a readable muted tone on any scheme.
|
||||||
|
dim: scheme === 'light' ? ansi('90', '39', enabled) : ansi('2', '22', enabled),
|
||||||
success: ansi('32', '39', enabled),
|
success: ansi('32', '39', enabled),
|
||||||
warning: ansi('33', '39', enabled),
|
warning: ansi('33', '39', enabled),
|
||||||
error: ansi('31', '39', enabled),
|
error: ansi('31', '39', enabled),
|
||||||
code: ansi('36', '39', enabled),
|
// ANSI 36 (cyan) is difficult to read on a light background — use
|
||||||
|
// ANSI 34 (blue) which is legible on both light and dark schemes.
|
||||||
|
code: scheme === 'light' ? ansi('34', '39', enabled) : ansi('36', '39', enabled),
|
||||||
added: ansi('32', '39', enabled),
|
added: ansi('32', '39', enabled),
|
||||||
removed: ansi('31', '39', enabled),
|
removed: ansi('31', '39', enabled),
|
||||||
bold: ansi('1', '22', enabled),
|
bold: ansi('1', '22', enabled),
|
||||||
@@ -1504,6 +1509,30 @@ export function createTuiChat(
|
|||||||
void shutdown(true)
|
void shutdown(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Swap the palette and all derived themes for the given terminal color scheme. */
|
||||||
|
const applyColorScheme = (scheme: TerminalColorScheme): void => {
|
||||||
|
if (scheme === currentScheme) return
|
||||||
|
currentScheme = scheme
|
||||||
|
Object.assign(palette, createPalette(resolved.color, scheme))
|
||||||
|
Object.assign(mdTheme, markdownTheme(palette))
|
||||||
|
rebuildTranscript(false)
|
||||||
|
setStatus(agent.status)
|
||||||
|
requestRender()
|
||||||
|
}
|
||||||
|
let currentScheme: TerminalColorScheme = 'dark'
|
||||||
|
|
||||||
|
// 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 => {
|
const toggleTools = (): void => {
|
||||||
toolsExpanded = !toolsExpanded
|
toolsExpanded = !toolsExpanded
|
||||||
for (const card of allToolCards) card.setExpanded(toolsExpanded)
|
for (const card of allToolCards) card.setExpanded(toolsExpanded)
|
||||||
@@ -1714,6 +1743,7 @@ export function createTuiChat(
|
|||||||
disposeStatus()
|
disposeStatus()
|
||||||
disposeError()
|
disposeError()
|
||||||
disposeAgent()
|
disposeAgent()
|
||||||
|
disposeSchemeListener()
|
||||||
disposeTargetListeners()
|
disposeTargetListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1399,4 +1399,57 @@ describe('terminal mounting', () => {
|
|||||||
expect(() => createTuiChat(ctx, { sessionId: 'missing' }, runtime)).toThrow('is not running')
|
expect(() => createTuiChat(ctx, { sessionId: 'missing' }, runtime)).toThrow('is not running')
|
||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('detects a light terminal color scheme and switches from dark- to light-optimised ANSI codes', async () => {
|
||||||
|
const result = await setup({ config: { color: true } })
|
||||||
|
// 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')
|
||||||
|
await tick()
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
// After switching to light-optimised palette: palette.dim uses ANSI 90
|
||||||
|
// (gray) instead of SGR 2. The header now uses \x1b[90m for the detail
|
||||||
|
// line. The cumulative output still contains the initial SGR 2 render,
|
||||||
|
// so we assert that a LATER write (appended after the scheme switch)
|
||||||
|
// uses ANSI 90 for the same header text.
|
||||||
|
expect(result.terminal.output).toContain('\x1b[90mdeepseek-v4-flash')
|
||||||
|
|
||||||
|
// Switch back to dark scheme.
|
||||||
|
result.terminal.send('\x1b[?997;1n')
|
||||||
|
await tick()
|
||||||
|
await tick()
|
||||||
|
// After switching back, a new write uses SGR 2 for the header detail.
|
||||||
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user