Skip to content

fix(native-chat): stop glued rapid sends from pinning queued bubbles - #14663

Merged
brennanb2025 merged 2 commits into
mainfrom
brennanb2025/fix-14262-glued-pending-supersede
Aug 15, 2026
Merged

brennanb2025 merged 2 commits into
mainfrom
brennanb2025/fix-14262-glued-pending-supersede

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

Two prompts sent quickly can appear in an agent transcript as one user row. Native Chat temporarily shows each send as a queued bubble, but its old glue matcher only understood rows joined with no separator. If the transcript kept a space, both temporary bubbles could remain pinned below newer replies.

This fix recognizes the normalized separator form and only considers transcript rows that arrived after the queued sends. It preserves the exact prompt bytes sent to the agent.

What Changed

  • Match consecutive pending texts joined with either no separator or one normalized whitespace separator per send boundary.
  • Require the complete transcript row to be consumed, so prefixes cannot retire unrelated sends.
  • Restrict glue candidates to rows after the oldest still-open pending send's message/time boundary.
  • Use a deterministic cursor walk with no intermediate candidate sets.
  • Add regression coverage for separator/no-separator glue, mixed boundaries, repeated and prefix prompts, incomplete rows, and older-turn false positives.

Why

Pending-send matching normalizes whitespace, while the agent transcript can retain a separator when rapid sends are glued into one row. The previous matcher only constructed the separator-free form.

Making the fuzzy matcher separator-aware without a send boundary is unsafe: an older row such as fix the bug could incorrectly retire newly queued fix the + bug sends. The boundary filter prevents that silent loss while preserving the existing exact-match behavior.

The final implementation does not trim or otherwise mutate the user's PTY payload; reconciliation adapts to the transcript representation instead.

Supersedes #14406

#14406 correctly identified the separator mismatch. This PR keeps that behavior and adds the send-boundary protection needed to prevent older turns from consuming newer optimistic echoes.

Fixes #14262

Visual Proof

Electron validation used the current PR worktree through Playwright CDP with an isolated profile. Real composer sends created two queued bubbles; a hermetic Claude transcript then landed the space-separated glued row plus later replies. Both bubbles retired and did not pin below the newer turn.

Screenshots and validation details: #14663 (comment)

Testing

  • 102 focused native-chat and integration tests passed in the final independent review.
  • typecheck:web passed.
  • Focused oxlint, oxfmt, max-lines ratchet, and git diff --check passed.
  • Electron before/after validation passed with zero console errors.
  • PR CI is the authoritative full-suite gate.

Scope and Compatibility

  • Four renderer reconciliation files only.
  • No mobile-facing code, protocol, persistence, dependency, or wire contract changed.
  • Pure renderer logic behaves identically for local, SSH, and remote PTYs.
  • No platform-specific path, shell, or keyboard behavior.
  • Pending sends remain capped at eight; the matcher is linear in row length and adds no polling, listeners, IPC, or subprocesses.
  • Mobile has a separate pending-echo implementation and is intentionally out of scope.

Author

  • X / Twitter: @BrennanKB5

Trim the draft at the send boundary so the PTY body and the optimistic
echo's match key agree, and bound the glue matcher to rows after the
oldest open echo's send boundary.

Fixes #14262
@coderabbitai

coderabbitai Bot commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 94a3b71b-54ed-4fdd-bb4a-f4acd7c8130c

📥 Commits

Reviewing files that changed from the base of the PR and between 73bbe20 and c744b73.

📒 Files selected for processing (4)
  • src/renderer/src/components/native-chat/native-chat-pending-occurrence.test.ts
  • src/renderer/src/components/native-chat/native-chat-pending-occurrence.ts
  • src/renderer/src/components/native-chat/native-chat-pending.test.ts
  • src/renderer/src/components/native-chat/native-chat-pending.ts

📝 Walkthrough

Walkthrough

The change updates glued pending-text matching for rapid sends. It supports direct concatenation and single-space separators while requiring complete transcript-row matches. Reconciliation now limits candidate rows to those after the oldest open pending send boundary. Pruning and optimistic-message visibility use the same bounded candidates. Tests cover concatenated prompts, separators, repeated and prefix prompts, incomplete rows, timestamps, and boundary regressions.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The description covers the required core sections, linked issues, visual proof, testing, scope, compatibility, and author details.
Linked Issues check ✅ Passed The description links the relevant issues and identifies #14262 as the issue fixed by this pull request.
Out of Scope Changes check ✅ Passed The changes remain focused on native-chat pending-message reconciliation and related regression tests.
Title check ✅ Passed The title clearly and concisely describes the fix for queued bubbles caused by glued rapid sends.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

