feat(mode): submit optional slash-command message

This commit is contained in:
Tianyi Cui
2026-07-22 15:51:02 +08:00
parent a8a18f309a
commit 1d28743b12
74 changed files with 124 additions and 95 deletions

View File

@@ -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. `/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.
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 step; `/plan <message>` also submits the message into that step. `/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.

View File

@@ -89,8 +89,8 @@
name: '@deepseek-ai/dsh-tool-todo'
# Session modes (the shipped `plan` definition): the TUI's command registry
# gains the plugin-registered /plan command, and the exit review rides the
# TUI's user-interaction provider.
# gains the plugin-registered /plan [message] command, and the exit review
# rides the TUI's user-interaction provider.
- id: mode
name: '@deepseek-ai/dsh-mode'
config:

View File

@@ -15,6 +15,13 @@
- id: token-meter
name: '@deepseek-ai/dsh-token-meter'
- id: mode
name: '@deepseek-ai/dsh-mode'
config:
modes:
plan:
section: 'Stay in plan mode for this scripted TUI test.'
- id: tui-agent
name: '@deepseek-ai/dsh-tui-demo'
config:

View File

@@ -21,7 +21,7 @@ buffer
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-99 fg=bright-blue
5| <blank>
6| " Entering plan mode (applies from the next turn). "
6| " Entering plan mode (applies from the next step). "
style 1-48 fg=bright-black
7| <blank>
8| "▌ "

View File

@@ -19,12 +19,12 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call' },
actions: [
{ waitFor: 'TUI agent ready.', send: '/plan\r' },
{ waitFor: 'Entering plan mode (applies from the next turn).', send: '/exit\r' },
{ waitFor: 'Entering plan mode (applies from the next step).', 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('Entering plan mode (applies from the next step).')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
@@ -38,12 +38,13 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
actions: [
{ waitFor: 'scripted TUI ready.', send: '/model\r' },
{ waitFor: 'Select model', send: '\x1b[B\r' },
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: 'exercise the TUI\r' },
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: '/plan exercise the TUI\r' },
{ waitFor: 'How should the scripted run proceed?', send: '\r' },
{ waitFor: 'Decision received. Scripted TUI run complete.', send: '/exit\r' },
],
})
expect(output).toContain('I need one decision before I continue.')
expect(output).toContain('Entering plan mode (applies from the next step).')
expect(output).toContain(String.raw`\x1b]2;MODEL_CONTROLLED\x07`)
expect(output).toContain(String.raw`\x1b[999CMODEL_CURSOR`)
expect(output).toContain(String.raw`\x9b31mMODEL_C1`)

View File

@@ -15,7 +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 ModesService, { 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'
@@ -275,14 +275,17 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
})
await settleTerminal(terminal)
let remainingPrompts = prompts
if (scenario.enterPlanMode === true) {
terminal.send('/plan')
const firstPrompt = prompts[0]!
terminal.send(`/plan ${firstPrompt}`)
terminal.send('\r')
await agent.whenIdle()
await settleTerminal(terminal)
expect(ctx.modes.get(agent)).toEqual({ current: DEFAULT_MODE, pending: PLAN_MODE })
remainingPrompts = prompts.slice(1)
}
for (const prompt of prompts) {
for (const prompt of remainingPrompts) {
terminal.send(prompt)
terminal.send('\r')
await agent.whenIdle()
@@ -303,6 +306,8 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
}
expect(modeSet.seq).toBeLessThan(firstHeader.seq)
expect(firstHeader.data.header.system).toContain('Snapshot plan mode instructions.')
const firstMessage = events.find(event => event.type === 'user/message')
expect(firstMessage?.data.content).toEqual([{ type: 'text', text: prompts[0] }])
}
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)