feat(mode): register one slash command per mode
Replace /mode [name] with same-named no-argument commands such as /plan. Update the TUI and ACP snapshots, and validate mode names against command syntax.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# tui-agent
|
||||
|
||||
The full-screen interactive coding agent: DeepSeek V4, local bash and filesystem tools, compaction, subagents, workflows and fresh-agent Ralph iteration, `todo_write`, session modes (`/mode`, plan mode with the reviewed exit), timeout/spill policy, and [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo).
|
||||
The full-screen interactive coding agent: DeepSeek V4, local bash and filesystem tools, compaction, subagents, workflows and fresh-agent Ralph iteration, `todo_write`, session modes (`/plan` enters plan mode, which has a reviewed exit), timeout/spill policy, and [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo).
|
||||
|
||||
## Run it
|
||||
|
||||
@@ -10,7 +10,7 @@ pnpm run demo:tui
|
||||
|
||||
The command needs `DEEPSEEK_API_KEY` in the environment or gitignored repository-root `.env`. Set `RESUME_SESSION_ID` to reopen a persisted conversation under `./.sessions`.
|
||||
|
||||
The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and the latest todo list. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model <model>` and `/model <provider>/<model>` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options.
|
||||
The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and the latest todo list. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/plan` selects plan mode for the next turn. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model <model>` and `/model <provider>/<model>` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options.
|
||||
|
||||
Run `pnpm run demo:code-mode tui` for the Code Mode overlay.
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
name: '@deepseek-ai/dsh-tool-todo'
|
||||
|
||||
# Session modes (the shipped `plan` definition): the TUI's command registry
|
||||
# gains the plugin-registered /mode command, and the exit review rides the
|
||||
# gains the plugin-registered /plan command, and the exit review rides the
|
||||
# TUI's user-interaction provider.
|
||||
- id: mode
|
||||
name: '@deepseek-ai/dsh-mode'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
terminal 100x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "Reply with exactly the word: — DSH TUI snapshot"
|
||||
cursor hidden column=1 viewportRow=28 bufferRow=28
|
||||
cursor hidden column=1 viewportRow=30 bufferRow=30
|
||||
buffer
|
||||
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
|
||||
style 0-99 fg=bright-blue
|
||||
@@ -21,50 +21,53 @@ buffer
|
||||
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
style 0-99 fg=bright-blue
|
||||
5| <blank>
|
||||
6| "▌ "
|
||||
6| " Entering plan mode (applies from the next turn). "
|
||||
style 1-48 fg=bright-black
|
||||
7| <blank>
|
||||
8| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
7| "▌ You "
|
||||
9| "▌ You "
|
||||
style 0-0 fg=bright-blue
|
||||
style 2-4 fg=bright-blue bold
|
||||
8| "▌ Reply with exactly the word: ONE. No tools. "
|
||||
10| "▌ Reply with exactly the word: ONE. No tools. "
|
||||
style 0-0 fg=bright-blue
|
||||
9| "▌ "
|
||||
11| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
10| <blank>
|
||||
11| " Reasoning "
|
||||
12| <blank>
|
||||
13| " Reasoning "
|
||||
style 1-9 fg=bright-black italic
|
||||
12| " The user wants me to reply with exactly the word \"ONE\" and use no tools. "
|
||||
14| " The user wants me to reply with exactly the word \"ONE\" and use no tools. "
|
||||
style 1-72 fg=bright-black italic
|
||||
13| <blank>
|
||||
14| " Assistant "
|
||||
15| <blank>
|
||||
16| " Assistant "
|
||||
style 1-9 fg=bright-magenta bold
|
||||
15| " ONE "
|
||||
16| <blank>
|
||||
17| "▌ "
|
||||
17| " ONE "
|
||||
18| <blank>
|
||||
19| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
18| "▌ You "
|
||||
20| "▌ You "
|
||||
style 0-0 fg=bright-blue
|
||||
style 2-4 fg=bright-blue bold
|
||||
19| "▌ Reply with exactly the word: TWO. No tools. "
|
||||
21| "▌ Reply with exactly the word: TWO. No tools. "
|
||||
style 0-0 fg=bright-blue
|
||||
20| "▌ "
|
||||
22| "▌ "
|
||||
style 0-0 fg=bright-blue
|
||||
21| <blank>
|
||||
22| " Reasoning "
|
||||
23| <blank>
|
||||
24| " Reasoning "
|
||||
style 1-9 fg=bright-black italic
|
||||
23| " The user wants me to reply with exactly the word \"TWO\" and no tools. "
|
||||
25| " The user wants me to reply with exactly the word \"TWO\" and no tools. "
|
||||
style 1-68 fg=bright-black italic
|
||||
24| <blank>
|
||||
25| " Assistant "
|
||||
26| <blank>
|
||||
27| " Assistant "
|
||||
style 1-9 fg=bright-magenta bold
|
||||
26| " TWO "
|
||||
27| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
28| " "
|
||||
style 1-1 inverse
|
||||
28| " TWO "
|
||||
29| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
30| "/tmp/dsh-tui-snapshot-multi-tu ↑2.9k ↓41 3% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
30| " "
|
||||
style 1-1 inverse
|
||||
31| "────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
style 0-99 dim
|
||||
32| "/tmp/dsh-tui-snapshot-multi-tu ↑2.9k ↓41 3% context tools:compact deepseek-v4-flash(reasoning:on)"
|
||||
style 0-29 dim
|
||||
style 42-99 dim
|
||||
31-35| <blank>
|
||||
33-35| <blank>
|
||||
|
||||
@@ -9,7 +9,7 @@ const scriptedConfigPath = fileURLToPath(new URL('./fixtures/tui-scripted.cordis
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
||||
it('boots pi-tui, renders the configured banner, accepts /exit, and restores the terminal', async () => {
|
||||
it('boots pi-tui, enters plan mode through /plan, accepts /exit, and restores the terminal', async () => {
|
||||
const output = await runTuiPtySmoke({
|
||||
label: 'tui-agent boot',
|
||||
tempDirPrefix: 'tui-agent-smoke-',
|
||||
@@ -17,10 +17,14 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call' },
|
||||
actions: [{ waitFor: 'TUI agent ready.', send: '/exit\r' }],
|
||||
actions: [
|
||||
{ waitFor: 'TUI agent ready.', send: '/plan\r' },
|
||||
{ waitFor: 'Entering plan mode (applies from the next turn).', send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).toContain('DEEPSEEK')
|
||||
expect(output).toContain('TUI agent ready.')
|
||||
expect(output).toContain('Entering plan mode (applies from the next turn).')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
|
||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import { installLlmReplay, parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
|
||||
import ModesService, { DEFAULT_MODE, PLAN_MODE } from '@deepseek-ai/dsh-mode'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -45,6 +46,7 @@ interface Scenario {
|
||||
expectedTools: string[]
|
||||
expectedEventCounts?: Record<string, number>
|
||||
childSessions?: number
|
||||
enterPlanMode?: boolean
|
||||
recorded: boolean
|
||||
seedWorkspace?: boolean
|
||||
}
|
||||
@@ -54,6 +56,8 @@ const SCENARIOS: Scenario[] = [
|
||||
name: 'multi-turn-conversation',
|
||||
composition: 'native',
|
||||
expectedTools: [],
|
||||
expectedEventCounts: { 'mode/set': 1 },
|
||||
enterPlanMode: true,
|
||||
recorded: true,
|
||||
},
|
||||
{
|
||||
@@ -204,6 +208,9 @@ async function mountScenarioContext(
|
||||
await ctx.plugin(ToolWorkflow)
|
||||
await ctx.plugin(ToolRalph)
|
||||
await ctx.plugin(CommandService)
|
||||
if (scenario.enterPlanMode === true) {
|
||||
await ctx.plugin(ModesService, { modes: { plan: { section: 'Snapshot plan mode instructions.' } } })
|
||||
}
|
||||
if (scenario.composition === 'code' || scenario.composition === 'advanced') {
|
||||
await ctx.plugin(WorkerCodeRuntime, {})
|
||||
}
|
||||
@@ -268,6 +275,13 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
|
||||
})
|
||||
await settleTerminal(terminal)
|
||||
|
||||
if (scenario.enterPlanMode === true) {
|
||||
terminal.send('/plan')
|
||||
terminal.send('\r')
|
||||
await settleTerminal(terminal)
|
||||
expect(ctx.modes.get(agent)).toEqual({ current: DEFAULT_MODE, pending: PLAN_MODE })
|
||||
}
|
||||
|
||||
for (const prompt of prompts) {
|
||||
terminal.send(prompt)
|
||||
terminal.send('\r')
|
||||
@@ -280,6 +294,16 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
|
||||
for (const [type, count] of Object.entries(scenario.expectedEventCounts ?? {})) {
|
||||
expect(events.filter(event => event.type === type), `${scenario.name} must emit ${type}`).toHaveLength(count)
|
||||
}
|
||||
if (scenario.enterPlanMode === true) {
|
||||
expect(ctx.modes.get(agent)).toEqual({ current: PLAN_MODE })
|
||||
const modeSet = events.find(event => event.type === 'mode/set')
|
||||
const firstHeader = events.find(event => event.type === 'request/header')
|
||||
if (modeSet === undefined || firstHeader === undefined) {
|
||||
throw new Error('plan-mode command snapshot needs a mode/set before its first request/header')
|
||||
}
|
||||
expect(modeSet.seq).toBeLessThan(firstHeader.seq)
|
||||
expect(firstHeader.data.header.system).toContain('Snapshot plan mode instructions.')
|
||||
}
|
||||
expect(events.filter(event => event.type === 'tool/result').every(event => !event.data.isError)).toBe(true)
|
||||
expect(events.filter(event => event.type === 'turn/end').every(event => event.data.reason.kind !== 'error')).toBe(true)
|
||||
if (scenario.name === 'dynamic-workflow' || scenario.name === 'cordis-dynamic-toolchain') {
|
||||
|
||||
Reference in New Issue
Block a user