Merge current master into invariant service seam
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { Context } from 'cordis'
|
||||
import type { Terminal } from '@earendil-works/pi-tui'
|
||||
import AgentRegistry, { type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { type Agent, type AgentOptions, type AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import type { ContentBlock, LlmModelInfo, LlmProviderInfo } from '@deepseek-ai/dsh-llm'
|
||||
import CommandService from '@deepseek-ai/dsh-commands'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import { createTuiChat, type Config } from '../src/index.ts'
|
||||
@@ -22,6 +23,15 @@ export interface TuiHarnessOptions {
|
||||
configureContext?: (ctx: Context) => Promise<void>
|
||||
beforeMount?: (session: Session) => void
|
||||
cwd?: string | null
|
||||
agentOptions?: AgentOptions
|
||||
contextWindow?: number
|
||||
contextTokens?: number
|
||||
now?: () => number
|
||||
catalog?: {
|
||||
providers: LlmProviderInfo[]
|
||||
models: LlmModelInfo[]
|
||||
listModels?: (provider: string) => Promise<LlmModelInfo[]>
|
||||
}
|
||||
}
|
||||
|
||||
export interface TuiHarness<TerminalType extends Terminal, Exit extends (code: number) => void> {
|
||||
@@ -50,6 +60,28 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(CommandService)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
const catalog = options.catalog ?? {
|
||||
providers: [{ id: 'deepseek', name: 'DeepSeek' }],
|
||||
models: [
|
||||
{ provider: 'deepseek', id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash' },
|
||||
{ provider: 'deepseek', id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro' },
|
||||
],
|
||||
}
|
||||
ctx.provide('llm', {
|
||||
listProviders() {
|
||||
return catalog.providers.map(provider => ({ ...provider }))
|
||||
},
|
||||
listModels(provider: string) {
|
||||
return catalog.listModels?.(provider)
|
||||
?? Promise.resolve(catalog.models.filter(model => model.provider === provider).map(model => ({ ...model })))
|
||||
},
|
||||
} as never)
|
||||
ctx.provide('tokenMeter', {
|
||||
contextWindow: options.contextWindow ?? 128_000,
|
||||
measure() {
|
||||
return { totalTokens: options.contextTokens ?? 0 }
|
||||
},
|
||||
} as never)
|
||||
if (options.configureContext === undefined) {
|
||||
const tools = options.tools ?? {}
|
||||
ctx.provide('tools', {
|
||||
@@ -60,6 +92,7 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
} else {
|
||||
await options.configureContext(ctx)
|
||||
}
|
||||
if (ctx.get('systemPrompt') === undefined) await ctx.plugin(SystemPrompt)
|
||||
const sessionId = SessionId('main-session')
|
||||
const session = ctx.sessions.create(
|
||||
sessionId,
|
||||
@@ -76,7 +109,7 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
const cancelled: string[] = []
|
||||
const agent: FakeAgent = {
|
||||
id: sessionId,
|
||||
options: { model: 'deepseek-v4-flash' },
|
||||
options: options.agentOptions ?? { provider: 'deepseek', model: 'deepseek-v4-flash' },
|
||||
session,
|
||||
status: options.status ?? 'idle',
|
||||
ctx,
|
||||
@@ -102,7 +135,7 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
welcome: 'Coding agent ready.',
|
||||
sessionId,
|
||||
color: false,
|
||||
}, options.config), { terminal, exit })
|
||||
}, options.config), { terminal, exit, now: options.now ?? (() => 0) })
|
||||
return { ctx, session, agent, terminal, exit, controller }
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,15 @@ describe('dsh-tui plugin export shape', () => {
|
||||
const unwrapped = loader.unwrapExports(tui) as Record<string, unknown>
|
||||
expect(unwrapped).toBe(tui)
|
||||
expect(unwrapped.name).toBe('ui-tui')
|
||||
expect(unwrapped.inject).toEqual(['agents', 'commands', 'userInteraction', 'tools'])
|
||||
expect(unwrapped.inject).toEqual([
|
||||
'agents',
|
||||
'commands',
|
||||
'userInteraction',
|
||||
'tools',
|
||||
'llm',
|
||||
'systemPrompt',
|
||||
'tokenMeter',
|
||||
])
|
||||
expect(unwrapped.Config).toBeDefined()
|
||||
expect(typeof unwrapped.apply).toBe('function')
|
||||
})
|
||||
|
||||
@@ -33,11 +33,12 @@ buffer
|
||||
9| "▌ /workspace/project "
|
||||
style 0-0 fg=green
|
||||
style 2-19 dim
|
||||
10| "▌ packages/ui/tui 100% "
|
||||
10| "▌ … +4 lines (Ctrl+O to expand) "
|
||||
style 0-0 fg=green
|
||||
11| "▌ … 4 more lines (Ctrl+O to expand) "
|
||||
style 2-30 dim
|
||||
11| "▌ [exit 0] "
|
||||
style 0-0 fg=green
|
||||
style 2-34 dim
|
||||
style 2-9 dim
|
||||
12| "▌ "
|
||||
style 0-0 fg=green
|
||||
13| <blank>
|
||||
@@ -53,12 +54,12 @@ buffer
|
||||
17| "▌ - old line "
|
||||
style 0-0 fg=green
|
||||
style 2-11 fg=red
|
||||
18| "▌ - keep "
|
||||
18| "▌ … +5 lines (Ctrl+O to expand) "
|
||||
style 0-0 fg=green
|
||||
style 2-7 fg=red
|
||||
19| "▌ … 5 more lines (Ctrl+O to expand) "
|
||||
style 2-30 dim
|
||||
19| "▌ + expect(screen).toMatchSnapshot() "
|
||||
style 0-0 fg=green
|
||||
style 2-34 dim
|
||||
style 2-35 fg=green
|
||||
20| "▌ "
|
||||
style 0-0 fg=green
|
||||
21| <blank>
|
||||
@@ -102,6 +103,6 @@ buffer
|
||||
style 1-1 inverse
|
||||
39| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
40| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
40| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 67-99 dim
|
||||
style 42-99 dim
|
||||
|
||||
@@ -122,6 +122,6 @@ buffer
|
||||
style 1-1 inverse
|
||||
48| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
49| "/workspace/project ↑0 ↓0 idle reasoning:on tools:expanded"
|
||||
49| "/workspace/project ↑0 ↓0 0% context tools:expanded deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 66-99 dim
|
||||
style 41-99 dim
|
||||
|
||||
@@ -46,7 +46,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
16| "────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-95 dim
|
||||
17| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
17| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 63-95 dim
|
||||
style 38-95 dim
|
||||
18-35| <blank>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
terminal 96x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
lifecycle started=1 stopped=0 progress=active
|
||||
title "DSH snapshot"
|
||||
cursor hidden column=1 viewportRow=17 bufferRow=17
|
||||
viewport
|
||||
@@ -41,12 +41,12 @@ viewport
|
||||
15| " Streaming visible state… "
|
||||
style 11-23 bold
|
||||
16| "────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-95 dim
|
||||
style 0-95 fg=bright-blue
|
||||
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
|
||||
style 0-95 fg=bright-blue
|
||||
19| "◒ Working · 0s esc interrupt"
|
||||
style 0-13 fg=bright-blue
|
||||
style 83-95 dim
|
||||
20-35| <blank>
|
||||
|
||||
@@ -53,7 +53,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
19| "────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-95 dim
|
||||
20| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
20| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 63-95 dim
|
||||
style 38-95 dim
|
||||
21-35| <blank>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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=29 bufferRow=29
|
||||
cursor visible column=0 viewportRow=30 bufferRow=30
|
||||
buffer
|
||||
0| "╭──────────────────────────────────────────────────────────────────────────────────────────╮"
|
||||
style 0-91 fg=bright-blue
|
||||
@@ -25,7 +25,7 @@ buffer
|
||||
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 "
|
||||
8| " Esc cancel active turn • Ctrl+O toggle 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
|
||||
@@ -38,28 +38,30 @@ buffer
|
||||
style 1-47 fg=bright-black
|
||||
14| " /help — Show keyboard shortcuts and commands "
|
||||
style 1-44 fg=bright-black
|
||||
15| " /reasoning — Toggle reasoning blocks "
|
||||
15| " /model [[provider/]model] — Show or switch this session's model "
|
||||
style 1-63 fg=bright-black
|
||||
16| " /reasoning — Toggle reasoning blocks "
|
||||
style 1-36 fg=bright-black
|
||||
16| " /redraw — Invalidate components and redraw the terminal "
|
||||
17| " /redraw — Invalidate components and redraw the terminal "
|
||||
style 1-55 fg=bright-black
|
||||
17| " /tools — Expand or collapse all tool cards "
|
||||
18| " /tools — Expand or collapse all tool cards "
|
||||
style 1-42 fg=bright-black
|
||||
18| <blank>
|
||||
19| " provider stream failed after partial output "
|
||||
19| <blank>
|
||||
20| " provider stream failed after partial output "
|
||||
style 1-43 fg=red
|
||||
20| <blank>
|
||||
21| " The previous process ended during this turn. "
|
||||
21| <blank>
|
||||
22| " The previous process ended during this turn. "
|
||||
style 1-44 fg=yellow
|
||||
22| <blank>
|
||||
23| " Unknown command: /unknown-advanced-command "
|
||||
23| <blank>
|
||||
24| " Unknown command: /unknown-advanced-command "
|
||||
style 1-42 fg=yellow
|
||||
24| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
25| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
25| " "
|
||||
26| " "
|
||||
style 1-1 inverse
|
||||
26| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
27| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
27| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
28| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 59-91 dim
|
||||
28-31| <blank>
|
||||
style 34-91 dim
|
||||
29-31| <blank>
|
||||
|
||||
@@ -33,8 +33,9 @@ buffer
|
||||
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' }), "
|
||||
11| "▌ … +1 lines (Ctrl+O to expand) "
|
||||
style 0-0 fg=yellow
|
||||
style 2-30 dim
|
||||
12| "▌ ]) "
|
||||
style 0-0 fg=yellow
|
||||
13| "▌ phase('Verify') "
|
||||
@@ -49,7 +50,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
18| "────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-95 dim
|
||||
19| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
19| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 63-95 dim
|
||||
style 38-95 dim
|
||||
20-35| <blank>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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=25 bufferRow=25
|
||||
cursor hidden column=1 viewportRow=26 bufferRow=26
|
||||
buffer
|
||||
0| "╭──────────────────────────────────────────────────────────────────────────────────────────╮"
|
||||
style 0-91 fg=bright-blue
|
||||
@@ -25,7 +25,7 @@ buffer
|
||||
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 "
|
||||
8| " Esc cancel active turn • Ctrl+O toggle 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
|
||||
@@ -38,28 +38,30 @@ buffer
|
||||
style 1-47 fg=bright-black
|
||||
14| " /help — Show keyboard shortcuts and commands "
|
||||
style 1-44 fg=bright-black
|
||||
15| " /reasoning — Toggle reasoning blocks "
|
||||
15| " /model [[provider/]model] — Show or switch this session's model "
|
||||
style 1-63 fg=bright-black
|
||||
16| " /reasoning — Toggle reasoning blocks "
|
||||
style 1-36 fg=bright-black
|
||||
16| " /redraw — Invalidate components and redraw the terminal "
|
||||
17| " /redraw — Invalidate components and redraw the terminal "
|
||||
style 1-55 fg=bright-black
|
||||
17| " /tools — Expand or collapse all tool cards "
|
||||
18| " /tools — Expand or collapse all tool cards "
|
||||
style 1-42 fg=bright-black
|
||||
18| <blank>
|
||||
19| " provider stream failed after partial output "
|
||||
19| <blank>
|
||||
20| " provider stream failed after partial output "
|
||||
style 1-43 fg=red
|
||||
20| <blank>
|
||||
21| " The previous process ended during this turn. "
|
||||
21| <blank>
|
||||
22| " The previous process ended during this turn. "
|
||||
style 1-44 fg=yellow
|
||||
22| <blank>
|
||||
23| " Unknown command: /unknown-advanced-command "
|
||||
23| <blank>
|
||||
24| " Unknown command: /unknown-advanced-command "
|
||||
style 1-42 fg=yellow
|
||||
24| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
25| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
25| " "
|
||||
26| " "
|
||||
style 1-1 inverse
|
||||
26| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
27| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
27| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
28| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 59-91 dim
|
||||
28-31| <blank>
|
||||
style 34-91 dim
|
||||
29-31| <blank>
|
||||
|
||||
52
packages/ui/tui/tests/snapshots/model-selector.expected.txt
Normal file
52
packages/ui/tui/tests/snapshots/model-selector.expected.txt
Normal 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=0 viewportRow=31 bufferRow=31
|
||||
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| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
6| " "
|
||||
style 1-1 inverse
|
||||
7| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
8| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 34-91 dim
|
||||
9-12| <blank>
|
||||
13| " ╭ Select model ────────────────────────────────────────────────────────╮ "
|
||||
style 10-81 fg=bright-blue
|
||||
14| " │ → deepseek/deepseek-v4-flash DeepSeek V4 Flash — current │ "
|
||||
style 10-10 fg=bright-blue
|
||||
style 12-72 fg=bright-blue inverse
|
||||
style 81-81 fg=bright-blue
|
||||
15| " │ deepseek/deepseek-v4-pro DeepSeek V4 Pro │ "
|
||||
style 10-10 fg=bright-blue
|
||||
style 38-60 fg=bright-black
|
||||
style 81-81 fg=bright-blue
|
||||
16| " │ │ "
|
||||
style 10-10 fg=bright-blue
|
||||
style 81-81 fg=bright-blue
|
||||
17| " │ ↑/↓ navigate • Enter select • Esc cancel │ "
|
||||
style 10-10 fg=bright-blue
|
||||
style 12-51 dim
|
||||
style 81-81 fg=bright-blue
|
||||
18| " ╰──────────────────────────────────────────────────────────────────────╯ "
|
||||
style 10-81 fg=bright-blue
|
||||
19-31| <blank>
|
||||
35
packages/ui/tui/tests/snapshots/model-switching.expected.txt
Normal file
35
packages/ui/tui/tests/snapshots/model-switching.expected.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
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=8 bufferRow=8
|
||||
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-pro • main-session │"
|
||||
style 0-0 fg=bright-blue
|
||||
style 2-33 dim
|
||||
style 91-91 fg=bright-blue
|
||||
4| "╰──────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
style 0-91 fg=bright-blue
|
||||
5| <blank>
|
||||
6| " Model selected: deepseek/deepseek-v4-pro. New steps will use it. "
|
||||
style 1-64 fg=bright-black
|
||||
7| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
8| " "
|
||||
style 1-1 inverse
|
||||
9| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 dim
|
||||
10| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-pro(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 36-91 dim
|
||||
11-31| <blank>
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
cursor hidden column=56 viewportRow=17 bufferRow=17
|
||||
viewport
|
||||
0| "╭──────────────────────────────────────────────────────╮"
|
||||
style 0-55 fg=bright-blue
|
||||
@@ -18,52 +18,30 @@ viewport
|
||||
style 0-0 fg=bright-blue
|
||||
style 2-35 dim
|
||||
style 55-55 fg=bright-blue
|
||||
4| "╰───╭ Coverage ────────────────────────────────────╮───╯"
|
||||
4| "╰──────────────────────────────────────────────────────╯"
|
||||
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>
|
||||
5| " "
|
||||
6| " Question 1/3 (3 unanswered) · Coverage "
|
||||
style 2-39 fg=bright-black
|
||||
7| " Which advanced TUI states belong in the required "
|
||||
8| " matrix? "
|
||||
9| " "
|
||||
10| " › 1. [ ] Code Mode run_code programs and capture "
|
||||
style 2-19 fg=bright-blue bold
|
||||
style 25-53 fg=bright-black
|
||||
11| " 2. [ ] Workflows phases and parallel agents "
|
||||
style 25-50 fg=bright-black
|
||||
12| " 3. [ ] Cordis tools inspect, mount, and unmount "
|
||||
style 25-51 fg=bright-black
|
||||
13| " 1/4 "
|
||||
style 2-4 dim
|
||||
14| " Tab custom answer • ↑/↓ navigate • Space toggle • "
|
||||
style 2-55 dim
|
||||
15| " Enter submit • Esc interrupt "
|
||||
style 2-29 dim
|
||||
16| " Select at least one option, or press Tab for a "
|
||||
style 2-55 fg=red
|
||||
17| " custom answer. "
|
||||
style 2-15 fg=red
|
||||
18| " "
|
||||
19| <blank>
|
||||
|
||||
@@ -20,48 +20,28 @@ viewport
|
||||
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 │ "
|
||||
5| "────────────────────────────────────────────────────────"
|
||||
style 0-55 dim
|
||||
6| " "
|
||||
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>
|
||||
7| " "
|
||||
8| " Question 1/3 (3 unanswered) · Coverage "
|
||||
style 2-39 fg=bright-black
|
||||
9| " Which advanced TUI states belong in the required "
|
||||
10| " matrix? "
|
||||
11| " "
|
||||
12| " › 1. [ ] Code Mode run_code programs and capture "
|
||||
style 2-19 fg=bright-blue bold
|
||||
style 25-53 fg=bright-black
|
||||
13| " 2. [ ] Workflows phases and parallel agents "
|
||||
style 25-50 fg=bright-black
|
||||
14| " 3. [ ] Cordis tools inspect, mount, and unmount "
|
||||
style 25-51 fg=bright-black
|
||||
15| " 1/4 "
|
||||
style 2-4 dim
|
||||
16| " Tab custom answer • ↑/↓ navigate • Space toggle • "
|
||||
style 2-55 dim
|
||||
17| " Enter submit • Esc interrupt "
|
||||
style 2-29 dim
|
||||
18| " "
|
||||
19| <blank>
|
||||
|
||||
@@ -42,7 +42,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
16| "────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-95 dim
|
||||
17| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
17| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 63-95 dim
|
||||
style 38-95 dim
|
||||
18-35| <blank>
|
||||
|
||||
@@ -39,7 +39,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
14| "────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-95 dim
|
||||
15| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
15| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 63-95 dim
|
||||
style 38-95 dim
|
||||
16-35| <blank>
|
||||
|
||||
@@ -43,7 +43,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
17| "────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-95 dim
|
||||
18| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
18| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 63-95 dim
|
||||
style 38-95 dim
|
||||
19-35| <blank>
|
||||
|
||||
@@ -39,7 +39,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
14| "────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-95 dim
|
||||
15| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
15| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 63-95 dim
|
||||
style 38-95 dim
|
||||
16-35| <blank>
|
||||
|
||||
@@ -35,7 +35,6 @@ buffer
|
||||
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
|
||||
13| " 0% context deepseek-v4-flash(reasoning:on)"
|
||||
style 1-43 dim
|
||||
14-17| <blank>
|
||||
|
||||
@@ -31,7 +31,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
10| "────────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-103 dim
|
||||
11| "/workspace/project ↑0 ↓0 idle reasoning:on tools:compact"
|
||||
11| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 71-103 dim
|
||||
style 46-103 dim
|
||||
12-29| <blank>
|
||||
|
||||
@@ -45,8 +45,9 @@ buffer
|
||||
style 2-19 dim
|
||||
15| "▌ packages/ui/tui 100% "
|
||||
style 0-0 fg=green
|
||||
16| "▌ 4016 tests passed "
|
||||
16| "▌ … +1 lines (Ctrl+O to expand) "
|
||||
style 0-0 fg=green
|
||||
style 2-30 dim
|
||||
17| "▌ 1 test skipped "
|
||||
style 0-0 fg=green
|
||||
18| "▌ coverage complete "
|
||||
@@ -62,6 +63,6 @@ buffer
|
||||
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
|
||||
24| "/workspace/pro ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-13 dim
|
||||
style 22-79 dim
|
||||
|
||||
@@ -49,36 +49,19 @@ buffer
|
||||
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 ─────────╮ "
|
||||
20| "▌ /unsafe/\\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 2-54 dim
|
||||
21| "▌ Unsafe output \\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\\│ │ "
|
||||
22| "▌ [signal SIG\\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m] "
|
||||
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 2-58 fg=red
|
||||
23| "▌ "
|
||||
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
|
||||
24| <blank>
|
||||
25| " Context · unsafe-\\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 1-62 dim
|
||||
26| " Unsafe context \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 1-60 fg=bright-black
|
||||
27| <blank>
|
||||
@@ -88,19 +71,17 @@ buffer
|
||||
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"
|
||||
32| " "
|
||||
33| " Question 1/1 (1 unanswered) · Unsafe header \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
style 2-90 fg=bright-black
|
||||
34| " Unsafe question \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
|
||||
35| " "
|
||||
36| " › 1. Unsafe option \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m Unsafe detail \\x1b]2;snapshot-c "
|
||||
style 2-65 fg=bright-blue bold
|
||||
style 67-97 fg=bright-black
|
||||
37| " Tab custom answer • ↑/↓ navigate • Enter submit • Esc interrupt "
|
||||
style 2-64 dim
|
||||
38| " "
|
||||
39| "/workspace/project ↑0 ↓0 0% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-24 dim
|
||||
style 67-99 dim
|
||||
style 42-99 dim
|
||||
|
||||
@@ -41,6 +41,8 @@ const CHECKPOINTS = [
|
||||
'surface-before-compaction',
|
||||
'surface-after-compaction-narrow',
|
||||
'surface-after-compaction-wide',
|
||||
'model-selector',
|
||||
'model-switching',
|
||||
'errors-and-help',
|
||||
'disposed-terminal',
|
||||
] as const
|
||||
@@ -202,6 +204,8 @@ describe('TUI terminal-state snapshots', () => {
|
||||
it('pins an in-flight reasoning and Markdown stream', async () => {
|
||||
const harness = await setupSnapshot()
|
||||
await renderAfter(harness, () => {
|
||||
harness.agent.status = 'running'
|
||||
harness.ctx.emit('agent/status', harness.agent, 'running')
|
||||
appendUser(harness.session, 'Show the live update.')
|
||||
harness.session.append('assistant/chunk', {
|
||||
turn: 1,
|
||||
@@ -465,25 +469,29 @@ describe('TUI terminal-state snapshots', () => {
|
||||
const harness = await setupSnapshot({
|
||||
config: {
|
||||
maxQuestionOptions: 3,
|
||||
questionDialogWidth: 48,
|
||||
questionDialogWidth: 200,
|
||||
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' },
|
||||
],
|
||||
}],
|
||||
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' },
|
||||
],
|
||||
},
|
||||
{ id: 'priority', question: 'Which state should be implemented first?' },
|
||||
{ id: 'notes', question: 'Any additional constraints?' },
|
||||
],
|
||||
signal: controller.signal,
|
||||
})
|
||||
const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
@@ -573,6 +581,21 @@ describe('TUI terminal-state snapshots', () => {
|
||||
await harness.ctx.fiber.dispose()
|
||||
await harness.terminal.dispose()
|
||||
})
|
||||
|
||||
it('pins the model selector and selection notice', async () => {
|
||||
const harness = await setupSnapshot({}, { columns: 92, rows: 32 })
|
||||
await renderAfter(harness, () => {
|
||||
harness.terminal.send('/model')
|
||||
harness.terminal.send('\r')
|
||||
})
|
||||
await checkpoint('model-selector', harness.terminal, { includeScrollback: true })
|
||||
await renderAfter(harness, () => {
|
||||
harness.terminal.send('\x1b[B')
|
||||
harness.terminal.send('\r')
|
||||
})
|
||||
await checkpoint('model-switching', harness.terminal, { includeScrollback: true })
|
||||
await disposeSnapshot(harness)
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -3,7 +3,8 @@ 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, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentRegistry, { agentEvents, assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { LlmCallConfig } from '@deepseek-ai/dsh-llm'
|
||||
import CommandService, { type CommandInvocation } from '@deepseek-ai/dsh-commands'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||
@@ -112,14 +113,26 @@ async function dispose(setupResult: Awaited<ReturnType<typeof setup>>): Promise<
|
||||
await disposeTuiTestHarness(setupResult)
|
||||
}
|
||||
|
||||
function provideTokenMeter(ctx: Context): void {
|
||||
ctx.provide('tokenMeter', {
|
||||
contextWindow: 128_000,
|
||||
measure() {
|
||||
return { totalTokens: 0 }
|
||||
},
|
||||
} as never)
|
||||
}
|
||||
|
||||
describe('TUI config', () => {
|
||||
it('defaults every direct-call TUI option', () => {
|
||||
expect(resolveTuiConfig(undefined)).toEqual({
|
||||
showReasoning: true,
|
||||
maxToolOutputLines: 12,
|
||||
maxToolOutputLines: 6,
|
||||
maxQuestionOptions: 8,
|
||||
questionDialogWidth: 72,
|
||||
maxModelOptions: 8,
|
||||
questionDialogWidth: 200,
|
||||
questionDialogMaxHeight: 20,
|
||||
modelDialogWidth: 72,
|
||||
modelDialogMaxHeight: 20,
|
||||
showHardwareCursor: false,
|
||||
color: true,
|
||||
title: 'DeepSeek Harness',
|
||||
@@ -128,8 +141,11 @@ describe('TUI config', () => {
|
||||
showReasoning: false,
|
||||
maxToolOutputLines: 2,
|
||||
maxQuestionOptions: 3,
|
||||
maxModelOptions: 4,
|
||||
questionDialogWidth: 60,
|
||||
questionDialogMaxHeight: 14,
|
||||
modelDialogWidth: 64,
|
||||
modelDialogMaxHeight: 16,
|
||||
showHardwareCursor: true,
|
||||
color: false,
|
||||
title: 'DSH',
|
||||
@@ -137,8 +153,11 @@ describe('TUI config', () => {
|
||||
showReasoning: false,
|
||||
maxToolOutputLines: 2,
|
||||
maxQuestionOptions: 3,
|
||||
maxModelOptions: 4,
|
||||
questionDialogWidth: 60,
|
||||
questionDialogMaxHeight: 14,
|
||||
modelDialogWidth: 64,
|
||||
modelDialogMaxHeight: 16,
|
||||
showHardwareCursor: true,
|
||||
color: false,
|
||||
title: 'DSH',
|
||||
@@ -148,7 +167,11 @@ describe('TUI config', () => {
|
||||
|
||||
describe('pi-tui chat lifecycle and transcript', () => {
|
||||
it('renders its header, footer, replay, streaming answer, todos, and status', async () => {
|
||||
let now = 0
|
||||
const result = await setup({
|
||||
contextWindow: 100,
|
||||
contextTokens: 42,
|
||||
now: () => now,
|
||||
beforeMount(session) {
|
||||
appendUser(session, 'restored prompt')
|
||||
appendAssistant(session, [
|
||||
@@ -174,9 +197,19 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(result.terminal.output).toContain('restored answer')
|
||||
expect(result.terminal.output).toContain('write tests')
|
||||
expect(result.terminal.output).toContain('↑1.3k ↓42')
|
||||
expect(result.terminal.output).toContain('42% context tools:compact deepseek-v4-flash(reasoning:on)')
|
||||
result.terminal.resize(52)
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('42% context deepseek-v4-flash(reasoning:on)')
|
||||
result.terminal.resize(65)
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('↑1.3k ↓42 42% context deepseek-v4-flash(reasoning:on)')
|
||||
result.terminal.resize(88)
|
||||
await tick()
|
||||
|
||||
result.agent.status = 'running'
|
||||
agentEvents(result.ctx, result.agent).emit('agent/status', 'running')
|
||||
now = 8_000
|
||||
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' })
|
||||
@@ -257,13 +290,13 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
)
|
||||
await tick()
|
||||
|
||||
expect(result.terminal.output).toContain('Working')
|
||||
expect(result.terminal.output).toContain('◒ Working · 8s')
|
||||
expect(result.terminal.output).toContain('esc interrupt')
|
||||
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', {
|
||||
@@ -280,6 +313,8 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
result.agent.status = 'idle'
|
||||
agentEvents(result.ctx, result.agent).emit('agent/status', 'idle')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('↑1.8k ↓50')
|
||||
expect(result.terminal.output).toContain('deepseek-v4-flash(reasoning:off)')
|
||||
expect(result.terminal.progress.at(-1)).toBe(false)
|
||||
await dispose(result)
|
||||
expect(result.terminal.stopped).toBe(1)
|
||||
@@ -449,6 +484,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
result.terminal.send('\r')
|
||||
|
||||
result.agent.status = 'running'
|
||||
result.ctx.emit('agent/status', result.agent, 'running')
|
||||
result.terminal.send('steer it')
|
||||
result.terminal.send('\r')
|
||||
expect(result.agent.steered).toEqual([[{ type: 'text', text: 'steer it' }]])
|
||||
@@ -503,6 +539,162 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
await dispose(disposedAgent)
|
||||
})
|
||||
|
||||
it('opens a keyboard selector and switches the session model without sending slash text to the agent', async () => {
|
||||
const result = await setup({
|
||||
agentOptions: { provider: 'alpha', model: 'a1' },
|
||||
catalog: {
|
||||
providers: [{ id: 'alpha', name: 'Alpha' }, { id: 'beta', name: 'Beta' }],
|
||||
models: [
|
||||
{ provider: 'alpha', id: 'a1', name: 'Alpha One', description: 'Fast' },
|
||||
{ provider: 'alpha', id: 'shared', name: 'Alpha Shared' },
|
||||
{ provider: 'beta', id: 'b1', name: 'Beta One' },
|
||||
{ provider: 'beta', id: 'shared', name: 'Beta Shared' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
for (const command of ['/model too many model arguments', '/model missing', '/model shared', '/model alpha/a1', '/model alpha a1']) {
|
||||
result.terminal.send(command)
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
}
|
||||
expect(result.terminal.output).toContain('Usage: /model')
|
||||
expect(result.terminal.output).toContain('Unknown model: missing')
|
||||
expect(result.terminal.output).toContain('advertised by multiple providers')
|
||||
expect(result.terminal.output).toContain('already alpha/a1')
|
||||
|
||||
result.agent.status = 'running'
|
||||
result.terminal.send('/model')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Select model')
|
||||
expect(result.terminal.output).toContain('alpha/a1')
|
||||
expect(result.terminal.output).toContain('Alpha One — Fast — current')
|
||||
result.terminal.send('\x1b[B')
|
||||
result.terminal.send('\x1b[B')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Model selected: beta/b1')
|
||||
expect(result.agent.sent).toEqual([])
|
||||
expect(result.agent.steered).toEqual([])
|
||||
|
||||
result.terminal.send('/model')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
result.terminal.send('\x1b')
|
||||
await tick()
|
||||
expect(result.agent.cancelled).not.toContain('cancelled from terminal')
|
||||
result.agent.status = 'idle'
|
||||
result.ctx.emit('agent/status', result.agent, 'idle')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('tools:compact b1(reasoning:on)')
|
||||
|
||||
const assembly = await result.ctx.systemPrompt.assemble(assembleContextFor(result.agent))
|
||||
expect(assembly.variables).toMatchObject({ provider: 'beta', model: 'b1' })
|
||||
const seed: LlmCallConfig = { provider: 'alpha', model: 'a1', temperature: 0.2 }
|
||||
const request = await agentEvents(result.ctx, result.agent).waterfall(
|
||||
'agent/request', 1, 0, seed, () => Promise.resolve(seed),
|
||||
)
|
||||
expect(request).toEqual({ provider: 'beta', model: 'b1', temperature: 0.2 })
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('restores the logged model, keeps an unlisted current model visible, and reports catalog failures', async () => {
|
||||
const resumed = await setup({
|
||||
agentOptions: { provider: 'alpha', model: 'configured' },
|
||||
catalog: { providers: [{ id: 'beta', name: 'Beta' }], models: [] },
|
||||
beforeMount(session) {
|
||||
session.append('request/header', {
|
||||
header: { config: { provider: 'beta', model: 'private' } },
|
||||
reason: 'initial',
|
||||
})
|
||||
},
|
||||
})
|
||||
resumed.terminal.send('/model')
|
||||
resumed.terminal.send('\r')
|
||||
await tick()
|
||||
expect(resumed.terminal.output).toContain('Select model')
|
||||
expect(resumed.terminal.output).toContain('beta/private')
|
||||
expect(resumed.terminal.output).toContain('private — current')
|
||||
await dispose(resumed)
|
||||
|
||||
const unset = await setup({
|
||||
agentOptions: {},
|
||||
catalog: {
|
||||
providers: [{ id: 'alpha', name: 'Alpha' }],
|
||||
models: [{ provider: 'alpha', id: 'a1', name: 'Alpha One' }],
|
||||
},
|
||||
})
|
||||
unset.terminal.send('/model')
|
||||
unset.terminal.send('\r')
|
||||
await tick()
|
||||
unset.terminal.send('\r')
|
||||
await tick()
|
||||
expect(unset.terminal.output).toContain('Model selected: alpha/a1')
|
||||
await dispose(unset)
|
||||
|
||||
const empty = await setup({ agentOptions: {}, catalog: { providers: [], models: [] } })
|
||||
empty.terminal.send('/model')
|
||||
empty.terminal.send('\r')
|
||||
await tick()
|
||||
expect(empty.terminal.output).toContain('Current model: unset')
|
||||
expect(empty.terminal.output).toContain('No models are advertised')
|
||||
const assembly = await empty.ctx.systemPrompt.assemble(assembleContextFor(empty.agent))
|
||||
expect(assembly.variables).toEqual({})
|
||||
const seed: LlmCallConfig = { provider: 'fallback', model: 'fallback' }
|
||||
await expect(agentEvents(empty.ctx, empty.agent).waterfall(
|
||||
'agent/request', 1, 0, seed, () => Promise.resolve(seed),
|
||||
)).resolves.toBe(seed)
|
||||
await dispose(empty)
|
||||
|
||||
const failed = await setup({
|
||||
catalog: {
|
||||
providers: [{ id: 'deepseek', name: 'DeepSeek' }],
|
||||
models: [],
|
||||
listModels: () => Promise.reject(new Error('catalog offline')),
|
||||
},
|
||||
})
|
||||
failed.terminal.send('/model')
|
||||
failed.terminal.send('\r')
|
||||
await tick()
|
||||
expect(failed.terminal.output).toContain('Could not read the model catalog: catalog offline')
|
||||
await dispose(failed)
|
||||
})
|
||||
|
||||
it('does not render a model catalog that resolves after TUI disposal', async () => {
|
||||
const deferred = Promise.withResolvers<never[]>()
|
||||
const result = await setup({
|
||||
catalog: {
|
||||
providers: [{ id: 'deepseek', name: 'DeepSeek' }],
|
||||
models: [],
|
||||
listModels: () => deferred.promise,
|
||||
},
|
||||
})
|
||||
result.terminal.send('/model')
|
||||
result.terminal.send('\r')
|
||||
await result.controller.dispose()
|
||||
deferred.resolve([])
|
||||
await tick()
|
||||
expect(result.terminal.output).not.toContain('Available models')
|
||||
await result.ctx.fiber.dispose()
|
||||
|
||||
const rejected = Promise.withResolvers<never[]>()
|
||||
const rejectedResult = await setup({
|
||||
catalog: {
|
||||
providers: [{ id: 'deepseek', name: 'DeepSeek' }],
|
||||
models: [],
|
||||
listModels: () => rejected.promise,
|
||||
},
|
||||
})
|
||||
rejectedResult.terminal.send('/model')
|
||||
rejectedResult.terminal.send('\r')
|
||||
await rejectedResult.controller.dispose()
|
||||
rejected.reject(new Error('late catalog failure'))
|
||||
await tick()
|
||||
expect(rejectedResult.terminal.output).not.toContain('late catalog failure')
|
||||
await rejectedResult.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('discovers and executes plugin commands, then removes TUI-local commands on disposal', async () => {
|
||||
const result = await setup()
|
||||
const handler = vi.fn(({ rawInput }: CommandInvocation) => ({
|
||||
@@ -712,7 +904,7 @@ describe('tool cards and surface replay', () => {
|
||||
}
|
||||
|
||||
it('uses terminal, diff, generic, fallback, and collapsed tool presentations', async () => {
|
||||
const result = await setup({ tools, config: { maxToolOutputLines: 1 } })
|
||||
const result = await setup({ tools, config: { maxToolOutputLines: 4 } })
|
||||
const calls = [
|
||||
['c1', 'bash', '{"command":"printf hello"}'],
|
||||
['c2', 'signal', '{}'],
|
||||
@@ -786,7 +978,7 @@ describe('tool cards and surface replay', () => {
|
||||
const output = result.terminal.output
|
||||
expect(output).toContain('Run command')
|
||||
expect(output).toContain('printf hello')
|
||||
expect(output).toContain('more lines')
|
||||
expect(output).toContain('lines (Ctrl+O to expand)')
|
||||
expect(output).toContain('SIGTERM')
|
||||
expect(output).toContain('Edit files')
|
||||
expect(output).toContain('Inspected')
|
||||
@@ -803,6 +995,11 @@ describe('tool cards and surface replay', () => {
|
||||
result.terminal.send('/redraw')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
const collapsed = result.terminal.output.slice(result.terminal.output.lastIndexOf('\x1b[2J'))
|
||||
expect(collapsed).toContain('Run command')
|
||||
expect(collapsed).toContain('[exit 0]')
|
||||
expect(collapsed).not.toContain('▌ hello')
|
||||
expect(collapsed).not.toContain('world')
|
||||
result.terminal.send('\x0f')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('world')
|
||||
@@ -856,6 +1053,7 @@ describe('TUI user-interaction dialogs', () => {
|
||||
})
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Choose a mode')
|
||||
expect(result.terminal.output).toContain('Question 1/1 (1 unanswered) · Mode')
|
||||
expect(result.terminal.output).toContain('1/2')
|
||||
result.terminal.send('\x1b[B')
|
||||
result.terminal.send('\r')
|
||||
@@ -875,7 +1073,7 @@ describe('TUI user-interaction dialogs', () => {
|
||||
questions: [{ id: 'other', question: 'Choose or type', options: [{ label: 'Default' }] }],
|
||||
})
|
||||
await tick()
|
||||
result.terminal.send('c')
|
||||
result.terminal.send('\t')
|
||||
result.terminal.send('my choice')
|
||||
result.terminal.send('\r')
|
||||
await expect(custom).resolves.toEqual({ answers: [{ id: 'other', selected: [], custom: 'my choice' }] })
|
||||
@@ -949,9 +1147,11 @@ describe('TUI user-interaction dialogs', () => {
|
||||
],
|
||||
})
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Question 1/2 (2 unanswered)')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Second?')
|
||||
expect(result.terminal.output).toContain('Question 2/2 (1 unanswered)')
|
||||
result.terminal.send('done')
|
||||
result.terminal.send('\r')
|
||||
await expect(batch).resolves.toEqual({ answers: [
|
||||
@@ -998,6 +1198,7 @@ describe('TUI user-interaction dialogs', () => {
|
||||
describe('terminal mounting', () => {
|
||||
it('starts immediately when the configured agent already exists', async () => {
|
||||
const ctx = new Context()
|
||||
provideTokenMeter(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(CommandService)
|
||||
@@ -1017,6 +1218,7 @@ describe('terminal mounting', () => {
|
||||
|
||||
it('waits for its configured agent before starting the TUI', async () => {
|
||||
const ctx = new Context()
|
||||
provideTokenMeter(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(CommandService)
|
||||
@@ -1046,6 +1248,7 @@ describe('terminal mounting', () => {
|
||||
|
||||
it('prints a matching live startup failure and exits instead of waiting forever', async () => {
|
||||
const ctx = new Context()
|
||||
provideTokenMeter(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(CommandService)
|
||||
@@ -1074,6 +1277,7 @@ describe('terminal mounting', () => {
|
||||
|
||||
it('renders an uncoercible startup failure without escaping the display boundary', async () => {
|
||||
const ctx = new Context()
|
||||
provideTokenMeter(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(CommandService)
|
||||
@@ -1095,6 +1299,7 @@ describe('terminal mounting', () => {
|
||||
|
||||
it('rolls back providers, listeners, and terminal state when startup fails', async () => {
|
||||
const ctx = new Context()
|
||||
provideTokenMeter(ctx)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(CommandService)
|
||||
@@ -1130,6 +1335,7 @@ describe('terminal mounting', () => {
|
||||
|
||||
it('throws when createTuiChat is called without the configured agent', async () => {
|
||||
const ctx = new Context()
|
||||
provideTokenMeter(ctx)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(CommandService)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
|
||||
Reference in New Issue
Block a user