feat(chat): steer prompts into a running ACP turn, hide stop while only background work runs
What & why
In ACP chat, a message sent while background subagents were running went into the queue and waited until they finished. The main agent was idle the whole time, so you couldn't talk to it.
The cause: @agentclientprotocol/claude-agent-acp keeps session/prompt open while subagents it spawned are still running (Turn.deferredSettle). The runtime saw a running turn and queued every follow-up. The composer also kept showing stop. Pressing it sent session/cancel, and Claude's adapter then cancels every background subagent (nativeSubagentRuntime.finishAll("cancelled")).
Steering into the running turn
- Claude's and Codex's ACP adapters support the
_session/steeringextension and advertise it asinitialize._meta.steering.supported. - Adapter (
acpAdapter.ts):steer()sends_session/steeringwithidleBehavior: "promptRequired". Claude then hands the message back if no turn is running. - When it steers:
- Plain Enter steers only while the turn is waiting on background work (see below). Otherwise it queues, as before. Claude steers at priority
now, so steering every Enter would cut off the reply in progress. - Cmd+Enter, and Steer on a queued prompt, steer at any time. They use
_session/steeringinstead of cancelling the turn. - When a turn becomes background-only, prompts queued while the agent was working go into it in order. They are restamped so the transcript shows them where they were delivered.
- Plain Enter steers only while the turn is waiting on background work (see below). Otherwise it queues, as before. Claude steers at priority
- Order: steers run one at a time, in send order. Pending steers count as busy, so a prompt sent later can't overtake one.
- Fallback: if the agent doesn't accept the message, if the queue is paused, or if a different turn is running by then, the message goes back to the queue. It goes ahead of prompts sent after it. A queued prompt you chose to steer stays first and interrupts the turn, as before.
- Turn check: a prompt that names a turn that is no longer running is queued.
- Stop: stop with pause also pauses steers still pending.
- Session end: a steer still pending when the session stops is discarded.
- Other agents: pi, OpenCode (checked 1.15.5) and Gemini don't advertise steering, so they keep today's queue.
No stop button while only background work runs
- Signal: the ACP adapter reports
awaitingBackgroundon session state. It is true when the agent supports steering, the turn is still open, the agent's last model cycle has ended, and background tasks are live. Claude sendsusage_updatewithcostat each result, so that marks the cycle end. - Composer: while
awaitingBackgroundis true it shows send instead of stop. Stopping background work stays in the background-tasks menu at the top. - Protocol:
awaitingBackgroundis a new optional field. Older clients ignore it, and newer clients against an older host see it missing and show stop as before.
Known limit (Codex): Codex has no promptRequired option. If its turn ends just as a steer arrives, Codex starts a new turn itself and answers startedNewTurn. codex-acp never tells the client when that turn ends (turn/completed stays inside the adapter), so the runtime can't track it. Status can read idle while Codex answers. The window is the moment between Codex finishing its turn and our session/prompt response. Codex never reports awaitingBackground, so only an explicit Steer can hit it. The real fix is upstream: codex-acp should honor promptRequired, which our adapter already sends.
How I tested it
- Unit tests:
packages/chat-runtime(223) andpackages/chat(42) pass, and typecheck passes for both. Repo-widebun run lintis clean. There is a regression test for each review finding. The ones for "plain Enter queues while working", "later prompt can't overtake", and "pause stops an undispatched steer" fail on the previous commit. - Real agents, through the runtime with a script:
- Claude: a message sent while Claude was working queued. Once the turn was background-only, it went in and Claude answered it. A later message went straight in and was answered in about 2 s while the subagent was still running.
awaitingBackgroundturned on during the hold and off while each answer streamed. - Claude baseline (main): the follow-up queued and was answered about 77 s later, after the subagent finished.
- Codex (
gpt-5.5): a steered message joined the running turn. The tool row endscompletedwith and without steering.
- Claude: a message sent while Claude was working queued. Once the turn was background-only, it went in and Claude answered it. A later message went straight in and was answered in about 2 s while the subagent was still running.
- Dev app over CDP (this branch, local stack, Local Admin, Claude chat):
- While Claude was working, an early follow-up queued, with Steer and delete controls and the stop button.
- Once the turn was background-only, it went in and was answered, in the right order. The composer showed send and the top indicator showed 1 background task.
- "17 + 25" went straight in: stop at 2.6 s, "42" at 2.8 s, back to send at 3.5 s.
- The turn closed normally when the subagent finished.
- Local typecheck of
desktop,host-serviceandmobilefails only on better-auth types inpackages/authandpackages/trpc, which this PR doesn't touch. Two copies of@better-auth/coreresolved in this checkout. CI Typecheck passes.
Checklist
- PR title follows conventional commits (
type(scope): subject) -
bun run lintpasses. Typecheck passes for the changed packages; see the note above for desktop, host-service and mobile locally. - "Allow edits from maintainers" is checked on fork PRs
Summary by CodeRabbit
- New Features
- Prompts can be sent directly to an eligible running turn when steering is supported, including while it waits on background work; prompts that cannot be accepted remain queued.
- Sessions indicate when a turn has finished its current cycle and is waiting for background work.
- Bug Fixes
- Turn cancellation is unavailable while background work is pending.
- Queued prompts retain their order when steering is declined, canceled, or unavailable.