Repository navigation
fix(native-chat): place mid-turn and ! prompts where they were sent - #26582
pawel-kotowski wants to merge 1 commit into
Conversation
Claude records a prompt sent while it is busy only as a `queued_command` attachment, and a `!` shell command as a `<bash-input>` row that chat hides as harness noise. Neither gave the optimistic echo a row to match, so the echo stayed pinned below every later turn. Decode both as the user row they stand for; keep `!` commands out of the turn lifecycle so they never mark the agent working. Fixes stablyai#14308, stablyai#20846, stablyai#25956. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
📝 Walkthrough
Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: 🔵 Low · up to Some shell-command messages may appear incorrectly or be treated as working turns. These limited cases warrant a fix or explicit acceptance but do not appear to block merging. 🚥 Pre-merge checks | ✅ 4 | ❌ 1
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: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
31e11c3e-874f-4858-9c17-03ac758d2eac
📒 Files selected for processing (6)
src/main/native-chat/transcript-line-decoders-claude-queued-prompt.test.tssrc/main/native-chat/transcript-line-decoders-claude.tssrc/main/native-chat/transcript-turn-lifecycle.test.tssrc/main/native-chat/transcript-turn-lifecycle.tssrc/shared/claude-bash-input.test.tssrc/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.
| // hidden as noise and the echo stayed pinned at the tail (#25956). | ||
| blocks: | ||
| role === 'user' | ||
| ? blocks.map((block) => unwrapClaudeBashInputBlock(unwrapClaudePastedContentBlock(block))) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Require a single Bash-input block before converting the user row.
If a user row contains a <bash-input> text block and another text block, this map converts the first block to a ! command. isClaudeBashInputRecord does not recognize that multi-block row and emits a working turn. Apply the single-envelope condition to the complete row so display and lifecycle decoding use the same boundary.
| * stripped. Returns the line as typed, or null when the text is anything else. */ | ||
| export function claudeBashInputAsTypedCommand(text: string): string | null { | ||
| const command = BASH_INPUT.exec(text)?.[1] | ||
| return command === undefined || command.includes('<bash-input>') ? null : `!${command}` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Allow literal <bash-input> text inside a shell command.
If the user types !echo '<bash-input>', the transcript contains <bash-input>echo '<bash-input>'</bash-input>. The substring check returns null, although the row has one outer envelope. The decoder leaves the command as markup, and the lifecycle decoder treats it as a working turn. Reject additional envelopes rather than every occurrence of the opening tag.
6dc0099 to
9d9f101
Compare
|
Have the same issue, waiting for it to be merged 😊 |
|
Tested this PR on Windows + WSL2 (Orca 1.4.227, Claude Code 2.1.302). It fixes #20846 there. Real transcript. A message sent while Claude was working left only a
In the app. I fed a transcript with the same shape into a background e2e build.
Other checks
One thing the tests don't cover. Claude stamps the |
|
Thanks @pawel-kotowski, the decoder change here was the right call. We rebuilt it on current main in #31204, with you as co-author. That PR also fixes the ordering: Claude stamps a mid-turn prompt with the time it was sent but writes it after the tool result it was read with, and desktop orders by time, so the prompt split tool calls from their results in about half of real sessions. Closing in favour of #31204. |
ELI5
If you send Claude a message while it is still working, or run a
!shell command from the chat box, the chat view shows your message stuck at the very bottom. Every later reply appears above it, so the conversation reads out of order. This PR makes those messages show up where you actually sent them.What Changed
Before: a message sent mid-turn, or a
!command, stays pinned under the newest reply forever (until restart). The terminal pane shows the right order; chat does not.After: the message appears in the conversation at the point Claude took it, and the temporary "just sent" bubble disappears like it does for any other message.
Mechanism: chat shows a temporary bubble when you send, then removes it once a matching user message shows up in Claude's transcript. In two cases no matching message ever showed up:
type:"user"record. The only record is atype:"attachment"withattachment.type === "queued_command", and the Claude decoder dropped every non-user/assistant record. The decoder now reads aqueued_commandattachment withcommandMode: "prompt"as a user message at that position in the file. It keeps dropping the ones the user did not type: task notifications (commandMode: "task-notification") and peer-agent messages (origin.kind: "peer"). Older Claude versions omitorigin, so a missing origin counts as human.!commands. Claude records! git statusas<bash-input> git status</bash-input>, which chat filters out as harness noise. The decoder now turns a user row that is exactly one<bash-input>envelope back into the line as typed (! git status), which matches the bubble. The turn-lifecycle decoder ignores these rows, so a shell command never flips the agent to "working". Command output (<bash-stdout>) stays hidden as before.The turn-lifecycle decoder already skips non-
userrecords, so the new attachment rows do not change working/done state.Why
The fix sits where the data is lost: the transcript reader. Once the user row exists, the existing bubble-matching code removes the bubble with no renderer changes, and desktop, mobile, and
worker-readall see the prompt.I considered and decided against:
I checked my own
~/.claude/projectstranscripts (about 1,250queued_commandrecords): a humanprompt-mode entry never also gets atype:"user"record, so this cannot double a message.Linked Issue
Fixes #14308
Fixes #20846
Fixes #25956
Related: #22086, #18494
Visual Proof
Before: the screenshots in #14308, #20846, #22086 and #25956 show the pinned bubble. After: replayed a real session transcript (Claude Code 2.1.292) in which two mid-turn prompts were stuck at the bottom. With this change both decode in place and their pending bubbles are pruned. A before/after recording will be attached in a comment.
Testing
New decoder tests in
transcript-line-decoders-claude-queued-prompt.test.tsuse the attachment shape Claude Code 2.1.292 writes: human prompt, legacy entry with no origin, content-block prompt, task notifications, peer messages, a malformed entry, and other attachment types.claude-bash-input.test.tscovers the!round trip, including the space after!and envelopes with surrounding text.transcript-turn-lifecycle.test.ts: neither a!row nor a queued prompt marks the turn as working.pnpm tcpasses. Vitest onsrc/main/native-chat/transcript*,src/shared/native-chat*,src/shared/harness-injected*andsrc/renderer/src/components/native-chat/: 4,531 tests pass.check:code-quality:changedpasses.Replayed a real Claude Code 2.1.292 transcript (macOS) through the decoder and the pending-bubble pruning.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Written with Claude Code (Claude Opus 5.5), reviewed by me.
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
attachmentline, which the decoder already parsed.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)🤖 Generated with Claude Code