Merge origin/master into worktree/explicit-turn-signal

This commit is contained in:
Yichen Jiang
2026-07-20 21:38:49 +08:00
1322 changed files with 47895 additions and 20257 deletions

View File

@@ -0,0 +1,131 @@
import { Context } from 'cordis'
import type { Terminal } from '@earendil-works/pi-tui'
import AgentRegistry, { type Agent, type AgentCancelCause, type AgentStatus } from '@deepseek-ai/dsh-agent'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import { createTuiChat, type Config } from '../src/index.ts'
interface FakeAgent extends Agent {
status: AgentStatus
sent: ContentBlock[][]
steered: ContentBlock[][]
cancelled: AgentCancelCause[]
}
export interface TuiHarnessOptions {
status?: AgentStatus
config?: Config
tools?: Record<string, ToolDefinition>
configureContext?: (ctx: Context) => Promise<void>
beforeMount?: (session: Session) => void
cwd?: string | null
}
export interface TuiHarness<TerminalType extends Terminal, Exit extends (code: number) => void> {
ctx: Context
session: Session
agent: FakeAgent
terminal: TerminalType
exit: Exit
controller: ReturnType<typeof createTuiChat>
}
/**
* Compose the production TUI around an in-memory session and controllable agent.
* @param terminal - Terminal boundary driven by the test.
* @param exit - Process-exit observer.
* @param options - Initial session, agent, tool, and TUI configuration.
* @returns The mounted TUI and every boundary the test may drive or inspect.
*/
export async function createTuiTestHarness<TerminalType extends Terminal, Exit extends (code: number) => void>(
terminal: TerminalType,
exit: Exit,
options: TuiHarnessOptions = {},
): Promise<TuiHarness<TerminalType, Exit>> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
if (options.configureContext === undefined) {
const tools = options.tools ?? {}
ctx.provide('tools', {
get(name: string) {
return tools[name]
},
} as never)
} else {
await options.configureContext(ctx)
}
const sessionId = SessionId('main-session')
const session = ctx.sessions.create(
sessionId,
options.cwd === null ? undefined : { meta: { cwd: options.cwd ?? '/workspace' } },
)
options.beforeMount?.(session)
const sent: ContentBlock[][] = []
const steered: ContentBlock[][] = []
const cancelled: AgentCancelCause[] = []
const agent: FakeAgent = {
id: sessionId,
options: { model: 'deepseek-v4-flash' },
session,
status: options.status ?? 'idle',
ctx,
sent,
steered,
cancelled,
send(content) {
sent.push(content)
},
steer(content) {
steered.push(content)
},
inject() {},
cancel(cause = { kind: 'user' }) {
cancelled.push(cause)
},
whenIdle() {
return Promise.resolve()
},
}
ctx.agents.register(agent)
const controller = createTuiChat(ctx, Object.assign({
welcome: 'Coding agent ready.',
sessionId,
color: false,
}, options.config), { terminal, exit })
return { ctx, session, agent, terminal, exit, controller }
}
/** Dispose the mounted TUI before its owning Cordis context. */
export async function disposeTuiTestHarness(
setup: Pick<TuiHarness<Terminal, (code: number) => void>, 'controller' | 'ctx'>,
): Promise<void> {
await setup.controller.dispose()
await setup.ctx.fiber.dispose()
}
/** Append a production-shaped user message to the active session surface. */
export function appendUser(session: Session, text: string): void {
session.append('user/message', {
content: [{ type: 'text', text }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}
/** Append a production-shaped assistant message to the active session surface. */
export function appendAssistant(
session: Session,
content: ContentBlock[],
usage?: { inputTokens: number; outputTokens: number },
): void {
session.append('assistant/message', {
turn: 1,
step: 0,
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
content,
...usage === undefined ? {} : { usage },
}, { surfaceOp: 'append' })
}

View File

@@ -0,0 +1,318 @@
import type { Terminal } from '@earendil-works/pi-tui'
import { Terminal as XtermTerminal, type IBufferCell } from '@xterm/headless'
const FRAME_END = '\x1b[?2026l'
const FRAME_TIMEOUT_MS = 2_000
const ANSI_COLORS = [
'black',
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white',
'bright-black',
'bright-red',
'bright-green',
'bright-yellow',
'bright-blue',
'bright-magenta',
'bright-cyan',
'bright-white',
] as const
interface FrameWaiter {
target: number
resolve: () => void
reject: (error: Error) => void
timer: ReturnType<typeof setTimeout>
}
interface RowSnapshot {
text: string
wrapped: boolean
styles: string[]
}
export interface TerminalSnapshotOptions {
/** Include the whole active buffer instead of only the visible viewport. */
includeScrollback?: boolean
}
function occurrenceCount(value: string, needle: string): number {
let count = 0
let offset = 0
while (true) {
const match = value.indexOf(needle, offset)
if (match < 0) return count
count += 1
offset = match + needle.length
}
}
function colorLabel(cell: IBufferCell, kind: 'fg' | 'bg'): string | undefined {
const isDefault = kind === 'fg' ? cell.isFgDefault() : cell.isBgDefault()
if (isDefault) return undefined
const isRgb = kind === 'fg' ? cell.isFgRGB() : cell.isBgRGB()
const value = kind === 'fg' ? cell.getFgColor() : cell.getBgColor()
if (isRgb) return `${kind}=#${value.toString(16).padStart(6, '0')}`
const name = ANSI_COLORS[value]
return `${kind}=${name ?? `ansi-${value}`}`
}
function styleLabel(cell: IBufferCell): string {
const labels = [
colorLabel(cell, 'fg'),
colorLabel(cell, 'bg'),
cell.isBold() !== 0 ? 'bold' : undefined,
cell.isDim() !== 0 ? 'dim' : undefined,
cell.isItalic() !== 0 ? 'italic' : undefined,
cell.isUnderline() !== 0 ? 'underline' : undefined,
cell.isBlink() !== 0 ? 'blink' : undefined,
cell.isInverse() !== 0 ? 'inverse' : undefined,
cell.isInvisible() !== 0 ? 'invisible' : undefined,
cell.isStrikethrough() !== 0 ? 'strike' : undefined,
cell.isOverline() !== 0 ? 'overline' : undefined,
].filter((label): label is string => label !== undefined)
return labels.join(' ')
}
function snapshotRow(terminal: XtermTerminal, row: number): RowSnapshot {
const line = terminal.buffer.active.getLine(row)
if (line === undefined) return { text: '', wrapped: false, styles: [] }
const styles: string[] = []
let activeStyle = ''
let activeStart = 0
for (let column = 0; column <= terminal.cols; column++) {
const cell = column < terminal.cols ? line.getCell(column) : undefined
const style = cell === undefined ? '' : styleLabel(cell)
if (style === activeStyle) continue
if (activeStyle !== '') styles.push(`${activeStart}-${column - 1} ${activeStyle}`)
activeStyle = style
activeStart = column
}
return {
text: line.translateToString(true),
wrapped: line.isWrapped,
styles,
}
}
function renderRows(rows: readonly RowSnapshot[], firstRow: number): string[] {
const rendered: string[] = []
let blankStart: number | undefined
const flushBlanks = (end: number): void => {
if (blankStart === undefined) return
rendered.push(blankStart === end ? `${blankStart}| <blank>` : `${blankStart}-${end}| <blank>`)
blankStart = undefined
}
for (let index = 0; index < rows.length; index++) {
const absoluteRow = firstRow + index
const row = rows[index] as RowSnapshot
if (row.text === '' && row.styles.length === 0 && !row.wrapped) {
blankStart ??= absoluteRow
continue
}
flushBlanks(absoluteRow - 1)
rendered.push(`${absoluteRow}${row.wrapped ? '~' : ''}| ${JSON.stringify(row.text)}`)
for (const style of row.styles) rendered.push(` style ${style}`)
}
flushBlanks(firstRow + rows.length - 1)
return rendered
}
/**
* Terminal emulator used by TUI snapshots. It consumes the same ANSI stream as
* a real terminal and exposes completed synchronized frames as an awaitable boundary.
*/
export class HeadlessTerminal implements Terminal {
readonly kittyProtocolActive = false
readonly drainInput = (): Promise<void> => Promise.resolve()
started = 0
stopped = 0
title = ''
progress = false
cursorVisible = true
frames = 0
private readonly emulator: XtermTerminal
private onInput: (data: string) => void = () => {}
private onResize: () => void = () => {}
private pendingWrite: Promise<void> = Promise.resolve()
private readonly frameWaiters = new Set<FrameWaiter>()
constructor(columns = 80, rows = 24) {
this.emulator = new XtermTerminal({
cols: columns,
rows,
scrollback: 1_000,
allowProposedApi: true,
drawBoldTextInBrightColors: false,
logLevel: 'off',
})
}
get columns(): number {
return this.emulator.cols
}
get rows(): number {
return this.emulator.rows
}
start(onInput: (data: string) => void, onResize: () => void): void {
this.started += 1
this.onInput = onInput
this.onResize = onResize
}
stop(): void {
this.stopped += 1
}
write(data: string): void {
const completedFrames = occurrenceCount(data, FRAME_END)
this.pendingWrite = new Promise((resolve) => {
this.emulator.write(data, () => {
this.frames += completedFrames
for (const waiter of this.frameWaiters) {
if (this.frames < waiter.target) continue
clearTimeout(waiter.timer)
this.frameWaiters.delete(waiter)
waiter.resolve()
}
resolve()
})
})
}
moveBy(lines: number): void {
if (lines > 0) this.write(`\x1b[${lines}B`)
if (lines < 0) this.write(`\x1b[${-lines}A`)
}
hideCursor(): void {
this.cursorVisible = false
this.write('\x1b[?25l')
}
showCursor(): void {
this.cursorVisible = true
this.write('\x1b[?25h')
}
clearLine(): void {
this.write('\x1b[K')
}
clearFromCursor(): void {
this.write('\x1b[J')
}
clearScreen(): void {
this.write('\x1b[2J\x1b[H')
}
setTitle(title: string): void {
this.title = title
this.write(`\x1b]0;${title}\x07`)
}
setProgress(active: boolean): void {
this.progress = active
}
send(data: string): void {
this.onInput(data)
}
resize(columns: number, rows = this.rows): void {
this.emulator.resize(columns, rows)
this.onResize()
}
/** Wait until pi-tui completes a synchronized frame newer than `after`. */
async waitForFrame(after = this.frames): Promise<void> {
if (this.frames <= after) {
await new Promise<void>((resolve, reject) => {
const waiter: FrameWaiter = {
target: after + 1,
resolve,
reject,
timer: setTimeout(() => {
this.frameWaiters.delete(waiter)
reject(new Error(`TUI did not complete frame ${after + 1} within ${FRAME_TIMEOUT_MS}ms`))
}, FRAME_TIMEOUT_MS),
}
this.frameWaiters.add(waiter)
})
}
await this.flush()
}
/** Await every terminal write queued through the current task. */
async flush(): Promise<void> {
let pending: Promise<void>
do {
pending = this.pendingWrite
await pending
} while (pending !== this.pendingWrite)
}
/**
* Reject palette output that would become theme-specific in a user's terminal.
* @returns One location per RGB, extended-palette, or explicit-background cell.
*/
themeViolations(): string[] {
const violations: string[] = []
const buffer = this.emulator.buffer.active
for (let row = 0; row < buffer.length; row++) {
const line = buffer.getLine(row)
if (line === undefined) continue
for (let column = 0; column < this.columns; column++) {
const cell = line.getCell(column)
if (cell === undefined) continue
const reasons = [
cell.isFgRGB() ? 'rgb-fg' : undefined,
cell.isBgRGB() ? 'rgb-bg' : undefined,
cell.isFgPalette() && cell.getFgColor() > 15 ? `extended-fg-${cell.getFgColor()}` : undefined,
cell.isBgPalette() && cell.getBgColor() > 15 ? `extended-bg-${cell.getBgColor()}` : undefined,
!cell.isBgDefault() ? 'explicit-bg' : undefined,
].filter((reason): reason is string => reason !== undefined)
if (reasons.length > 0) violations.push(`${row}:${column} ${reasons.join(',')}`)
}
}
return violations
}
/** Serialize terminal cells and metadata into a stable, reviewable expected output. */
async snapshot(options: TerminalSnapshotOptions = {}): Promise<string> {
await this.flush()
const buffer = this.emulator.buffer.active
const firstRow = options.includeScrollback === true ? 0 : buffer.viewportY
const rowCount = options.includeScrollback === true ? buffer.length : this.rows
const rows = Array.from({ length: rowCount }, (_, index) => snapshotRow(this.emulator, firstRow + index))
const cursorBufferRow = buffer.baseY + buffer.cursorY
const cursorViewportRow = cursorBufferRow - buffer.viewportY
return [
`terminal ${this.columns}x${this.rows} buffer=${buffer.type} length=${buffer.length} base=${buffer.baseY} viewport=${buffer.viewportY}`,
`lifecycle started=${this.started} stopped=${this.stopped} progress=${this.progress ? 'active' : 'inactive'}`,
`title ${JSON.stringify(this.title)}`,
`cursor ${this.cursorVisible ? 'visible' : 'hidden'} column=${buffer.cursorX} viewportRow=${cursorViewportRow} bufferRow=${cursorBufferRow}`,
options.includeScrollback === true ? 'buffer' : 'viewport',
...renderRows(rows, firstRow),
'',
].join('\n')
}
async dispose(): Promise<void> {
await this.flush()
for (const waiter of this.frameWaiters) {
clearTimeout(waiter.timer)
waiter.reject(new Error('terminal disposed before the requested frame completed'))
}
this.frameWaiters.clear()
this.emulator.dispose()
}
}

View File

@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import Loader from '@cordisjs/plugin-loader'
import * as tui from '../src/index.ts'
/** Real Loader export-path guard for the namespace TUI plugin. */
describe('dsh-tui plugin export shape', () => {
it('preserves name, inject, Config, and apply through Loader unwrapping', () => {
expect('default' in tui).toBe(false)
expect(typeof tui.apply).toBe('function')
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(tui) as Record<string, unknown>
expect(unwrapped).toBe(tui)
expect(unwrapped.name).toBe('ui-tui')
expect(unwrapped.inject).toEqual(['agents', 'userInteraction', 'tools'])
expect(unwrapped.Config).toBeDefined()
expect(typeof unwrapped.apply).toBe('function')
})
})

View File

@@ -0,0 +1,107 @@
terminal 100x40 buffer=normal length=41 base=1 viewport=1
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=37 bufferRow=38
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-99 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 99-99 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 99-99 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 99-99 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-99 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=green
7| "▌ ✓ pnpm run test:coverage "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-25 bold
8| "▌ Run the coverage gate "
style 0-0 fg=green
style 2-22 fg=bright-black
9| "▌ /workspace/project "
style 0-0 fg=green
style 2-19 dim
10| "▌ packages/ui/tui 100% "
style 0-0 fg=green
11| "▌ … 4 more lines (Ctrl+O to expand) "
style 0-0 fg=green
style 2-34 dim
12| "▌ "
style 0-0 fg=green
13| <blank>
14| "▌ "
style 0-0 fg=green
15| "▌ ✓ Edit renderer "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-16 bold
16| "▌ src/view.ts "
style 0-0 fg=green
style 2-12 bold
17| "▌ - old line "
style 0-0 fg=green
style 2-11 fg=red
18| "▌ - keep "
style 0-0 fg=green
style 2-7 fg=red
19| "▌ … 5 more lines (Ctrl+O to expand) "
style 0-0 fg=green
style 2-34 dim
20| "▌ "
style 0-0 fg=green
21| <blank>
22| "▌ "
style 0-0 fg=green
23| "▌ ✓ Delegate renderer audit "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-26 bold
24| "▌ The renderer has explicit lifecycle ownership. "
style 0-0 fg=green
25| "▌ "
style 0-0 fg=green
26| <blank>
27| "▌ "
style 0-0 fg=green
28| "▌ ✓ Read output from background task subagent-7 "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-46 bold
29| "▌ audit complete "
style 0-0 fg=green
30| "▌ [status: completed] "
style 0-0 fg=green
31| "▌ "
style 0-0 fg=green
32| <blank>
33| "▌ "
style 0-0 fg=green
34| "▌ ✓ Load skill dsh-code-review "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-29 bold
35| "▌ Loaded review instructions. "
style 0-0 fg=green
36| "▌ "
style 0-0 fg=green
37| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
38| " "
style 1-1 inverse
39| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
40| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 67-99 dim

View File

@@ -0,0 +1,127 @@
terminal 100x40 buffer=normal length=50 base=10 viewport=10
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=37 bufferRow=47
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-99 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 99-99 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 99-99 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 99-99 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-99 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=green
7| "▌ ✓ pnpm run test:coverage "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-25 bold
8| "▌ Run the coverage gate "
style 0-0 fg=green
style 2-22 fg=bright-black
9| "▌ /workspace/project "
style 0-0 fg=green
style 2-19 dim
10| "▌ packages/ui/tui 100% "
style 0-0 fg=green
11| "▌ 4016 tests passed "
style 0-0 fg=green
12| "▌ 1 test skipped "
style 0-0 fg=green
13| "▌ coverage complete "
style 0-0 fg=green
14| "▌ [exit 0] "
style 0-0 fg=green
style 2-9 dim
15| "▌ "
style 0-0 fg=green
16| <blank>
17| "▌ "
style 0-0 fg=green
18| "▌ ✓ Edit renderer "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-16 bold
19| "▌ src/view.ts "
style 0-0 fg=green
style 2-12 bold
20| "▌ - old line "
style 0-0 fg=green
style 2-11 fg=red
21| "▌ - keep "
style 0-0 fg=green
style 2-7 fg=red
22| "▌ + new line "
style 0-0 fg=green
style 2-11 fg=green
23| "▌ + keep "
style 0-0 fg=green
style 2-7 fg=green
24| "▌ "
style 0-0 fg=green
25| "▌ tests/view.spec.ts "
style 0-0 fg=green
style 2-19 bold
26| "▌ + expect(screen).toMatchSnapshot() "
style 0-0 fg=green
style 2-35 fg=green
27| "▌ "
style 0-0 fg=green
28| <blank>
29| "▌ "
style 0-0 fg=green
30| "▌ ✓ Delegate renderer audit "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-26 bold
31| "▌ The renderer has explicit lifecycle ownership. "
style 0-0 fg=green
32| "▌ "
style 0-0 fg=green
33| <blank>
34| "▌ "
style 0-0 fg=green
35| "▌ ✓ Read output from background task subagent-7 "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-46 bold
36| "▌ audit complete "
style 0-0 fg=green
37| "▌ [status: completed] "
style 0-0 fg=green
38| "▌ "
style 0-0 fg=green
39| <blank>
40| "▌ "
style 0-0 fg=green
41| "▌ ✓ Load skill dsh-code-review "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-29 bold
42| "▌ Loaded review instructions. "
style 0-0 fg=green
43| "▌ "
style 0-0 fg=green
44| <blank>
45| " Tool cards expanded. "
style 1-20 fg=bright-black
46| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
47| " "
style 1-1 inverse
48| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
49| "/workspace/project ↑0 ↓0 idle reasoning:on tools:expanded"
style 0-24 dim
style 66-99 dim

View File

@@ -0,0 +1,52 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=15 bufferRow=15
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=yellow
7| "▌ ◌ const first = await tools.bash({ command: 'echo CODE_ONE' }) "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-95 bold
8| "▌ const second = await tools.bas "
style 0-0 fg=yellow
style 2-31 bold
9| "▌ const first = await tools.bash({ command: 'echo CODE_ONE' }) "
style 0-0 fg=yellow
10| "▌ const second = await tools.bash({ command: 'echo CODE_TWO' }) "
style 0-0 fg=yellow
11| "▌ console.log(first, second) "
style 0-0 fg=yellow
12| "▌ return `${first}+${second}` "
style 0-0 fg=yellow
13| "▌ "
style 0-0 fg=yellow
14| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
15| " "
style 1-1 inverse
16| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
17| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
18-35| <blank>

View File

@@ -0,0 +1,52 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=17 bufferRow=17
viewport
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=bright-blue
7| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
8| "▌ Show the live update. "
style 0-0 fg=bright-blue
9| "▌ "
style 0-0 fg=bright-blue
10| <blank>
11| " Reasoning "
style 1-9 fg=bright-black italic
12| " Inspecting width and styles. "
style 1-28 fg=bright-black italic
13| <blank>
14| " Assistant "
style 1-9 fg=bright-magenta bold
15| " Streaming visible state… "
style 11-23 bold
16| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
17| " "
style 1-1 inverse
18| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
19| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
20-35| <blank>

View File

@@ -0,0 +1,59 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=18 bufferRow=18
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ ◌ Inspect cordis runtime: tools "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-32 bold
7| <blank>
8| "▌ "
style 0-0 fg=yellow
9| "▌ ◌ Mount plugin into live cordis runtime "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-40 bold
10| "▌ { "
style 0-0 fg=yellow
11| "▌ \"code\": \"return { name: 'snapshot-marker', apply(ctx) { ctx.provide('snapshotMarker', { "
style 0-0 fg=yellow
12| "▌ ready: true }) } }\" "
style 0-0 fg=yellow
13| "▌ } "
style 0-0 fg=yellow
14| "▌ "
style 0-0 fg=yellow
15| <blank>
16| "▌ ◌ Unmount dyn-1 "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-16 bold
17| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
18| " "
style 1-1 inverse
19| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
20| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
21-35| <blank>

View File

@@ -0,0 +1,52 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=1 progress=inactive
title "DSH snapshot"
cursor visible column=0 viewportRow=22 bufferRow=22
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-91 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 91-91 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 91-91 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 91-91 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-91 fg=bright-blue
5| <blank>
6| " Keyboard shortcuts "
style 1-18 fg=bright-blue bold
7| " Enter send • Shift/Alt+Enter newline • Up/Down prompt history "
style 1-61 fg=bright-black
8| " Esc cancel active turn • Ctrl+O expand tool cards • Ctrl+R toggle reasoning "
style 1-75 fg=bright-black
9| " Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit "
style 1-73 fg=bright-black
10| " /help /clear /cancel /reasoning /tools /redraw /exit "
style 1-52 fg=bright-black
11| <blank>
12| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow
13| <blank>
14| " provider stream failed after partial output "
style 1-43 fg=red
15| <blank>
16| " The previous process ended during this turn. "
style 1-44 fg=yellow
17| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
18| " "
style 1-1 inverse
19| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
20| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 59-91 dim
21-31| <blank>

View File

@@ -0,0 +1,55 @@
terminal 96x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=17 bufferRow=17
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-95 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 95-95 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=yellow
7| "▌ ◌ workflow: tui-matrix "
style 0-0 fg=yellow
style 2-2 fg=yellow bold
style 3-23 bold
8| "▌ phase('Inspect') "
style 0-0 fg=yellow
9| "▌ const reports = await parallel([ "
style 0-0 fg=yellow
10| "▌ () => agent('Audit layout', { label: 'layout', phase: 'Inspect' }), "
style 0-0 fg=yellow
11| "▌ () => agent('Audit lifecycle', { label: 'lifecycle', phase: 'Inspect' }), "
style 0-0 fg=yellow
12| "▌ ]) "
style 0-0 fg=yellow
13| "▌ phase('Verify') "
style 0-0 fg=yellow
14| "▌ return { reports, verdict: 'covered' } "
style 0-0 fg=yellow
15| "▌ "
style 0-0 fg=yellow
16| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
17| " "
style 1-1 inverse
18| "────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-95 dim
19| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 63-95 dim
20-35| <blank>

View File

@@ -0,0 +1,52 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=18 bufferRow=18
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-91 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 91-91 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 91-91 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 91-91 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-91 fg=bright-blue
5| <blank>
6| " Keyboard shortcuts "
style 1-18 fg=bright-blue bold
7| " Enter send • Shift/Alt+Enter newline • Up/Down prompt history "
style 1-61 fg=bright-black
8| " Esc cancel active turn • Ctrl+O expand tool cards • Ctrl+R toggle reasoning "
style 1-75 fg=bright-black
9| " Ctrl+C cancel while running; clear input or exit while idle • Ctrl+D exit "
style 1-73 fg=bright-black
10| " /help /clear /cancel /reasoning /tools /redraw /exit "
style 1-52 fg=bright-black
11| <blank>
12| " Unknown command: /unknown-advanced-command "
style 1-42 fg=yellow
13| <blank>
14| " provider stream failed after partial output "
style 1-43 fg=red
15| <blank>
16| " The previous process ended during this turn. "
style 1-44 fg=yellow
17| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
18| " "
style 1-1 inverse
19| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
20| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 59-91 dim
21-31| <blank>

View File

@@ -0,0 +1,69 @@
terminal 56x20 buffer=normal length=20 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=56 viewportRow=13 bufferRow=13
viewport
0| "╭──────────────────────────────────────────────────────╮"
style 0-55 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 55-55 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 55-55 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 55-55 fg=bright-blue
4| "╰───╭ Coverage ────────────────────────────────────╮───╯"
style 0-55 fg=bright-blue
5| "────│ Which advanced TUI states belong in the │────"
style 0-3 dim
style 4-4 fg=bright-blue
style 6-50 bold
style 51-51 fg=bright-blue bold
style 52-55 dim
6| " │ required matrix? │ "
style 1-1 inverse
style 4-4 fg=bright-blue
style 6-21 bold
style 51-51 fg=bright-blue
7| "────│ │────"
style 0-3 dim
style 4-4 fg=bright-blue
style 51-51 fg=bright-blue
style 52-55 dim
8| "/wor│ › [ ] Code Mode — run_code programs and capt │:com"
style 0-3 dim
style 4-4 fg=bright-blue
style 6-6 fg=bright-blue inverse
style 7-20 inverse
style 21-49 fg=bright-black inverse
style 51-51 fg=bright-blue
style 52-55 dim
9| " │ [ ] Workflows — phases and parallel agents │ "
style 4-4 fg=bright-blue
style 21-49 fg=bright-black
style 51-51 fg=bright-blue
10| " │ [ ] Cordis tools — inspect, mount, and unm │ "
style 4-4 fg=bright-blue
style 24-49 fg=bright-black
style 51-51 fg=bright-blue
11| " │ 1/4 │ "
style 4-4 fg=bright-blue
style 6-8 dim
style 51-51 fg=bright-blue
12| " │ ↑↓ navigate • Space toggle • Enter submit • │ "
style 4-4 fg=bright-blue
style 6-49 dim
style 51-51 fg=bright-blue
13| " │ Select at least one option, or press C for a │ "
style 4-4 fg=bright-blue
style 6-49 fg=red
style 51-51 fg=bright-blue
14| " ╰──────────────────────────────────────────────╯ "
style 4-51 fg=bright-blue
15-19| <blank>

View File

@@ -0,0 +1,67 @@
terminal 56x20 buffer=normal length=20 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=0 viewportRow=19 bufferRow=19
viewport
0| "╭──────────────────────────────────────────────────────╮"
style 0-55 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 55-55 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 55-55 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 55-55 fg=bright-blue
4| "╰──────────────────────────────────────────────────────╯"
style 0-55 fg=bright-blue
5| "────╭ Coverage ────────────────────────────────────╮────"
style 0-3 dim
style 4-51 fg=bright-blue
style 52-55 dim
6| " │ Which advanced TUI states belong in the │ "
style 1-1 inverse
style 4-4 fg=bright-blue
style 6-50 bold
style 51-51 fg=bright-blue bold
7| "────│ required matrix? │────"
style 0-3 dim
style 4-4 fg=bright-blue
style 6-21 bold
style 51-51 fg=bright-blue
style 52-55 dim
8| "/wor│ │:com"
style 0-3 dim
style 4-4 fg=bright-blue
style 51-51 fg=bright-blue
style 52-55 dim
9| " │ › [ ] Code Mode — run_code programs and capt │ "
style 4-4 fg=bright-blue
style 6-6 fg=bright-blue inverse
style 7-20 inverse
style 21-49 fg=bright-black inverse
style 51-51 fg=bright-blue
10| " │ [ ] Workflows — phases and parallel agents │ "
style 4-4 fg=bright-blue
style 21-49 fg=bright-black
style 51-51 fg=bright-blue
11| " │ [ ] Cordis tools — inspect, mount, and unm │ "
style 4-4 fg=bright-blue
style 24-49 fg=bright-black
style 51-51 fg=bright-blue
12| " │ 1/4 │ "
style 4-4 fg=bright-blue
style 6-8 dim
style 51-51 fg=bright-blue
13| " │ ↑↓ navigate • Space toggle • Enter submit • │ "
style 4-4 fg=bright-blue
style 6-49 dim
style 51-51 fg=bright-blue
14| " ╰──────────────────────────────────────────────╯ "
style 4-51 fg=bright-blue
15-19| <blank>

View File

@@ -0,0 +1,41 @@
terminal 44x18 buffer=normal length=18 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=11 bufferRow=11
buffer
0| "╭──────────────────────────────────────────╮"
style 0-43 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 43-43 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 43-43 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 43-43 fg=bright-blue
4| "╰──────────────────────────────────────────╯"
style 0-43 fg=bright-blue
5| <blank>
6| " Context · compact "
style 1-17 dim
7| " Compacted summary: the prior command "
style 1-43 fg=bright-black
8| " completed and its details were retired "
style 1-43 fg=bright-black
9| " from the active surface. "
style 1-24 fg=bright-black
10| "────────────────────────────────────────────"
style 0-43 dim
11| " "
style 1-1 inverse
12| "────────────────────────────────────────────"
style 0-43 dim
13| "/workspace/project ↑0 ↓0 idle reasoning:o"
style 0-24 dim
style 27-43 dim
14-17| <blank>

View File

@@ -0,0 +1,37 @@
terminal 104x30 buffer=normal length=30 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=9 bufferRow=9
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-103 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 103-103 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 103-103 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 103-103 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-103 fg=bright-blue
5| <blank>
6| " Context · compact "
style 1-17 dim
7| " Compacted summary: the prior command completed and its details were retired from the active surface. "
style 1-100 fg=bright-black
8| "────────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-103 dim
9| " "
style 1-1 inverse
10| "────────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-103 dim
11| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 71-103 dim
12-29| <blank>

View File

@@ -0,0 +1,67 @@
terminal 80x24 buffer=normal length=25 base=1 viewport=1
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=21 bufferRow=22
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────╮"
style 0-79 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 79-79 fg=bright-blue
2| "│ Snapshot agent ready. │"
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 79-79 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 79-79 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────╯"
style 0-79 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=bright-blue
7| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
8| "▌ Old prompt with a long line that exercises wrapping before compaction. "
style 0-0 fg=bright-blue
9| "▌ "
style 0-0 fg=bright-blue
10| <blank>
11| "▌ "
style 0-0 fg=green
12| "▌ ✓ pnpm run test:coverage "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-25 bold
13| "▌ Run the coverage gate "
style 0-0 fg=green
style 2-22 fg=bright-black
14| "▌ /workspace/project "
style 0-0 fg=green
style 2-19 dim
15| "▌ packages/ui/tui 100% "
style 0-0 fg=green
16| "▌ 4016 tests passed "
style 0-0 fg=green
17| "▌ 1 test skipped "
style 0-0 fg=green
18| "▌ coverage complete "
style 0-0 fg=green
19| "▌ [exit 0] "
style 0-0 fg=green
style 2-9 dim
20| "▌ "
style 0-0 fg=green
21| "────────────────────────────────────────────────────────────────────────────────"
style 0-79 dim
22| " "
style 1-1 inverse
23| "────────────────────────────────────────────────────────────────────────────────"
style 0-79 dim
24| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 47-79 dim

View File

@@ -0,0 +1,106 @@
terminal 100x34 buffer=normal length=40 base=6 viewport=6
lifecycle started=1 stopped=0 progress=inactive
title "Unsafe terminal title \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m"
cursor hidden column=100 viewportRow=33 bufferRow=39
buffer
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-99 fg=bright-blue
1| "│ DEEPSEEK HARNESS │"
style 0-0 fg=bright-blue
style 2-9 fg=bright-blue bold
style 11-17 bold
style 99-99 fg=bright-blue
2| "│ Unsafe welcome \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m │"
style 0-0 fg=bright-blue
style 2-61 fg=bright-black
style 99-99 fg=bright-blue
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-35 dim
style 99-99 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-99 fg=bright-blue
5| <blank>
6| "▌ "
style 0-0 fg=bright-blue
7| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
8| "▌ Unsafe user \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 0-0 fg=bright-blue
9| "▌ "
style 0-0 fg=bright-blue
10| <blank>
11| " Reasoning "
style 1-9 fg=bright-black italic
12| " Unsafe reasoning \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 1-62 fg=bright-black italic
13| <blank>
14| " Assistant "
style 1-9 fg=bright-magenta bold
15| " Unsafe assistant \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
16| <blank>
17| "▌ "
style 0-0 fg=green
18| "▌ ✓ Unsafe title \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 0-0 fg=green
style 2-2 fg=green bold
style 3-61 bold
19| "▌ Unsafe description \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 0-0 fg=green
style 2-65 fg=bright-black
20| "▌ /unsafe/\\x1b╭ Unsafe header \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m ─────────╮ "
style 0-0 fg=green
style 2-13 dim
style 14-85 fg=bright-blue
21| "▌ Unsafe outpu│ Unsafe question \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m │ "
style 0-0 fg=green
style 14-14 fg=bright-blue
style 16-76 bold
style 85-85 fg=bright-blue
22| "▌ [signal SIG\\│ │ "
style 0-0 fg=green
style 2-13 fg=red
style 14-14 fg=bright-blue
style 85-85 fg=bright-blue
23| "▌ │ › ● Unsafe option \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m — Un │ "
style 0-0 fg=green
style 14-14 fg=bright-blue
style 16-16 fg=bright-blue inverse
style 17-17 inverse
style 18-18 fg=bright-blue inverse
style 19-78 inverse
style 79-83 fg=bright-black inverse
style 85-85 fg=bright-blue
24| " │ ↑↓ navigate • Enter select • C custom • Esc cancel │ "
style 14-14 fg=bright-blue
style 16-65 dim
style 85-85 fg=bright-blue
25| " Context · uns╰──────────────────────────────────────────────────────────────────────╯ "
style 1-13 dim
style 14-85 fg=bright-blue
26| " Unsafe context \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 1-60 fg=bright-black
27| <blank>
28| " Prompt blocked: Unsafe policy \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 1-75 fg=yellow
29| <blank>
30| " Unsafe turn error \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 1-63 fg=red
31| <blank>
32| " Unsafe live error \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 1-63 fg=red
33| <blank>
34| "Plan"
style 0-3 fg=bright-blue bold
35| " ● Unsafe todo \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m"
style 2-2 fg=yellow
36| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
37| " "
style 1-1 inverse
38| "────────────────────────────────────────────────────────────────────────────────────────────────────"
style 0-99 dim
39| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
style 0-24 dim
style 67-99 dim

View File

@@ -0,0 +1,499 @@
import { mkdir, readdir, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterAll, describe, expect, it } from 'vitest'
import type { Context } from 'cordis'
import { CallId, type ContentBlock } from '@deepseek-ai/dsh-llm'
import type { Session } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { type ToolDefinition, type ToolResultView } from '@deepseek-ai/dsh-tools'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
import {
appendAssistant,
appendUser,
createTuiTestHarness,
disposeTuiTestHarness,
type TuiHarness,
type TuiHarnessOptions,
} from './harness.ts'
import { HeadlessTerminal, type TerminalSnapshotOptions } from './headless-terminal.ts'
const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
const REFRESHING = process.env.DSH_SNAPSHOT === 'refresh'
const CHECKPOINTS = [
'conversation-streaming',
'code-mode-pending',
'dynamic-workflow-pending',
'cordis-tools-pending',
'advanced-cards-collapsed',
'advanced-cards-expanded',
'untrusted-controls',
'question-dialog',
'question-dialog-validation',
'surface-before-compaction',
'surface-after-compaction-narrow',
'surface-after-compaction-wide',
'errors-and-help',
'disposed-terminal',
] as const
type Checkpoint = typeof CHECKPOINTS[number]
type SnapshotHarness = TuiHarness<HeadlessTerminal, (code: number) => void>
const observedCheckpoints = new Set<Checkpoint>()
async function checkpoint(
name: Checkpoint,
terminal: HeadlessTerminal,
options: TerminalSnapshotOptions = {},
): Promise<void> {
observedCheckpoints.add(name)
expect(terminal.themeViolations(), `${name} must remain theme-agnostic`).toEqual([])
const snapshot = await terminal.snapshot(options)
const path = join(SNAPSHOTS_DIR, `${name}.expected.txt`)
if (REFRESHING) {
await mkdir(SNAPSHOTS_DIR, { recursive: true })
await writeFile(path, snapshot)
}
await expect(snapshot).toMatchFileSnapshot(path)
}
async function setupSnapshot(
options: TuiHarnessOptions = {},
size: { columns?: number; rows?: number } = {},
): Promise<SnapshotHarness> {
const terminal = new HeadlessTerminal(size.columns ?? 96, size.rows ?? 36)
const before = terminal.frames
const result = await createTuiTestHarness(terminal, () => {}, {
...options,
cwd: options.cwd === undefined ? '/workspace/project' : options.cwd,
config: Object.assign({
welcome: 'Snapshot agent ready.',
color: true,
title: 'DSH snapshot',
}, options.config),
})
await terminal.waitForFrame(before)
return result
}
async function renderAfter(harness: SnapshotHarness, action: () => void): Promise<void> {
const before = harness.terminal.frames
action()
await harness.terminal.waitForFrame(before)
}
async function disposeSnapshot(harness: SnapshotHarness): Promise<void> {
await disposeTuiTestHarness(harness)
await harness.terminal.dispose()
}
async function configureAdvancedTools(ctx: Context): Promise<void> {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry, { mode: 'code' })
ctx.provide('workflows', {} as never)
await ctx.plugin(ToolWorkflow, { toolName: 'workflow', maxResultChars: 50_000 })
await ctx.plugin(ToolCordis, { vmTimeoutMs: 5_000 })
}
interface ToolCallFixture {
id: string
name: string
arguments: unknown
}
function appendToolCalls(session: Session, calls: readonly ToolCallFixture[]): void {
appendAssistant(session, calls.map(call => ({
type: 'tool-call',
id: CallId(call.id),
name: call.name,
arguments: JSON.stringify(call.arguments),
})))
for (const call of calls) {
session.append('tool/call', {
turn: 1,
step: 0,
callId: CallId(call.id),
name: call.name,
arguments: JSON.stringify(call.arguments),
})
}
}
function appendToolResult(
session: Session,
id: string,
content: ContentBlock[],
options: { isError?: boolean; meta?: unknown } = {},
): void {
session.append('tool/result', {
turn: 1,
step: 0,
callId: CallId(id),
content,
isError: options.isError ?? false,
...options.meta === undefined ? {} : { meta: options.meta },
}, { surfaceOp: 'append' })
}
function visualTool(
name: string,
call: NonNullable<ToolDefinition['presentCall']>,
result?: NonNullable<ToolDefinition['presentResult']>,
): ToolDefinition {
return {
name,
description: `${name} snapshot fixture`,
parameters: {},
execute: () => Promise.resolve([]),
presentCall: call,
...result === undefined ? {} : { presentResult: result },
}
}
const ADVANCED_CARD_TOOLS: Record<string, ToolDefinition> = {
bash: visualTool(
'bash',
() => ({ card: 'terminal', title: 'pnpm run test:coverage', description: 'Run the coverage gate', cwd: '/workspace/project' }),
() => ({ card: 'terminal', output: 'packages/ui/tui 100%\n4016 tests passed\n1 test skipped\ncoverage complete', exitCode: 0 }),
),
edit: visualTool(
'edit',
() => ({ card: 'diff', title: 'Edit renderer', diffs: [{ path: 'src/view.ts', oldText: 'old line', newText: 'new line' }] }),
(): ToolResultView => ({
card: 'diff',
diffs: [
{ path: 'src/view.ts', oldText: 'old line\nkeep', newText: 'new line\nkeep' },
{ path: 'tests/view.spec.ts', oldText: null, newText: 'expect(screen).toMatchSnapshot()' },
],
}),
),
subagent: visualTool('subagent', args => ({
card: 'generic',
title: 'Delegate renderer audit',
rawInput: (args as { prompt: string }).prompt,
})),
task_output: visualTool('task_output', args => ({
card: 'generic',
kind: 'read',
title: `Read output from background task ${(args as { task_id: string }).task_id}`,
rawInput: (args as { task_id: string }).task_id,
})),
skill: visualTool('skill', args => ({
card: 'generic',
kind: 'read',
title: `Load skill ${(args as { name: string }).name}`,
rawInput: (args as { name: string }).name,
})),
}
const CONTROL_PROBE = '\u001b]2;snapshot-controlled\u0007\t\u007f\u009b31m'
const DISPLAYED_CONTROL_PROBE = String.raw`\x1b]2;snapshot-controlled\x07\x09\x7f\x9b31m`
describe('TUI terminal-state snapshots', () => {
it('pins an in-flight reasoning and Markdown stream', async () => {
const harness = await setupSnapshot()
await renderAfter(harness, () => {
appendUser(harness.session, 'Show the live update.')
harness.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-start', index: 0, blockType: 'reasoning' },
})
harness.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'reasoning-delta', index: 0, text: 'Inspecting width and styles.' },
})
harness.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-start', index: 1, blockType: 'text' },
})
harness.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'text-delta', index: 1, text: 'Streaming **visible state**…' },
})
})
await checkpoint('conversation-streaming', harness.terminal)
await disposeSnapshot(harness)
})
it('pins Code Mode run_code with its production presenter', async () => {
const harness = await setupSnapshot({ configureContext: configureAdvancedTools })
const call = {
id: 'code-1',
name: 'run_code',
arguments: {
code: "const first = await tools.bash({ command: 'echo CODE_ONE' })\nconst second = await tools.bash({ command: 'echo CODE_TWO' })\nconsole.log(first, second)\nreturn `${first}+${second}`",
},
}
await renderAfter(harness, () => { appendToolCalls(harness.session, [call]) })
await checkpoint('code-mode-pending', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('pins a dynamic workflow with phases, parallel agents, and structured output', async () => {
const harness = await setupSnapshot({ configureContext: configureAdvancedTools })
const call = {
id: 'workflow-1',
name: 'workflow',
arguments: {
meta: {
name: 'tui-matrix',
description: 'Audit terminal states from independent angles',
phases: [
{ title: 'Inspect', detail: 'Map renderer branches' },
{ title: 'Verify', detail: 'Challenge missing states', provider: 'deepseek', model: 'deepseek-v4-flash' },
],
},
args: { packages: ['ui/tui', 'workflow/tool-workflow'] },
script: "phase('Inspect')\nconst reports = await parallel([\n () => agent('Audit layout', { label: 'layout', phase: 'Inspect' }),\n () => agent('Audit lifecycle', { label: 'lifecycle', phase: 'Inspect' }),\n])\nphase('Verify')\nreturn { reports, verdict: 'covered' }",
},
}
await renderAfter(harness, () => { appendToolCalls(harness.session, [call]) })
await checkpoint('dynamic-workflow-pending', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('pins cordis inspect, dynamic mount, and unmount cards with production presenters', async () => {
const harness = await setupSnapshot({ configureContext: configureAdvancedTools })
const calls = [
{ id: 'cordis-1', name: 'cordis_inspect', arguments: { what: 'tools' } },
{
id: 'cordis-2',
name: 'cordis_mount',
arguments: { code: "return { name: 'snapshot-marker', apply(ctx) { ctx.provide('snapshotMarker', { ready: true }) } }" },
},
{ id: 'cordis-3', name: 'cordis_unmount', arguments: { id: 'dyn-1' } },
]
await renderAfter(harness, () => { appendToolCalls(harness.session, calls) })
await checkpoint('cordis-tools-pending', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('pins terminal, diff, subagent, task, skill, collapsed, and expanded cards', async () => {
const harness = await setupSnapshot({
tools: ADVANCED_CARD_TOOLS,
config: { maxToolOutputLines: 3 },
}, { columns: 100, rows: 40 })
const calls = [
{ id: 'advanced-1', name: 'bash', arguments: { command: 'pnpm run test:coverage' } },
{ id: 'advanced-2', name: 'edit', arguments: { file_path: 'src/view.ts' } },
{ id: 'advanced-3', name: 'subagent', arguments: { prompt: 'Review renderer ownership and report only gaps.' } },
{ id: 'advanced-4', name: 'task_output', arguments: { task_id: 'subagent-7', wait: true } },
{ id: 'advanced-5', name: 'skill', arguments: { name: 'dsh-code-review' } },
]
await renderAfter(harness, () => {
appendToolCalls(harness.session, calls)
appendToolResult(harness.session, 'advanced-1', [{ type: 'text', text: 'raw process output' }])
appendToolResult(harness.session, 'advanced-2', [{ type: 'text', text: 'edit complete' }])
appendToolResult(harness.session, 'advanced-3', [{ type: 'text', text: 'The renderer has explicit lifecycle ownership.' }])
appendToolResult(harness.session, 'advanced-4', [{ type: 'text', text: 'audit complete\n[status: completed]' }])
appendToolResult(harness.session, 'advanced-5', [{ type: 'text', text: 'Loaded review instructions.' }])
})
await checkpoint('advanced-cards-collapsed', harness.terminal, { includeScrollback: true })
await renderAfter(harness, () => { harness.terminal.send('\x0f') })
await checkpoint('advanced-cards-expanded', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('renders terminal controls as inert text across transcripts, tools, dialogs, diagnostics, and title', async () => {
const tools = {
unsafe: visualTool(
'unsafe',
() => ({
card: 'terminal',
title: `Unsafe title ${CONTROL_PROBE}`,
description: `Unsafe description ${CONTROL_PROBE}`,
cwd: `/unsafe/${CONTROL_PROBE}`,
}),
() => ({
card: 'terminal',
output: `Unsafe output ${CONTROL_PROBE}`,
signal: `SIG${CONTROL_PROBE}`,
}),
),
}
const harness = await setupSnapshot({
tools,
config: {
welcome: `Unsafe welcome ${CONTROL_PROBE}`,
title: `Unsafe terminal title ${CONTROL_PROBE}`,
},
beforeMount(session) {
appendUser(session, `Unsafe user ${CONTROL_PROBE}`)
appendAssistant(session, [
{ type: 'reasoning', text: `Unsafe reasoning ${CONTROL_PROBE}` },
{ type: 'text', text: `Unsafe assistant ${CONTROL_PROBE}` },
])
appendToolCalls(session, [{ id: 'unsafe-1', name: 'unsafe', arguments: { value: CONTROL_PROBE } }])
appendToolResult(session, 'unsafe-1', [{ type: 'text', text: `Unsafe fallback ${CONTROL_PROBE}` }])
session.append('todo/write', {
todos: [{ content: `Unsafe todo ${CONTROL_PROBE}`, status: 'in_progress' }],
})
session.append('context/message', {
content: [{ type: 'text', text: `Unsafe context ${CONTROL_PROBE}` }],
source: { kind: 'plugin', plugin: `unsafe-${CONTROL_PROBE}` },
}, { surfaceOp: 'append' })
session.append('prompt/blocked', {
content: [{ type: 'text', text: 'blocked' }],
source: { kind: 'user' },
reason: `Unsafe policy ${CONTROL_PROBE}`,
})
session.append('turn/end', {
turn: 7,
reason: { kind: 'error', step: 2, message: `Unsafe turn error ${CONTROL_PROBE}` },
})
},
}, { columns: 100, rows: 34 })
expect(harness.terminal.title).toContain(DISPLAYED_CONTROL_PROBE)
expect(harness.terminal.title).not.toContain('\u001b')
expect(harness.terminal.title).not.toContain('\u009b')
const controller = new AbortController()
const beforeQuestion = harness.terminal.frames
const answer = harness.ctx.userInteraction.ask({
questions: [{
id: 'unsafe-question',
header: `Unsafe header ${CONTROL_PROBE}`,
question: `Unsafe question ${CONTROL_PROBE}`,
options: [{ label: `Unsafe option ${CONTROL_PROBE}`, description: `Unsafe detail ${CONTROL_PROBE}` }],
}],
signal: controller.signal,
})
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
await harness.terminal.waitForFrame(beforeQuestion)
await renderAfter(harness, () => {
harness.ctx.emit('agent/error', harness.agent, 8, 3, new Error(`Unsafe live error ${CONTROL_PROBE}`))
})
await checkpoint('untrusted-controls', harness.terminal, { includeScrollback: true })
controller.abort()
await rejected
await disposeSnapshot(harness)
})
it('pins a constrained multi-select question and its validation state', async () => {
const harness = await setupSnapshot({
config: {
maxQuestionOptions: 3,
questionDialogWidth: 48,
questionDialogMaxHeight: 16,
},
}, { columns: 56, rows: 20 })
const controller = new AbortController()
const beforeQuestion = harness.terminal.frames
const answer = harness.ctx.userInteraction.ask({
questions: [{
id: 'coverage',
header: 'Coverage',
question: 'Which advanced TUI states belong in the required matrix?',
multiSelect: true,
options: [
{ label: 'Code Mode', description: 'run_code programs and captured output' },
{ label: 'Workflows', description: 'phases and parallel agents' },
{ label: 'Cordis tools', description: 'inspect, mount, and unmount' },
{ label: 'Compaction', description: 'surface replacement and reflow' },
],
}],
signal: controller.signal,
})
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
await harness.terminal.waitForFrame(beforeQuestion)
await checkpoint('question-dialog', harness.terminal)
await renderAfter(harness, () => { harness.terminal.send('\r') })
await checkpoint('question-dialog-validation', harness.terminal)
controller.abort()
await rejected
await disposeSnapshot(harness)
})
it('pins compaction surface replacement and narrow-to-wide reflow', async () => {
let replacementStart = 0
let replacementEnd = 0
let replacementSources: number[] = []
const harness = await setupSnapshot({
tools: ADVANCED_CARD_TOOLS,
beforeMount(session) {
const user = session.append('user/message', {
content: [{ type: 'text', text: 'Old prompt with a long line that exercises wrapping before compaction.' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
const assistant = session.append('assistant/message', {
turn: 1,
step: 0,
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
content: [{ type: 'tool-call', id: CallId('old-tool'), name: 'bash', arguments: '{}' }],
}, { surfaceOp: 'append' })
session.append('tool/call', { turn: 1, step: 0, callId: CallId('old-tool'), name: 'bash', arguments: '{}' })
const result = session.append('tool/result', {
turn: 1,
step: 0,
callId: CallId('old-tool'),
content: [{ type: 'text', text: 'obsolete output that must disappear' }],
isError: false,
}, { surfaceOp: 'append' })
replacementStart = user.seq
replacementEnd = result.seq
replacementSources = [user.seq, assistant.seq, result.seq]
},
}, { columns: 80, rows: 24 })
await checkpoint('surface-before-compaction', harness.terminal, { includeScrollback: true })
await renderAfter(harness, () => {
harness.session.append('context/message', {
content: [{ type: 'text', text: 'Compacted summary: the prior command completed and its details were retired from the active surface.' }],
source: { kind: 'plugin', plugin: 'compact' },
}, {
surfaceOp: { op: 'replace', start: replacementStart, end: replacementEnd },
sourceEventSeqs: replacementSources,
})
harness.terminal.resize(44, 18)
})
await checkpoint('surface-after-compaction-narrow', harness.terminal, { includeScrollback: true })
await renderAfter(harness, () => { harness.terminal.resize(104, 30) })
await checkpoint('surface-after-compaction-wide', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
})
it('pins help, unknown commands, live errors, turn failures, and terminal restoration', async () => {
const harness = await setupSnapshot({}, { columns: 92, rows: 32 })
await renderAfter(harness, () => {
harness.terminal.send('/help')
harness.terminal.send('\r')
harness.terminal.send('/unknown-advanced-command')
harness.terminal.send('\r')
harness.ctx.emit('agent/error', harness.agent, 3, 1, new Error('provider stream failed after partial output'))
harness.session.append('turn/end', {
turn: 3,
reason: { kind: 'error', step: 1, message: 'provider stream failed after partial output' },
})
harness.session.append('turn/end', {
turn: 4,
reason: { kind: 'interrupted' },
})
})
await checkpoint('errors-and-help', harness.terminal, { includeScrollback: true })
await harness.controller.dispose()
await harness.terminal.flush()
await checkpoint('disposed-terminal', harness.terminal, { includeScrollback: true })
await harness.ctx.fiber.dispose()
await harness.terminal.dispose()
})
})
afterAll(async () => {
expect([...observedCheckpoints].sort()).toEqual([...CHECKPOINTS].sort())
const files = (await readdir(SNAPSHOTS_DIR))
.filter(file => file.endsWith('.expected.txt'))
.sort()
expect(files).toEqual(CHECKPOINTS.map(name => `${name}.expected.txt`).sort())
})

View File

@@ -0,0 +1,939 @@
import { homedir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import type { Terminal } from '@earendil-works/pi-tui'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import {
createTuiChat,
mountTui,
resolveTuiConfig,
type TuiRuntime,
} from '../src/index.ts'
import {
appendAssistant,
appendUser,
createTuiTestHarness,
disposeTuiTestHarness,
type TuiHarnessOptions,
} from './harness.ts'
class FakeTerminal implements Terminal {
columns = 88
rows = 32
kittyProtocolActive = false
output = ''
title = ''
progress: boolean[] = []
started = 0
stopped = 0
drainInput = vi.fn(() => Promise.resolve())
private onInput: (data: string) => void = () => {}
private onResize: () => void = () => {}
start(onInput: (data: string) => void, onResize: () => void): void {
this.started += 1
this.onInput = onInput
this.onResize = onResize
}
stop(): void {
this.stopped += 1
}
write(data: string): void {
this.output += data
}
moveBy(lines: number): void {
this.output += `[move:${lines}]`
}
hideCursor(): void {
this.output += '[hide]'
}
showCursor(): void {
this.output += '[show]'
}
clearLine(): void {
this.output += '[clear-line]'
}
clearFromCursor(): void {
this.output += '[clear-rest]'
}
clearScreen(): void {
this.output += '[clear-screen]'
}
setTitle(title: string): void {
this.title = title
}
setProgress(active: boolean): void {
this.progress.push(active)
}
send(data: string): void {
this.onInput(data)
}
resize(columns: number, rows = this.rows): void {
this.columns = columns
this.rows = rows
this.onResize()
}
}
async function tick(): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 25))
}
async function setup(options: TuiHarnessOptions = {}) {
const terminal = new FakeTerminal()
const exit = vi.fn()
const result = await createTuiTestHarness(terminal, exit, {
...options,
cwd: options.cwd === undefined ? process.cwd() : options.cwd,
})
await tick()
return result
}
async function dispose(setupResult: Awaited<ReturnType<typeof setup>>): Promise<void> {
await disposeTuiTestHarness(setupResult)
}
describe('TUI config', () => {
it('defaults every direct-call TUI option', () => {
expect(resolveTuiConfig(undefined)).toEqual({
showReasoning: true,
maxToolOutputLines: 12,
maxQuestionOptions: 8,
questionDialogWidth: 72,
questionDialogMaxHeight: 20,
showHardwareCursor: false,
color: true,
title: 'DeepSeek Harness',
})
expect(resolveTuiConfig({
showReasoning: false,
maxToolOutputLines: 2,
maxQuestionOptions: 3,
questionDialogWidth: 60,
questionDialogMaxHeight: 14,
showHardwareCursor: true,
color: false,
title: 'DSH',
})).toEqual({
showReasoning: false,
maxToolOutputLines: 2,
maxQuestionOptions: 3,
questionDialogWidth: 60,
questionDialogMaxHeight: 14,
showHardwareCursor: true,
color: false,
title: 'DSH',
})
})
})
describe('pi-tui chat lifecycle and transcript', () => {
it('renders its header, footer, replay, streaming answer, todos, and status', async () => {
const result = await setup({
beforeMount(session) {
appendUser(session, 'restored prompt')
appendAssistant(session, [
{ type: 'reasoning', text: 'restored thought' },
{ type: 'text', text: '**restored answer**' },
], { inputTokens: 1_250, outputTokens: 42 })
session.append('todo/write', {
todos: [
{ content: 'read code', status: 'completed' },
{ content: 'write tests', status: 'in_progress' },
{ content: 'ship', status: 'pending' },
],
})
},
})
expect(result.terminal.started).toBe(1)
expect(result.terminal.title).toBe('DeepSeek Harness')
expect(result.terminal.output).toContain('DEEPSEEK')
expect(result.terminal.output).toContain('Coding agent ready.')
expect(result.terminal.output).toContain('restored prompt')
expect(result.terminal.output).toContain('restored thought')
expect(result.terminal.output).toContain('restored answer')
expect(result.terminal.output).toContain('write tests')
expect(result.terminal.output).toContain('↑1.3k ↓42')
result.agent.status = 'running'
result.ctx.emit('agent/status', result.agent, 'running')
result.session.append('user/message', { content: [{ type: 'text', text: ' ' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
result.session.append('steering/message', { turn: 2, content: [{ type: 'text', text: 'steering note' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
result.session.append('steering/message', { turn: 2, content: [{ type: 'text', text: '' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
result.session.append('context/message', { content: [{ type: 'text', text: 'user context' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
result.session.append('context/message', { content: [{ type: 'text', text: '' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
result.session.append('prompt/blocked', { content: [{ type: 'text', text: 'blocked' }], source: { kind: 'user' }, reason: 'test policy' })
appendAssistant(result.session, [])
result.session.append('turn/end', { turn: 9, reason: { kind: 'aborted' } })
result.session.append('turn/end', { turn: 10, reason: { kind: 'completed' } })
result.session.append('step/start', { turn: 11, step: 0 })
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-start', index: 0, blockType: 'reasoning' },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'reasoning-delta', index: 0, text: 'live thought' },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'reasoning-delta', index: 9, text: 'unannounced thought' },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-end', index: 0, block: { type: 'reasoning', text: 'live thought complete' } },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-start', index: 1, blockType: 'text' },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'text-delta', index: 1, text: 'live answer' },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-end', index: 1, block: { type: 'text', text: 'live answer done' } },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-start', index: 2, blockType: 'tool-call' },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'block-end', index: 2, block: { type: 'tool-call', id: 'stream-tool' as never, name: 'tool', arguments: '{}' } },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'tool-call-delta', index: 2, id: 'stream-tool' as never, argumentsDelta: '{}' },
})
result.session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'usage', usage: { inputTokens: 1, outputTokens: 2 } },
})
await tick()
expect(result.terminal.output).toContain('live thought')
result.terminal.send('\x12')
await tick()
appendAssistant(result.session, [{ type: 'text', text: 'final live answer' }], { inputTokens: 500, outputTokens: 8 })
await tick()
expect(result.terminal.output).toContain('Working')
expect(result.terminal.output).toContain('Steering')
expect(result.terminal.output).toContain('user context')
expect(result.terminal.output).toContain('Prompt blocked')
expect(result.terminal.output).toContain('Turn cancelled')
expect(result.terminal.output).toContain('final live answer')
expect(result.terminal.output).toContain('↑1.8k ↓50')
expect(result.terminal.progress).toContain(true)
result.session.append('assistant/chunk', {
turn: 3,
step: 0,
chunk: { type: 'text-delta', index: 0, text: 'cleared stream' },
})
result.terminal.send('/clear')
result.terminal.send('\r')
appendAssistant(result.session, [{ type: 'text', text: 'answer after clear' }])
await tick()
expect(result.terminal.output).toContain('answer after clear')
result.agent.status = 'idle'
result.ctx.emit('agent/status', result.agent, 'idle')
await tick()
expect(result.terminal.progress.at(-1)).toBe(false)
await dispose(result)
expect(result.terminal.stopped).toBe(1)
expect(result.terminal.drainInput).toHaveBeenCalledWith(100, 20)
})
it('renders the ANSI palette and every markdown/content style', async () => {
const result = await setup({
config: { color: true },
beforeMount(session) {
session.append('user/message', {
content: [
{ type: 'text', text: '# Heading\n\n[link](https://example.com) `code`\n\n```ts\nconst x = 1\n```\n\n> quote\n\n---\n\n- item\n\n**bold** *italic* ~~strike~~' },
{ type: 'tool-call', id: 'nested' as never, name: 'nested_tool', arguments: '{}' },
{ type: 'tool-result', toolCallId: 'nested' as never, content: [{ type: 'reasoning', text: 'nested result' }] },
{ type: 'future-block' } as never,
{} as never,
],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
appendAssistant(session, [
{ type: 'reasoning', text: 'styled reasoning' },
{ type: 'text', text: 'styled answer' },
], { inputTokens: 2_000_000, outputTokens: 1_500_000 })
session.append('todo/write', { todos: [
{ content: 'done', status: 'completed' },
{ content: 'active', status: 'in_progress' },
{ content: 'later', status: 'pending' },
] })
},
})
result.terminal.send('/')
await tick()
result.terminal.send('zz')
await tick()
result.terminal.send('\x0c')
await tick()
expect(result.terminal.output).toContain('\x1b[')
expect(result.terminal.output).toContain('Heading')
expect(result.terminal.output).toContain('nested_tool({})')
expect(result.terminal.output).toContain('nested result')
expect(result.terminal.output).toContain('[future-block]')
expect(result.terminal.output).toContain('[content]')
expect(result.terminal.output).toContain('↑2.0m ↓1.5m')
await dispose(result)
})
it('suppresses stale replay chunks and does not duplicate editor history on rebuild', async () => {
const result = await setup({
beforeMount(session) {
appendUser(session, 'first prompt')
appendUser(session, 'second prompt')
session.append('assistant/chunk', {
turn: 2,
step: 0,
chunk: { type: 'text-delta', index: 0, text: 'stale partial response' },
})
},
})
expect(result.terminal.output).not.toContain('stale partial response')
result.terminal.send('/reasoning')
result.terminal.send('\r')
result.terminal.send('\x1b[A')
result.terminal.send('\x1b[A')
result.terminal.send('\x1b[A')
result.terminal.send('\r')
expect(result.agent.sent).toEqual([[{ type: 'text', text: 'first prompt' }]])
await dispose(result)
})
it('formats large token totals and cwd variants', async () => {
const home = homedir()
const homeResult = await setup({
cwd: home,
beforeMount(session) {
appendAssistant(session, [{ type: 'text', text: 'home' }], { inputTokens: 25_000, outputTokens: 10_000 })
},
})
expect(homeResult.terminal.output).toContain('~ ↑25k ↓10k')
await dispose(homeResult)
const childResult = await setup({ cwd: join(home, 'projects', 'dsh-tui') })
expect(childResult.terminal.output).toContain(join('~', 'projects', 'dsh-tui'))
await dispose(childResult)
const unsetResult = await setup({ cwd: null })
expect(unsetResult.terminal.output).toContain('cwd unset')
await dispose(unsetResult)
const outsideResult = await setup({ cwd: '/opt' })
expect(outsideResult.terminal.output).toContain('/opt')
await dispose(outsideResult)
})
it('sends, steers, handles commands, global keys, and disposed-agent input', async () => {
const result = await setup()
result.terminal.send('do the work')
result.terminal.send('\r')
expect(result.agent.sent).toEqual([[{ type: 'text', text: 'do the work' }]])
result.terminal.send(' ')
result.terminal.send('\r')
result.agent.status = 'running'
result.terminal.send('steer it')
result.terminal.send('\r')
expect(result.agent.steered).toEqual([[{ type: 'text', text: 'steer it' }]])
result.terminal.send('\x1b')
result.terminal.send('\x04')
result.terminal.send('\x03')
result.terminal.send('\x12')
result.terminal.send('\x0f')
result.terminal.send('/cancel')
result.terminal.send('\r')
expect(result.agent.cancelled).toContainEqual({ kind: 'user' })
result.agent.status = 'idle'
for (const command of ['/help', '/reasoning', '/tools', '/redraw']) {
result.terminal.send(command)
result.terminal.send('\r')
await tick()
}
for (const command of ['/clear', '/cancel', '/wat']) {
result.terminal.send(command)
result.terminal.send('\r')
}
await tick()
result.terminal.send('draft')
result.terminal.send('\x03')
result.terminal.send('\x04')
await tick()
expect(result.terminal.output).toContain('Keyboard shortcuts')
expect(result.terminal.output).toContain('Reasoning blocks')
expect(result.terminal.output).toContain('Tool cards')
expect(result.terminal.output).toContain('already idle')
expect(result.terminal.output).toContain('Unknown command')
expect(result.exit).toHaveBeenCalledWith(0)
await result.controller.dispose()
await result.ctx.fiber.dispose()
const ctrlCExit = await setup()
ctrlCExit.terminal.send('\x03')
await tick()
expect(ctrlCExit.exit).toHaveBeenCalledWith(0)
await ctrlCExit.controller.dispose()
await ctrlCExit.ctx.fiber.dispose()
const disposedAgent = await setup()
disposedAgent.agent.status = 'disposed'
disposedAgent.terminal.send('late input')
disposedAgent.terminal.send('\r')
await tick()
expect(disposedAgent.terminal.output).toContain('is disposed')
await dispose(disposedAgent)
})
it('cancels before /exit while running and handles agent errors/disposal', async () => {
const result = await setup({ status: 'running' })
result.terminal.send('/exit')
result.terminal.send('\r')
await tick()
expect(result.agent.cancelled).toContainEqual({ kind: 'user' })
expect(result.exit).toHaveBeenCalledWith(0)
const events = await setup()
const unrelatedSession = events.ctx.sessions.create(SessionId('unrelated-session'))
const unrelatedAgent = { ...events.agent, id: unrelatedSession.id, session: unrelatedSession }
unrelatedSession.append('todo/write', { todos: [{ content: 'hidden', status: 'pending' }] })
events.ctx.emit('agent/status', unrelatedAgent, 'running')
events.ctx.emit('agent/error', unrelatedAgent, 1, 1, new Error('hidden error'))
events.ctx.emit('agent/disposed', unrelatedAgent)
events.ctx.emit('agent/error', events.agent, 3, 2, new Error('live failure'))
events.session.append('turn/end', { turn: 3, reason: { kind: 'error', step: 2, message: 'live failure' } })
events.session.append('turn/end', { turn: 4, reason: { kind: 'error', step: 1, message: 'durable failure' } })
events.session.append('turn/end', { turn: 5, reason: { kind: 'aborted' } })
events.session.append('turn/end', { turn: 6, reason: { kind: 'max-tokens' } })
events.session.append('turn/end', { turn: 7, reason: { kind: 'rejected', reason: 'policy' } })
events.session.append('turn/end', { turn: 8, reason: { kind: 'interrupted' } })
events.ctx.emit('agent/disposed', events.agent)
await tick()
expect(events.terminal.output).toContain('live failure')
expect(events.terminal.output).toContain('durable failure')
expect(events.terminal.output).toContain('Turn cancelled')
expect(events.terminal.output).toContain('output-token limit')
expect(events.terminal.output).toContain('Turn rejected')
expect(events.terminal.output).toContain('previous process ended')
expect(events.terminal.output).toContain('was disposed')
await dispose(events)
})
})
describe('tool cards and surface replay', () => {
const tools: Record<string, ToolDefinition> = {
bash: {
name: 'bash', description: '', parameters: {}, execute: async () => [],
presentCall: () => ({ card: 'terminal', title: 'printf hello', description: 'Run command', cwd: '/tmp' }),
presentResult: () => ({ card: 'terminal', output: 'hello\nworld\nthird', exitCode: 0 }),
},
signal: {
name: 'signal', description: '', parameters: {}, execute: async () => [],
presentCall: () => ({ card: 'terminal', title: 'sleep 10' }),
presentResult: () => ({ card: 'terminal', signal: 'SIGTERM' }),
},
edit: {
name: 'edit', description: '', parameters: {}, execute: async () => [],
presentCall: () => ({
card: 'diff',
title: 'Edit files',
diffs: [
{ path: 'a.txt', oldText: 'old', newText: 'new' },
{ path: 'b.txt', oldText: 'before', newText: 'after' },
],
}),
presentResult: () => ({ card: 'diff', diffs: [{ path: 'a.txt', oldText: null, newText: 'created' }] }),
},
generic: {
name: 'generic', description: '', parameters: {}, execute: async () => [],
presentCall: () => ({ card: 'generic', title: 'Inspect value', rawInput: { alpha: 1 } }),
presentResult: () => ({ card: 'generic', title: 'Inspected', content: [{ type: 'text', text: 'result text' }] }),
},
throwing: {
name: 'throwing', description: '', parameters: {}, execute: async () => [],
presentCall: () => { throw new Error('call presenter boom') },
presentResult: () => { throw new Error('result presenter boom') },
},
rawTerminal: {
name: 'rawTerminal', description: '', parameters: {}, execute: async () => [],
presentCall: () => ({ card: 'terminal', title: 'raw command' }),
},
undefinedViews: {
name: 'undefinedViews', description: '', parameters: {}, execute: async () => [],
presentCall: () => undefined,
presentResult: () => undefined,
},
empty: {
name: 'empty', description: '', parameters: {}, execute: async () => [],
presentCall: () => ({ card: 'generic', title: 'Empty card' }),
},
terminalResult: {
name: 'terminalResult', description: '', parameters: {}, execute: async () => [],
presentCall: () => ({ card: 'generic', title: 'Becomes terminal' }),
presentResult: () => ({ card: 'terminal', output: 'converted terminal' }),
},
symbolic: {
name: 'symbolic', description: '', parameters: {}, execute: async () => [],
presentCall: () => ({ card: 'generic', title: 'Symbol input', rawInput: Symbol('input') }),
},
}
it('uses terminal, diff, generic, fallback, and collapsed tool presentations', async () => {
const result = await setup({ tools, config: { maxToolOutputLines: 1 } })
const calls = [
['c1', 'bash', '{"command":"printf hello"}'],
['c2', 'signal', '{}'],
['c3', 'edit', '{}'],
['c4', 'generic', '{}'],
['c5', 'throwing', '{}'],
['c6', 'unknown', 'not-json'],
['c7', 'rawTerminal', '{"value":"raw"}'],
['c8', 'undefinedViews', '{"value":8}'],
['c10', 'empty', '{}'],
['c11', 'terminalResult', '{}'],
['c12', 'symbolic', '{}'],
] as const
appendAssistant(result.session, [
{ type: 'text', text: 'Calling tools' },
...calls.map(([id, name, args]) => ({
type: 'tool-call' as const, id: id as never, name, arguments: args,
})),
])
for (const [id, name, args] of calls) {
result.session.append('tool/call', { turn: 1, step: 0, callId: id as never, name, arguments: args })
}
await tick()
expect(result.terminal.output).toContain('$ raw command')
result.terminal.send('/reasoning')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('call presenter boom')
expect(result.terminal.output).toContain('Symbol(input)')
result.session.append('tool/result', {
turn: 1, step: 0, callId: 'c1' as never, content: [{ type: 'text', text: 'raw bash' }], isError: false,
}, { surfaceOp: 'append' })
result.session.append('tool/result', {
turn: 1, step: 0, callId: 'c2' as never, content: [{ type: 'text', text: 'stopped' }], isError: true,
}, { surfaceOp: 'append' })
result.session.append('tool/result', {
turn: 1, step: 0, callId: 'c3' as never, content: [{ type: 'text', text: 'done' }], isError: false,
}, { surfaceOp: 'append' })
result.session.append('tool/result', {
turn: 1, step: 0, callId: 'c4' as never, content: [{ type: 'text', text: 'raw generic' }], isError: false,
}, { surfaceOp: 'append' })
result.session.append('tool/result', {
turn: 1, step: 0, callId: 'c5' as never, content: [{ type: 'text', text: 'raw throwing' }], isError: false,
meta: { value: 1 },
}, { surfaceOp: 'append' })
result.session.append('tool/result', {
turn: 1, step: 0, callId: 'c7' as never,
content: [
{ type: 'tool-call', id: 'inner' as never, name: 'inner', arguments: '{}' },
{ type: 'tool-result', toolCallId: 'inner' as never, content: [{ type: 'text', text: 'nested output' }] },
{ type: 'future-result' } as never,
],
isError: false,
}, { surfaceOp: 'append' })
result.session.append('tool/result', {
turn: 1, step: 0, callId: 'c8' as never, content: [{ type: 'text', text: '\nundefined presenter output\n\nkept tail\n' }], isError: false,
}, { surfaceOp: 'append' })
result.session.append('tool/result', {
turn: 1, step: 0, callId: 'c11' as never, content: [{ type: 'text', text: '\nconverted terminal\n\nfinished\n' }], isError: false,
}, { surfaceOp: 'append' })
result.session.append('tool/result', {
turn: 1, step: 0, callId: 'orphan' as never, content: [{ type: 'text', text: 'orphan result' }], isError: false,
}, { surfaceOp: 'append' })
await tick()
const output = result.terminal.output
expect(output).toContain('Run command')
expect(output).toContain('printf hello')
expect(output).toContain('more lines')
expect(output).toContain('SIGTERM')
expect(output).toContain('Edit files')
expect(output).toContain('Inspected')
expect(output).toContain('result text')
expect(output).toContain('Presenter failed')
expect(output).toContain('not-json')
expect(output).toContain('nested output')
expect(output).toContain('[future-result]')
expect(output).toContain('undefined presenter output')
expect(output).toContain('Empty card')
expect(output).toContain('converted terminal')
expect(output).toContain('orphan result')
result.terminal.send('/redraw')
result.terminal.send('\r')
await tick()
result.terminal.send('\x0f')
await tick()
expect(result.terminal.output).toContain('world')
expect(result.terminal.output).toContain('+ created')
await dispose(result)
})
it('rebuilds after a surface replacement and hides shadowed tool calls', async () => {
const result = await setup({ tools })
appendUser(result.session, 'old prompt')
const assistant = result.session.append('assistant/message', {
turn: 1,
step: 0,
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
content: [{ type: 'tool-call', id: 'old-call' as never, name: 'bash', arguments: '{}' }],
}, { surfaceOp: 'append' })
result.session.append('tool/call', {
turn: 1, step: 0, callId: 'old-call' as never, name: 'bash', arguments: '{}',
})
const toolResult = result.session.append('tool/result', {
turn: 1, step: 0, callId: 'old-call' as never, content: [{ type: 'text', text: 'old output' }], isError: false,
}, { surfaceOp: 'append' })
const start = result.session.surface.nodes[0] as number
result.session.append('context/message', {
content: [{ type: 'text', text: 'summary replacement' }],
source: { kind: 'plugin', plugin: 'compact' },
}, {
surfaceOp: { op: 'replace', start, end: toolResult.seq },
sourceEventSeqs: [start, assistant.seq, toolResult.seq],
})
await tick()
result.terminal.resize(89)
await tick()
const lastFullRender = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
expect(lastFullRender).toContain('summary replacement')
expect(lastFullRender).not.toContain('old output')
await dispose(result)
})
})
describe('TUI user-interaction dialogs', () => {
it('answers single-select, multi-select, custom, and optionless questions', async () => {
const result = await setup({ config: { maxQuestionOptions: 1 } })
const single = result.ctx.userInteraction.ask({
questions: [{
id: 'mode', header: 'Mode', question: 'Choose a mode',
options: [{ label: 'Safe', description: 'Use checks' }, { label: 'Fast' }],
}],
})
await tick()
expect(result.terminal.output).toContain('Choose a mode')
expect(result.terminal.output).toContain('1/2')
result.terminal.send('\x1b[B')
result.terminal.send('\r')
await expect(single).resolves.toEqual({ answers: [{ id: 'mode', selected: ['Fast'] }] })
const multi = result.ctx.userInteraction.ask({
questions: [{ id: 'targets', question: 'Pick targets', multiSelect: true, options: [{ label: 'Code' }, { label: 'Docs' }] }],
})
await tick()
result.terminal.send(' ')
result.terminal.send('\x1b[B')
result.terminal.send(' ')
result.terminal.send('\r')
await expect(multi).resolves.toEqual({ answers: [{ id: 'targets', selected: ['Code', 'Docs'] }] })
const custom = result.ctx.userInteraction.ask({
questions: [{ id: 'other', question: 'Choose or type', options: [{ label: 'Default' }] }],
})
await tick()
result.terminal.send('c')
result.terminal.send('my choice')
result.terminal.send('\r')
await expect(custom).resolves.toEqual({ answers: [{ id: 'other', selected: [], custom: 'my choice' }] })
const free = result.ctx.userInteraction.ask({ questions: [{ id: 'note', question: 'Add a note' }] })
await tick()
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Enter an answer before submitting')
result.terminal.send('ship it')
result.terminal.send('\r')
await expect(free).resolves.toEqual({ answers: [{ id: 'note', selected: [], custom: 'ship it' }] })
await dispose(result)
})
it('handles option wrapping, deselection errors, and returning from custom input', async () => {
const result = await setup({ config: { color: true } })
const single = result.ctx.userInteraction.ask({
questions: [{ id: 'single', question: 'Single options', options: [{ label: 'One' }, { label: 'Two' }] }],
})
const singleRejected = expect(single).rejects.toMatchObject({ code: 'ASK_ABORTED' })
await tick()
expect(result.terminal.output).toContain('Two')
result.terminal.send('\x03')
await singleRejected
const answer = result.ctx.userInteraction.ask({
questions: [{
id: 'options',
question: 'Exercise options',
multiSelect: true,
options: [{ label: 'One', description: 'first' }, { label: 'Two' }],
}],
})
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
await tick()
result.terminal.send('\x1b[A')
result.terminal.send('\x1b[B')
result.terminal.send('\x1b[B')
result.terminal.send('\x1b[A')
result.terminal.send(' ')
await tick()
result.terminal.send('x')
result.terminal.send(' ')
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Select at least one option')
result.terminal.send('c')
await tick()
result.terminal.send('\x1b')
await tick()
expect(result.terminal.output).toContain('Space toggle')
result.terminal.send('\x03')
await rejected
await dispose(result)
})
it('asks batches in order and rejects cancelled or aborted work', async () => {
const result = await setup()
const preAborted = new AbortController()
preAborted.abort()
await expect(result.ctx.userInteraction.ask({
questions: [{ id: 'pre-aborted', question: 'Already cancelled?' }],
signal: preAborted.signal,
})).rejects.toMatchObject({ code: 'ASK_ABORTED' })
const batch = result.ctx.userInteraction.ask({
questions: [
{ id: 'first', question: 'First?', options: [{ label: 'Yes' }] },
{ id: 'second', question: 'Second?' },
],
})
await tick()
result.terminal.send('\r')
await tick()
expect(result.terminal.output).toContain('Second?')
result.terminal.send('done')
result.terminal.send('\r')
await expect(batch).resolves.toEqual({ answers: [
{ id: 'first', selected: ['Yes'] },
{ id: 'second', selected: [], custom: 'done' },
] })
const cancelled = result.ctx.userInteraction.ask({ questions: [{ id: 'cancel', question: 'Cancel?' }] })
const cancelledExpectation = expect(cancelled).rejects.toMatchObject({ code: 'ASK_ABORTED' })
await tick()
result.terminal.send('\x1b')
await cancelledExpectation
const controller = new AbortController()
const active = result.ctx.userInteraction.ask({ questions: [{ id: 'active', question: 'Active?' }], signal: controller.signal })
const queuedController = new AbortController()
const queued = result.ctx.userInteraction.ask({ questions: [{ id: 'queued', question: 'Queued?' }], signal: queuedController.signal })
const activeExpectation = expect(active).rejects.toMatchObject({ code: 'ASK_ABORTED' })
const queuedExpectation = expect(queued).rejects.toMatchObject({ code: 'ASK_ABORTED' })
await tick()
queuedController.abort()
controller.abort()
await activeExpectation
await queuedExpectation
await dispose(result)
})
it('rejects active and queued dialogs on disposal', async () => {
const result = await setup()
const active = result.ctx.userInteraction.ask({ questions: [{ id: 'active', question: 'Active?' }] })
const queued = result.ctx.userInteraction.ask({ questions: [{ id: 'queued', question: 'Queued?' }] })
const activeExpectation = expect(active).rejects.toMatchObject({ code: 'ASK_ABORTED' })
const queuedExpectation = expect(queued).rejects.toMatchObject({ code: 'ASK_ABORTED' })
await tick()
await result.controller.dispose()
await activeExpectation
await queuedExpectation
await expect(result.ctx.userInteraction.ask({ questions: [{ id: 'late', question: 'Late?' }] }))
.rejects.toMatchObject({ code: 'NO_PROVIDER' })
await result.ctx.fiber.dispose()
})
})
describe('terminal mounting', () => {
it('starts immediately when the configured agent already exists', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
const session = ctx.sessions.create(SessionId('main'))
ctx.agents.register({
id: session.id, options: {}, session, status: 'idle', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
const terminal = new FakeTerminal()
mountTui(ctx, { color: false }, { terminal, exit: vi.fn() })
await tick()
expect(terminal.started).toBe(1)
await ctx.fiber.dispose()
})
it('waits for its configured agent before starting the TUI', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
const terminal = new FakeTerminal()
mountTui(ctx, { sessionId: 'late-session', color: false }, { terminal, exit: vi.fn() })
expect(terminal.started).toBe(0)
const otherSession = ctx.sessions.create(SessionId('other-session'))
ctx.agents.register({
id: otherSession.id, options: {}, session: otherSession, status: 'idle', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
expect(terminal.started).toBe(0)
const session = ctx.sessions.create(SessionId('late-session'))
const agent = {
id: session.id, options: {}, session, status: 'idle', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
} as Agent
ctx.agents.register(agent)
await tick()
expect(terminal.started).toBe(1)
await ctx.fiber.dispose()
})
it('prints a matching live startup failure and exits instead of waiting forever', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
const terminal = new FakeTerminal()
const exit = vi.fn()
mountTui(ctx, { sessionId: 'main-session', color: false }, { terminal, exit })
ctx.emit('agent-loop/config-start-failed', SessionId('other-session'), new Error('other failed'))
expect(terminal.output).toBe('')
expect(exit).not.toHaveBeenCalled()
ctx.emit('agent-loop/config-start-failed', SessionId('main-session'), new Error('resume \u001b]2;failure-controlled\u0007'))
expect(terminal.output).toBe('ui-tui: session "main-session" failed to start: resume \\x1b]2;failure-controlled\\x07\n')
expect(exit).toHaveBeenCalledWith(1)
const session = ctx.sessions.create(SessionId('main-session'))
ctx.agents.register({
id: session.id, options: {}, session, status: 'idle', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
await tick()
expect(terminal.started).toBe(0)
await ctx.fiber.dispose()
})
it('renders an uncoercible startup failure without escaping the display boundary', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
const terminal = new FakeTerminal()
const exit = vi.fn()
mountTui(ctx, { sessionId: 'main-session', color: false }, { terminal, exit })
ctx.emit('agent-loop/config-start-failed', SessionId('main-session'), {
toString(): string { throw new Error('coercion failed') },
})
expect(terminal.started).toBe(0)
expect(terminal.output).toBe('ui-tui: session "main-session" failed to start: <unrenderable value>\n')
expect(exit).toHaveBeenCalledWith(1)
await ctx.fiber.dispose()
})
it('rolls back providers, listeners, and terminal state when startup fails', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
const session = ctx.sessions.create(SessionId('failed-start-session'))
ctx.agents.register({
id: session.id, options: {}, session, status: 'running', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
const terminal = new FakeTerminal()
terminal.start = () => { throw new Error('terminal startup failed') }
expect(() => createTuiChat(ctx, { sessionId: 'failed-start-session', color: false }, { terminal, exit: vi.fn() }))
.toThrow('terminal startup failed')
expect(terminal.stopped).toBe(1)
expect(terminal.progress).toEqual([false, true, false])
await expect(ctx.userInteraction.ask({ questions: [{ id: 'late', question: 'Late?' }] }))
.rejects.toMatchObject({ code: 'NO_PROVIDER' })
session.append('assistant/chunk', {
turn: 1,
step: 0,
chunk: { type: 'text-delta', index: 0, text: 'must not render' },
})
await tick()
expect(terminal.output).not.toContain('must not render')
await ctx.fiber.dispose()
})
it('throws when createTuiChat is called without the configured agent', async () => {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
const runtime: TuiRuntime = { terminal: new FakeTerminal(), exit: vi.fn() }
expect(() => createTuiChat(ctx, { sessionId: 'missing' }, runtime)).toThrow('is not running')
await ctx.fiber.dispose()
})
})