Skip to content

feat(chat): steer a queued follow-up into the running turn - #386

Open
heath-s wants to merge 2 commits into
mainfrom
fix/next-message-to-steer
Open

feat(chat): steer a queued follow-up into the running turn#386
heath-s wants to merge 2 commits into
mainfrom
fix/next-message-to-steer

Conversation

@heath-s

@heath-s heath-s commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

A message sent while a turn is streaming lands in the queue and waits for that turn to finish. Each queued chip now also offers a one-click action that promotes the item into the response that is already running, so the user no longer has to interrupt the turn or wait it out.

Changes

Features

  • chat: steer a staged queue item into the live turn from the queue chip. The button appears only while mid-turn steering is enabled, the provider supports it, the turn is live (not stalled), and the item is a store-backed entry with no attachments — attachments still cannot ride along with a steer, so those items keep waiting for the automatic dispatch.
  • store: submitIntent: "steer" is now the single exception to the "a queued item cannot be dispatched during a live turn" rule. On success only that item leaves the queue and the composer draft is left exactly as typed; every failure path (steer-unavailable, steer-delivery-unknown, blocked) returns before mutating, so the item stays queued and still auto-dispatches when the turn ends.

Refactors / Chores

  • store: extract the post-steer prompt-draft resolution into applySteeredPromptDraft in prompt-draft-send.ts, keeping app.store.ts under its max-lines ratchet.

Test Evidence

  • bun run typecheck — clean
  • bun run check:max-lines-ratchet / check:doc-paths / check:switch-exhaustiveness / check:reliability-gates — all pass
  • bun test tests/custom-theme.test.ts tests/prompt-input-queue-mode.test.tsx tests/prompt-draft-send.test.ts tests/bridge-persistence-regression.test.ts tests/provider-steer-delivery.test.ts tests/chat-state-helpers.test.ts tests/queued-task-turn-dispatch.test.ts — 145 pass, 0 fail
  • Full bun test: 3849 pass / 12 fail, byte-identical to the same 12 pre-existing failures on origin/main in this environment (provider executable resolution, secret env, untracked-diff timeout — all cross-file pollution unrelated to this change; each passes in isolation).

New coverage:

  • tests/prompt-draft-send.test.tsapplySteeredPromptDraft drops only the steered item, preserves the composer, and never clobbers a newer draft written while the steer was in flight.
  • tests/bridge-persistence-regression.test.ts — end-to-end store flow: a queued dispatch without the steer intent is still blocked; with it the item is steered, leaves the queue alone apart from itself, and a rejected steer puts nothing at risk.
  • tests/prompt-input-queue-mode.test.tsx — the chip action renders per eligible item and stays hidden for an item with attachments or a non-steerable turn.

Notes

UI change uses existing semantic tokens only (text-muted-foreground, hover:text-primary); no new theme token or built-in theme update was required.

A message sent while a turn is streaming lands in the queue and waits for
that turn to finish. Each queued chip now also offers a one-click action
that promotes the item into the response that is already running, so the
user does not have to interrupt the turn or wait it out.

- store: `submitIntent: "steer"` is now the one dispatch of a queued item
  allowed during a live turn; on success only that item leaves the queue
  and the composer draft is left untouched. Every failure path returns
  before mutating, so the item stays queued.
- store: extract the post-steer draft resolution into
  `applySteeredPromptDraft` so `app.store.ts` stays under its ratchet.
- ui: `onSteerQueuedTurn` / `canSteerQueuedTurn` on `PromptInput`, gated on
  the mid-turn steering setting, provider support, a live (non-stalled)
  turn, and a store-backed item with no attachments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@heath-s
heath-s requested a review from a team as a code owner August 21, 2026 11:33
@heath-s
heath-s requested review from JongSikLim and paul-seo73 and removed request for a team August 21, 2026 11:33

@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: c9d16078c8

ℹ️ 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".

