feat(mode): submit optional slash-command message
This commit is contained in:
@@ -6,4 +6,4 @@ Session modes: named, logged, per-agent collaboration states, with **plan mode**
|
||||
|---|---|---|
|
||||
| `mode/` | `mode/set` vocabulary + fold, the `ctx.modes` service (list/get/set with the turn-boundary flush), the `mode:policy` guidance section, and the model-facing `exit_plan_mode` review tool | `ctx.modes` |
|
||||
|
||||
The mode in force is a pure function of the session log (`SessionEventMap['mode/set']`, last one wins), so resume and fork restore it with no extra machinery. The deployment supplies plan instructions through Cordis config, while `exit_plan_mode` remains registered in every mode to keep the request tool catalog stable. UIs read flips off `session/event`; the [ACP bridge](../ui/acp) maps the vocabulary to the session-mode picker, and a composed [command registry](../ui/commands) gains one entry command per configured definition (`/plan` for the required definition). Design: [plan-mode Agent Note](../../.agents/notes/implemented/feature/2026-07-07-plan-mode.md).
|
||||
The mode in force is a pure function of the session log (`SessionEventMap['mode/set']`, last one wins), so resume and fork restore it with no extra machinery. The deployment supplies plan instructions through Cordis config, while `exit_plan_mode` remains registered in every mode to keep the request tool catalog stable. UIs read flips off `session/event`; the [ACP bridge](../ui/acp) maps the vocabulary to the session-mode picker, and a composed [command registry](../ui/commands) gains one entry command per configured definition (`/plan [message]` for the required definition, with an optional next-step message). Design: [plan-mode Agent Note](../../.agents/notes/implemented/feature/2026-07-07-plan-mode.md).
|
||||
|
||||
@@ -22,7 +22,7 @@ There is no creation-time mode option: a UI (or a plugin) selects through `set()
|
||||
|
||||
## Per-mode slash commands
|
||||
|
||||
When a command registry (`@deepseek-ai/dsh-commands`) is composed, each configured definition contributes its own entry command to interactive front doors. The required definition supplies `/plan`; a further `review` definition supplies `/review`. These commands accept no arguments, record the switch through `set()`, and report that it applies from the next turn. `default` is the absence of a definition and contributes no command. Without a commands service the child never mounts and nothing else changes.
|
||||
When a command registry (`@deepseek-ai/dsh-commands`) is composed, each configured definition contributes its own entry command to interactive front doors. The required definition supplies `/plan [message]`; a further `review` definition supplies `/review [message]`. Each command records its named switch through `set()`; when the optional message is non-empty, the handler trims it and passes it to `agent.steer()` so a running agent receives it in its next step and an idle agent starts a new turn. `default` is the absence of a definition and contributes no command. Without a commands service the child never mounts and nothing else changes.
|
||||
|
||||
Definition names must match `/^[a-z][a-z0-9_-]*$/u`, the shared mode/command subset; config fails at load before a definition can become selectable but undispatchable.
|
||||
|
||||
@@ -78,6 +78,20 @@ Each qualifying transition adds one short conversation message once. The dynamic
|
||||
|
||||
The notice itself is append-only conversation growth. A real mode transition also changes the earlier order-50 section, so that section remains the limiting cache boundary.
|
||||
|
||||
### Per-mode command message
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The command name and result remain in the direct command plane. A non-empty optional suffix is trimmed and submitted as one ordinary user text block through `agent.steer()` after the mode selection, so the resulting step sees the selected mode.
|
||||
|
||||
#### Token effect
|
||||
|
||||
The command itself adds no tokens. An optional message has the same history and token cost as submitting that text separately.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The optional message is append-only conversation growth. Entering the mode still changes the order-50 system-prompt section for the affected step.
|
||||
|
||||
### Exit tool schema and review exchange
|
||||
|
||||
#### What the model sees
|
||||
|
||||
@@ -301,12 +301,12 @@ export class ModesService extends Service {
|
||||
commandCtx.commands.register({
|
||||
name: mode,
|
||||
description: `Enter ${mode} mode`,
|
||||
input: { hint: '[message]' },
|
||||
handler: ({ agent, rawInput }) => {
|
||||
if (rawInput.trim() !== '') {
|
||||
return { kind: 'error', text: `Usage: /${mode}` }
|
||||
}
|
||||
const message = rawInput.trim()
|
||||
this.set(agent, mode)
|
||||
return { kind: 'success', text: `Entering ${mode} mode (applies from the next turn).` }
|
||||
if (message !== '') agent.steer([{ type: 'text', text: message }])
|
||||
return { kind: 'success', text: `Entering ${mode} mode (applies from the next step).` }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -610,21 +610,23 @@ describe('per-mode slash commands', () => {
|
||||
// The `ctx.inject` child mounts asynchronously once `commands` resolves.
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
const agent = await agentWithSession(ctx)
|
||||
const steer = vi.fn()
|
||||
;(agent as unknown as { steer: typeof steer }).steer = steer
|
||||
expect(ctx.commands.list(agent)).toEqual([
|
||||
{ name: 'plan', description: 'Enter plan mode' },
|
||||
{ name: 'review', description: 'Enter review mode' },
|
||||
{ name: 'plan', description: 'Enter plan mode', input: { hint: '[message]' } },
|
||||
{ name: 'review', description: 'Enter review mode', input: { hint: '[message]' } },
|
||||
])
|
||||
|
||||
const signal = new AbortController().signal
|
||||
expect(await ctx.commands.execute(agent, '/mode', signal)).toBeUndefined()
|
||||
expect(await ctx.commands.execute(agent, '/plan later', signal))
|
||||
.toEqual({ kind: 'error', text: 'Usage: /plan' })
|
||||
const plan = await ctx.commands.execute(agent, '/plan', signal)
|
||||
expect(plan).toEqual({ kind: 'success', text: 'Entering plan mode (applies from the next turn).' })
|
||||
const plan = await ctx.commands.execute(agent, '/plan draft the migration ', signal)
|
||||
expect(plan).toEqual({ kind: 'success', text: 'Entering plan mode (applies from the next step).' })
|
||||
expect(ctx.modes.get(agent)).toEqual({ current: DEFAULT_MODE, pending: PLAN_MODE })
|
||||
expect(steer).toHaveBeenCalledExactlyOnceWith([{ type: 'text', text: 'draft the migration' }])
|
||||
const review = await ctx.commands.execute(agent, '/review', signal)
|
||||
expect(review).toEqual({ kind: 'success', text: 'Entering review mode (applies from the next turn).' })
|
||||
expect(review).toEqual({ kind: 'success', text: 'Entering review mode (applies from the next step).' })
|
||||
expect(ctx.modes.get(agent)).toEqual({ current: DEFAULT_MODE, pending: 'review' })
|
||||
expect(steer).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('removes every contributed command when the mode plugin is disposed', async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ Plugin-owned human-command registry shared by the TUI and ACP adapters. The [plu
|
||||
|
||||
`parseCommand()` recognizes a slash at byte zero, a lowercase name containing letters, digits, `_`, or `-`, and either end-of-input or whitespace. It returns every byte after the name as `rawInput`, including separator whitespace; consumers own their command-specific grammar and may normalize only what that grammar permits.
|
||||
|
||||
Handlers return `success` or `error` plus optional UI text. Results are rendered directly by the adapter and never enter model history. The registry races handler completion against the supplied abort signal, but an uncooperative handler may continue its own external side effects after the caller stops awaiting it.
|
||||
Handlers return `success` or `error` plus optional UI text. Results are rendered directly by the adapter and never enter model history. The registry never submits `rawInput` to the agent implicitly; a command producer may explicitly schedule model-visible work through the receiving `Agent`, in which case that producer owns the resulting message contract. The registry races handler completion against the supplied abort signal, but an uncooperative handler may continue its own external side effects after the caller stops awaiting it.
|
||||
|
||||
## Composition
|
||||
|
||||
@@ -22,11 +22,11 @@ The terminal and ACP app bundles mount this service with their consuming front d
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Nothing. Known slash commands execute in the UI command plane, and their `CommandResult` text is not submitted as a user message. Unknown slash-command input is rejected by shipped adapters instead of becoming a model prompt.
|
||||
The registry itself submits nothing. Known slash commands execute in the UI command plane, and their `CommandResult` text is not submitted as a user message. Unknown slash-command input is rejected by shipped adapters instead of becoming a model prompt. A command producer may explicitly use the receiving `Agent`; for example, [`dsh-mode`](../../mode/mode/README.md#per-mode-slash-commands) submits the optional message in `/plan [message]` after selecting the mode.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Command discovery, execution, and UI output add no model tokens. A command plugin may separately mutate a model-visible domain through that domain's durable APIs.
|
||||
Command discovery, execution, and UI output add no model tokens. Explicit agent work scheduled by a command producer has the same token effect as the corresponding agent input.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ An embedding may provide `TuiRuntime.formatCwd` when its logical workspace label
|
||||
|
||||
Before model output, session events, tool presenters, questions, configuration, or diagnostics reach pi-tui's ANSI-aware renderers or the terminal title, the TUI renders C0 and C1 controls other than line feeds as visible `\xNN` text. Those sources cannot add terminal control sequences; the TUI and pi-tui retain ownership of terminal rendering and styling.
|
||||
|
||||
While the agent is running, ordinary editor submissions call `agent.steer()`; otherwise they call `agent.send()`. A slash at the start of the submitted line enters `ctx.commands` instead: known commands execute directly, unknown commands produce a warning, and neither path reaches the model. The TUI registers `/help`, `/model`, `/clear`, `/cancel`, `/reasoning`, `/tools`, `/redraw`, and `/exit` as agent-scoped definitions; every other effective command joins autocomplete and `/help` dynamically. Ctrl+C or Escape cancels a running turn. Tool cards collapse long bodies into a configurable head/tail preview; Ctrl+O toggles every card between its preview and full output. Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle.
|
||||
While the agent is running, ordinary editor submissions call `agent.steer()`; otherwise they call `agent.send()`. A slash at the start of the submitted line enters `ctx.commands` instead: known commands execute directly and unknown commands produce a warning, with no automatic fallthrough to the model. A command producer may explicitly schedule agent work; [`dsh-mode`](../../mode/mode/README.md#per-mode-slash-commands) uses that contract for `/plan [message]`. The TUI registers `/help`, `/model`, `/clear`, `/cancel`, `/reasoning`, `/tools`, `/redraw`, and `/exit` as agent-scoped definitions; every other effective command joins autocomplete and `/help` dynamically. Ctrl+C or Escape cancels a running turn. Tool cards collapse long bodies into a configurable head/tail preview; Ctrl+O toggles every card between its preview and full output. Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle.
|
||||
|
||||
`/model` opens the advisory `ctx.llm` catalog as a keyboard selector: Up/Down moves, Enter selects, and Escape closes it. `/model <model>` still selects an unambiguous model id directly, while `/model <provider>/<model>` selects an exact target. The configured target or latest logged request header initializes the selector, and an unlisted current model remains visible because catalogs are advisory. Selection is local to this TUI session. Prompt assembly snapshots the target for one step, replaces `{{provider}}` and `{{model}}`, and applies the same pair through `agent/request`; a switch during assembly therefore starts with a later step. The request header durably records targets that reach the model, while an unused selection remains process-local.
|
||||
|
||||
@@ -58,7 +58,7 @@ The palette uses the standard 16-color ANSI foregrounds and SGR attributes, whic
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Each non-empty ordinary editor submission becomes one text block, sent with `agent.send()` while the target agent is idle and `agent.steer()` while it is running. Slash commands and keybindings are TUI-only; command results remain terminal notices.
|
||||
Each non-empty ordinary editor submission becomes one text block, sent with `agent.send()` while the target agent is idle and `agent.steer()` while it is running. Slash commands and keybindings are TUI-only; command results remain terminal notices. A command producer may schedule a separate agent input, such as the optional message accepted by `/plan [message]`.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
Reference in New Issue
Block a user