feat(coding-agent): add /steer and /followup slash commands#2673
feat(coding-agent): add /steer and /followup slash commands#2673metaphorics wants to merge 1 commit into
Conversation
Steering (interrupt the running turn) and follow-up (queue a message to drain after the turn) were reachable only via keybindings — plain Enter while streaming steers; app.message.followUp (Ctrl+Q / Ctrl+Enter) queues. Add /steer <message> and /followup <message> as a typed, discoverable alternative; the keybindings are unchanged. The command name is authoritative: a shared handleTui factory forces the streamingBehavior (steer vs followUp) and fully consumes the line, so the command wins regardless of which key submitted it. It rejects with a notice when the agent is idle, queues during compaction (parity with the keybinding paths), and shows a usage hint on empty input. Both are TUI-only (no `handle`), so they are naturally excluded from ACP, which has native steer/follow_up RPC.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds /steer and /followup built-in slash commands to mirror the existing “steer” and “follow-up” submission behaviors, including documentation and tests.
Changes:
- Implement
/steerand/followup(with/follow-upalias) in the built-in slash command registry. - Add Bun tests validating behavior across streaming/idle/compacting states.
- Document the new commands and note them in the changelog.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/coding-agent/src/slash-commands/builtin-registry.ts | Adds a shared TUI handler factory and registers the new slash commands. |
| packages/coding-agent/test/steer-followup-slash.test.ts | Adds tests ensuring behavior matches streaming/compaction expectations. |
| packages/coding-agent/CHANGELOG.md | Notes the new slash command feature under Unreleased. |
| docs/keybindings.md | Documents /steer + /followup as alternatives to keybindings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const message = command.args.trim(); | ||
| if (!message) { | ||
| ctx.showStatus(`Usage: /${displayName} <message>`); | ||
| ctx.editor.setText(""); | ||
| return; | ||
| } | ||
| // Compaction in progress: queue exactly like the keybinding paths so a | ||
| // message typed mid-compaction is not lost. The slash dispatcher does not | ||
| // clear the editor for `handleTui`, so clear it here. | ||
| if (ctx.session.isCompacting) { | ||
| const images = ctx.pendingImages.length > 0 ? [...ctx.pendingImages] : undefined; | ||
| ctx.editor.addToHistory(message); | ||
| ctx.editor.setText(""); | ||
| ctx.editor.imageLinks = undefined; | ||
| ctx.pendingImages = []; | ||
| ctx.pendingImageLinks = []; | ||
| ctx.queueCompactionMessage(message, behavior, images); | ||
| return; | ||
| } | ||
| // No active turn: reject — there is nothing to steer/queue behind. | ||
| if (!ctx.session.isStreaming) { | ||
| ctx.showStatus( | ||
| behavior === "steer" | ||
| ? "Nothing to steer — the agent is not running." | ||
| : "Nothing to queue — the agent is not running.", | ||
| ); | ||
| ctx.editor.setText(""); | ||
| return; | ||
| } |
| if (!message) { | ||
| ctx.showStatus(`Usage: /${displayName} <message>`); | ||
| ctx.editor.setText(""); | ||
| return; | ||
| } |
| if (ctx.session.isCompacting) { | ||
| const images = ctx.pendingImages.length > 0 ? [...ctx.pendingImages] : undefined; | ||
| ctx.editor.addToHistory(message); | ||
| ctx.editor.setText(""); | ||
| ctx.editor.imageLinks = undefined; | ||
| ctx.pendingImages = []; | ||
| ctx.pendingImageLinks = []; | ||
| ctx.queueCompactionMessage(message, behavior, images); | ||
| return; | ||
| } | ||
| // No active turn: reject — there is nothing to steer/queue behind. | ||
| if (!ctx.session.isStreaming) { | ||
| ctx.showStatus( | ||
| behavior === "steer" | ||
| ? "Nothing to steer — the agent is not running." | ||
| : "Nothing to queue — the agent is not running.", | ||
| ); | ||
| ctx.editor.setText(""); | ||
| return; | ||
| } | ||
| // Active turn: force this behavior. Record the local-submission signature | ||
| // so the queued message's eventual delivery leaves any later draft intact. | ||
| ctx.editor.addToHistory(message); | ||
| ctx.editor.setText(""); | ||
| ctx.editor.imageLinks = undefined; | ||
| const images = ctx.pendingImages.length > 0 ? [...ctx.pendingImages] : undefined; | ||
| ctx.pendingImages = []; | ||
| ctx.pendingImageLinks = []; |
| it("shows usage and does not prompt when /steer has no message", async () => { | ||
| const { ctx, prompt, showStatus } = makeCtx({ isStreaming: true }); | ||
| await executeBuiltinSlashCommand("/steer", { ctx }); | ||
| expect(prompt.mock.calls.length).toBe(0); | ||
| expect(showStatus.mock.calls[0]?.[0]).toMatch(/Usage/); | ||
| }); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b099d2901
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ctx.editor.addToHistory(message); | ||
| ctx.editor.setText(""); | ||
| ctx.editor.imageLinks = undefined; | ||
| const images = ctx.pendingImages.length > 0 ? [...ctx.pendingImages] : undefined; |
There was a problem hiding this comment.
Preserve post-hook images for queued slash messages
When /steer or /followup is submitted with Enter after an input extension rewrites the pending images, the controller stores the transformed attachments only in its local inputImages value before dispatching built-in slash commands, while this handler rereads ctx.pendingImages. That makes these new slash commands queue stale or dropped attachments in that context, unlike the existing streaming Enter path that sends inputImages; pass the post-hook images into the slash runtime or update the context before invoking the handler.
Useful? React with 👍 / 👎.
| // Compaction in progress: queue exactly like the keybinding paths so a | ||
| // message typed mid-compaction is not lost. The slash dispatcher does not | ||
| // clear the editor for `handleTui`, so clear it here. | ||
| if (ctx.session.isCompacting) { |
There was a problem hiding this comment.
Dispatch slash commands before the compaction shortcut
This compaction handling is only reached from the plain Enter slash dispatcher; the app.message.followUp path returns earlier from handleFollowUp when session.isCompacting and enqueues the raw line instead. In that context, submitting /steer fix or /followup fix with the follow-up key during compaction bypasses this parser, so the queued text keeps the slash command prefix (and /steer is queued with follow-up behavior) rather than honoring the command name. Move slash dispatch ahead of that compaction shortcut or special-case these commands there.
Useful? React with 👍 / 👎.
| ? "Nothing to steer — the agent is not running." | ||
| : "Nothing to queue — the agent is not running.", | ||
| ); | ||
| ctx.editor.setText(""); |
There was a problem hiding this comment.
Clear rejected slash-command images
When /steer or /followup is consumed but rejected because the session is idle, this only clears the editor text and leaves pendingImages/pendingImageLinks attached. If the user tried to steer with a pasted image, the marker disappears but the hidden image remains and is silently submitted with the next normal prompt; clear the image buffers in the rejected/usage paths as the successful queue path does.
Useful? React with 👍 / 👎.
Closes #2672
What
Adds two built-in slash commands to the interactive TUI as a typed, autocomplete-discoverable alternative to the steer / follow-up keybindings (which are unchanged):
/steer <message>— interrupt the running turn with a message/followup <message>(alias/follow-up) — queue a message to send after the current turnWhy
Previously these behaviors were only reachable by key chord (Enter-while-streaming for steer;
app.message.followUp= Ctrl+Q / Ctrl+Enter for follow-up). There was no typed path and no way to force a specific behavior independent of the submit key.Behavior
/steer/followupstreamingBehavior: "steer"streamingBehavior: "followUp"queueCompactionMessagequeueCompactionMessageThe command name is authoritative — a shared
handleTuifactory forces the behavior and fully consumes the line, so the command wins regardless of which key submitted it.Implementation
makeQueueSlashHandler) plus two entries inBUILTIN_SLASH_COMMAND_REGISTRY(src/slash-commands/builtin-registry.ts), placed next to/force. The factory mirrors the streaming-submit block inInputController(withLocalSubmission→session.prompt(..., { streamingBehavior, images }), clear editor/pending images, repaint).handle), soACP_BUILTIN_SLASH_COMMANDSexcludes them automatically — ACP has nativesteer/follow_upRPC. Autocomplete, reserved names, and ACP filtering all derive from the same registry array, so no other wiring is needed.Tests
New
test/steer-followup-slash.test.tsdrives the realexecuteBuiltinSlashCommandwith a faked context and asserts: steer-while-streaming, followUp-while-streaming, thefollow-upalias, idle-reject, compaction-queue, and empty-args usage.Verification
bun check(typecheck + biome): passbun test test/steer-followup-slash.test.ts: 6 pass / 0 fail