Copy link
Copy Markdown
Contributor Author

Electron visual proof (PR #14663)

Validated in the current worktree via Playwright CDP. Rapid-send TUI timing is not deterministic, so this uses the existing hermetic transcript technique: real native-chat composer sends create the optimistic queued bubbles, then a Claude JSONL transcript lands the glued user row plus later replies.

Identity (window.api.app.getIdentity()):

  • devRepoRoot: /Users/brennanbenson/orca/workspaces/orca/fix-14262-glued-pending-supersede
  • devBranch: brennanb2025/fix-14262-glued-pending-supersede
  • isDev: true
  • CDP 9338 / renderer 5178

Before — two optimistic queued bubbles after rapid composer sends

tell me a joke and continue sit as separate user rows after the prior hello / Hi — ready turn. This is the state that stayed pinned below newer replies when a glued transcript row could not retire them.

02-two-queued-bubbles.png

After — space-separated glued row plus later replies; queued bubbles retired

Transcript advanced to one user row tell me a joke continue (the old-matcher miss), an assistant joke, then a newer what's next / later reply. Separate tell me a joke and continue bubbles are gone and do not pin below the newer turn.

03-after-glued-row-no-pinned-bubbles.png

Baseline context (before the two sends)

01-baseline-native-chat.png

Console

Playwright reported 0 errors. Pre-existing warnings only: Radix tooltip controlled/uncontrolled, React Grab outdated, one xterm.js task-queue deadline. No native-chat send/reconcile errors during the flow.

Evidence files were not committed.

@brennanb2025
brennanb2025 merged commit aaa877d into main Aug 15, 2026
44 checks passed
@erishforG

Copy link
Copy Markdown
Contributor

Thanks for carrying this forward and adding the send-boundary protection. That closes the older-row false-match gap cleanly, and I appreciate the attribution to #14406.

brennanb2025 added a commit that referenced this pull request Aug 17, 2026
…tching (STA-4477) (#14935)

* fix(native-chat): enforce each pending send's own boundary in glue matching

Glue matching filtered candidate rows against the OLDEST still-open send and
then matched the entire open queue against them. A prompt queued after a glued
row landed could therefore be judged "already delivered" by that older row and
pruned — the queued prompt disappeared with no bubble and no transcript turn.

Each send now carries its own transcript boundary into the match:
`gluedCandidateRows` tags every candidate row with the set of pending indices
it actually landed after, and the matcher stops a run at the first send the row
predates rather than skipping over it (adjacency is what makes a row glue).
Exact single matches still belong to the occurrence path, unchanged.

`native-chat-pending.ts` sat at 299 of its 300 effective-line budget, so the
slash-command marker cache — a separate rule that never took part in pending
pruning — moves verbatim to `native-chat-command-marker.ts`. Pure move: no
behavior change, imports only. (max-lines is never bumped or disabled.)

Refs STA-4477. Original PR #14663.

* test(native-chat): cover the glue adjacency break and unmask the render path

The `break` on a send the row cannot represent is the fix's central semantic
choice, and swapping it for `continue` was passing the whole suite: nothing
exercised a queue whose middle send is unrepresentable. Add that case.

The mixed-age case also asserted both call sites in one `it`, so a prune-path
failure masked the render-path assertion — and the render path is the one that
makes a queued bubble visually vanish. Split it.

Skip the per-send boundary scans when fewer than two sends are open: the glue
matcher already returns nothing there, so a lone queued echo was walking the
transcript twice per render for a discarded result.

* fix(native-chat): migrate the live-session benchmark off the renamed glue exports

Renaming the glue matcher's exports left this caller behind, and it crashed at
runtime after printing six result rows:

  TypeError: matchingNativeChatUserTexts is not a function

No gate caught it. config/scripts/** is in no tsconfig include and the file is
not a *.test.ts, so neither typecheck nor vitest ever loads it.

The empty-pending arm passes no pending sends, so the matcher takes its
empty-queue exit without ever reading the rows — which is also why the renderer
skips candidate-row construction entirely in that case. Escaping the row scan
directly keeps what this arm actually measures identical to before, rather than
fabricating per-row boundary sets that no production path builds.
paidaxingyo666 pushed a commit to paidaxingyo666/Manta that referenced this pull request Aug 21, 2026
…tching (STA-4477) (stablyai#14935)

* fix(native-chat): enforce each pending send's own boundary in glue matching

Glue matching filtered candidate rows against the OLDEST still-open send and
then matched the entire open queue against them. A prompt queued after a glued
row landed could therefore be judged "already delivered" by that older row and
pruned — the queued prompt disappeared with no bubble and no transcript turn.

Each send now carries its own transcript boundary into the match:
`gluedCandidateRows` tags every candidate row with the set of pending indices
it actually landed after, and the matcher stops a run at the first send the row
predates rather than skipping over it (adjacency is what makes a row glue).
Exact single matches still belong to the occurrence path, unchanged.

`native-chat-pending.ts` sat at 299 of its 300 effective-line budget, so the
slash-command marker cache — a separate rule that never took part in pending
pruning — moves verbatim to `native-chat-command-marker.ts`. Pure move: no
behavior change, imports only. (max-lines is never bumped or disabled.)

Refs STA-4477. Original PR stablyai#14663.

* test(native-chat): cover the glue adjacency break and unmask the render path

The `break` on a send the row cannot represent is the fix's central semantic
choice, and swapping it for `continue` was passing the whole suite: nothing
exercised a queue whose middle send is unrepresentable. Add that case.

The mixed-age case also asserted both call sites in one `it`, so a prune-path
failure masked the render-path assertion — and the render path is the one that
makes a queued bubble visually vanish. Split it.

Skip the per-send boundary scans when fewer than two sends are open: the glue
matcher already returns nothing there, so a lone queued echo was walking the
transcript twice per render for a discarded result.

* fix(native-chat): migrate the live-session benchmark off the renamed glue exports

Renaming the glue matcher's exports left this caller behind, and it crashed at
runtime after printing six result rows:

  TypeError: matchingNativeChatUserTexts is not a function

No gate caught it. config/scripts/** is in no tsconfig include and the file is
not a *.test.ts, so neither typecheck nor vitest ever loads it.

The empty-pending arm passes no pending sends, so the matcher takes its
empty-queue exit without ever reading the rows — which is also why the renderer
skips candidate-row construction entirely in that case. Escaping the row scan
directly keeps what this arm actually measures identical to before, rather than
fabricating per-row boundary sets that no production path builds.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…tablyai#14663)

* fix(native-chat): stop glued rapid sends from pinning queued bubbles

Trim the draft at the send boundary so the PTY body and the optimistic
echo's match key agree, and bound the glue matcher to rows after the
oldest open echo's send boundary.

Fixes stablyai#14262

* fix(native-chat): preserve exact prompt payloads
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…tching (STA-4477) (stablyai#14935)

* fix(native-chat): enforce each pending send's own boundary in glue matching

Glue matching filtered candidate rows against the OLDEST still-open send and
then matched the entire open queue against them. A prompt queued after a glued
row landed could therefore be judged "already delivered" by that older row and
pruned — the queued prompt disappeared with no bubble and no transcript turn.

Each send now carries its own transcript boundary into the match:
`gluedCandidateRows` tags every candidate row with the set of pending indices
it actually landed after, and the matcher stops a run at the first send the row
predates rather than skipping over it (adjacency is what makes a row glue).
Exact single matches still belong to the occurrence path, unchanged.

`native-chat-pending.ts` sat at 299 of its 300 effective-line budget, so the
slash-command marker cache — a separate rule that never took part in pending
pruning — moves verbatim to `native-chat-command-marker.ts`. Pure move: no
behavior change, imports only. (max-lines is never bumped or disabled.)

Refs STA-4477. Original PR stablyai#14663.

* test(native-chat): cover the glue adjacency break and unmask the render path

The `break` on a send the row cannot represent is the fix's central semantic
choice, and swapping it for `continue` was passing the whole suite: nothing
exercised a queue whose middle send is unrepresentable. Add that case.

The mixed-age case also asserted both call sites in one `it`, so a prune-path
failure masked the render-path assertion — and the render path is the one that
makes a queued bubble visually vanish. Split it.

Skip the per-send boundary scans when fewer than two sends are open: the glue
matcher already returns nothing there, so a lone queued echo was walking the
transcript twice per render for a discarded result.

* fix(native-chat): migrate the live-session benchmark off the renamed glue exports

Renaming the glue matcher's exports left this caller behind, and it crashed at
runtime after printing six result rows:

  TypeError: matchingNativeChatUserTexts is not a function

No gate caught it. config/scripts/** is in no tsconfig include and the file is
not a *.test.ts, so neither typecheck nor vitest ever loads it.

The empty-pending arm passes no pending sends, so the matcher takes its
empty-queue exit without ever reading the rows — which is also why the renderer
skips candidate-row construction entirely in that case. Escaping the row scan
directly keeps what this arm actually measures identical to before, rather than
fabricating per-row boundary sets that no production path builds.
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.

[Bug]: Native chat stale "queued" bubbles pin below the newest reply after a separator-glued rapid send

2 participants