Repository navigation
feat(composer): keep the composer usable while an approval is pending - #845
Merged
Merged
Conversation
…swer A respond-approval call now carries `unanswerable: true` when the turn has no live session or the runtime no longer knows the request id, so the renderer can tell a stale request from a delivery failure. Timeouts stay plain failures because the provider may still accept the answer. The steer response keeps its existing shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`resolveApproval` now resolves with how the answer ended (delivered, skipped or failed), and with `onUnanswerable: "close"` it closes a request the provider can no longer answer as interrupted instead of failing. `sendUserMessage` accepts `declinePendingApprovals`: a steer into a running turn may pass the pending-approval guard, and the callback runs only after every other steer check has passed, so a refused message leaves the approvals alone. Such a redirect steers even with Mid-Turn Steering off. `sendWithApprovalRedirect` declines the approvals one at a time, stops on the first failure, drops text edited meanwhile, and queues the message when the turn cannot take a steer instead of dropping it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a running turn waits on approvals, the composer stays enabled. Its placeholder says that sending declines the pending approvals and redirects the agent, and the send button reads "Decline and steer" (or "Decline and queue next turn" when the turn's provider cannot take a steer). Sending declines every pending approval, then steers the message into the same turn; a failed decline sends nothing and keeps the draft. `/aside`, queued-message edits and messages the send path refuses leave the approvals alone. The approval card and its Enter/Tab shortcuts behave as before, and Enter/Tab in an empty composer act on the card too. The window shortcuts move into `useApprovalWindowShortcuts`, and the placeholder and send labels into `prompt-input-submit-labels.ts`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
astyfx
requested review from
heath-s and
png-1105
and removed request for
a team
October 28, 2026 00:11
|
| Commit | Scanned at | New | Resolved | Net |
|---|---|---|---|---|
9d287c0 |
2026-10-28 00:11 UTC | 0 | 0 | 0 |
1bfcab3 |
2026-10-28 00:50 UTC | 0 | 0 | 0 |
b86e8f4 |
2026-10-28 00:56 UTC | 0 | 0 | 0 |
20f1eaa |
2026-10-28 01:15 UTC | 0 | 0 | 0 |
9351395 < |
2026-10-28 01:42 UTC | 0 | 0 | 0 |
Last scanned: 9351395 · 2026-10-28 01:42 UTC
The redirect read the pending approvals once, so an approval that arrived while an earlier one was being declined (a parallel tool call, a helper) stayed pending while the message was steered and the draft cleared. On the queue path the send was refused with no explanation. `declinePendingApprovals` now reads the list again after each decline and declines every request it has not tried yet, at most 32. A request still pending after its decline, or approvals that keep arriving past the bound, fail the redirect: nothing is sent and the draft stays. A send refused because an approval waits now reports `reason: "approval-pending"`, and the composer explains it instead of staying silent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the user pressed Stop while a redirect was declining approvals, the steer into the stopped turn was refused and the redirect fell back to the queue path. With no turn running, that started a new turn and cleared the draft. The redirect now captures the turn it targets when it starts. After the declines, it checks whether that turn was recorded as stopped (`retainedTurnActivityByTask` with outcome `stopped`, which Stop writes). If so, it ends with `turn-stopped`: nothing is steered, queued or started, the draft stays, and no toast is shown. A turn that ends on its own is recorded as completed, so its redirect still delivers the message as the next turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The redirect read the stop from the retained turn activity cache when it needed it. That cache keeps the latest eight tasks only. If enough other tasks finished turns between the Stop and the end of the decline, the `stopped` record was evicted, the stop went unseen, and the redirect started a new turn and cleared the draft. While a redirect runs, `submitApprovalRedirect` now subscribes to the store and latches the stop as soon as the targeted turn is recorded as stopped. Per update the listener compares only this task's retained entry reference. A stop already recorded before the subscription still counts, and the subscription is always disposed in a `finally`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
astyfx
enabled auto-merge (squash)
October 28, 2026 01:32
Bring in #839, #840, #841, #842, #844 and #846. - Placeholder: one resolver. Main's `resolvePromptPlaceholder` now also takes `approvalRedirect` and returns the decline-and-steer or decline-and-queue keys. The duplicate `resolvePromptInputPlaceholder` is removed, and its tests now go through `resolvePromptPlaceholder`. - Approval keys: main's `useComposerApprovalShortcuts` now returns the Enter/Tab handler the empty editor uses. `use-approval-window-shortcuts.ts` is removed. - The composer's redirect-mode derivation moves into `resolveComposerApprovalRedirectMode`, which keeps `ChatInputComposer.tsx` and `prompt-input.tsx` within main's lowered line limits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
While a running turn waits on approvals, the composer now stays usable. Sending a message declines every pending approval and then steers the message into the same turn. Before this change, any pending approval disabled the composer until each request was answered on the card.
Behavior
approval-interruptedand skipped, so it does not block the send. Main reports this asunanswerable: trueonprovider:respond-approval. Timeouts stay failures because the provider may still accept the answer./aside, queued-message edits (the queue shelf stays disabled, as before), and messages the send path refuses. The steer path calls the decline callback only after every other steer check has passed. The queue path checks the Stave Auto refusal before declining.Provider coverage
canUseToolresolvesdenywithoutinterrupt, so the turn continuesclaude-sdk-runtime.tsapproval responder and steer responder). Store test with the bridge mockeddecision: "decline"/action: "decline"(notcancel, which ends the turn); the JSON-RPC response is written beforeturn/steerturn/steeron the same turncodex-app-server-runtime.ts). Store test with the bridge mockedreject_once_session/steeracp-provider-runtime.ts). Capability-to-mode testreject_oncereject_onceNo live provider run was done; the provider side is verified by code reading and mocked-bridge tests only.
Verification
bun run lint: pass (warnings only, none in touched code)bun run typecheck: passbun run check:i18n: passbun run check:max-lines-ratchet: pass.ChatInputComposer.tsxwent from 1898 to 1874 lines,prompt-input.tsxfrom 900 to 885, andapp-store-send-user-message.tsis at 1667 lines (limit 1669). The ratchet was not raised.bun run check:design-system,check:style-channel,check:switch-exhaustiveness,check:reliability-gates,check:doc-paths: passbun run build:pages: passtests/approval-redirect-send.test.ts(15): success, decline failure, draft change, unanswerable skip, refusal before decline, steer unavailable → queue, steer rejected after decline → queue, non-steering provider, queue refusal, unconfirmed steer, no pending approvals, mode per providertests/approval-redirect-store.test.ts(8): real store, mocked bridge. Covers the order of declines before the steer,enabled: truewith steering off, failed decline, draft change, an unanswerable request closed as interrupted, the queue path, a rejected steer that gets queued, the card's Decline still reporting failures, and a plain send still blockedtests/prompt-input-submit-labels.test.ts(4)tests/provider-runtime-respond-miss.test.ts(4)bridge-persistence-regression,fleet-inline-interaction,approval-request-routing,chat-input-approval-queue,provider-runtime-steertests/e2e/composer-approval-request.e2e.ts: 6/6 pass, including 3 new tests on the real app composer (decline both, then steer;/asideleaves approvals waiting; Enter in the empty composer allows)bun run test:isolated: pass (1042 files: 965 shared, 77 isolated)case=needs-input): checked light, dark, Korean, the queue variant and a 420px composer. Before the change the editor hadcontenteditable=false. After it, the editor is editable and the send button reads "Decline and steer".approval-pendingand the composer explains it. New tests: 6 inapproval-redirect-send.test.ts(21 total), 3 inapproval-redirect-store.test.ts(11 total), 8 of them fail on the previous code.lint,typecheck,check:i18n,check:max-lines-ratchet,check:switch-exhaustiveness,check:doc-paths,build:pages: pass.retainedTurnActivityByTask(outcomestopped) for the turn captured when the redirect starts. A turn that ends on its own still gets the message as the next turn. New tests: 4 inapproval-redirect-send.test.ts(25 total) and 2 inapproval-redirect-store.test.ts(13 total). The real-store test for Stop during the decline (steer and queue) fails on the previous code with a second turn started.lint,typecheck,check:i18n,check:max-lines-ratchet,check:switch-exhaustiveness,check:doc-paths,build:pages: pass.finally. New real-store test: Stop during the decline, then 8 other tasks retire turns (thestoppedrecord is evicted). Steer and queue both send nothing, start no new turn and keep the draft. On b86e8f4 this test fails: a second turn starts. Every store test now asserts no store subscription is left after the redirect.approval-redirect-store.test.ts14 pass,approval-redirect-send.test.ts25 pass.lint,typecheck,check:i18n,check:max-lines-ratchet,check:switch-exhaustiveness,check:doc-paths,build:pages: pass.Risks
🤖 Generated with Claude Code