feat(tui): add session controls and compact views
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Context } from 'cordis'
|
||||
import type { Terminal } from '@earendil-works/pi-tui'
|
||||
import AgentRegistry, { type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import AgentRegistry, { type Agent, type AgentOptions, type AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import type { ContentBlock, LlmModelInfo, LlmProviderInfo } 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'
|
||||
@@ -21,6 +22,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> {
|
||||
@@ -48,6 +58,28 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
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', {
|
||||
@@ -58,6 +90,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,
|
||||
@@ -69,7 +102,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,
|
||||
@@ -95,7 +128,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,7 @@ 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', 'userInteraction', 'tools'])
|
||||
expect(unwrapped.inject).toEqual(['agents', '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>
|
||||
|
||||
@@ -25,12 +25,12 @@ 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
|
||||
10| " /help /clear /cancel /reasoning /tools /redraw /exit "
|
||||
style 1-52 fg=bright-black
|
||||
10| " /help /model /clear /cancel /reasoning /tools /redraw /exit "
|
||||
style 1-59 fg=bright-black
|
||||
11| <blank>
|
||||
12| " Unknown command: /unknown-advanced-command "
|
||||
style 1-42 fg=yellow
|
||||
@@ -46,7 +46,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
19| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 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 59-91 dim
|
||||
style 34-91 dim
|
||||
21-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>
|
||||
|
||||
@@ -25,12 +25,12 @@ 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
|
||||
10| " /help /clear /cancel /reasoning /tools /redraw /exit "
|
||||
style 1-52 fg=bright-black
|
||||
10| " /help /model /clear /cancel /reasoning /tools /redraw /exit "
|
||||
style 1-59 fg=bright-black
|
||||
11| <blank>
|
||||
12| " Unknown command: /unknown-advanced-command "
|
||||
style 1-42 fg=yellow
|
||||
@@ -46,7 +46,7 @@ buffer
|
||||
style 1-1 inverse
|
||||
19| "────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-91 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 59-91 dim
|
||||
style 34-91 dim
|
||||
21-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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,6 +35,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
|
||||
@@ -196,6 +198,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: 2,
|
||||
@@ -382,25 +386,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' })
|
||||
@@ -488,6 +496,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, { 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 SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
@@ -110,14 +111,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',
|
||||
@@ -126,8 +139,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',
|
||||
@@ -135,8 +151,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',
|
||||
@@ -146,7 +165,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, [
|
||||
@@ -172,9 +195,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'
|
||||
result.ctx.emit('agent/status', result.agent, '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' })
|
||||
@@ -247,13 +280,13 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
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('◒ 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', {
|
||||
@@ -270,6 +303,8 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
result.agent.status = 'idle'
|
||||
result.ctx.emit('agent/status', result.agent, '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)
|
||||
@@ -377,6 +412,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' }]])
|
||||
@@ -431,6 +467,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('cancels before /exit while running and handles agent errors/disposal', async () => {
|
||||
const result = await setup({ status: 'running' })
|
||||
result.terminal.send('/exit')
|
||||
@@ -525,7 +717,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', '{}'],
|
||||
@@ -594,7 +786,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')
|
||||
@@ -611,6 +803,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')
|
||||
@@ -664,6 +861,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')
|
||||
@@ -683,7 +881,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' }] })
|
||||
@@ -757,9 +955,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: [
|
||||
@@ -806,6 +1006,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(UserInteractionService)
|
||||
@@ -824,6 +1025,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(UserInteractionService)
|
||||
@@ -852,6 +1054,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(UserInteractionService)
|
||||
@@ -879,6 +1082,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(UserInteractionService)
|
||||
@@ -899,6 +1103,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(UserInteractionService)
|
||||
@@ -929,6 +1134,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(UserInteractionService)
|
||||
ctx.provide('tools', { get: () => undefined } as never)
|
||||
|
||||
Reference in New Issue
Block a user