Skip to content

feat(native-chat): mid-turn messages wait as editable cards above the composer - #23731

Merged
brennanb2025 merged 224 commits into
mainfrom
mid-turn-queue-desktop
Sep 30, 2026
Merged

brennanb2025 merged 224 commits into
mainfrom
mid-turn-queue-desktop

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 22 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2982 $\color{#cf222e}{\Huge{\mathbf{−}}}$​6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2976
Prod 39 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1526 $\color{#cf222e}{\Huge{\mathbf{−}}}$​133 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1393

Where this fits: the plan for messages sent while the agent is working

The problem. In a structured (native) chat, a message you sent while the agent was still working went straight to the agent. What happened next depended on the agent:

  • Claude folds it into the work it is already doing, at its next step.
  • Other agents start a new turn or refuse it.

Orca then drew the conversation wrong:

  • The "Working for" bar jumped to the newest message.
  • A single piece of work showed up as two turns (for example "Worked for 17s" and "Worked for 52s"), with the time counted twice.
  • On the phone, the running turn's rows could disappear.

The plan. A message sent mid-turn waits as a card above the message box, with three actions:

  • Steer: send it now, into the running turn.
  • Edit.
  • Delete.

Otherwise it sends by itself when the turn ends. Only Steer puts a message into a running turn. With queueing turned off in Settings, mid-turn messages still go straight in, as they do today.

The work is split into six PRs:

# PR What it does Why the queue needs it
1 #23573 (merged) The turn's bar stays on the message that opened the turn. A steered message still lands mid-turn, and so does every mid-turn message with queueing off.
2 #23671 (merged) Chat rows are grouped by the turn that produced them, not by the nearest message above them. A steered message sits among its turn's rows. A turn the agent opens by itself gets its own bar.
3 #23621 (merged) Claude: a message folded into the running turn is recorded as joining that turn, not as a new turn. Steer on Claude is exactly this fold. Without it, every Steer splits the turn in two; desktop QA of the queue showed that split.
4 #23726 (merged) The queue itself, held by the computer running the agent. The core.
5 #23731 Desktop cards. The desktop interface.
6 #23736 (merged) Phone cards. The phone interface.

Steer on Codex also needs Codex's turn-steer support from #21062, which is still open.

The queue stays switched off, meaning the host does not advertise it and every client behaves as today, until PRs 1–6 and #21062 have landed. Turning it on is then a one-line change.

This PR is step 5: the desktop cards for the queue in #23726.

ELI5

On desktop, a message you send while the agent is still working now waits as a card just above the message box, instead of going straight into the agent's current work. You can Steer it (send it now, into the running turn), edit it, or delete it. Otherwise it sends by itself when the turn ends. This is the desktop half of the queue from #23726.

What Changed

Before: pressing Enter mid-turn sent the message into the running turn immediately, and it appeared in the transcript as if it had already been answered in order.

After, against a host that queues messages (#23726):

  • Enter mid-turn queues the message. It shows as a card above the composer and never as a transcript bubble. It becomes a normal bubble when it actually sends, either at turn end, one per turn, or when you press Steer.
  • Card actions:
    • Steer: send now, without waiting for the turn to end.
    • Delete.
    • ⋯ → Edit message: the text goes back into the composer and the card goes away.
    • ⋯ → Turn off queueing: switches the setting off from the card.
      Every action hands focus back to the composer.
  • Cmd/Ctrl+Enter mid-turn. With an empty composer, it steers the newest card. With text typed, it queues that text, so typed text is never skipped.
  • Where the queue sits. The queued messages are one box at the top of the area above the message box. Running shells and agents (the "1 shell command — 10s" strip) sit below the queue, right next to the message box, so what is working now is closest to where you type. Before this change the strip sat above the queue.
  • A paused queue shows one row above the cards, with a ▶ Resume button: "Queue paused because you interrupted", "Queue paused because Orca restarted", or "Queue paused after you cleared the conversation". The cards keep Steer, Delete and ⋯ while paused. The row shows only while Resume would send something.
  • Steer carries the ↳ icon and the tooltip "Submit without interrupting the model"; each queued card starts with a list icon.
  • Captions show why a single card is held:
    • "Couldn't send — press Send to retry";
    • a card the host sent back is worded from its typed rejection, the same words a refused message gets; one a Stop took back reads "Stopped before it was sent";
    • "Waiting — a message ahead needs attention".
  • Setting. Settings → Experimental → Chat UI gets a "Queue follow-ups" switch, on by default. It is shown only when the host actually queues messages, since otherwise it would do nothing. Searching "queue" finds it. Messages with images always send right away, and the switch's description says so.
  • Old hosts are unaffected. Against a host without the capability, the client never asks for queueing, and Stop and /clear send exactly today's requests.

Mechanism. The cards render the host's published queuedMessages list: the client stores no queue of its own. Card actions are the host's queued-message RPCs.

The send outbox asks for queue delivery only when the host advertises the capability. Each outbox entry records what its first attempt actually sent (sentDelivery), and a retry of the same id replays exactly that, so the host's record of the operation always matches. While the host's support is still unknown, a new send simply goes out as today; nothing waits on it. A send the host visibly holds as a draft frees the outbox's single in-flight slot, so the next message isn't stuck behind it.

The desktop never compares a draft id with a submission id. The host names the draft on every submission it makes from one (queuedMessageId, #23726), and the desktop reads that link to hide a card once its message is on its way, to drop its own outbox entry, and to decide what Stop may hand back.

Stop and a send already on its way. A queued send that already went out is never handed back to the composer by Stop, because the host may hold it as a paused card, and handing it back would put the same text in two places. It is marked instead: nothing resends it automatically, it shows "Message delivery is unconfirmed" with a Retry, and Retry replays the same id. A refusal that arrives after Stop is handled exactly as it would be without Stop.

Stop and /clear stay plain requests. The host keeps the drafts, and no draft text travels back to the client.

Why

Rendering the host's list means the desktop, the phone and a second window always show the same queue, and it survives a reload or restart.

The alternative, a client-side queue, was rejected. Devices would disagree, a reload would lose it, and it would need its own restore logic. Earlier versions that returned draft text to the composer on Stop kept producing restore bugs, so Stop now simply pauses the cards.

Linked Issue

None; follows the mid-turn send problems found during QA of #23537 and #23573.

Visual Proof

Queueing is switched on in a local build only; the capability stays dark until the stack lands.

Queue on top, running shells next to the message box The agent finished on its own; the queued message sent as its own turn Two queued messages send one per turn after the agent finishes
Steer: ↳ icon and tooltip Stop pauses the queue: one row with Resume Resume drains the cards one per turn
Steer while paused After /clear A Stop with an empty queue leaves no pause

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Latest live QA (current head, 64d9ecf3d4e). Second Mac, real Claude CLI, queueing switched on in a local build only. Both scenarios pass, with nothing touched while the agent worked:

  • One queued message, agent finishes on its own. A turn ran shell commands for 59 s. A message queued 20 s in ("Reply with the word banana.") showed as a row above the "1 shell command — 10s" strip. The strip sat directly on the message box (measured: queue bottom 860, strip 864–906, message box 914). When the turn ended by itself, the message sent as its own turn and Claude answered "banana". No pause row appeared.
  • Two queued messages, agent finishes on its own. After the long turn ended, "fig" sent and was answered, then "grape". One per turn, in order, nothing lost or sent twice.

Earlier live QA (queue-level pause). Live desktop QA on a second Mac with a real Claude CLI, queueing switched on in a local build only: 6/6 scenarios pass at e4e63654a1c (tooltip and icons; Stop with two cards then Resume; Stop then a new message; Steer while paused; /clear; a Stop with an empty queue leaves no pause), and the grouped-box restyle was re-shot at a7658beb4f9.

Live desktop QA ran on a second Mac with a real Claude CLI, 8/8 scenarios passing:

  • queue and drain;
  • Delete;
  • Edit;
  • Steer;
  • Stop pausing two cards, then draining after the next message;
  • /clear keeping a paused card;
  • turning queueing off;
  • Cmd+Enter.

That run was at an earlier head of this stack, before it was rebuilt on #23026's final Stop design and before four review rounds of fixes (the draft link, what Stop keeps, and the recorded wire field). The card flows are the same; the Stop and lost-answer cases changed since, and are covered by the automated tests below. The phone QA for #23726 and PR4 also checked that the desktop shows the same cards as the phone at every step.

Automated tests cover:

  • card states and actions;
  • the Enter and Cmd+Enter queue and steer gating, including the remembered setting against a host with and without the capability;
  • the outbox freeing its in-flight slot for a held draft;
  • a lost Stop or /clear answer;
  • Stop and /clear sending today's exact requests to an old host;
  • the settings switch's visibility (removing the capability check fails two tests);
  • the search keyword;
  • the draft link: card hiding, outbox drop, and composer restore read only queuedMessageId;
  • Stop during a send in flight, in doubt, or after a reload: kept, never resent automatically, retried under the same id; a refusal after Stop rotates as usual;
  • the recorded wire field: a retry replays what was first sent, an unknown capability never holds a send, a host known not to queue gets a plain replay.

The PR's 19 test files (142 tests) pass, as do main's outbox, Stop, withdrawal and delivery-notice suites next to them. Web and node typecheck, oxlint, the anti-slop and code-quality lints, the changed-code quality gate, and the localization catalog and extraction checks all pass. The strings are in all six catalogs.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

brennanb2025 and others added 30 commits September 24, 2026 23:36
No client ever called agentSession.requestHandoff or mounted the handoff
chrome. Delete the handoff coordinator, the terminal-owner runtime, the
proof write path and the unmounted UI. Keep agentSession.handoffStatus,
which released desktop clients read for worktree activation, and let
records an older build left mid handoff reconcile through the ordinary
restart and recovery paths.
Eviction now drains delivered events before quit's resume-offer snapshot. An
unbounded wait there sits ahead of the provider stop, so a sink whose journal
write stalls kept the child running until the step deadline aborted the
eviction. The offer is advisory: bound the drain and stop the child regardless.

Co-Authored-By: Claude <noreply@anthropic.com>
`claudeAuthEnvCarriedForward`, `isPathWithinDirectory` and
`queryWindowsProcessRowsFresh` lost their last caller with the handoff. The
fresh-scan tests now go through `queryWindowsProcessDescendants({ fresh: true })`,
the teardown path that still depends on that contract.

Co-Authored-By: Claude <noreply@anthropic.com>
Six comments still named the handoff coordinator, a handoff suspend, or a
terminal-owned session as live participants in the flows they describe.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ement

The removed restart handoff test pinned this branch; nothing else did.

Co-Authored-By: Claude <noreply@anthropic.com>
The handoff removal dropped the per-session queue from `handoffStatus`, so a
read landing mid-start reported the reservation (no owner) instead of the
settled chat owner, and shipped desktop clients blocked worktree activation on
it. The read is queued again, as it was before the removal.

Co-Authored-By: Claude <noreply@anthropic.com>
The gate only refused a write when a PTY had been bound to a chat session, and the
only code that ever bound one was the terminal handoff this branch removes. With it
gone, every admit/readmit returned "admitted" unconditionally, so the checks on the
renderer write path, the runtime controller backstop, terminal.send, agent prompts,
preview input and orchestration pointers, the refusal fields on terminal.send and
worker-start receipts, the plugin and CLI refusal copy, and the adopted-pane
orchestration routing could no longer run. Ordinary writes take the same path in
the same order as before.

Co-Authored-By: Claude <noreply@anthropic.com>
…alled

appendLegacyTranscriptMessages fed the terminal transcript catch-up and
proveClaudeTranscriptBranch backed the terminal owner's exit proof. Both lost
their last caller with the handoff. Their tests now go through the live entry
points instead: the roster bounds through the legacy import, the pinned-read and
growth tests through the ancestry replay the history window uses, and the marker
rules through the string proof in their own file rather than the session-file
resolver's.

Co-Authored-By: Claude <noreply@anthropic.com>
A send refused because the chat's owner is not settled showed "The session is
mid-handoff (<stage>)." in the composer. With the handoff gone, the stages that
reach it are a chat that is still starting, or one whose previous agent process
has not yet been confirmed stopped. The message now says which of the two it is.
The refusal code is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
With the terminal handoff gone, the module named codex-tui-rollout-proof holds
only the pinned rollout lookup that structured Codex launches use to resume a
thread, so the name described code that no longer exists. Rename the module and
its options type. Also drop a mobile allowlist assertion that pinned the
removed agentSession.requestHandoff method, which no longer exists to allow.
The handoffStatus reply type still listed the terminal handoff's fields and
states (terminal placement, host label, proof retry, queued and waiting phases,
the to-terminal direction). No host writes them any more and the only client
reader parses the reply as unknown, so they described nothing. The reply on the
wire is unchanged.
…t decode

Nothing in this build writes a terminal owner (`runtimeKind: 'tui'`) or the
handoff's `preparing` / `old-owner-stopped` stages, but the in-memory types
still admitted them, so readers across the host kept branches for values no
path produces and the compiler could not point at them.

The store now validates the on-disk shape, which still accepts those values so
an older record is not quarantined, and maps them once while parsing:

- `preparing` and `old-owner-stopped` become `recovering`
- a `tui` lease becomes `native`; when it records a process it also becomes
  `conflicted`, the claim every build probes but never stops. A plain native
  owner would be stopped by restart recovery, here and in older builds.

Revisions are taken over the normalized state on both sides of every compare,
and the mapped record reaches disk with the store's first transaction, the
same way the tab-id backfill does.

The in-memory types narrow to what this build writes, and the branches that
existed only for the removed values go. Structured-worker identity keeps its
verdict for a former terminal owner by refusing a conflicted claim rather
than a non-native kind.
…ation

A reservation only ever names a native owner now, so the request no longer
carries a kind and the reserved lease records `native` directly. The attach
params keep `runtimeKind`: agentSession.ensure and create accept it, and the
operation fingerprint stored in the ledger covers it.
…at changes nothing else

Hiding a tab also committed the visibility index, so the no-op transaction
wrote the file even when its open-time revision was wrong. Committing the index
first leaves the pending rewrite as the only reason to write.
…ration

A write carried the fence of the last frame the pane read, and the host refused it
unless that fence was still current. An idle release and the restart after it each
move the fence, and the release publishes nothing, so a send after a release was
refused "Expected runtime fence 1; the session is at 3", and a Stop queued behind a
cold start was refused as stale.

Every write already names what it acts on: a send its conversation, a cancel its
turn, a prompt answer its item revision, a rewind its epoch; an option is
last-writer-wins. So admission stops comparing the client's fence, and the rebase
that papered over one restart (admitAtResumedFence, resumedFromFence) goes with it.
The writer-lease check stays, and so does the attach's compare-and-swap.

Frames now stamp the fence read when each frame is sent instead of a copy each
subscriber kept, which went stale on the same release.
A journal write and its delivery to open readers were two calls, and some
writers made only the first. A failed start whose lease could not be handed
back, a provider revision with no frame behind it, and eviction's settlement
were all journaled without reaching an open chat.

A journal handle now reports every durable change, and the host's session map
binds that report to the session's readers when the handle is set. Writers no
longer publish what they append; the per-writer publish calls are deleted.
…ackfill cannot supply its rewrite

The seeded record had no surface tab id, so the next open backfilled one and
that rewrite alone made the no-op transaction write. The test passed with the
legacy-lease rewrite signal removed.
The handoff removal deleted it alongside the terminal-owner tests, but it
covers the surfaced-PTY block that still guards resume, including an agent
whose ownership is unknown.
Each commit now delivers itself, so the publish a provider frame still sends
afterwards found every reader caught up but still read rows and rebuilt the
timeline for each one. A caught-up reader now skips the read.
# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts
A send to a chat with no running agent restarted the agent inside the send
call, before the message was recorded, so the client waited for the whole
start and a failed restart refused the message. Claude held prompts sent
during startup, and those could settle as "unconfirmed".

A send is now accepted inside the session's serialized queue: one ledger row
and one submission row marked handoverRecorded, published, answered pending.
A per-session delivery loop exists while a message is queued. It starts the
agent through the same serialized attach a hold uses, waits outside the queue
for a Claude child to prove its start, and hands the oldest queued message
over as its own serialized step, writing dispatch{pending} before the adapter
call. A start it needed and did not get writes one error-tone row and rejects
every queued message with the same words; a start Stop cancelled writes none.

Settlement follows from the rows. A queued message is provably unwritten, so a
close, an eviction or an exit rejects it. A handed-over message stays in doubt.
A queued row at or below the sequence a handle found when it opened was left
by an earlier process and is rejected at open, with no latch. Stop withdraws
queued messages with no writer lease and no fence. An attach failure keeps the
conversation open, and the attach adopts its journal. Owed work counts the
loop and queued rows.

A compaction or rewind found prepared when a conversation opens was started
under a child this process no longer has, so the open settles it rather than
leaving it to refuse every send until a view attaches. The open cursor is
scoped to its epoch, because sequences restart when an epoch is replaced.

Deleted: restart-before-admission, recordFailedRestart, the fence rebase,
Claude's startup gate, the attach's forget on failure and its own crash
boundary. Clients without agent-session.accepted-send.v1 get their reply held
until the handover; the desktop and paired desktop lists advertise it.
A child that proved its start and then exited before its message was handed
over left the message queued: the exit settlement returned early when nothing
else was in flight. Delivery then started another child for it, and a child
that died the same way started another, without end and without a row.

A retried settlement for an earlier generation, run by the attach that
delivery started, did the opposite: with that generation's turn unfinished it
rejected the message queued for the child being attached.

The settlement now takes the rejection for queued messages from its caller.
The unexpected exit and the eviction pass one, and it applies even with no
other work in flight; the retry for an earlier generation passes none.
…when that settlement was skipped

The Stop checked the draft table for a card to pause. When the per-row hook
that settles a withdrawn hand-off was skipped, that card was still
'dispatched', so the Stop recorded no pause; the drain later healed it back to
waiting and sent it, although the user had pressed Stop. Retirement had the
same blind spot and could drop a pause while such a card was owed.

What a pause holds back is now one predicate, judged inside the transaction
that records or retires it: a waiting card with no hold of its own (one SQL
EXISTS), or a dispatched card whose consumed submission was rejected with a
settlement back to waiting (read against the journal's submissions). The Stop
first runs the owed settlement, as the drain does; if that fails, the owed
card still counts, so the pause is recorded rather than skipped. recordPause
now checks inside its own transaction and returns whether it recorded, and any
draft-table write (and the per-row hook, the consume and the open-time
repair) retires a pause that no longer holds anything back.
…judgement when no pause exists

The retirement test recorded its second pause over a queue with nothing to hold
back, so the recording returned false and the "retired" assertion proved
nothing; ablating the per-row hook's retirement passed every test. The hold
case now asserts the pause was recorded, and a new test has a delivered echo,
through the per-row hook, withdraw the last card a recorded pause holds back.

Retirement runs on every appended journal row, so it now checks the pause row
by key first and judges nothing when no pause is recorded. Two comments were
brought in line with the owed-hand-off rule and rewrapped.

@pullfrog pullfrog Bot 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.

ℹ️ No new issues found in this delta. Two earlier inline notes remain open and unaddressed, so this is not an approval.

Reviewed changes

Re-reviewed the commits since the prior pullfrog review (b4da190 → e4e6365): the queue-level pause redesign and its follow-up fixes.

  • The Stop's per-card hold became a queue-level pause derived from the journal. A new queued_message_pauses row records where a Stop (or a /clear carry) took effect; structuredQueuePause derives the pause from that fact plus whether a person's provider-accepted turn has started since, so no hold is stored on the cards.
  • Resume. agentSession.queuedMessagesResume lifts whichever pause holds the queue, with a fresh operation id per press; a /clear's replacement queue starts paused as cleared, not as an interrupt.
  • Restart pause. A waiting card written by another host process pauses the queue as restarted, adopted into the current instance once a post-open person turn starts or Resume is pressed.
  • Publication. The host publishes {queuedMessages, queuePause} together, gated on hasResumableQueuedMessage so a Resume header never offers to send nothing; the reducer and coalescer keep the pause with its list, and a pause-only frame at an unchanged cursor still updates state.
  • Card header. NativeChatQueuePauseRow renders one reason row above the list with Resume; cards keep Steer, and a per-card caption remains only for a failed conversion.

I traced the Stop → withdrawal → pause-record, restart → adopt, /clear carry, returned-card-blocking, and Resume paths, and ran the delta's suites (structured-agent-session-queued-messages, structured-agent-session-queued-pause-lift, queued-message-store, the reducer/coalescer tests, and the renderer list/resume tests — all pass). I could not construct a path where a paused draft auto-sends without the user acting, or where the pause cannot be lifted.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…tooltip spaces its shortcut

The pause row, when shown, is the box's first row and each card a row below it, divided rather than individually bordered. The Steer tooltip groups its hint and the shortcut chips with the house gap, so the chips no longer touch the text.
#23059 makes /compact a message the chat sends and reads turn facts from the
turn record. The queue follows it:

- /compact is now a queued message and then a turn, not a command holding the
  session's lane for its whole provider call. A capable send during it becomes
  a card held by the ordinary `working` hold, and the drain waits behind the
  compaction's turn like any turn. So the compact side lane
  (structured-conversation-command-lane.ts), draft-only admission, Send-now's
  up-front refusal and the queue gate's `command` hold are gone. Delete, Resume
  and Send-now run on the session's lane, which a compaction no longer holds,
  so they answer at once; Send-now's card is accepted and handed over after the
  compaction, the order every send keeps. A /compact from the command RPC is
  the person's own turn (origin 'client').
- The queue gate's `blocked` hold is now main's send block itself, so a clear in
  doubt, a superseded source and an uncertain rewind hold the queue by one rule.
- Submission folding moved to journal-submission-fold.ts on main; the draft
  link, origin, the latest person's turn and the shared settlement guard are
  carried there. The /clear carry is re-applied to main's clear flow.
- To stay within max-lines, the row writer's cursor-returning append replaces
  the store's appendRow, the open-time draft repair rides the restore, and the
  queue wiring reads the host's mutation context.
# Conflicts:
#	src/renderer/src/components/native-chat/use-structured-agent-session.ts
…he opener's bar

A Steer hands the draft over under a fresh submission id, linked by queuedMessageId, and the host scopes its row to the running turn; the transcript keeps it inside that turn with no bar of its own, settled or running.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Status: ready to merge after #23726. Head 2c6a46a5a9e, CI all green, no conflicts.

@brennanb2025
brennanb2025 changed the base branch from mid-turn-queue-host to main September 29, 2026 22:24
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 865ee2a0-465e-459d-b044-eb73ff7d560f

📥 Commits

Reviewing files that changed from the base of the PR and between 429909d and 64d9ecf.

📒 Files selected for processing (2)
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.goal-dock.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.


📝 Walkthrough

Walkthrough

Adds host-aware follow-up queueing for structured chat. The change stores queued-message state and delivery details, projects host drafts into cards, and adds Resume, Steer, Edit, and Delete actions. It also adds a queue setting, capability checks, composer steering shortcuts, and translations. Outbox handling now reconciles queued-message hand-offs and holds certain attempted sends after Stop until an explicit retry.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 64d9e

The supplied changes identify no concrete merge-blocking issue. Queue controls and composer integration appear consistent with the intended behavior, subject to normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 64d9e

The queue changes when messages can reach the agent and how interrupted sends recover. Inspected controls preserve session identity and prevent several duplicate-send and unintended-resend scenarios. No introduced security weakness was established, but reconnection and ownership-transfer behavior remains only partially verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected execution path affects drafts and agent submissions in the addressed session on the selected runtime target. Session-scoped storage limits message-ID selection; broader account, tenant, or environment exposure was not established by this inspection.

Trust Boundaries and Controls

  • observed — Queue mutations preserve the session and runtime-fence envelope, use a caller-derived operation key, and undergo host fingerprint and operation admission checks. Replay that would perform a first execution must pass admission again rather than act without a live owner.

Resilience and Maintainability Implications

  • observed — Queue consumption uses an expected-state transition linked to a distinct submission ID. Already-dispatched cards resolve to their existing submission, and conflicting consumption is refused. These controls contain duplicate execution from repeated actions or ambiguous acknowledgements.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 79 functions across 51 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: mid-turn native-chat messages become editable cards above the composer.
Description check ✅ Passed The description is detailed and covers the user impact, implementation, rationale, testing, visual proof, compatibility, and known limitations. The Linked Issue is listed as none, the Review section i…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot 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.

ℹ️ No new issues found in this delta. Two earlier inline notes remain open and unaddressed, so this is not an approval.

Reviewed changes

Re-reviewed the commit since the prior pullfrog review (429909d → 64d9ecf): one authored commit that changes where the queued-message area renders.

  • Queued cards moved above the status area. NativeChatQueuedMessageList now renders before NativeChatStructuredSessionStatus instead of between the thread-goal banner and the prompt cards, so the background-task strip (running shells and agents) sits below the cards. NativeChatStructuredSession.tsx is the only production file touched.
  • A DOM-invariant test pins the new order. NativeChatStructuredSession.goal-dock.test.tsx adds a case asserting a queued card precedes the task strip and that the strip still docks directly on the goal tab. Reverting the component placement makes the new assertion fail (4 vs 0), so it can fail for the right reason.

I ran the four NativeChatStructuredSession* suites (47 tests) at this head: all pass.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Status: ready to merge. Head 64d9ecf3d4e, CI all green, no conflicts.

  • Order swapped (asked for in review). Queued messages now sit at the top, and the running shells and agents strip sits right next to the message box. A new test pins the order and fails with the old one. Web typecheck and 90 layout tests pass.
  • Live check at this head (second Mac, real Claude, queue switched on locally only):
    • A message queued while the agent ran shell commands was sent as its own turn once the agent finished on its own, with no Stop or Resume ("banana").
    • Two queued messages were sent one per turn, in order ("fig", then "grape").
    • The measured layout: queue, then the shell strip, then the message box.
    • Screenshots are in the description.
  • The phone half (feat(mobile): mid-turn messages wait as cards above the phone composer #23736) has merged.

queue above the shell strip

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