Repository navigation
Conversation
Claude writes one transcript record per send, not per image, so a two-image send arrives as a single turn carrying two [Image: source: …] text blocks. The fold matched only a sole-block turn, so every send past the first image lost its attachments and leaked the raw markers as prose. Collect the paths from all of a turn's blocks, and only when every block is a marker so a turn that also carries prose is left alone. Fixes stablyai#16972
|
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 (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe change adds support for multiple image-source text blocks in user turns. Validation now requires every block to contain a valid image marker and collects all image paths. Transcript normalization aggregates paths across consecutive source turns, merges them with a following prompt when applicable, and emits separate image references otherwise. Tests cover multi-image merging, standalone turns, mixed prose, and turn classification. Merge Risk: ⚪ Minimal · up to The change preserves multiple image attachments instead of leaving their source markers as visible text, while mixed prose remains untouched. It is merge-ready after normal checks and review, with no actionable merge-blocking risk remaining. 🚥 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 |
ELI5
Attach one image to a chat message and it shows up. Attach two and both disappear, and the raw
[Image: source: /path]text Claude uses to record them shows up as if you had typed it. The fold that turns those markers into attachments only ever matched a one-image send.What Changed
src/shared/native-chat-image-transcript-markers.ts:soleTextwithimageSourcePaths(message), which collects a marker path from every block of a turn. It returns empty unless every block is a text block that is a marker, so a turn that also carries prose is never rewritten.isImageSourceUserTurnandnormalizeImageTranscriptMessagesboth go through it; the run-walk over consecutive marker turns now appends each turn's whole path list.image-refper path from that turn only —indexis not advanced past the run there, so following marker turns are still emitted by their own iteration.No change to how a marker turn pairs with its prompt, to
[Image #N]stripping, or to the ordering the fold expects. The only thing that moved is the block-count gate.Why
Claude writes one transcript record per send, not per image. An N-image send arrives as a prompt record with N
imageparts plus a marker record carrying N[Image: source: …]text parts, anddecodeClaudeTranscriptLinemaps that record to one message with N blocks. The gate required exactly one:So only a single-image send took the fold. Everything above it produced zero
image-refblocks and left the markers as prose.Shapes sampled from local Claude transcripts, prompt record ↔ its marker record:
contentpartscontentpartstext, imagetexttext, image, imagetext, texttext, image ×3text ×3text, image ×4text ×4text, image ×24text ×24Only the first row worked.
Linked Issue
Fixes #16972
Visual Proof
No new UI. The change is that a multi-image send renders as attachments instead of as raw marker text, which is the pipeline output below rather than a new surface.
Before, on
cf5e087246—normalizeImageTranscriptMessagesover a real-shaped two-image send:After:
I did not capture the rendered pane; what I measured is the pipeline output the pane consumes.
Testing
Final merge base:
cf5e087246Automated — four cases added to
src/shared/native-chat-image-transcript-markers.test.ts:image-ref ×2+ the prompt textimage-ref ×3isImageSourceUserTurnConfirmed RED on the merge base: the first, second and fourth failed (
expected [ …(2) ] to have a length of 1 but got 2, markers still text,expected false to be true). The mixed-prose case already passed, so it is a guard rather than new behavior — it fails if the collector is loosened to "any block is a marker".tsc --noEmitclean for bothconfig/tsconfig.tc.web.jsonandconfig/tsconfig.node.json; oxlint and oxfmt clean on the changed files.Platforms: mobile re-exports these functions verbatim from
mobile/src/session/mobile-native-chat-image-transcript-markers.ts("Single-sources the marker logic"), so it picks the fix up with no mobile change. I could not run the mobile suite —mobile/fails to resolveexpo/tsconfig.base.jsonin this environment and all 136 files fail that way on a clean merge base too. Exercised on macOS; the logic is pure and platform-independent.AI Disclosure
Investigated and written with Claude (Opus 5) via Claude Code, then reviewed with Codex (
codex review), which returned no findings. The transcript shapes above were measured from real local transcripts, and the RED/GREEN runs and suite numbers were actually run.Review
Adjacent open work, no overlap that I can see: #14939 touches the mobile re-export for literal-marker stripping, and #11712 preserves attachments across transcript refresh. Neither changes the block-count gate this PR fixes.
While reporting #16972 I also noticed the doc comment says marker turns come before the prompt, while every sampled occurrence has them after — the single-image case survives only via the standalone branch, so an image renders as its own bubble rather than as an attachment on the message it was sent with. That is left alone here; happy to follow up if it is not intended.