Repository navigation
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughClaude queued-command attachments now decode into queued user messages. Native Chat readers and the transcript watcher anchor queued prompts to transcript file order while preserving state across incremental reads, pagination, and snapshot replacement. The change adds unit tests for decoding and timestamp anchoring, live-tail and pagination tests, replay and pending-send regression coverage, and an end-to-end test for Native Chat display order. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Description checkExplanation The description is detailed and covers the change, rationale, linked issue, visual proof, testing, AI disclosure, review considerations, limitations, and checklist. The Agent skill upstream boundary section and explicit Notes heading are not included, but the description is otherwise complete and directly relevant.
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: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 39f2e92a-bfe1-4583-bf36-fe5f2bce87d3
📒 Files selected for processing (10)
src/main/native-chat/queued-prompt-file-order.test.tssrc/main/native-chat/queued-prompt-file-order.tssrc/main/native-chat/transcript-line-decoders-claude.tssrc/main/native-chat/transcript-line-decoders.claude.test.tssrc/main/native-chat/transcript-reader.tssrc/main/native-chat/transcript-tail-reader.tssrc/main/native-chat/transcript-watch-engine.tssrc/main/native-chat/transcript-watch-queued-prompt.test.tssrc/shared/native-chat-types.tstests/e2e/native-chat-queued-prompt-visible.spec.ts
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6b85ab4b-fe30-44d0-af87-f8d0e9403bd5
📒 Files selected for processing (6)
src/main/native-chat/queued-prompt-anchor.tssrc/main/native-chat/queued-prompt-file-order.test.tssrc/main/native-chat/queued-prompt-file-order.tssrc/main/native-chat/transcript-tail-reader-queued-page.test.tssrc/main/native-chat/transcript-tail-reader.tssrc/main/native-chat/transcript-watch-engine.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/main/native-chat/queued-prompt-file-order.test.ts
- src/main/native-chat/transcript-tail-reader.ts
- src/main/native-chat/transcript-watch-engine.ts
- src/main/native-chat/queued-prompt-file-order.ts
|
I came at this record gap from a different direction — tracking down why a composer Independent replay against this branch. I pulled a contiguous 17-record window The same replay on How often it comes up. In that session, of 6 prompts typed while the agent was On whether a queued prompt might not have been delivered. I wondered about One small thing you may already have weighed. The filter here is One more note in case it is worth mentioning in the PR body: this same gap is what The redacted fixture and the replay test are yours if they are useful — happy to |
|
Thank you for going as far as replaying a redacted production window through the reader on this branch — that independent replay is the strongest external validation this PR has had, since everything else so far is my own local sampling. And retiring your own branch to consolidate here was generous; I appreciate it. Your point 3 settles something I had been unsure about myself. I did not have a clean argument that a On
I inspected all 8 records the stricter check would drop. The two with no On the composer echo: that matches what the change does, but I have not traced that path myself yet, so I will not claim it in the PR body until I have. I will look. And yes, I would like the fixture and replay test — drop them in a comment or open them against the branch, whichever is less work for you. Whether they land in this PR or a follow-up is not only my call, though: CI has not been approved to run yet (first-time-contributor gating), so the branch is currently verified locally and by your replay rather than by this repo's CI, and a maintainer has now been assigned. I would rather let them weigh in on scope. |
|
@grboy6770 — yes, please open it against this PR In my last comment I said I'd defer on scope, but I've decided I want the fixture and replay test in this PR. Please open a PR against @brennanb2025 — CI is blocked on a workflow approval Every "PR Checks" run on this PR (four since 2026-08-13) is stuck at Where the PR stands:
|
|
The fixture and replay test are up against your branch — four cases, three of which One correction to my earlier comment, since it is still sitting above and I would
Those 63 are ordinary typed prompts from a version that did not write the field |
|
@grboy6770 You were right — I traced it, and the stranded echo is the same gap. Good catch. The retirement path is Before this change, a prompt typed mid-turn produced no user record at all — its key never appeared, the advanced count stayed at zero, and the echo was kept indefinitely. And since I pushed two characterization tests to |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/native-chat/__fixtures__/queued-prompt-trace.ts (1)
1-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCondense the fixture header comment.
Keep the file-order rationale. Remove the repeated transcript-shape details.
As per coding guidelines, comments must be concise, non-obvious, and brief; prefer one line.
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4051ec68-54da-48c1-91ef-93250a370720
📒 Files selected for processing (2)
src/main/native-chat/__fixtures__/queued-prompt-trace.tssrc/main/native-chat/queued-prompt-trace-replay.test.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
a7ec427 to
c79dd50
Compare
A prompt sent mid-turn is queued by Claude Code, and the queue entry is almost always the only record it ever gets — no `type: "user"` line is written for it. `decodeClaudeTranscriptLine` returns null for every record that is not `user`/`assistant`, so the prompt was missing from chat entirely: the terminal showed it, the model answered it, and the transcript pane had no trace. The renderer's optimistic echo then had nothing to match, so it pinned below the newest reply until `PENDING_SEND_LIMIT` evicted it. Users read that as their prompt vanishing. Decode `attachment` records carrying a `queued_command` into an ordinary user message. Content goes through `claudeContentBlocks`, the same path ordinary turns take, so image-carrying queued prompts decode too. Restricted to `commandMode: 'prompt'`: the other mode is the harness queueing task-notification blobs for itself, and the orchestration transcript path has no noise filter to drop those downstream. Anchor the decoded prompt to the position the transcript gave it. Claude stamps the record when the prompt was enqueued but appends it once the queue drains, so sorting by time alone lifts it above turns that already happened. Placing it one tick past its predecessor keeps chat in the order the terminal showed. The watch engine carries that predecessor across reads because a live append batch can open with the queued record itself, leaving nothing in-batch to anchor against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three paths could still show a queued prompt out of transcript order. Equal timestamps fell through unanchored, but the sorter breaks those ties on id, which can lift the prompt above the record it was appended after. Anchor those too. The tail reader anchored after windowing, so a queued prompt opening a page kept its enqueue stamp once its predecessor fell outside the slice. Anchor the whole chronological read, then window the anchored array. The watch engine seeded its carry-over timestamp only on the bounded snapshot path; an unbounded initial drain emitted unanchored messages and left the seed null, so a later queued-only append had nothing to anchor against either. That carry-over now lives in a small anchor object, so every emit path shares one way of tracking the predecessor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The seconds-only template produced `10:00:60` for the final turn, which is not a valid ISO time — `Date.parse` returned NaN and that record decoded with a null timestamp. The assertions happened to target the queued prompt, so the fixture lied without failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ands The optimistic echo is retired by matching a transcript user turn's content, and only once a later turn advances past it. A prompt typed mid-turn left no user row at all, so the echo never matched — and it carries the send time, so it sat below the turns that landed after it instead of in place. Two cases record both halves: no user row keeps the echo forever, and the queued user turn this branch decodes retires it. Behaviour only, no production change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A synthetic transcript window — the queue pair, the queued_command attachment placed after the row it follows while carrying the earlier enqueue time, and the assistant turns that bracket them — goes through the production reader. Covers the cold read, the incremental append a live pane sees, and a negative for the attachments the agent queues for itself. Three of the four fail on the commit this branch started from.
The case read batches straight from `readIncrementalTranscriptMessages`, which hands back physical file order, and asserted on the concatenation — so it passed with anchoring removed. Routing the batches through the same anchor the watcher holds across reads was not enough on its own either: the record the prompt has to stay behind is a tool result, and the text assertion filters those out, so the sorted order looked right regardless. Assert the relationship instead. The prompt's enqueue stamp predates the record it was appended after, and only the anchor moves it past. Disabling `anchorQueuedPromptsToFileOrder` now fails this case and nothing else in the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
c79dd50 to
1710575
Compare
Greptile SummaryThe PR makes Claude prompts queued during an active turn visible in native chat and preserves their transcript-file ordering.
Confidence Score: 5/5The PR appears safe to merge with no actionable correctness or security defects identified. The queued attachment decoder is narrowly gated, and ordering state is consistently applied across full reads, pagination, live appends, and transcript replacement paths with focused regression coverage.
|
| Filename | Overview |
|---|---|
| src/main/native-chat/transcript-line-decoders-claude.ts | Adds narrowly gated decoding of Claude queued prompt attachments into transcript-backed user messages. |
| src/main/native-chat/queued-prompt-file-order.ts | Introduces an idempotent ordering pass that adjusts queued-message timestamps relative to their physical predecessors. |
| src/main/native-chat/queued-prompt-anchor.ts | Carries the latest anchored timestamp across incremental transcript batches and supports snapshot adoption and reset. |
| src/main/native-chat/transcript-tail-reader.ts | Anchors the complete chronological read before pagination so page-boundary prompts retain their predecessor context. |
| src/main/native-chat/transcript-watch-engine.ts | Integrates stateful anchoring into initial snapshots, replacements, incremental batches, and transcript resets. |
| src/shared/native-chat-types.ts | Adds an optional backward-compatible marker identifying queued transcript messages. |
| tests/e2e/native-chat-queued-prompt-visible.spec.ts | Verifies a queued prompt renders exactly once and in transcript-file order through the native-chat UI path. |
Sequence Diagram
sequenceDiagram
participant U as User
participant C as Claude
participant F as Transcript
participant R as Orca reader
participant V as Native chat
U->>C: Send prompt while busy
C->>C: Queue prompt with enqueue timestamp
C->>F: Append preceding assistant/tool records
C->>F: Append queued_command attachment
R->>F: Decode records in file order
R->>R: Anchor queued prompt after predecessor
R-->>V: Emit ordered user and assistant messages
Reviews (1): Last reviewed commit: "test(native-chat): make the replay's inc..." | Re-trigger Greptile
|
@brennanb2025 The description is rewritten — it now opens with why this is still needed after #16117. The short version:
The settling evidence: the production reader on current Your #16117 measurement (1,049 of 1,132 human queued sends producing an attachment and no user row) showed better matching cannot help, and I agree — this PR does not try to match better, it makes the record exist. The two changes are complementary. Rebased onto CI has never run on this PR: every workflow sits at For scale: production code is +143/−6 across 7 files; the remaining +741 is tests and fixtures. |
|
Additional decoder evidence from Claude Code 2.1.278: after I added a sanitized cross-layer regression and a narrow decoder change in my fork. All 15 cases passed again on Windows today: decode the attachment, reconcile the pending echo, leave the turn lifecycle unchanged, and reject non-human, queue-only or unsupported records. No message was resent. The fixture replaces actual text and identifiers. This only supplies decoding/delivery evidence; it does not fix timestamp-versus-file ordering. Your ordering work in this PR remains relevant. I am sharing this here rather than opening a duplicate PR. Codex assisted with the local fix and this verification. |
|
Thanks for the detailed investigation and contribution! Merged #31204 now reads Claude’s queued prompts into chat history in the order Claude consumed them, including after a reload. Closing this PR as covered by that fix. |
Still needed after #16117
#16117 (
fix(native-chat): anchor an unmatched chat echo where it was sent, merged 2026-08-27) diagnosed the same root cause this PR fixes: Claude consumes a mid-turn send through aqueued_commandattachment and writes notype:"user"record — 1,049 of 1,132 human queued sends in that PR's measurement. It concluded "no amount of better matching changes that" and fixed the placement of the unmatched echo bubble. This PR makes the queued record decode into a real user message in the transcript read. Three reasons #16117 does not cover it, each verified:fix(native-chat): anchor an unmatched chat echo where it was sent #16117 is mobile-only. All four of its files are under
mobile/; it touches zero files undersrc/. The desktop chat pane is untouched — on desktop the prompt is still absent entirely, not merely misplaced.The echo is memory-only.
native-chat-pending.tsholds pending sends inconst pendingSendCache = new Map<...>(). fix(native-chat): anchor an unmatched chat echo where it was sent #16117 keeps that in-flight bubble in the right position, but the bubble does not exist after a reload, on another client, or for anyone reading the session later. This PR puts the prompt into the transcript read itself, so it is durable.The gap is still open on
main. Currentmain(df14d1a29) has noqueued_commandhandling and noattachmentdecoding anywhere undersrc/main/native-chat/. Running the production reader on currentmainover the synthetic 13-record fixture that ships with this PR:The mid-turn prompt is missing. With this PR the same fixture renders it between the turns that bracket it. (The harness for this check was a throwaway test importing only
main'sreadNativeChatTranscript; it is not part of the diff.)The two changes are complementary and do not conflict: this PR touches only
src/andtests/, #16117 onlymobile/. Once a mid-turn prompt decodes into a real user row, echoes for it become matchable normally, and #16117's anchoring becomes the fallback rather than the only answer.ELI5
Send a prompt while the agent is mid-turn and, in the desktop chat pane today, it never appears — the terminal shows it, the agent answers it, chat has no trace it was ever said. The mechanism: when you message a coworker who's busy, they finish what they're doing, then read your message. Claude Code does the same — a mid-turn prompt gets queued, and its transcript record is stamped when you sent it but written to the file when the agent finally took it. Chat did two wrong things with that record: it threw it away entirely, and — once taught to read it — sorted it by its stamp, placing it before a reply the terminal showed it after. This PR decodes the queue record as a normal user message and pins it to the spot the transcript file gave it, so chat and terminal tell the same story. A second visible fix falls out of the same change: the temporary echo bubble chat shows the instant you hit send used to linger forever at the bottom after a mid-turn send — no real message ever arrived to confirm it — and now retires normally.
What Changed
Where to look first: the whole change is two small pieces — everything else is wiring and tests.
src/main/native-chat/transcript-line-decoders-claude.tsqueued_commandattachments intorole:'user'messagessrc/main/native-chat/queued-prompt-file-order.tssrc/main/native-chat/transcript-reader.ts/transcript-tail-reader.ts/transcript-watch-engine.tssrc/shared/native-chat-types.tsqueuedflag onNativeChatMessagequeued-prompt-file-order.test.ts/transcript-line-decoders.claude.test.ts/transcript-watch-queued-prompt.test.tssrc/renderer/src/components/native-chat/native-chat-pending.test.tstests/e2e/native-chat-queued-prompt-visible.spec.ts1 — Decode the record instead of dropping it.
Content goes through
claudeContentBlocks, the same path ordinary turns take, so image-carrying queued prompts decode too. Restricted tocommandMode === 'prompt'(see Why).2 — Anchor it to file order.
anchorQueuedPromptsToFileOrderwalks a batch in file order; when a queued prompt's timestamp precedes its predecessor's, it restamps topredecessor + 1. One tick, not equal —compareMessagesbreaks timestamp ties onidlexicographically, so an equal stamp would sort it right back up.3 — Apply it at every seam chat reads through:
readNativeChatTranscriptTailFile(initial snapshot, replacement, pagination),readNativeChatTranscript, and the watch engine's append emit. The watch engine carries the predecessor timestamp across reads (queuedAnchorTimestamp) and resets it on file replacement — a live append batch can contain only the queued record, leaving nothing in-batch to anchor against.Why
Claude stamps the queue record at enqueue time but appends it once the queue drains, so timestamp and file position disagree — and the queue entry is almost always the only record the prompt ever gets (no
type:"user"line is written for it):sequenceDiagram participant T as Terminal participant C as Claude Code participant F as transcript.jsonl T->>C: prompt B (agent mid-turn) Note over C: B queued — record stamped NOW (enqueue time) C->>F: append reply to prompt A (newer timestamp) C->>F: append B's queued record (older timestamp) Note over F: file order: reply, B — time order: B, reply ⚠️Time-sorting trusts the stamp; the terminal and the agent followed file order. Anchoring keeps the position the file already assigned, which is the order the user actually experienced.
Why the
commandModefilter: across real transcripts,commandMode: 'prompt'is a typed prompt andtask-notificationis a harness-generated XML blob the agent queues for itself — roughly half (~46%) of allqueued_commandrecords, and no other values exist. The orchestration transcript path (src/main/runtime/orchestration/worker-transcript-read.ts→boundWorkerTranscriptMessages) has nostripNoiseMessagesand a 40-message window, so without the filter a supervising agent would read harness XML as human input.A second fix that falls out: the stranded composer echo. Sending from the chat composer immediately renders an optimistic "queued" echo bubble so the send feels instant.
prunePendingSends(src/renderer/src/components/native-chat/native-chat-pending.ts) retires that echo viaadvancedNativeChatUserContentCounts(native-chat-pending-occurrence.ts), which talliesrole: 'user'messages by normalized content key and promotes a tally only once a non-user turn follows it. Before this PR a mid-turn prompt produced no user record at all, so its content key never appeared, the advanced count stayed at zero, and the echo was kept indefinitely — and becausependingSendsAsMessagesstamps the echo with the local send time, it sorted to the end of the list, sinking below turns that landed after it instead of holding its position. With this PR the decoder emits that prompt as a realrole:'user'turn anchored between its bracketing turns, the following assistant turn advances the count, and the echo retires normally. The counter filters on neithersourcenor thequeuedflag, so no renderer change was needed. This is the same stranded echo #16117 anchors on mobile; here it retires because a real user row now lands. Found by @grboy6770 while independently replaying a redacted production transcript against the branch; verified by reading the code path and pinned by two characterization tests (see Testing) — not exercised in a running app.Blast radius — verified by reading each call site, not assumed
attachmentrecords previously always decoded tonull, so the decode change is structurally "was null → now a message". Checked against everything downstream of the shared decoder:transcript-turn-lifecycle.ts) filters to user/assistant before the shared decoder — unaffected./clearboundaries, id/turnId dedup, tail-reader offsets: safe.turnIdis intentionally unset, matching all four sibling decoders.isSidechain: truequeued commands: none exist in the data, so subagent chatter cannot leak in.Known limitations, stated honestly:
+1tick could collide if the following record's timestamp were within 1ms of the predecessor's. Simulated over 2,407 local transcripts: 0 collisions; 866 of 881 queued prompts restamped, median shift 17.5s.userline (~0.45% in local data), both survive — the assembler deliberately refuses to merge two same-source prompts with identical text, so genuine repeats are preserved.Linked Issue
Fixes #14308
Visual Proof
🔴 BEFORE — chat is missing the queued prompt the terminal shows
Order on screen:
start the long running task→working through the first task now→both done. The queued prompt (and check the config while you are at it) is nowhere, even though the agent answered it.🟢 AFTER — same transcript; the prompt is present, in the order the terminal showed
Order on screen:
start the long running task→working through the first task now→and check the config while you are at it→both done. The prompt sits after the reply it was appended behind — the same order the terminal showed.Branch State
mainatdf14d1a29; branch head1710575bb; 0 commits behind; GitHub reportsMERGEABLE.transcript-watch-engine.ts; both conflicts were import blocks only — upstream added imports where this branch also adds one. No logic conflict.Testing
pnpm typecheck/pnpm lintelectron-vite build, both desktop targets)transcript-tail-reader-queued-page.test.ts)transcript-watch-queued-prompt.test.ts)tests/e2e/native-chat-queued-prompt-visible.spec.ts)element(s) not found), passing with itnative-chat-pending.test.ts)src/main/native-chat/queued-prompt-trace-replay.test.ts)mainat the top of this description shows the same gapcommandModeguard fails exactly the task-notification test; disabling live-append anchoring fails exactly the new live-tail test — both guards genuinely covered, not incidentally passingmain(df14d1a29): 66,720 passed / 44 failed in 16 files. This branch: 66,743 passed / 48 failed in 18 files. The three files failing only on this branch —transcript-watch.test.ts,run-electron-vite-dev.test.ts,relay/subprocess.test.ts— each pass in isolation (28, 6, 31 tests), and cleanmainhas one failure this branch does not (git-admission-storm-measurement.test.ts). Load-dependent flakiness on both sides; this PR introduces no failure, but neither run is greenpnpm testitself could not start locally —mainnow pinspnpm@12.0.0and the local toolchain could not switch to it — so vitest was invoked directly.Three independent code review passes ran before this PR (architecture, TypeScript/runtime, ordering); findings are folded in — including one blocker, where the first ordering fix missed the live-append path entirely.
AI Disclosure
Claude Opus 5 (Claude Code) investigated the bug, wrote the fix, the unit and live-tail tests, and the e2e spec.
Review
claudeContentBlocks, the exact path ordinary user turns already take.queuedflag is a new optional field (safe perdocs/reference/remote-wire-compatibility.md), documented as main-process-ordering only — renderers must not branch on it.mobile/files, so no overlap with fix(native-chat): anchor an unmatched chat echo where it was sent #16117.nullnow decode to a message; nothing that decoded before decodes differently.type === 'attachment'; the anchoring pass is a single linear walk that returns the same array reference when nothing changed.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)