@xingular/harness 0.1.7 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +29 -10
- package/dist/cli.js +5179 -2976
- package/dist/terminal-pi.js +365 -24
- package/docs/README.md +3 -0
- package/docs/decisions/ADR-0002-native-subagent-lifecycle.md +49 -0
- package/docs/decisions/ADR-0003-shared-asynchronous-subagents.md +68 -0
- package/docs/decisions/ADR-0004-fullscreen-terminal-layout.md +49 -0
- package/docs/decisions/ADR-0005-app-provided-oauth-clients.md +54 -0
- package/docs/terminal-tui.md +100 -10
- package/package.json +5 -4
- package/src/accounts/accounts.test.ts +87 -1
- package/src/accounts/accounts.ts +120 -18
- package/src/core/bus.test.ts +22 -0
- package/src/core/bus.ts +17 -2
- package/src/harness.ts +12 -2
- package/src/index.ts +7 -2
- package/src/mcp/connections.ts +39 -12
- package/src/mcp/service.test.ts +49 -3
- package/src/mcp/service.ts +14 -3
- package/src/mcp/types.ts +10 -0
- package/src/memory/curation.test.ts +44 -1
- package/src/memory/dispatch.ts +5 -0
- package/src/memory/maintenance.test.ts +17 -1
- package/src/memory/maintenance.ts +169 -8
- package/src/memory/recall.ts +2 -1
- package/src/memory/trajectory.test.ts +178 -0
- package/src/memory/trajectory.ts +220 -0
- package/src/memory/wiki.ts +2 -1
- package/src/memory/wikiIndex.ts +3 -2
- package/src/plugins/builtin.ts +4 -2
- package/src/plugins/catalogue.test.ts +1 -1
- package/src/server/methods.ts +6 -0
- package/src/server/server.test.ts +20 -1
- package/src/sessions/approvals.test.ts +49 -0
- package/src/sessions/approvals.ts +26 -4
- package/src/sessions/conversation.test.ts +56 -0
- package/src/sessions/lineageJoin.ts +1 -1
- package/src/sessions/registry.ts +38 -7
- package/src/sessions/runStore.ts +14 -0
- package/src/sessions/runner.ts +9 -3
- package/src/sessions/sessions.test.ts +46 -7
- package/src/sessions/sessions.ts +31 -39
- package/src/sessions/subagentService.test.ts +159 -0
- package/src/sessions/subagentService.ts +236 -0
- package/src/sessions/subagents.ts +97 -0
- package/src/terminal/controller.ts +73 -10
- package/src/terminal/piExtension.ts +42 -6
- package/src/terminal/socket.test.ts +23 -0
- package/src/terminal/socket.ts +42 -18
- package/src/terminal/standalone.ts +12 -0
- package/src/terminal/terminal.test.ts +150 -15
- package/src/terminal/terminals.ts +113 -5
- package/src/terminal/trajectory.test.ts +66 -0
- package/src/terminal/trajectory.ts +44 -0
- package/src/terminal/tui/application.test.ts +184 -8
- package/src/terminal/tui/application.ts +243 -81
- package/src/terminal/tui/commands.test.ts +69 -9
- package/src/terminal/tui/commands.ts +89 -7
- package/src/terminal/tui/editor.test.ts +56 -27
- package/src/terminal/tui/editor.ts +49 -19
- package/src/terminal/tui/keybindings.test.ts +8 -0
- package/src/terminal/tui/keybindings.ts +10 -1
- package/src/terminal/tui/planMode.test.ts +10 -0
- package/src/terminal/tui/planMode.ts +12 -0
- package/src/terminal/tui/runtime.socket.test.ts +77 -3
- package/src/terminal/tui/runtime.test.ts +10 -0
- package/src/terminal/tui/runtime.ts +24 -9
- package/src/terminal/tui/screen.test.ts +143 -0
- package/src/terminal/tui/screen.ts +121 -0
- package/src/terminal/tui/skillInvocation.test.ts +71 -0
- package/src/terminal/tui/skillInvocation.ts +51 -0
- package/src/terminal/tui/status.test.ts +24 -46
- package/src/terminal/tui/status.ts +42 -69
- package/src/terminal/tui/theme.ts +18 -7
- package/src/terminal/tui/trajectoryPanel.test.ts +99 -0
- package/src/terminal/tui/trajectoryPanel.ts +226 -0
- package/src/terminal/tui/transcript.test.ts +14 -0
- package/src/terminal/tui/transcript.ts +11 -0
- package/src/terminal/tui/ui.ts +2 -2
- package/src/terminal/tui/welcome.ts +4 -4
- package/src/terminal/tui/workingStatus.test.ts +44 -0
- package/src/terminal/tui/workingStatus.ts +238 -0
- package/src/tools/definitions.ts +42 -1
- package/src/tools/maintenance.ts +6 -1
- package/src/tools/spawnSubagent.ts +40 -85
- package/src/tools/types.ts +2 -0
- package/docs/evidence/terminal-2026-10-17/120x40-completed-transcript.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/40x20-settings-interaction.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/40x20-settings-keyboard.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/40x20-welcome.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17/verification.json +0 -130
- package/docs/evidence/terminal-2026-10-17-followup/120x40-completed-transcript.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-interaction.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/40x20-settings-keyboard.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/40x20-welcome.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-plugin-light.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-question-batch.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-plugin-light.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question-batch.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/app-bundle/verification.json +0 -95
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-approval-0.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-plugin-light.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question-batch.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/80x24-question.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/README.md +0 -27
- package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command-80x24.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/global-install/shell-command.json +0 -18
- package/docs/evidence/terminal-2026-10-17-followup/global-install/verification.json +0 -95
- package/docs/evidence/terminal-2026-10-17-followup/harness-reference-80x24.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/harness-reference-final-80x24.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/harness-reference-scenario.json +0 -60
- package/docs/evidence/terminal-2026-10-17-followup/live-codex/README.md +0 -24
- package/docs/evidence/terminal-2026-10-17-followup/live-codex/complete-80x24.ansi.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/live-codex/verification.json +0 -57
- package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24-early.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/omp-reference-80x24.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/omp-reference-scenario.json +0 -58
- package/docs/evidence/terminal-2026-10-17-followup/omp-supplemental-static.json.gz +0 -0
- package/docs/evidence/terminal-2026-10-17-followup/rea-artifacts.json +0 -320
- package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.json +0 -30
- package/docs/evidence/terminal-2026-10-17-followup/reference-comparison.md +0 -133
- package/docs/evidence/terminal-2026-10-17-followup/reference-source-observations.json +0 -20
- package/docs/evidence/terminal-2026-10-17-followup/verification.json +0 -207
|
@@ -1,44 +1,134 @@
|
|
|
1
|
-
import { CustomEditor, type ExtensionUIContext, type Theme } from '@earendil-works/pi-coding-agent'
|
|
1
|
+
import { CustomEditor, type AgentSessionEvent, type ExtensionUIContext, type Theme } from '@earendil-works/pi-coding-agent'
|
|
2
|
+
import { stripTerminalSequences } from '@earendil-works/pi-tui'
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
3
4
|
|
|
4
5
|
vi.mock('../control', () => ({ controlCall: vi.fn(async () => ({})) }))
|
|
5
6
|
import { controlCall } from '../control'
|
|
6
7
|
import { TerminalApplication } from './application'
|
|
8
|
+
import type { TerminalDecision } from './runtime'
|
|
7
9
|
import { terminalKeybindings } from './keybindings'
|
|
8
|
-
import { createHarnessTheme } from './theme'
|
|
10
|
+
import { autocompleteRuleColor, createHarnessTheme } from './theme'
|
|
11
|
+
import { TerminalTranscript } from './transcript'
|
|
9
12
|
|
|
10
13
|
function setup() {
|
|
11
14
|
const application = new TerminalApplication([])
|
|
12
15
|
const app = application as unknown as {
|
|
13
|
-
|
|
16
|
+
theme: Theme
|
|
17
|
+
ui: ExtensionUIContext & {
|
|
18
|
+
wrapAutocomplete<T>(value: T): T
|
|
19
|
+
dispose(): void
|
|
20
|
+
state: { workingVisible: boolean; workingMessage?: string; workingIndicator?: { frames?: string[]; intervalMs?: number } }
|
|
21
|
+
}
|
|
14
22
|
keybindings: ReturnType<typeof terminalKeybindings>
|
|
15
23
|
editor: CustomEditor
|
|
16
|
-
runtime: { interrupt: ReturnType<typeof vi.fn>; dispose: ReturnType<typeof vi.fn>; session:
|
|
24
|
+
runtime: { interrupt: ReturnType<typeof vi.fn>; dispose: ReturnType<typeof vi.fn>; session: object; snapshot(): { hasUserMessages: boolean; cwd?: string; model?: { name: string; reasoning: boolean; contextWindow: number }; thinkingLevel?: string; account?: { label: string } | null; context?: { percent: number | null; window: number }; costUsd?: number; subscription?: boolean } }
|
|
17
25
|
commands: { execute: ReturnType<typeof vi.fn> }
|
|
26
|
+
masthead: { render(width: number): string[] }
|
|
27
|
+
header?: { render(width: number): string[]; invalidate(): void }
|
|
28
|
+
branch: string | null
|
|
29
|
+
transcript: TerminalTranscript
|
|
18
30
|
initialized: boolean
|
|
19
31
|
commandBusy: boolean
|
|
20
32
|
mountEditor(factory?: Parameters<ExtensionUIContext['setEditorComponent']>[0]): void
|
|
21
33
|
dispatchInput(data: string): { consume?: boolean } | undefined
|
|
22
34
|
submit(text: string): void
|
|
35
|
+
accept(event: AgentSessionEvent): void
|
|
36
|
+
band(width: number): string
|
|
37
|
+
titleLines(width: number): string[]
|
|
38
|
+
resetWorkingStatus(): void
|
|
39
|
+
phase?: string
|
|
23
40
|
}
|
|
24
41
|
vi.spyOn(application.tui, 'requestRender').mockImplementation(() => {})
|
|
25
42
|
vi.spyOn(application.tui, 'stop').mockImplementation(() => {})
|
|
26
43
|
vi.spyOn(application.terminal, 'drainInput').mockResolvedValue()
|
|
27
|
-
|
|
44
|
+
vi.spyOn(application.terminal, 'setProgress').mockImplementation(() => {})
|
|
45
|
+
const ui = { theme: createHarnessTheme({ getColorMode: () => 'truecolor' } as Theme), state: { workingVisible: true }, wrapAutocomplete: <T>(value: T) => value,
|
|
28
46
|
setToolsExpanded: vi.fn(), dispose: vi.fn() } as unknown as typeof app.ui
|
|
29
47
|
app.ui = ui
|
|
48
|
+
app.transcript = new TerminalTranscript(application.tui, () => ui.theme, () => undefined)
|
|
30
49
|
app.keybindings = terminalKeybindings('/unused/isolated-account')
|
|
31
50
|
app.initialized = true
|
|
32
51
|
const context = {}
|
|
33
|
-
app.runtime = { interrupt: vi.fn(async () => {}), dispose: vi.fn(async () => {}),
|
|
52
|
+
app.runtime = { interrupt: vi.fn(async () => {}), dispose: vi.fn(async () => {}), snapshot: () => ({
|
|
53
|
+
hasUserMessages: false, cwd: '/work/project', model: { name: 'Claude Sonnet 4.6', reasoning: true, contextWindow: 200_000 },
|
|
54
|
+
thinkingLevel: 'high', account: { label: 'Tanny' }, context: { percent: null, window: 0 }, costUsd: 0, subscription: false
|
|
55
|
+
}), session: {
|
|
34
56
|
extensionRunner: { getShortcuts: () => new Map(), createContext: () => context },
|
|
35
|
-
state: { messages: [] }, settingsManager: {}
|
|
57
|
+
state: { messages: [] }, settingsManager: { getShowTerminalProgress: () => false },
|
|
58
|
+
getSessionStats: () => ({ tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } })
|
|
36
59
|
} }
|
|
37
|
-
application.tui.addChild(application)
|
|
38
60
|
return { application, app, ui }
|
|
39
61
|
}
|
|
40
62
|
|
|
41
63
|
describe('the owned terminal application', () => {
|
|
64
|
+
it('shows a stable animated verb, then keeps completion status until the next turn', async () => {
|
|
65
|
+
const { application, app } = setup()
|
|
66
|
+
let now = 1000
|
|
67
|
+
const dateNow = vi.spyOn(Date, 'now').mockImplementation(() => now)
|
|
68
|
+
const random = vi.spyOn(Math, 'random').mockReturnValue(0)
|
|
69
|
+
|
|
70
|
+
app.accept({ type: 'agent_start' } as AgentSessionEvent)
|
|
71
|
+
const initialStatus = app.titleLines(100)[0] ?? ''
|
|
72
|
+
expect(initialStatus).toContain(app.theme.fg('warning', '· Accomplishing'))
|
|
73
|
+
const orangeRgb = autocompleteRuleColor(app.theme).slice(1).match(/.{2}/g)?.map(value => Number.parseInt(value, 16)) ?? []
|
|
74
|
+
expect(initialStatus).toContain(`38;2;${orangeRgb.join(';')}`)
|
|
75
|
+
expect(stripTerminalSequences(initialStatus).trim()).toContain('· Accomplishing')
|
|
76
|
+
now = 1080
|
|
77
|
+
const nextFrameStatus = app.titleLines(100)[0] ?? ''
|
|
78
|
+
expect(nextFrameStatus).toContain(app.theme.fg('warning', '✢ Accomplishing'))
|
|
79
|
+
expect(stripTerminalSequences(nextFrameStatus).trim()).toContain('✢ Accomplishing')
|
|
80
|
+
|
|
81
|
+
now = 2450
|
|
82
|
+
app.accept({ type: 'agent_settled' } as AgentSessionEvent)
|
|
83
|
+
const completed = stripTerminalSequences(app.titleLines(100)[0] ?? '').trim()
|
|
84
|
+
expect(completed).toMatch(/^✻ (Baked|Brewed|Churned|Cogitated|Cooked|Crunched|Sautéed|Worked) for 1s$/)
|
|
85
|
+
expect(stripTerminalSequences(app.titleLines(100)[0] ?? '').trim()).toBe(completed)
|
|
86
|
+
|
|
87
|
+
now = 3000
|
|
88
|
+
app.accept({ type: 'agent_start' } as AgentSessionEvent)
|
|
89
|
+
expect(stripTerminalSequences(app.titleLines(100)[0] ?? '').trim()).toContain('· Accomplishing')
|
|
90
|
+
app.resetWorkingStatus()
|
|
91
|
+
expect(app.titleLines(100)).toEqual([])
|
|
92
|
+
|
|
93
|
+
random.mockRestore()
|
|
94
|
+
dateNow.mockRestore()
|
|
95
|
+
await application.shutdown()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('keeps extension working overrides and phase labels ahead of the default verb', async () => {
|
|
99
|
+
const { application, app, ui } = setup()
|
|
100
|
+
ui.state.workingMessage = 'Inspecting'
|
|
101
|
+
ui.state.workingIndicator = { frames: ['●'], intervalMs: 500 }
|
|
102
|
+
|
|
103
|
+
app.accept({ type: 'agent_start' } as AgentSessionEvent)
|
|
104
|
+
expect(stripTerminalSequences(app.titleLines(100)[0] ?? '').trim()).toContain('● Inspecting')
|
|
105
|
+
app.accept({ type: 'compaction_start' } as AgentSessionEvent)
|
|
106
|
+
expect(stripTerminalSequences(app.titleLines(100)[0] ?? '').trim()).toContain('● Compacting context…')
|
|
107
|
+
ui.state.workingVisible = false
|
|
108
|
+
expect(stripTerminalSequences(app.titleLines(100)[0] ?? '').trim()).toContain('● Compacting context…')
|
|
109
|
+
app.phase = undefined
|
|
110
|
+
expect(app.titleLines(100)).toEqual([])
|
|
111
|
+
|
|
112
|
+
await application.shutdown()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('shows current-conversation input plus cache-read totals and the last completed response rate', async () => {
|
|
116
|
+
const { application, app } = setup()
|
|
117
|
+
app.branch = 'main'
|
|
118
|
+
Object.assign(app.runtime.session, { getSessionStats: () => ({ tokens: { input: 1200, output: 540, cacheRead: 300, cacheWrite: 100 } }) })
|
|
119
|
+
const now = vi.spyOn(performance, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(3000).mockReturnValueOnce(4000).mockReturnValueOnce(6000)
|
|
120
|
+
app.accept({ type: 'turn_start' } as AgentSessionEvent)
|
|
121
|
+
expect(stripTerminalSequences(app.band(160))).toContain('— tok/s in 1,500 out 540')
|
|
122
|
+
app.accept({ type: 'message_end', message: { role: 'assistant', content: [], usage: { output: 60 } } } as unknown as AgentSessionEvent)
|
|
123
|
+
expect(stripTerminalSequences(app.band(160))).toBe('project | main | Tanny · Sonnet 4.6 · high | 30 tok/s in 1,500 out 540')
|
|
124
|
+
app.accept({ type: 'turn_start' } as AgentSessionEvent)
|
|
125
|
+
expect(stripTerminalSequences(app.band(160))).toContain('30 tok/s in 1,500 out 540')
|
|
126
|
+
app.accept({ type: 'message_end', message: { role: 'assistant', content: [], usage: { output: 40 } } } as unknown as AgentSessionEvent)
|
|
127
|
+
expect(stripTerminalSequences(app.band(160))).toContain('20 tok/s in 1,500 out 540')
|
|
128
|
+
now.mockRestore()
|
|
129
|
+
await application.shutdown()
|
|
130
|
+
})
|
|
131
|
+
|
|
42
132
|
it('lets trusted custom editor modes consume Escape before delegating host actions', async () => {
|
|
43
133
|
const { application, app } = setup()
|
|
44
134
|
let normalMode = false
|
|
@@ -71,6 +161,64 @@ describe('the owned terminal application', () => {
|
|
|
71
161
|
await application.shutdown()
|
|
72
162
|
})
|
|
73
163
|
|
|
164
|
+
it('opens the keyboard list on ? over an empty prompt, and types ? into a draft', async () => {
|
|
165
|
+
const { application, app } = setup()
|
|
166
|
+
app.mountEditor()
|
|
167
|
+
expect(app.dispatchInput('?')).toEqual({ consume: true })
|
|
168
|
+
expect(app.editor.getText()).toBe('')
|
|
169
|
+
expect(app.transcript.render(120).join('\n')).toContain('PgUp/PgDn: scroll transcript')
|
|
170
|
+
app.editor.setText('draft')
|
|
171
|
+
expect(app.dispatchInput('?')).toBeUndefined()
|
|
172
|
+
await application.shutdown()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('offers Allow once, a tool-wide grant only when eligible, and Deny in the approval prompt', async () => {
|
|
176
|
+
const { application, app } = setup()
|
|
177
|
+
let shown = ''
|
|
178
|
+
let chooseGlobal = true
|
|
179
|
+
const ui = app.ui as unknown as { custom: (factory: (...args: unknown[]) => unknown) => Promise<string | undefined> }
|
|
180
|
+
ui.custom = factory => new Promise(resolve => {
|
|
181
|
+
const panel = factory(application.tui, app.ui.theme, app.keybindings, (value: unknown) => resolve(typeof value === 'string' ? value : undefined)) as {
|
|
182
|
+
handleInput(data: string): void
|
|
183
|
+
render(width: number): string[]
|
|
184
|
+
}
|
|
185
|
+
shown = stripTerminalSequences(panel.render(100).join('\n'))
|
|
186
|
+
if (chooseGlobal) panel.handleInput('\x1b[B')
|
|
187
|
+
panel.handleInput('\r')
|
|
188
|
+
})
|
|
189
|
+
const choose = (application as unknown as { chooseApproval(request: TerminalDecision, signal: AbortSignal): Promise<{ decision: string; alwaysAllow: boolean }> }).chooseApproval.bind(application)
|
|
190
|
+
const remembered = await choose({ type: 'approval', requestId: 'r1', target: 'bash', rememberable: true, action: '{"command":"git status"}' } as TerminalDecision, new AbortController().signal)
|
|
191
|
+
expect(shown).toContain('Always allow bash everywhere')
|
|
192
|
+
expect(shown).toContain('Allow once')
|
|
193
|
+
expect(shown).toContain('Deny')
|
|
194
|
+
expect(remembered).toEqual({ decision: 'approved', alwaysAllow: true })
|
|
195
|
+
|
|
196
|
+
chooseGlobal = false
|
|
197
|
+
const oneTime = await choose({ type: 'approval', requestId: 'r2', target: 'bash', rememberable: false, action: '{}' } as TerminalDecision, new AbortController().signal)
|
|
198
|
+
expect(shown).not.toContain('Always allow bash everywhere')
|
|
199
|
+
expect(oneTime).toEqual({ decision: 'approved', alwaysAllow: false })
|
|
200
|
+
await application.shutdown()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('hides the product lockup once the chat has a user message', async () => {
|
|
204
|
+
const { application, app } = setup()
|
|
205
|
+
expect(app.masthead.render(80).join('\n')).toContain('Xingular Harness')
|
|
206
|
+
app.runtime.snapshot = () => ({ hasUserMessages: true })
|
|
207
|
+
expect(app.masthead.render(80)).toEqual([])
|
|
208
|
+
app.runtime.snapshot = () => ({ hasUserMessages: false })
|
|
209
|
+
expect(app.masthead.render(80).join('\n')).toContain('Xingular Harness')
|
|
210
|
+
await application.shutdown()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('keeps an extension header after the chat starts', async () => {
|
|
214
|
+
const { application, app } = setup()
|
|
215
|
+
vi.spyOn(application.terminal, 'write').mockImplementation(() => {})
|
|
216
|
+
app.header = { render: () => ['custom header'], invalidate: () => {} }
|
|
217
|
+
app.runtime.snapshot = () => ({ hasUserMessages: true })
|
|
218
|
+
expect(app.masthead.render(80)).toContain('custom header')
|
|
219
|
+
await application.shutdown()
|
|
220
|
+
})
|
|
221
|
+
|
|
74
222
|
it('routes leading-space commands and serializes command dialogs', async () => {
|
|
75
223
|
const { application, app } = setup()
|
|
76
224
|
app.mountEditor()
|
|
@@ -86,6 +234,34 @@ describe('the owned terminal application', () => {
|
|
|
86
234
|
await application.shutdown()
|
|
87
235
|
})
|
|
88
236
|
|
|
237
|
+
it('toggles plan mode on Shift+Tab, narrowing to read-only tools and leaving thinking alone', async () => {
|
|
238
|
+
const { application, app } = setup()
|
|
239
|
+
const session = app.runtime.session as { getActiveToolNames(): string[]; setActiveToolsByName(names: string[]): void; cycleThinkingLevel(): void }
|
|
240
|
+
const setActiveToolsByName = vi.fn()
|
|
241
|
+
const cycleThinkingLevel = vi.fn()
|
|
242
|
+
Object.assign(session, { getActiveToolNames: () => ['read', 'bash', 'edit', 'write', 'grep', 'spawn_subagent'], setActiveToolsByName, cycleThinkingLevel })
|
|
243
|
+
expect(app.dispatchInput('\x1b[Z')).toEqual({ consume: true })
|
|
244
|
+
expect(setActiveToolsByName).toHaveBeenLastCalledWith(['read', 'grep'])
|
|
245
|
+
expect(app.dispatchInput('\x1b[Z')).toEqual({ consume: true })
|
|
246
|
+
expect(setActiveToolsByName).toHaveBeenLastCalledWith(['read', 'bash', 'edit', 'write', 'grep', 'spawn_subagent'])
|
|
247
|
+
expect(cycleThinkingLevel).not.toHaveBeenCalled()
|
|
248
|
+
await application.shutdown()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('steps thinking effort with shift arrows and stops at the ends of the model levels', async () => {
|
|
252
|
+
const { application, app } = setup()
|
|
253
|
+
const session = app.runtime.session as { thinkingLevel: string; getAvailableThinkingLevels(): string[]; supportsThinking(): boolean; setThinkingLevel(level: string): void }
|
|
254
|
+
const setThinkingLevel = vi.fn((level: string) => { session.thinkingLevel = level })
|
|
255
|
+
Object.assign(session, { thinkingLevel: 'low', getAvailableThinkingLevels: () => ['off', 'minimal', 'low', 'medium', 'high'], supportsThinking: () => true, setThinkingLevel })
|
|
256
|
+
expect(app.dispatchInput('\x1b[1;2A')).toEqual({ consume: true })
|
|
257
|
+
expect(app.dispatchInput('\x1b[1;2A')).toEqual({ consume: true })
|
|
258
|
+
expect(app.dispatchInput('\x1b[1;2A')).toEqual({ consume: true })
|
|
259
|
+
expect(setThinkingLevel.mock.calls).toEqual([['medium'], ['high']])
|
|
260
|
+
expect(app.dispatchInput('\x1b[1;2B')).toEqual({ consume: true })
|
|
261
|
+
expect(setThinkingLevel).toHaveBeenLastCalledWith('medium')
|
|
262
|
+
await application.shutdown()
|
|
263
|
+
})
|
|
264
|
+
|
|
89
265
|
it('restores terminal modes and finishes even when runtime and plugin cleanup fail', async () => {
|
|
90
266
|
const { application, app } = setup()
|
|
91
267
|
const header = { dispose: vi.fn(() => { throw new Error('header cleanup failed') }) }
|