Repository navigation
Conversation
Claude consumes a send made mid-turn through a `queued_command` attachment and
writes no `type:"user"` row for it, so its optimistic echo has no row to match —
ever. Held at the list tail, it re-reads below every turn of the reply it
triggered, which is what makes the conversation look re-ordered: the answer
appears above the question still sitting underneath it.
Observed in a Claude Code transcript, line numbers as written:
6 {"type":"user", ...} "check status, first time"
25 {"type":"assistant", ...} "I'll check the current state..."
27 {"type":"queue-operation","operation":"enqueue","content":"check status, second time"}
28 {"type":"queue-operation","operation":"remove", ...,"reason":"absorbed_mid_turn"}
33 {"type":"attachment","attachment":{"type":"queued_command","prompt":"check status, second time"}}
41 {"type":"assistant", ...} "Working tree is clean..."
68 {"type":"assistant", ...} "The discriminating test has fired..."
The echo belongs at 33; it rendered after 68.
Mobile already anchors for this reason (`baselineTailMessageId`). Desktop
records the same boundary as `NativeChatPendingSend.afterMessageId` and only
ever used it for matching, so this reuses it for placement.
Only an echo the transcript has since moved past is repositioned: one still at
the tail keeps today's order, after the streaming bubble. A boundary a bounded
read has paged out falls back to the tail rather than guessing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughPending echoes are anchored after the transcript message identified by their send boundary. Echoes without a usable boundary remain trailing. Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to Pending echoes retain their intended placement in the reachable ordering flow, including timestamp ties. No actionable merge-blocking risk remains. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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 |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 400d0fea-1b28-487f-b7d8-2404dc6dbf0d
📒 Files selected for processing (3)
src/renderer/src/components/native-chat/NativeChatResolvedView.tsxsrc/renderer/src/components/native-chat/native-chat-pending-anchor.test.tssrc/renderer/src/components/native-chat/native-chat-pending-anchor.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
…ming Review note on stablyai#20847: the tests exercised `anchorPendingMessagesToSendBoundary` directly, so nothing pinned what `NativeChatResolvedView` actually renders — the anchored echo has to land before the command markers and the streaming bubble, and the trailing one after both. Asserts the whole sequence for one anchored and one trailing send, so a regression in the assembly order fails rather than just the anchoring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
The anchoring this PR adds is discarded before render, so #20846 is not fixed. The list re-sorts every pending:* row back to the tail after nativeChatMessagesWithPending has placed it — see the inline comment for the trace.
Reviewed changes — reviewed the full diff (3 files, 2 commits): a new native-chat-pending-anchor.ts, its unit tests, and the NativeChatResolvedView wiring; plus the follow-up assembly-order test at 053ce516e3.
- Anchor pure function —
anchorPendingMessagesToSendBoundaryinterleaves an echo after itsafterMessageIdboundary and trails echoes that are missing, null, or still at the tail. - Assembly helper —
nativeChatMessagesWithPendingorders transcript → anchored echoes → markers → streaming → trailing echoes. - View wiring —
NativeChatResolvedViewnow assembles the pending list through the helper instead of appendingpendingMessages. - Tests — cover the pure function and the assembly output, but never the order the list actually renders.
⚠️ The tests stop one layer short of the render path
Both the anchor tests and the follow-up nativeChatMessagesWithPending test assert the array produced before NativeChatMessageList projects it, and that projection is exactly what reorders echoes today. Add a test that runs the assembled messages through createNativeChatMessageListProjection (or orderNativeChatMessages) and asserts the echo lands between its boundary and the next turn; it should fail against the current comparator.
Technical details
# Anchored order is discarded by the list projection
## Affected sites
- src/renderer/src/components/native-chat/NativeChatResolvedView.tsx:287 — anchored array assigned to `sessionWithPending.messages`
- src/renderer/src/components/native-chat/NativeChatMessageList.tsx:142-145 — `projectMessages(session.messages)`
- src/renderer/src/components/native-chat/native-chat-message-list-projection.ts:29 — `orderNativeChatMessages(messages)`
- src/renderer/src/components/native-chat/native-chat-message-grouping.ts:51-53 — `[...messages].sort(compareMessages)`
- src/renderer/src/components/native-chat/native-chat-session-assembler.ts:97-105 — `messageSortRank` returns 2 for any `pending:*` / `launch-pending:*` id
- src/renderer/src/components/native-chat/native-chat-pending.ts:254 — echoes are minted as `pending:${entry.id}`
## Required outcome
- The position chosen by `anchorPendingMessagesToSendBoundary` must survive to the rendered row order.
- Add a test that feeds the assembled array through the projection and asserts the anchored position (the existing `native-chat-message-grouping.test.ts:36-43` pins the opposite, so it will need to change with the fix).
## Suggested approach (optional)
- Make the comparator honor the anchor (e.g. a sort-rank/timestamp override carried on anchored echoes), apply anchoring after the projection rather than before, or exempt already-ordered rows from the defensive re-sort.
## Open questions for the human (optional)
- Is the rank-2 tail pin load-bearing for the still-at-tail echo (streaming renders before pending)? Any fix must keep that case.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…vives sorting @pullfrog was right on stablyai#20847: the anchored array never reached the DOM. `NativeChatMessageList` re-sorts through `orderNativeChatMessages`, and `messageSortRank` pins every `pending:*` row to rank 2 — the tail — so array order alone proved nothing, and the earlier tests asserted the wrong layer. A test driving `orderNativeChatMessages` over the anchored array reproduces it: - "pending:p1" "a2", "a3", + "pending:p1" Fix, keeping the tier the rank exists to protect: - `pending-at:` is a second pending id form. `isPendingMessageId` accepts both, so pruning and visibility are unchanged; `isAnchoredPendingMessageId` marks only the positioned ones. - `messageSortRank` returns 0 for an anchored echo — it owns a place between real rows — while a still-at-tail echo keeps rank 2, so the streaming bubble still sorts ahead of it, which is what the existing comment guards. - An anchored echo takes its anchor's timestamp plus half a millisecond, so it sorts after the anchor and before the next row: transcript rows carry whole milliseconds. Echoes sharing an anchor step 0.5/1.0/1.5 and keep send order. - An anchor with a null timestamp (transcripts that supply none) falls back to the tail rather than guessing. Side effect worth a maintainer's eye: the projection folds after ordering, so tool rows produced after the send no longer fold into the assistant message above the echo — they render as their own rows beneath it. That matches the terminal pane, where those calls also come after the prompt. Verified in a local `build:unpack` on Windows: sent a second message mid-turn and the bubble held its place while the rest of the reply landed below it, with the chat view and the terminal pane finally in the same order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Important
The anchored echo now survives the list re-sort, but it interrupts turn folding: tool results emitted after a mid-turn send are dropped from the rendered list instead of shown.
Reviewed changes — reviewed the delta since the prior pullfrog review (053ce516e3 → 69a11fc525): the pending-at: id form and rank-0 override that let the anchored position survive orderNativeChatMessages, plus the projection-level test.
- Anchored echoes re-minted under
pending-at:—anchorPendingMessagesToSendBoundarygives a positioned echo its anchor's timestamp plus a0.5nnudge, andmessageSortRanknow rankspending-at:rows as content whilepending:rows keep the rank-2 tail tier. - Tail-boundary trailing removed — a boundary that is still the tail (with a timestamp) is now anchored rather than trailed, so a folded turn that grows in place keeps the echo above its later output.
- Projection-level test added —
native-chat-pending-anchor.test.tsdrives the anchored array throughorderNativeChatMessages, which fails against the old rank-2 comparator.
The comment below is the one thing I would not merge without addressing.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…tput Two regressions @pullfrog found in the anchoring itself, both real, both mine. **A tool result went missing.** An echo placed between an assistant message and the tool rows that followed ends the `foldToolMessages` run, so a tool-result-only row is no longer attributed and `dropUnattributableToolResults` deletes it — the call renders in flight, with no output. Worse than the bug this PR set out to fix, and only in the scenario it targets. An optimistic echo is a view artefact sitting where the user typed, not a transcript row, so it no longer ends the run. The id prefixes move to `native-chat-types` because the shared fold pass has to recognise them; `isPendingMessageId` now just re-exports the shared predicate. **The streaming tier flipped.** Dropping the tail-boundary case in the previous commit gave a still-at-tail echo rank 0, so it sorted ahead of the streaming preview — the tier the comment on `messageSortRank` and this PR both call load-bearing. The guard is back: an echo still at the tail keeps `pending:` and rank 2, and only an echo the transcript has moved past is repositioned. Worth recording why that guard was dropped at all: I read its failure as folded turns growing in place. Wrong — the first attempt failed because of the rank pin alone, which is now fixed, so the guard does what it was written to do. Both are pinned by tests through the path the list actually renders. The tool-result one fails without the fold change — `expected [] to deeply equal [ 'a.txt' ]` — which my first version of it did not, because I modelled the call as a text assistant message that re-opened the fold run on its own. 1547 tests in native-chat, 457 in shared/native-chat, typecheck and oxlint clean. Mobile deps do not install on this host (react-native patch fails to parse), so mobile is left to CI; the fold change touches it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 9ebc384d-307b-485f-a271-2cbac4591fd6
📒 Files selected for processing (6)
src/renderer/src/components/native-chat/native-chat-pending-anchor.test.tssrc/renderer/src/components/native-chat/native-chat-pending-anchor.tssrc/renderer/src/components/native-chat/native-chat-pending.tssrc/renderer/src/components/native-chat/native-chat-session-assembler.tssrc/shared/native-chat-tool-fold.tssrc/shared/native-chat-types.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
There was a problem hiding this comment.
✅ No new issues found. The prior review's two findings are addressed and their threads resolved.
Reviewed changes — reviewed the delta since the prior pullfrog review (69a11fc525 → cf56081), which closes out both findings from that review.
- Fold tolerates a pending echo —
foldToolMessagesno longer ends its run on apending:/pending-at:row, so tool results that follow the echo stay attributable instead of being deleted bydropUnattributableToolResults; pinned throughcreateNativeChatMessageListProjection. - Still-at-tail echo keeps its tier — the tail-boundary guard is restored, so an echo whose boundary is still the transcript tail stays
pending:/ rank 2 and the streaming preview sorts ahead of it; pinned by anorderNativeChatMessagestest. - Id prefixes shared with the fold pass —
NATIVE_CHAT_PENDING_ID_PREFIX,NATIVE_CHAT_ANCHORED_PENDING_ID_PREFIX, andisNativeChatPendingRowIdmove tonative-chat-types, withnative-chat-pendingre-exporting them so renderer call sites keep their names.
Verified locally: the full src/renderer/src/components/native-chat suite passes (153 files, 1548 tests); no other consumer of isPendingMessageId depends on the narrower pending:-only meaning.
ℹ️ The PR description now states the opposite tool-row behavior
The "side effect a maintainer should weigh" paragraph says tool rows produced after the send "no longer fold into the assistant message above the echo — they render as their own rows beneath it." cf56081 deliberately reverses that: the fold run stays open across the echo, so those rows fold into the assistant above it. The code is the intended fix, but the description no longer matches it.
Technical details
# Stale "side effect" paragraph in the PR body
## Affected sites
- PR #20847 body, "Side effect a maintainer should weigh" paragraph
- src/shared/native-chat-tool-fold.ts:123 — pending echo no longer ends the fold run
## Required outcome
- The description should describe the shipped behavior: tool rows emitted after the echo fold into the assistant above it (their results are preserved, which is the point of the fold exemption).
## Open questions for the human (optional)
- If the tool-only-assistant-after-echo ordering matters for the visual proof, a capture of that shape would confirm whether the folded placement is what you want.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…stamp ties @coderabbitai on stablyai#20847: the half-millisecond nudge only worked while the next row sat on a later millisecond. Two transcript rows sharing one millisecond — common enough — put the echo after the row it was sent before, because the tie broke on id. Taking its advice, which is simpler than the nudge it replaces: an anchored echo now carries its anchor's own timestamp, and `compareMessages` returns 0 on a tie involving a `pending-at:` row instead of comparing ids. `sort` is stable, so placement order survives — for the anchor/echo pair and for several echoes on one anchor, without a nudge to tune or an anchor field on the shared message type. Unrelated messages keep id tie-breaking exactly as before. Both conditions are pinned by tests through `orderNativeChatMessages`, and the first fails without the change: expected [ 'u1', 'a1', 'a2', 'pending-at:p1' ] to deeply equal [ 'u1', 'a1', 'pending-at:p1', 'a2' ] 1549 tests in native-chat, 457 in shared/native-chat, typecheck and oxlint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found. The prior review's findings remain addressed, and the tie-break added since then is verified correct.
Reviewed changes — reviewed the delta since the prior pullfrog review (cf5608175f → 65e6e2b20e): the timestamp tie-break that keeps an anchored echo beside its anchor.
- Anchored echoes carry their anchor's exact timestamp —
anchorPendingMessagesToSendBoundarydrops the0.5msnudge, so an echo no longer sorts past a following row that shares the anchor's millisecond. - Tie-break defers to placement order —
compareMessagesreturns0when either side is apending-at:id, relying onArray.prototype.sortstability to preserve the anchor → echo adjacency the anchoring step built. - Two tie tests added — a same-millisecond following row, and multiple echoes under one anchor, both asserted through
orderNativeChatMessages.
I fuzzed the full nativeChatMessagesWithPending output (content + markers + streaming + trailing) through compareMessages and the echo stayed adjacent to its anchor in every case; the invariant holds because the view feeds content already sorted by (timestamp, id). The new test genuinely fails against the old nudge (a1, a2, pending-at:p1), so it guards the regression.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

