Skip to content

feat(coding-agent): add /steer and /followup slash commands#2673

Open
metaphorics wants to merge 1 commit into
can1357:mainfrom
metaphorics:feat/steer-followup-slash-commands
Open

feat(coding-agent): add /steer and /followup slash commands#2673
metaphorics wants to merge 1 commit into
can1357:mainfrom
metaphorics:feat/steer-followup-slash-commands

Conversation

@metaphorics

Copy link
Copy Markdown
Contributor

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 turn

Why

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

State /steer /followup
streaming force streamingBehavior: "steer" force streamingBehavior: "followUp"
compacting queue via queueCompactionMessage queue via queueCompactionMessage
idle notice, no new turn notice, no new turn
empty args usage hint usage hint

The command name is authoritative — a shared handleTui factory forces the behavior and fully consumes the line, so the command wins regardless of which key submitted it.

Implementation

  • One shared factory (makeQueueSlashHandler) plus two entries in BUILTIN_SLASH_COMMAND_REGISTRY (src/slash-commands/builtin-registry.ts), placed next to /force. The factory mirrors the streaming-submit block in InputController (withLocalSubmissionsession.prompt(..., { streamingBehavior, images }), clear editor/pending images, repaint).
  • Both are TUI-only (no handle), so ACP_BUILTIN_SLASH_COMMANDS excludes them automatically — ACP has native steer/follow_up RPC. Autocomplete, reserved names, and ACP filtering all derive from the same registry array, so no other wiring is needed.
  • Host-only for collab guests, consistent with other session-mutating built-ins.

Tests

New test/steer-followup-slash.test.ts drives the real executeBuiltinSlashCommand with a faked context and asserts: steer-while-streaming, followUp-while-streaming, the follow-up alias, idle-reject, compaction-queue, and empty-args usage.

Verification

  • bun check (typecheck + biome): pass
  • bun test test/steer-followup-slash.test.ts: 6 pass / 0 fail

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.
Copilot AI review requested due to automatic review settings June 15, 2026 13:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /steer and /followup (with /follow-up alias) 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.

Comment on lines +124 to +152
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;
}
Comment on lines +125 to +129
if (!message) {
ctx.showStatus(`Usage: /${displayName} <message>`);
ctx.editor.setText("");
return;
}
Comment on lines +133 to +160
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 = [];
Comment on lines +74 to +79
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/);
});

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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("");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add /steer and /followup slash commands mirroring the steer/follow-up keybindings

2 participants