cmux NEXT agent chat: long prompts collapse to ten lines with Show more; pasted blocks are chips
Problem
Leo (2026-10-21): a long sent prompt draws in full, so a 2,000-line paste becomes a bubble tens of thousands of pixels tall that you have to scroll past every time, including prompts loaded from history. Claude collapses long user messages instead.
Fix
- Collapse (
conversation/PromptText.tsx,promptCollapse.ts): a prompt taller than 10 lines shows its first 10. The last two fade out under a translucentmask-image, so the bubble's glass shows through and nothing covers it. Under them is a Show more / Show less text button: hover pill only, notitle, reusing the localizedmessage.more/message.less. The hover row (time, receipt, Copy, Edit, Retry, Fork, Pin) is unchanged, and Copy still copies the full text (row.text). Whether a prompt is long is measured against its line height after layout. Before layout, a text heuristic decides. - State survives virtualization and history: open/closed is stored in the transcript disclosure store (
transcriptDisclosure.tsx) underprompt:<row id>. Row ids areuser-<seq>, so the state holds when a row is scrolled out and back, and on reload. Prompts from history collapse the same way, because the logic lives in the bubble, not the composer. - Find: if the current match is in a collapsed prompt (or a closed paste chip), that prompt opens and the highlights repaint, using the same
FindRevealpathIncomingMessageCarduses. - Row estimate (
model.ts): user rows are estimated at their collapsed height (10 lines plus the 24px Show more line), so rows don't jump when a long prompt mounts. Chief prompts keep their own bubble. - Pasted text chip: if a
user_messagecarriespasted: [{start, end}], each pasted block draws as the composer's attachment chip from #23100 (acpmux-attachment acpmux-attachment-file acpmux-attachment-link) without the ×. The chip shows the paste's first words and its line count ("2,300 lines", a new plural string in all 21 locales). Clicking it opens the paste in place, and opening it also opens the prompt. - Gallery (
agent-pane.user-bubble): new stateslong-collapsed,long-expanded,short(unchanged),pasted-chipandpasted-chip-open.
Paste marker: what composer-polish and acpmux must send
I read the code and checked whether a paste marker survives the whole path. It does not reach the transcript today. session/prompt keeps the blocks and their _meta, but the daemon records user_message as plain joined text:
cmux-tui/crates/acpmux/src/hub/turns.rs:263prompt_text(&blocks)(hub/session.rs:197joins the non-context text blocks with"\n"). Thenturns.rs:303builds{text, client, promptId, turnId}, andappend_prompt(hub/prompt_images.rs:82) adds onlyimages. Steers go the same way (hub/steer_end.rs:81-84).- History, reload and
fetchOlderpage the sameuser_messagerecords (hub/paging.rs), so the loss applies there too. - The Claude stdio bridge (
claude_stdio/outbound.rs:63-100) drops_metafrom each text block. The agent still gets plain text, which is intended. The webview reads only the muxuser_message(direct.ts), not the echoeduser_message_chunk.
Contract this PR reads:
- Composer (composer-polish): send each paste as its own ACP text content block after the typed block, with
_meta: {"cmux": {"pasted": true}}. - acpmux (separate Rust PR, needs fleet cargo tests): in
Hub::append_prompt, walk the same blocksprompt_textjoins (non-context blocks withtext, separated by"\n"). Recordmsg["pasted"] = [{"start": u, "end": u}, ...]: the UTF-16 offsets intomsg.textof each block whose_meta.cmux.pasted == true. Leave the key out when there are none. The same key can go onqueued(hub/queue.rs:291). - Webview (this PR):
direct.tsreadsmsg.pasteddefensively (pasteRanges: integers only, inside the text, sorted, non-overlapping). It applies them only while the row shows the text exactly as recorded. With no marker, a paste just collapses with the rest of the prompt.
Changelog
- Changed: long sent messages in agent chat collapse to 10 lines with Show more / Show less, and a pasted block shows as a "Pasted text" chip that expands in place.
Testing
Red then green. Both commits ran on a fleet light step (scripts/ci/run-webviews-tests.sh):
- Red, e1f009e97ee (tests only):
test/user-prompt-collapse.test.tsxandsrc/agent-session/acpmux/rowEstimates.test.tsFAIL (Cannot find module './conversation/promptCollapse'). Stepumc-red-e1f009e:2 files, 2 failed. - Latest green, dbd104d805d, step
umc-green7-dbd104d80:23 files, 0 failed(my 5 files plus the 18 transcript and agent-pane files that react-apps-check ran). - Green, 6f946de000d (fix): step
umc-green3-6f946de00:5 files, 0 failed. PASStest/user-prompt-collapse.test.tsx(8 tests),rowEstimates.test.ts, and the neighborssent-image-lifecycle.test.tsx,ChiefPromptBubble.test.tsx,strings-build-output.test.ts. An earlier green run at 4ff08f4b318 caught a real bug: a bubble reused for another prompt kept the old measured length. 6f946de fixes it.
New tests:
test/user-prompt-collapse.test.tsx(JSDOM): a 2,000-line prompt opens clamped and faded with Show more; the toggle has notitle; the hover row stays. The toggle opens and closes it, and the choice survives an unmount and remount. Copy copies the full 2,000 lines. A short prompt is unchanged. A Find match opens a collapsed prompt. A pasted block is a chip ("error at frame 1", "2,300 lines", no ×) that opens in place, and Copy is still the full prompt. A Find match opens a closed chip.pasteRangesdrops malformed input.rowEstimates.test.ts: a 2,000-line prompt is estimated at 16 + 10 lines + 24 + 28 + 8, under 10.5 lines. A short prompt has no Show more line. A 2,300-line paste estimates as its typed line plus one 40px chip.test/agent-pane-long-prompt.test.ts(Playwright, GitHub CI only; fleet workers have no browsers): a 2,000-line prompt from history draws at most 10 lines tall, its mask is a gradient, the next row starts where it ends, Show more opens it (>1,000 lines), Show less closes it again, and a short prompt has no toggle.
Observed / Not run
Observed: the fleet red and green runs above; scripts/web-lint.sh passed (webviews bun run check: Oxfmt, Oxlint, type check, style checks); scripts/cmux-next/prepush-gate.py: no new violations.
Not run: the Playwright tests agent-pane-long-prompt.test.ts and agent-pane-chat-column.test.ts (fleet workers have no browsers, so they run only in GitHub CI; dbd104d805d makes an unclamped prompt's text wrapper display: contents again, so the glass bubble test reads the same 12px top inset as before); gallery captures (the advisory gallery run renders the new states); no macOS app build or real-app dogfood. The clone is sparse (webviews, scripts, schemas, tools, config, Packages/Shared, Packages/macOS, Resources, KeyboardPinningLab, web/data, cmux-tui/crates, root files). Lint needed those paths. No animation was added for opening and closing: the change applies in one frame, so it needs no motion gate.
Agent session: cosmic-gray
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Long sent prompts in agent chat now collapse to their first ten lines with a Show more/Show less toggle, so a 2,000-line paste no longer draws as a bubble tens of thousands of pixels tall. Pasted blocks draw as chips (first words plus line count) that expand in place.
- The last two collapsed lines fade out under a translucent mask; Copy still copies the full text, and Find opens a collapsed prompt or chip holding the current match, with chip labels never counting as matches.
- The open/closed state is stored per row id, so it survives virtualization and applies to prompts loaded from history.
- Row estimates use the collapsed height so nothing below jumps when a long prompt mounts; a bubble reused for another prompt re-measures instead of keeping the old length, and an unclamped prompt's text wrapper is
display: contentsso the bubble insets its text as before.
Paste marker
The pasted ranges this PR reads come from user_message.pasted, which acpmux does not record yet. composer-polish sends each paste as its own ACP text block with _meta.cmux.pasted, and acpmux needs a follow-up to write the UTF-16 ranges into the recorded text. Until then, pastes collapse with the rest of the prompt.