feat(workspace): mid-turn abort + steering for foreman chat #586

Merged
code-lead merged 2 commits from dev/564 into main 2026-04-30 19:40:32 +00:00
Collaborator

Summary

  • Abort: Press Esc (workspace-level, no composer focus required) or click the Stop button while a foreman turn is streaming. The server flips the AbortController; the SDK tears down; the task lands cancelled in task_history; a synthetic result { subtype: "aborted" } SSE event clears the client's ?task= param and triggers a session refetch. Partial assistant text is persisted via appendMessage before returning.
  • Steering: While streaming, the composer remains editable and a Queue button (replacing Send) enqueues a steering message via POST /foreman/steer/:task_id. Queued steer pills render above the streaming bubble and flip from pending (accent) to delivered (success) when the SDK consumes the message.
  • queueForemanSteer was added to bypass the forbidden_foreman guard in queueSteer, and wired through ForemanAgentDispatch + runForemanTurn (with registerSteerTask/steerInputIterator + try/finally cleanup).
  • "cancelled" added to TaskResult.status and handled in registry.ts onFinish to bypass resolveShutdownStatus.

Closes #564

Test plan

  • Start a foreman chat turn; press Esc mid-stream → turn stops, transcript shows partial text, ?task= clears
  • Stop button appears next to Send while streaming; click stops the turn same as Esc
  • While streaming, type a message and click Queue → steer pill appears (pending), flips to delivered when consumed
  • Multiple steers queue in order (FIFO), each pill tracks its own state
  • After abort, task history shows cancelled status
  • Normal (non-aborted) turns complete unchanged

🤖 Generated with Claude Code

## Summary - **Abort**: Press Esc (workspace-level, no composer focus required) or click the Stop button while a foreman turn is streaming. The server flips the `AbortController`; the SDK tears down; the task lands `cancelled` in `task_history`; a synthetic `result { subtype: "aborted" }` SSE event clears the client's `?task=` param and triggers a session refetch. Partial assistant text is persisted via `appendMessage` before returning. - **Steering**: While streaming, the composer remains editable and a Queue button (replacing Send) enqueues a steering message via `POST /foreman/steer/:task_id`. Queued steer pills render above the streaming bubble and flip from pending (accent) to delivered (success) when the SDK consumes the message. - `queueForemanSteer` was added to bypass the `forbidden_foreman` guard in `queueSteer`, and wired through `ForemanAgentDispatch` + `runForemanTurn` (with `registerSteerTask`/`steerInputIterator` + try/finally cleanup). - `"cancelled"` added to `TaskResult.status` and handled in `registry.ts` `onFinish` to bypass `resolveShutdownStatus`. Closes #564 ## Test plan - [ ] Start a foreman chat turn; press Esc mid-stream → turn stops, transcript shows partial text, `?task=` clears - [ ] Stop button appears next to Send while streaming; click stops the turn same as Esc - [ ] While streaming, type a message and click Queue → steer pill appears (pending), flips to delivered when consumed - [ ] Multiple steers queue in order (FIFO), each pill tracks its own state - [ ] After abort, task history shows `cancelled` status - [ ] Normal (non-aborted) turns complete unchanged 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(workspace): mid-turn abort + steering for foreman chat (#564)
Some checks failed
qa / dockerfile (pull_request) Successful in 5s
qa / qa (pull_request) Failing after 1m20s
3c2fa0b905
- Esc (workspace-level, capture phase) or Stop button aborts the running turn;
  server flips AbortController, task lands `cancelled` in task_history, and a
  synthetic `result { subtype: "aborted" }` SSE event clears the client stream.
- Partial assistant text is persisted via `appendMessage` before the abort returns.
- Queue button (replaces Send while streaming) enqueues a steering message via
  `POST /foreman/steer/:task_id`; queued pills render above the streaming bubble
  and flip to "delivered" when the SDK consumes the steer.
- `queueForemanSteer` bypasses the `forbidden_foreman` guard in `queueSteer`.
- `registerSteerTask` / `steerInputIterator` wired into `runForemanTurn` with
  try/finally cleanup; `onSteerQueued`/`onSteerDelivered` forwarded through
  `ForemanAgentDispatch`; `"cancelled"` added to `TaskResult.status`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(ci): update composer test to match busy-state steering behaviour
All checks were successful
qa / dockerfile (pull_request) Successful in 8s
qa / qa (pull_request) Successful in 2m44s
7985035437
When busy=true the textarea now stays enabled for mid-turn steering and
the Send button is replaced by a Queue button. The old assertion that
expected `input` to be disabled and `planner-send` to exist were both
wrong after #564 landed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-30 19:31:44 +00:00
dev force-pushed dev/564 from 7985035437
All checks were successful
qa / dockerfile (pull_request) Successful in 8s
qa / qa (pull_request) Successful in 2m44s
to db9a40ff19
All checks were successful
qa / dockerfile (pull_request) Successful in 8s
qa / qa (pull_request) Successful in 2m1s
2026-04-30 19:32:00 +00:00
Compare
Collaborator

CI still pending at review time (run #2611, sha db9a40f). Stepping off the review request — will be re-dispatched automatically when CI completes.

CI still pending at review time (run #2611, sha `db9a40f`). Stepping off the review request — will be re-dispatched automatically when CI completes.
dev requested review from reviewer 2026-04-30 19:34:13 +00:00
reviewer approved these changes 2026-04-30 19:35:53 +00:00
reviewer left a comment

All ACs from #564 met; CI green (run #2611).

Abort path is correct: currentAbort?.abort() → SDK loop breaks → partial text persisted → synthetic result { subtype: "aborted" } emitted → cancelled status threaded through TaskResult, registry.ts onFinish, and resolveShutdownStatus bypass. deregisterSteerTask in finally is the right place.

Steering path is correct: queueForemanSteer (no forbidden_foreman guard), steerInputIterator wired through ForemanAgentDispatch, steer pills build correctly from steer_queued/steer_delivered events.

Two nits not worth blocking:

  • handleForemanSteer: empty message passes null to queueForemanSteer → 409 "task not registered" instead of 400 "message required". Unreachable via the UI (client guards !trimmed) but misleading if hit directly.
  • Esc double-press: if the user presses Esc twice before the synthetic result SSE clears streaming, the second abortMutation.mutate() gets a 404 and shows an error toast despite the abort having succeeded. Could be guarded with abortMutation.isPending.
All ACs from #564 met; CI green (run #2611). Abort path is correct: `currentAbort?.abort()` → SDK loop breaks → partial text persisted → synthetic `result { subtype: "aborted" }` emitted → `cancelled` status threaded through `TaskResult`, `registry.ts` `onFinish`, and `resolveShutdownStatus` bypass. `deregisterSteerTask` in `finally` is the right place. Steering path is correct: `queueForemanSteer` (no `forbidden_foreman` guard), `steerInputIterator` wired through `ForemanAgentDispatch`, steer pills build correctly from `steer_queued`/`steer_delivered` events. Two nits not worth blocking: - `handleForemanSteer`: empty message passes `null` to `queueForemanSteer` → 409 "task not registered" instead of 400 "message required". Unreachable via the UI (client guards `!trimmed`) but misleading if hit directly. - Esc double-press: if the user presses Esc twice before the synthetic `result` SSE clears `streaming`, the second `abortMutation.mutate()` gets a 404 and shows an error toast despite the abort having succeeded. Could be guarded with `abortMutation.isPending`.
code-lead deleted branch dev/564 2026-04-30 19:40:34 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!586
No description provided.