Repository navigation
fix(native-chat): stop glued rapid sends from pinning queued bubbles - #14663
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe 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)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
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 (
Before — two optimistic queued bubbles after rapid composer sends
After — space-separated glued row plus later replies; queued bubbles retiredTranscript advanced to one user row Baseline context (before the two sends)ConsolePlaywright 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. |
|
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. |
…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.
…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.
…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
…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.



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
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 bugcould incorrectly retire newly queuedfix the+bugsends. 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
typecheck:webpassed.git diff --checkpassed.Scope and Compatibility
Author