Comment thread src/store/app.store.ts
}),
}
: promptDraftByTask;
const nextPromptDraftByTask = applySteeredPromptDraft({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reserve queued prompts before awaiting steer delivery

When the steer acknowledgment is delayed until after the active turn completes, the queued item remains visible to createQueuedTaskTurnDispatcher, which automatically starts it as a fresh turn; this later mutation only removes it after the steer reports success, so an accepted steer can execute the same prompt twice and append its transcript entry after the new turn has begun. The same duplication risk is unavoidable when delivery returns unknown, because the provider may have accepted the steer but the item is never removed before automatic dispatch. Mark the item in flight or otherwise exclude it from queue draining before awaiting delivery.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Valid — fixed in 00204c9.

Confirmed the window is real and wide: submitSteerWithDeadline waits up to RENDERER_STEER_ACK_TIMEOUT_MS (32s), and turn completion calls dispatchNextQueuedTaskTurn, which took the queue head unconditionally.

Added createSteerQueueReservations (src/store/steer-queue-reservations.ts). The item is reserved before the await and the reservation is what resolves it:

  • in-flight — no path may dispatch it (the dispatcher skips it, and sendUserMessage returns blocked for a manual send-now or a second steer of the same item).
  • accepted — released; the caller drops it from the queue in the same tick as before.
  • rejected — released, so it goes back to waiting for its normal turn.
  • unknown — kept out of automatic dispatch only. A duplicate run is worse than a prompt the user re-sends deliberately, and they already got the "delivery unconfirmed" toast, so manual send/re-steer stays open.

Holds are in-memory and keyed by the item UUID, so a reload drops them with the turn they were ambiguous about and a stale key can never match a different item.

Covered by tests/steer-queue-reservations.test.ts, a dispatcher case in tests/queued-task-turn-dispatch.test.ts (reserved head skipped, next unreserved item dispatched), and a store-level case in tests/bridge-persistence-regression.test.ts.

Comment thread src/components/session/ChatInput.tsx Outdated
Comment on lines +645 to +649
const canSteerQueuedTurns =
midTurnSteeringEnabled &&
providerSupportsMidTurnSteering({ providerId: args.activeProvider }) &&
args.isTurnActive &&
providerTurnDisplayState !== "stalled";

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 Check the running provider before offering queue steering

When the user changes the enabled model/provider selector while a turn is running, args.activeProvider becomes the newly selected task provider rather than the provider serving the active turn. Consequently, switching away from a steer-capable running provider hides these new actions, while switching toward one exposes actions that resolveMidTurnSteeringContext rejects after the click. Derive this capability from the matching providerTurnActivity.providerId (with the same history fallback used by the store) instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Right in principle — fixed in 00204c9, though it is latent today: both descriptors set supportsMidTurnSteering: true, so no provider switch can currently make the UI and resolveMidTurnSteeringContext disagree. It would start biting silently the moment a provider turns the capability off, so worth closing now.

Extracted the store's own resolution into resolveActiveTurnProviderId (activity snapshot matching activeTurnId, else the getRespondingProviderId history fallback) and used it in both places, so the chip affordance and the dispatch gate cannot drift.

Left canSteerActiveTurn (composer Enter/Tab) on the selector: that is pre-existing behaviour and this PR deliberately does not touch the Enter/Tab affordances. Happy to follow up separately if you want them unified.

@upwind-code-us

upwind-code-us Bot commented Aug 21, 2026

Copy link
Copy Markdown

Upwind Upwind Code Scan - ✅ Proceed with Deployment

0 newly introduced vulnerabilities · 0 resolved · 81 total in this PR vs main

🔴 1 Critical | 🔶 30 High | 🟡 47 Medium | 🟢 3 Low

View full analysis in Upwind Console

Scan completed in 16s

Scan history (2 scans)
Commit Scanned at New Resolved Net
c9d1607 2026-08-21 11:36 UTC 0 0 0
00204c9 < 2026-08-21 12:24 UTC 0 0 0

Last scanned: 00204c9 · 2026-08-21 12:24 UTC

@upwind-code-us

upwind-code-us Bot commented Aug 21, 2026

Copy link
Copy Markdown

Upwind Upwind IaC Scan - ✅ Proceed with Deployment

0 newly introduced misconfigurations · 0 resolved · 0 total in this PR vs main

View full analysis in Upwind Console →

Scan completed in 4s

Scan history (2 scans)
Commit Scanned at New Resolved Net
c9d1607 2026-08-21 11:36 UTC 0 0 0
00204c9 < 2026-08-21 12:24 UTC 0 0 0

Last scanned: 00204c9 · 2026-08-21 12:24 UTC

Steering a staged queue item awaits the provider acknowledgement for up to
RENDERER_STEER_ACK_TIMEOUT_MS. The running turn can finish inside that window,
and turn completion drains the queue — so the very item being steered could be
started as a fresh turn while the provider was still accepting it into the old
one, running the prompt twice. Reserve the item before the await: nothing may
dispatch it while delivery is undecided, an accepted steer drops it as before, a
rejected steer returns it to the queue, and an unconfirmed one stays queued but
out of automatic dispatch so the user decides whether to send it again.

Also derive queue-steer eligibility from the provider serving the running turn
instead of the model selector, which the user can switch mid-turn without
retargeting that turn. Extracted for the app.store.ts max-lines ratchet:
buildFailedSteerResult, plus resolveActiveTurnProviderId now shared by the store
gate and the chip affordance so the two cannot drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@heath-s
heath-s enabled auto-merge (squash) August 21, 2026 12:28
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.

1 participant