Skip to content

fix(native-chat): a queue action still on its way greys its controls, like the phone - #29120

Merged
brennanb2025 merged 18 commits into
mainfrom
brennanb2025/queue-busy-controls
Oct 17, 2026
Merged

brennanb2025 merged 18 commits into
mainfrom
brennanb2025/queue-busy-controls

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​142 $\color{#cf222e}{\Huge{\mathbf{−}}}$​33 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​109
Prod 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​102 $\color{#cf222e}{\Huge{\mathbf{−}}}$​105 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3

ELI5

Messages you send while the agent works wait as cards above the chat box. Each card has Steer (or Send), Delete and a "…" menu, and a paused queue's heading has Clear all and Resume. When the computer running the chat is slow or far away (a remote host over SSH, a busy machine), the answer to one of those presses can take seconds. Until now nothing on screen changed in that time: the card and its buttons looked exactly as before, so it looked like the press did nothing, and pressing again also seemed to do nothing.

Now, the moment you press, the buttons that action is using turn grey, the way they already do on the phone. They come back when the computer answers.

What Changed

Before (desktop):

  • Pressing a card's Delete, Steer or Send on a slow host: no change on screen until the host answered. A second press was quietly ignored.
  • Pressing Clear all on a paused queue: no change at all, header, count and cards included, until every delete answered. A second press quietly joined the first.
  • Only Resume greyed out while it was running.

After (desktop):

  • While a card's own action is running, that card's Steer/Send, Delete and "…" menu are greyed out.
  • While Clear all is running, Clear all and Resume are greyed out, and so is every card it is deleting. While Resume is running, Resume and Clear all are greyed out.
  • While any card's own Delete or Steer is still on its way, Clear all is greyed out too. When that answers, Clear all comes back and deletes every card shown, including one whose own Delete just failed. Before this, Clear all stayed pressable and quietly left that card out: if its Delete then failed, the card was still there after Clear all, and when every card was busy the press did nothing.
  • Cmd/Ctrl+Enter (steer the newest card) does nothing while that card is greyed, instead of scrolling the chat with nothing sent.
  • Everything comes back when the host answers. If a delete fails, the cards stay where they were and the existing single error toast shows. No new words, banner or toast.

The phone is unchanged; it already greys a pressed control.

Mechanism. The desktop had three separate "already running" guards, all invisible to rendering except one: a per-card set in a ref, a Resume flag (the only one drawn), and Clear all's shared in-flight promise. They are replaced by the phone's one mechanism: a keyed in-flight set, kept both as a ref (it catches a double click within the same frame, before React re-renders) and as render state (so the buttons can grey out). Its keys are each card's message id, plus one shared key for the header's two actions, Resume and Clear all, so each refuses the other while it runs (Resume during Clear would start sending cards Clear is deleting, and the reverse). Clear all also adds every card id it deletes, so those cards grey out with it. It never skips a card: while any card's own action is running, Clear all is unavailable (the same way it already is while a just-sent message has no card on the host yet), and a same-frame press that overlaps a running card is refused whole by the in-flight set rather than narrowed. The controller exposes busy(messageId) and queueBusy (which replaces resuming); the card takes one busy prop that disables Steer/Send, Delete and the menu (which holds Edit). Clear all's joining of a repeated press is gone: the button is disabled while it runs, and the ref refuses a same-frame repeat.

Why

This is the same decision ("one action per control at a time, and it shows") that the phone and the desktop each kept a copy of. The desktop's copy kept the guard and dropped the drawn half. Moving the desktop onto the phone's shape removes the gap for every queue action at once, rather than adding a fourth flag next to the three. It is a small net deletion in production code.

Alternatives considered: adding render state beside each of the three existing guards (three copies of the same thing, the shape that let this drift), or a "Deleting…" caption on the row. The caption would flash on every delete on a local host, where the answer comes back immediately.

Differences from the common pattern:

  • Greyed-out controls with no "Deleting…" / "Sending…" words: intended. A word that every delete passes through would flash on a fast host; greyed controls match the phone and the existing Resume.
  • Clear all has no counterpart in the common pattern: intended. It is this queue's own control from the base PR, not new here.
  • Only the card being acted on greys out; the common pattern greys every queued row while any one action runs. Intended: on a remote host each action is a round trip, and one slow delete shouldn't lock every other card. This matches the phone.
  • While a pressed button is greyed, keyboard focus leaves it (a disabled button can't hold focus) and returns to the chat box when the host answers. Intended: the common pattern disables or hides the pressed control the same way.
  • Cmd/Ctrl+Enter while the newest card is greyed is handed back to the chat box, which does nothing when the box is empty; the common pattern keeps the shortcut in the queue and ignores it. Intended: same result for the user, one less special case.
  • While Clear all runs, the desktop also greys Resume and the cards being deleted; the phone greys only Clear all. Temporary: the phone should take the same rule (tracked in STA-10676).

Reverses a call in #28654. #28654 kept Resume and card Steer usable during Clear all and joined a repeated Clear press, to keep a late review fix narrow. That let Resume start sending cards Clear was deleting, and let a card's Steer race its own delete (ending in "This message was already sent."). Here they wait for Clear to finish instead.

Not changed: a card open in the inline editor is still deleted by Clear all, and its editor isn't greyed while that happens. That is how #28654 behaves too.

Linked Issue

Stacked on #28654 (Clear all exists only there).

Visual Proof

Before/after screenshots from rendered QA (deletes and Resume held for several seconds on the host) are in the review status comment below.

Testing

  • A rendered test over the real controller and list for the Clear all rule: while one card's Delete is held, Clear all is greyed; after that Delete fails, Clear all deletes both cards. It fails on the previous head.
  • One rendered test over the real controller and list: with the host's delete held, pressing Clear all (twice) greys Clear all, Resume and each card's Delete, Steer and menu, sends exactly one delete per card; when the delete then fails, every control comes back, both cards stay, and exactly one toast shows.
  • The existing controller test for Clear now pins eight deletes at a time and that a repeated Clear sends nothing more (it used to pin the joined promise, which is gone).
  • CI runs lint, typecheck and the full test suite.
  • Rendered QA on macOS (hidden test copies, isolated home, stand-in agents), feat(native-chat): one queue header that becomes the pause row; drop the composer Resume and the send dialog #28654's head vs this branch with the same steps: slow Clear all (pressed twice), slow Clear all that fails, slow single Delete (pressed twice), slow Resume, the fast path, focus, and Cmd/Ctrl+Enter during a slow Clear. All pass; on the base, Cmd/Ctrl+Enter during a slow Clear even sent a card that was being deleted.
  • Rendered recheck of the Clear all rule on macOS (same rig), previous head vs this head: with card A's Delete held 10 s and then failing, the previous head kept Clear all live, deleted only B, and left A behind after Clear all; this head greys Clear all, sends nothing on a press, and after A's Delete fails one Clear all deletes A and B.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Renderer only: no wire, host or saved-data change, so SSH, remote and mixed-version hosts behave as before apart from the greyed controls. No platform-specific code.

Author: Brennan Benson (X: @BrennanKB5)

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…mposer Resume or send dialog

The queued cards now sit under one header row on desktop and phone: "Queued"
and the count normally, and "Paused" with the count, Clear all and Resume
while the host holds the queue after a Stop. A chevron folds only the cards;
the choice is local to the view and resets when the queue empties.

The composer's Send no longer turns into a second Resume, and a message sent
into a paused queue no longer asks "Send message?": it sends, its accepted
turn lifts the pause, and the cards follow it. Clear all deletes every card
through the existing per-card delete. No wire or host change.
… like the phone

The desktop kept in-flight queue actions only in refs (one per card, one for
Resume, a joined promise for Clear all), so on a slow or remote host pressing
Clear all, Delete, Steer or Send changed nothing on screen until the host
answered. One keyed in-flight set, held as a ref and as render state, now
covers cards, Resume and Clear all; Clear marks every card it deletes.
… guard; the steer chord skips a card already on its way

Resume and Clear all now hold one shared in-flight key, so each refuses the
other in the guard rather than only in the drawn state, and both header
buttons grey on it. Cmd/Ctrl+Enter returns false when the newest card's
action (or a Clear deleting it) is still out, so the chord no longer
scrolls to the latest with nothing sent. Test controllers gain the new
fields.
The queued-list test file passed its line limit; the in-flight test and the
held-write render it needs now live beside it, and the list test file is
back to the base's helpers.
@brennanb2025

brennanb2025 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

On a slow or remote host, clicking a desktop queued message’s Delete, Steer or Send, or the queue’s Clear all, left the controls looking usable until the host replied. Repeat presses appeared to do nothing because duplicate suppression was hidden from the user.

This PR greys the controls while their request is pending and restores them when it settles. A single-card action greys that card and Clear all; independent cards remain usable. Clear all greys both header actions and every captured card. Resume greys both header actions. Failures retain unsent cards, restore controls and use the existing error notice; a partly successful Clear retains the cards whose deletion failed. There is no new progress text or saved state.

Review complete on 4743bb3dd1c95f06082b73af9cb78d5ee33420f0. Fixed during the loop: controller test fixtures now match the new fields; Resume and Clear refuse conflicting presses in the request guard as well as on screen; a busy-card shortcut no longer scrolls without sending. The final Clear rule waits until any card action finishes, then claims every card shown at the moment of the press, so a failed Delete cannot silently survive an overlapping Clear. This deliberately replaces the earlier #28654 rule that left Resume/Steer usable during Clear. The existing shared request state releases in finally, including on failure. Four review rounds, the design ruling, and narrow final-merge verification are complete, with no open P0/P1/P2 findings. The final merge preserves the reviewed PR logic and includes #29268’s header dot and matching test text.

Verified: the full PR workflow passed on this exact head: static analysis/typecheck, all five test shards, relay integration, both packaging jobs and the aggregate PR checks passed. The PR is out of draft and GitHub reports CLEAN.

Real rendered QA passed on a separate M4Air, using signed-in Codex (GPT-6.1-Sol High) and a Grok driver through Playwright’s Chrome DevTools Protocol connection. The six flow types covered pending Delete and independent cards, finite Delete failure/recovery, held/repeated Clear and partial failure, slow Resume, busy Steer/keyboard repeats, and ordinary send/delete plus folder/git workspace neighbors and composer focus. Duplicate presses added no requests. A fresh full app build on the final head confirmed the header dot, Delete greying/recovery, and repeated Clear producing exactly two deletes for two captured cards, then an empty queue.

Evidence limits: the six-flow run used 6dde318; final-head 4743bb3 received the focused fresh-app checks above. The main comparison uses three queue renderer files from b61b9da inside the same initial PR app, not a full main build. Delay/failure injection and temporary enablement of the queue were identical across arms; real agent turns and host queue operations were used. Clear-failure recovery was exercised on the PR, not repeated on main. Windows/Linux rendering, SSH, native operating-system focus and a request that never settles were not exercised. The evidence archive was transferred and SHA-256 verified (a038de990b8aab88e0ead17afbbf93f81361e45f8d095e5f4451929f5e8039b3).

Deferred: the inline editor remains usable while Clear deletes its card; “Turn off queueing” greys with that card’s menu; held shortcut repeat behavior is unchanged. Phone Clear/Resume parity is tracked in STA-10676. Independent-card concurrency is an intended difference from the common pattern so one slow request does not lock every other card. Focus leaving a newly disabled button is intended; normal settlement refocus remains.

QA also recorded these follow-ups outside this PR’s pending-control change:

  • After a full restart, previously paused cards reappeared as “Queued · 3” without Clear/Resume until the chat reopened. During a two-minute watch they stayed waiting and did not auto-send. Main restart was not compared; this remains a queue-lane follow-up.
  • Codex’s running shell command remained a background task after Stop on both arms.
  • A follow-up entered exactly as a turn ended could join that turn instead of queuing; message order held and nothing was lost.
  • Real ~/.codex/config.toml and ~/.claude.json hashes changed during QA. Every Orca test process used a throwaway home; the real agent used its own home by design. With concurrent real-home agents, attribution remains unproven. This is not reported as a clean isolation check.

For Brennan’s review

Each image is a whole hidden window. Main means the disclosed three-file baseline. PR images for Delete pending, Clear pending and Clear settled are from final head 4743bb3; the other flow images are from the six-flow run at 6dde318. Card counts and prompt labels vary where the queue was replenished.

Flow Main queue files This PR
Delete pending main 04 delete pending pr final 02 delete pending
Delete failure; card and controls recover main 06 delete fail toast pr 23 flow2 delete fail toast
Clear pending main 07 clear pending pr final 04 clear repeat pending
Resume pending main 12 resume pending pr 25 flow4 resume pending
Steer pending main 09 steer pending pr 27 flow5 steer pending
Neighboring folder workspace main 15 folder workspace pr 30 folder workspace

Final-head repeated Clear settled with each card deleted once:

pr final 05 clear repeat settled

Partial Clear failure retained the failed card and recovered controls:

pr 24 flow3 clear fail toast

Three places where the behavior is decided:

…skipping the card

Clear all now greys while any card in the queue has its own Steer or Delete
on its way, and once that settles it claims every card. Before, it stayed
enabled and quietly left the busy cards out, so a failed Delete left a card
behind after Clear all, and with every card busy the click did nothing.
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/queue-paused-header to main October 15, 2026 22:36
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 8 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: dc876910-ea2b-4409-8bde-e7cf329266d4
📥 Commits

Reviewing files that changed from the base of the PR and between fc81cd4 and 4743bb3.

📒 Files selected for processing: 11
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageCard.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.in-flight.test.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.inline-edit.test.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx
  • src/renderer/src/components/native-chat/native-chat-submission-navigation.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.resume.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.ts
  • src/renderer/src/components/native-chat/use-structured-native-chat-submit-reveal.test.ts

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: 2c601d2b-4921-4edc-a830-c59192587c87
📥 Commits

Reviewing files that changed from the base of the PR and between 84ee048 and fc81cd4.

📒 Files selected for processing: 1
  • src/renderer/src/components/native-chat/use-structured-native-chat-submit-reveal.test.ts

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

📝 Walkthrough

The queued-message controller replaces separate Resume and Clear all tracking with shared in-flight tracking. It exposes queue-wide and per-card busy state. The renderer uses that state to disable queue-header and card actions. Tests cover pending Clear all, card deletion, Resume state, and controller fixture updates.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to fc81c

Queue controls prevent overlapping actions, and the busy-card keyboard path does not send an empty message. No material PR-introduced risk remains that should block merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 28 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: queue controls become disabled while an action is in progress.
Description check Passed The description is complete and focused. It explains the user-visible behavior, implementation, motivation, alternatives, testing, visual proof, compatibility considerations, and checklist status. It …
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.
Full details: Description check

Explanation

The description is complete and focused. It explains the user-visible behavior, implementation, motivation, alternatives, testing, visual proof, compatibility considerations, and checklist status. It references issue #28654 under Linked Issue, although it uses “Stacked on” instead of the template’s “Fixes #” format.

✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@brennanb2025
brennanb2025 merged commit 8a4c031 into main Oct 17, 2026
32 checks passed
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