fix(chat): carry a steered message's attachments into the live turn
A message steered into a running turn rendered as text alone while the turn streamed. The image only appeared after closing and reopening the conversation, which reads the agent's own transcript instead. The bytes reached the agent all along — the wire carries the hydrated draft — but `FeedbackItem`, the note that is recorded and broadcast, held only `text`, and `text` is the composer's display form, which collapses attachments into words. The note now carries what was sent as well, projected by `user_blocks_from_prompt` after hydration: the same contract an ordinary prompt's `user_message` already follows, so a steered image and a prompted one travel identically. Nothing in the renderer changed — `extractUserImagesFromBlocks` already turns image blocks on a user turn into the attachment strip. Widening those broadcast blocks back into content blocks is now one shared helper, since the viewer's `user_message` echo and an adopted steered message have to agree on the result. That also restores the suppression that keeps such a message from rendering twice. `suppressPersistedSteeredPrompts` matches the persisted copy on content and its key folds in full image data, so keying the live copy on `text` alone could never match an image steer; the duplicate surfaced whenever the detail refetched mid-turn. Both keys are built from the same blocks now. The retained bytes are bounded per turn. `MAX_FEEDBACK_CHARS` bounds a note's text and leans on "the count scales with human typing", an argument that does not survive base64 image data, so without an aggregate bound a run of image steers grows `SessionState.feedback` — and every snapshot rebuilt from it — without limit. The check sits at the single authorized writer, sharing one critical section with the append, so concurrent steers and a replay node are bounded alike. Past the budget the note still delivers and simply records no blocks, falling back to its text: the agent has already been handed the content, so refusing the message would be the wrong trade. A text-only steer is unchanged end to end, pinned by its own assertion.
X
xintaofei committed
e137f0ea2de853a34e6bfb9798d438759724219c
Parent: 35fe8af