ELI5
If you type a second message while the agent is still working, Claude folds it into the turn already running and never writes it down as a message of its own. The chat view had nowhere to put that bubble, so it parked it at the bottom — underneath the answer it caused. This puts the bubble back where you typed it, without costing the turn anything else.
What Changed
An echo the transcript will never match has to be placed, not pinned.
native-chat-pending-anchor.ts—anchorPendingMessagesToSendBoundaryputs an echo directly after the row it was sent against;nativeChatMessagesWithPendingassembles transcript → markers/streaming → the echoes still belonging at the tail.NativeChatResolvedViewcalls it instead of spreadingpendingMessagesonto the end.orderNativeChatMessages. It sorts by rank before timestamp, andmessageSortRankpins everypending:*row to rank 2, so array order never reaches the DOM. An anchored echo is minted under a second id form,pending-at:, ranked as content, and takes its anchor's timestamp plus half a millisecond — transcript rows carry whole milliseconds, so it lands after the anchor and before the next row. Echoes sharing an anchor step 0.5/1.0/1.5 and keep send order.foldToolMessagesrun, which strands a tool-result-only row and letsdropUnattributableToolResultsdelete it — the call renders in flight with no output. The pending id prefixes therefore live innative-chat-types, and the fold pass skips them when deciding to end a run.isPendingMessageIdre-exports the shared predicate and accepts both forms, so pruning, visibility and occurrence matching are unchanged.Deliberately narrow:
pending:and rank 2, so the streaming preview still sorts ahead of it — the tier the comment onmessageSortRankexists to hold. Only an echo the transcript has moved past is repositioned.Why
Claude consumes a mid-turn send through a
queued_commandattachment and writes notype:"user"row for it, so that echo has no row to match, ever. Held at the tail it re-reads below every turn of the reply it triggered, which is what makes the conversation look re-ordered.Mobile already fixes this and its test says so directly (
mobile-native-chat-render-data.test.ts):Mobile anchors on
baselineTailMessageId. Desktop already records the same boundary —NativeChatPendingSend.afterMessageId, "Last authoritative transcript message visible when this send was issued" — and only ever used it for matching. This reuses it for placement rather than adding a parallel concept.Scope vs the overlapping PRs
This is a renderer-layer change. It touches no decoder and no main-process transcript path, so it neither competes with nor duplicates the queued-prompt admission work:
transcript-line-decoders-claude.tsnative-chat-pending-anchor.tsThose admit the persisted
queued_commandrecord so a transcript row exists, after which the existing pending reconciliation removes the echo. This decides where an echo is placed when no row will ever exist.That case is not hypothetical. As lurunzi notes, the admission filter deliberately excludes older records lacking
origin.kind/humanTurn, and cannot help "prompts that never reached the process". Those echoes have no row to match, ever, and are exactly what this anchors.The two layers compose: with admission landed, fewer echoes reach the fallback; without it, none of them are placed correctly. Happy to land this under #19428 if that is the preferred shape.
Linked Issue
Fixes #20846
Visual Proof
Before
After
Testing
Windows 11 (26200), against Claude Code v2.1.272.
pnpm test src/renderer/src/components/native-chat— 152 files, 1547 tests pass;src/shared/native-chat— 457 pass.native-chat-pending-anchor.test.ts(10 tests). Three of them only mean anything because they fail without the change:orderNativeChatMessages— without the rank work,pending:p1sorts to the tail;createNativeChatMessageListProjection— without the fold work,expected [] to deeply equal [ 'a.txt' ];pnpm tcclean;oxlintand the pre-commitoxlint --config config/oxlint-react-doctor.json+oxfmtclean.src/shared(agent-hook-listener-relay-dependency,git-fetch-head-lock-key-derivation) are pre-existing on a Windows host — verified by stashing this change and re-running them. Mobile deps do not install on this host (react-native patch fails to parse), so mobile is left to CI; the fold change touches it.Verified in a real build, not just unit tests:
pnpm build:unpack, sent a second message mid-turn, and the bubble held its position while the rest of the reply landed below it — chat view and terminal pane finally in the same order. Two earlier attempts on this branch each passed their tests and still failed here, which is why that check is worth calling out separately.AI Disclosure
Written with Claude Opus 5 (Claude Code). The transcript evidence in the linked issue is a real capture from my own machine.
Worth stating plainly for reviewers: the first three commits on this branch were wrong in ways the automated review caught — the anchored array never reached the DOM, then the fix dropped a tool result and flipped the streaming tier. Every one of those was found by @pullfrog and @coderabbitai, not by me. The tests that now guard them were each written to fail first.
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
afterMessageIdis already written by every send path; entries missing it fall through to today's tail behaviour.Mapand oneSetover the rendered window, inside the existinguseMemo; the early return is unchanged when nothing is pending.foldToolMessages. The change there is one added exclusion in a reset condition and cannot affect a transcript row; mobile keeps its own anchoring.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)