Skip to content

fix(native-chat): show Claude prompts sent mid-turn and ! commands where Claude read them - #31204

Merged
AmoabaKelvin merged 1 commit into
mainfrom
fix/native-chat-mid-turn-order
Oct 20, 2026
Merged

AmoabaKelvin merged 1 commit into
mainfrom
fix/native-chat-mid-turn-order

Conversation

@AmoabaKelvin

@AmoabaKelvin AmoabaKelvin commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​269 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​269
Prod 8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​113 $\color{#cf222e}{\Huge{\mathbf{−}}}$​8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​105

ELI5

In a Claude chat, two kinds of messages got stuck at the very bottom: a message you send while Claude is still working, and a ! shell command. Every later reply showed up above them, so the conversation read out of order. Now they show up where Claude actually read them.

What Changed

Before: a message sent while Claude was busy, or a ! command, stayed pinned below the newest reply until you restarted. The terminal showed the right order; the chat did not.

After: the message appears at the point Claude took it in, and its temporary "just sent" bubble goes away like any other message's.

Mechanism: chat shows a temporary bubble when you send, and removes it once Claude's transcript has a matching user message. In two cases it never had one:

  • A message sent while Claude is busy. Claude takes it in at its next tool result and writes no normal user record, only a queued_command record. The Claude transcript reader dropped those records. It now reads one as a user message when you typed it (commandMode: "prompt"), and still drops the ones you didn't type: background-task notices and messages from other agents. Older Claude versions leave out who sent it, and those entries were always typed by the user.
  • A ! command. Claude records ! git status as <bash-input> git status</bash-input>, which chat hid as Claude's own bookkeeping. The reader now turns that row back into the line you typed. Command output stays hidden as before, and the row still doesn't mark the turn as working, the same as today.

Ordering. Claude stamps a queued message with the time you sent it, but writes it into the file after the tool result it was read with. Desktop chat orders Claude's rows by time, so in more than half of the real cases the message landed between a tool call and its result. The call then showed no output, and the output became a stray row under your message. The transcript readers now give each row a time no earlier than the rows above it in the file (one millisecond past them when it would be earlier). Mobile already keeps file order and gets the same rows.

Why

The fix sits where the information was lost: the host's transcript reader. Once the user row exists, the existing bubble matching removes the bubble with no renderer changes, and desktop, mobile and worker-read all see the message.

Considered and decided against:

  • Re-anchoring the stuck bubble in the renderer (fix(native-chat): anchor a mid-turn send where it was issued #20847). It places the bubble better, but the bubble stays pending forever and the message is still missing after a reload.
  • Ordering Claude rows by file position instead of time. Desktop draws some of its own rows among Claude's by time, such as the "Ran /model" line for a slash command. Those rows have no file position, so they would have dropped below every later turn.
  • Fixing the order in the renderer. The desktop's live updates are merged by id and can repeat rows it already has, so the renderer can't tell file order. The readers already read lines in file order and keep per-file state between appends.

This rebuilds #26582 by @pawel-kotowski on current main, with the ordering fix added. It also covers the open decoder PR #14309 (and the closed #16121 and #19428).

Linked Issue

Fixes #14308
Fixes #20846
Fixes #25956
Fixes #22086
Fixes #18494

#22086 and #18494 are the same mid-turn case. In #18494 the stuck bubble ("ten en cuenta que hora son las 12 y 1") is a message Claude read during a tool call and answered, which the resumed terminal shows. #22086 happens only when sending while Claude works, and not every time, because a message still waiting when the turn ends does get a normal user record.

#29212 (mobile) is a different bug. There the phone kept only the newest 40 rows during a turn, which #28413 already fixed (desktop 1.4.228 and later serve that page).

Visual Proof

Claude Code 2.1.304 terminal chat on macOS, hidden dev builds of main and this branch.

A message sent while Claude runs a 35s command ("Also say banana at the end."):

Before (main) After
before-midturn.png after-midturn.png

On main the message is stuck below Claude's answer to it. After, it sits where Claude read it, and the command keeps its output.

A ! command, then one more message:

Before (main) After
before-bang.png after-bang.png

On main both the queued message and ! echo hello-bang stay pinned at the bottom, and Claude's reply to the ! is merged into the first turn ("Worked for 1m 56s"). After, each sits in order with its own reply.

While Claude is still working after reading a second message that asks for another command:

after-midturn2-during-2.png

The first turn folds as soon as Claude reads the new message, and the work that follows shows under it, as in the terminal.

Known and left for a follow-up: until the turn ends, the newest turn's live "Working for" counts from the start of the whole run, so it reads 31s here. It settles once a later turn arrives. Main also miscounts in this case: it gives the pinned bubble its own "Worked for 39s".

Testing

  • transcript-file-order.test.ts runs a real-shaped mid-turn sequence through the full read, the tail read and a live append after the first snapshot, then through the chat projection. It checks that the message sits after the tool run and that the call keeps its result. All three cases fail without the reader change, and the append case also fails without the snapshot-to-append handoff.

  • Decoder tests use the record shapes Claude Code 2.1.292 and 2.1.304 write: a typed message, an older entry with no sender, a message with content blocks, background-task notices, messages from other agents, and a malformed entry. The ! tests cover a command with and without a space after !.

  • Replayed all 507 Claude sessions on my machine through the real reader and chat projection. Main: 12 tool results without their call (all predate this work). This branch: still 12, with 64 messages sent mid-turn now shown. The decoder change alone would have added 35 more.

  • Measured against 152,727 real Claude rows and 46,404 Codex rows: the new ordering moves 77 Claude rows (the 66 mid-turn messages plus 11 tool results already stamped earlier than the row above them) and no Codex rows.

  • Drove the real Claude Code 2.1.304 CLI to capture the ! rows. It writes <bash-input> echo hello-bang</bash-input> (space kept) and <bash-input>echo nospace</bash-input>.

  • pnpm lint, pnpm tc, check:code-quality:changed, check:react-doctor:changed, and the native-chat suites (815 files, 8,815 tests) pass.

  • Tested on macOS.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

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

  • Cross-platform: transcript parsing and timestamps only, no paths or platform APIs.
  • SSH and mixed versions: the host publishes extra ordinary user rows and adjusted row times. Nothing on the wire changes shape, and older clients draw these rows like any other user message.
  • Mobile: receives the same rows, already in file order.
  • Performance: one comparison per decoded row, plus one field check per attachment line the reader already parsed.

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)

…laude read them

Claude records a prompt it takes while busy only as a `queued_command`
attachment, and a `!` command as a `<bash-input>` row that chat hid as
harness noise, so neither echo ever matched a row and both stayed pinned
below every later turn. Decode both as the user row they stand for.

The queue entry carries the time it was sent but sits where Claude read
it, after the tool result. Desktop orders terminal transcripts by time, so
the readers now stamp each row no earlier than the rows above it in the
file; otherwise the prompt split tool calls from their results.

Fixes #14308, #20846, #25956.

Co-authored-by: Paweł Kotowski <p.kotowski89@gmail.com>
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Claude transcript decoding now recognizes eligible queued prompts as user messages and restores typed Bash commands from Bash-input blocks. Lifecycle decoding excludes these records from starting a new working generation. Full, tail, and incremental transcript readers normalize timestamps to preserve file order. Tests cover decoding and ordering across reads and subscriptions.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🔵 Low · up to 32ed0

Some transcript messages recorded at the same time can appear in the wrong order. The fix is localized; merge with that issue addressed or explicitly accepted.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 12 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 changes: showing Claude prompts sent mid-turn and ! commands at their transcript positions.
Linked Issues check Passed The active linked issues are satisfied. For [#14308] and [#20846], decodeClaudeTranscriptLine converts human queued_command records with commandMode: "prompt" into user messages. It excludes non…
Out of Scope Changes check Passed The changes stay within the linked issue scope. Production changes are limited to Claude transcript decoding, ! command unwrapping, file-order timestamp normalization, incremental and tail reader st…
Description check Passed The pull request description is complete and follows the repository template. It includes an ELI5 summary, user-facing changes, mechanism, rationale and alternatives, linked issues, visual proof, test…
Full details: Linked Issues check

Explanation

The active linked issues are satisfied. For [#14308] and [#20846], decodeClaudeTranscriptLine converts human queued_command records with commandMode: "prompt" into user messages. It excludes non-human origins and invalid prompts. inTranscriptFileOrder is applied to full, tail, and incremental transcript reads. The ordering tests cover tool results, full reads, tail reads, and appended rows. For [#25956], Claude &lt;bash-input&gt; rows are converted to ! commands. Lifecycle decoding ignores these rows as turn-starting activity. Decoder and lifecycle tests cover these cases. Closed [#16121] is historical context only.

Full details: Out of Scope Changes check

Explanation

The changes stay within the linked issue scope. Production changes are limited to Claude transcript decoding, ! command unwrapping, file-order timestamp normalization, incremental and tail reader state, and lifecycle handling. The added shared parser and reader tests directly support [#14308], [#20846], and [#25956]. No unrelated product behavior is established by the reviewed changes.

Full details: Description check

Explanation

The pull request description is complete and follows the repository template. It includes an ELI5 summary, user-facing changes, mechanism, rationale and alternatives, linked issues, visual proof, testing details, cross-platform and compatibility notes, AI skill boundary, and a completed checklist. It also documents the known follow-up limitation. The description does not explicitly state whether the AI Disclosure section is applicable, and it does not report a pnpm build result, but these are minor omissions and do not prevent review.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 02b9a63f-a819-4dd9-8431-0a991ff105c7
📥 Commits

Reviewing files that changed from the base of the PR and between 6ac9458 and 32ed020.

📒 Files selected for processing: 12
  • src/main/native-chat/transcript-file-order.test.ts
  • src/main/native-chat/transcript-file-order.ts
  • src/main/native-chat/transcript-incremental-reader.ts
  • src/main/native-chat/transcript-line-decoders-claude-queued-prompt.test.ts
  • src/main/native-chat/transcript-line-decoders-claude.ts
  • src/main/native-chat/transcript-stream-lines.ts
  • src/main/native-chat/transcript-tail-reader.ts
  • src/main/native-chat/transcript-turn-lifecycle.test.ts
  • src/main/native-chat/transcript-turn-lifecycle.ts
  • src/main/native-chat/transcript-watch-engine.ts
  • src/shared/claude-bash-input.test.ts
  • src/shared/claude-bash-input.ts

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

Comment on lines +1 to +17
import type { NativeChatMessage } from '../../shared/native-chat-types'

/** Desktop orders terminal transcript rows by time, but the file holds the true order:
* Claude stamps a prompt it took mid-turn with when it was queued, then writes it
* where it read it. So a row never sorts above the rows before it. `latest` is the
* newest time above this row; returns the row and the newest time including it. */
export function inTranscriptFileOrder(
message: NativeChatMessage,
latest: number
): { message: NativeChatMessage; latest: number } {
if (message.timestamp === null || message.timestamp >= latest) {
return { message, latest: Math.max(latest, message.timestamp ?? latest) }
}
// Why: one past, not equal: equal times fall back to id order, which can still put it first.
const timestamp = latest + 1
return { message: { ...message, timestamp }, latest: timestamp }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,100p' src/main/native-chat/transcript-file-order.ts
rg -n 'sort\\(|timestamp.*id|compare.*[Mm]essage|latestTimestamp|inTranscriptFileOrder' src/main/native-chat src/shared | head -130

Repository: stablyai/orca

Length of output: 1188


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- helper usages ---'
rg -n -F -- 'inTranscriptFileOrder' src || test "$?" -eq 1
printf '%s\n' '--- native-chat sort-related references ---'
rg -n -F --glob '*.{ts,tsx}' -e '.sort(' -e 'sort(' -e 'timestamp' -e 'id' src/main/native-chat src/shared || test "$?" -eq 1
printf '%s\n' '--- changed-file diff against requested base ---'
git diff --no-ext-diff --unified=80 6ac9458d0b8beffc4d7d82e9f0e4538cd3a476d1 32ed0204fe106daa3e266b2dbe11cbf3da89d385 -- src/main/native-chat/transcript-file-order.ts

Repository: stablyai/orca

Length of output: 45666


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- caller blocks ---'
sed -n '105,155p' src/main/native-chat/transcript-incremental-reader.ts
sed -n '135,180p' src/main/native-chat/transcript-tail-reader.ts
sed -n '1,60p' src/main/native-chat/transcript-stream-lines.ts
printf '%s\n' '--- focused sort calls in transcript-related source ---'
rg -n -F --glob '*.{ts,tsx}' -e 'messages.sort(' -e 'rows.sort(' -e 'transcript.sort(' -e 'sort((a, b)' -e 'sort((left, right)' src/main src/renderer src/shared || test "$?" -eq 1
printf '%s\n' '--- timestamp comparator candidates ---'
rg -n --glob '*.{ts,tsx}' 'timestamp[[:space:]]*[-+]|[.]timestamp[[:space:]]*[<>]|compare.*timestamp|timestamp.*compare|id.*localeCompare|localeCompare.*id' src/main src/renderer src/shared || test "$?" -eq 1
printf '%s\n' '--- realistic equal-time fixture ---'
sed -n '1,125p' src/main/native-chat/transcript-line-decoders-claude-queued-prompt.test.ts

Repository: stablyai/orca

Length of output: 41839


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- native-chat renderer files ---'
rg --files src/renderer/src | rg '/native-chat/|native-chat'
printf '%s\n' '--- native-chat renderer ordering references ---'
rg -n --glob '*.{ts,tsx}' -e 'NativeChatMessage' -e 'messages' -e 'timestamp' -e 'sort' -e 'toSorted' -e 'toReversed' src/renderer/src/components/native-chat src/renderer/src/store src/shared/native-chat* src/shared/agent-session* || test "$?" -eq 1
printf '%s\n' '--- transcript consumer references repository-wide ---'
rg -n --glob '*.{ts,tsx}' -e 'readNativeChatTranscript' -e 'decodeTranscriptStream' -e 'transcriptMessages' -e 'NativeChatMessage\\[\\]' -e 'nativeChat.*messages' src || test "$?" -eq 1

Repository: stablyai/orca

Length of output: 45667


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- message-list projection ---'
nl -ba src/renderer/src/components/native-chat/native-chat-message-list-projection.ts
printf '%s\n' '--- transcript items ---'
nl -ba src/renderer/src/components/native-chat/NativeChatTranscriptItems.tsx
printf '%s\n' '--- message list ordering-relevant blocks ---'
rg -n -F --glob '*.tsx' -e 'NativeChatTranscriptItems' -e 'messages.map' -e 'messageList' -e 'sort' -e 'timestamp' src/renderer/src/components/native-chat/NativeChatMessageList.tsx src/renderer/src/components/native-chat/NativeChatView.tsx src/renderer/src/components/native-chat/NativeChatTranscriptRow.tsx || test "$?" -eq 1
printf '%s\n' '--- file-order tests ---'
nl -ba src/main/native-chat/transcript-file-order.test.ts

Repository: stablyai/orca

Length of output: 13950


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- compareMessages definition and callers ---'
rg -n -F -- 'compareMessages' src/renderer/src/components/native-chat src/shared src/main || test "$?" -eq 1
printf '%s\n' '--- native-chat session assembler relevant block ---'
rg -n -F --glob '*.ts' -e 'function compareMessages' -e 'const compareMessages' -e 'export.*compareMessages' src/renderer/src/components/native-chat src/shared || test "$?" -eq 1
printf '%s\n' '--- shared transcript projection ---'
nl -ba src/shared/native-chat-transcript-projection.ts

Repository: stablyai/orca

Length of output: 12237


🏁 Script executed:

nl -ba src/renderer/src/components/native-chat/native-chat-session-assembler.ts | sed -n '90,125p'
printf '%s\n' '--- nearby comparator tests ---'
rg -n -F --glob '*.{ts,tsx}' -e 'compareMessages(' -e 'same timestamp' -e 'equal timestamp' -e 'timestamp.*id' src/renderer/src/components/native-chat src/shared/native-chat* src/main/native-chat || test "$?" -eq 1

Repository: stablyai/orca

Length of output: 3006


🏁 Script executed:

nl -ba src/main/native-chat/transcript-turn-lifecycle.test.ts | sed -n '55,190p'
printf '%s\n' '--- repeated timestamp records in native-chat tests ---'
rg -n -B3 -A3 -F -- '2026-07-16T23:45:37.608Z' src/main/native-chat || test "$?" -eq 1

Repository: stablyai/orca

Length of output: 12141


Make equal transcript timestamps strictly increasing.

inTranscriptFileOrder leaves a row unchanged when its timestamp equals latest. The renderer then sorts transcript rows by timestamp and uses the semantic row ID as the tie-breaker. A later file row with a lower ID can therefore render before the earlier row.

Change the comparison to > so equal timestamps receive latest + 1.

Suggested fix
-  if (message.timestamp === null || message.timestamp >= latest) {
+  if (message.timestamp === null || message.timestamp > latest) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment