Repository navigation
cmux-next composer: a chat's unsent pictures and files come back when it is reopened or restored - #23139
Conversation
…ed or restored Leo 2026-10-20: a draft's text came back and its pasted screenshots did not, after reopening a chat and after archiving or deleting it and restoring it, not only after a quit. acpmux keeps the text with the session. The host now keeps each chat's chips by session (AgentPaneDraftAttachments: a folder per chat beside the launch snapshot, contents as files of their own): the page writes them once they settle (chat.draftAttachments) and every page that shows the chat again reads them back (chat.draftAttachments.get). Sending or removing the last chip forgets them. A chip's contents travel once; later writes name it and the file stays. AgentPaneAttachmentFiles is the file work AgentPaneRelaunchWake had, shared by both stores. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cf0-c81e-7bb3-b6cf-e26d341251d0&host=cosmic-gray
… sent
Review of the per-chat draft chips:
- The page op used CmuxNextSettings.JSONValue as if it were Codable; it now goes through
foundationObject and JSONSerialization, off the main actor.
- A write still resting is dropped when the chips go back to what the host has (pasted and
sent within 300 ms, or put back by a refusal), so a sent chip never returns and a restored one
is not erased.
- The host answers which chips' contents it holds ({held}); the page sends any other chip in
full, so a chip put back after a send, a cap or a prune keeps its picture.
- Each chat's reads and writes run in one queue taken before the first wait, so a small later
write never overtakes a large earlier one and a read waits for both.
- A write still resting goes when the page hides (pagehide, visibilitychange), not only at
unmount: a closed tab's page may never unmount.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cf0-c81e-7bb3-b6cf-e26d341251d0&host=cosmic-gray
… before encoding Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cf0-c81e-7bb3-b6cf-e26d341251d0&host=cosmic-gray
|
Owned by projects-34 on cosmic-gray · session |
|
Owned by projects-34 on cosmic-gray · session |
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Unclear: CI failed on
Failures:
Matched log linesNot re-run automatically: Written by red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 29d8bbf to compare; do not merge until green; see lane-rules
Agents: |
|
Bead |
|
Found 49 test failures on Blacksmith runners: Failures
...and 39 more test failures. View all on Blacksmith: run 39562295975, run 39562296834
|
…ite, so cmux-next checks is green again (#23208) `cmux-next checks` has been red on every PR since #23139: the guard refuses a `visibilitychange` listener anywhere in the agent page, and the kept attachments effect added one. That listener is the case the guard's own rule allows — it SENDS a write that is still resting before the page can go away, it reads nothing — so it goes in ALLOWED_LISTENERS with that reason, which is what the guard's message asks for. Observed, big-red: `scripts/cmux-next/tests/check-switch-fetches.test.sh` exits 1 with "1 hit(s)" on untouched base a6aba2b (and on 2df94d1 and 323b08a), naming `webviews/src/agent-session/acpmux/composer/keptAttachments.ts:134`; with this it prints "check-switch-fetches: ok" and exits 0. The checker's 14-case self-test and its own `--self-test` pass either way. Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cb8-bbf0-7010-8d04-f9f2c0cb55df&host=big-red Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
Shipped in:
|
…wn text block
Leo 2026-10-21, as in Claude: a paste of more than 20 lines or 2,000
characters into the composer no longer lands as pages of prompt. It becomes a
"Pasted text" attachment chip in the chips row (its first words over
"Pasted text · N lines"), and the prompt keeps what was typed and its caret.
- pastedText.ts holds the limits (PASTE_CHIP_LINES, PASTE_CHIP_CHARS), the
label and line count, and the one wire-shape helper, pastedTextBlocks: each
paste goes to the agent as its own ACP text block after the prompt's, with
_meta {"cmux":{"pasted":true}} (promptBlocks). The bubble and queue text stay
the typed prompt.
- The field reports a paste to onBeforeInput as such (state.paste, with the
clipboard's plain text as copied); the composer makes the chip there
(composer/pastedChips.ts). A paste under the limit goes in as before and
still scrolls to its end (#22789).
- Paste as Plain Text keeps the raw paste: the prompt menu's item and the
Cmd-Shift-V key (Paste and Match Style, a system menu key the bridge sends no
keycap for) mark the next paste, within 2 s, as raw.
- Click on the chip: the simple expand. Its text goes back into the prompt at
the caret as a paste of it, and the chip goes. Its right-click menu has Edit
in Prompt and Remove; x removes it. (No Backspace-removes-chip exists for
image chips, so none here.)
- A pasted chip is kept with the chat's unsent chips like pictures (#23139):
kind "pasted" with its text; the host keeps both as they are.
- Strings: composer.pastedText, composer.pastedText.edit and the plural
composer.pastedText.lines, in the catalog's 21 languages.
- Gallery: agent-pane.composer#text-pasted. composer-paste-end now pastes 15
lines, under the chip limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray
* test(acpmux): a huge paste into the composer is a Pasted text chip (red) A paste over 20 lines or 2,000 characters becomes a "Pasted text" chip (first words, line count) and the prompt keeps what was typed; a smaller paste goes in as text; Paste as Plain Text (Cmd-Shift-V) keeps the raw paste; x removes the chip; a click puts its text back in the prompt; and the text goes to the agent as its own text block after the prompt's, with _meta.cmux.pasted. Fails on the base: no chip exists, every paste lands in the prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: a huge paste is a Pasted text chip, sent as its own text block Leo 2026-10-21, as in Claude: a paste of more than 20 lines or 2,000 characters into the composer no longer lands as pages of prompt. It becomes a "Pasted text" attachment chip in the chips row (its first words over "Pasted text · N lines"), and the prompt keeps what was typed and its caret. - pastedText.ts holds the limits (PASTE_CHIP_LINES, PASTE_CHIP_CHARS), the label and line count, and the one wire-shape helper, pastedTextBlocks: each paste goes to the agent as its own ACP text block after the prompt's, with _meta {"cmux":{"pasted":true}} (promptBlocks). The bubble and queue text stay the typed prompt. - The field reports a paste to onBeforeInput as such (state.paste, with the clipboard's plain text as copied); the composer makes the chip there (composer/pastedChips.ts). A paste under the limit goes in as before and still scrolls to its end (#22789). - Paste as Plain Text keeps the raw paste: the prompt menu's item and the Cmd-Shift-V key (Paste and Match Style, a system menu key the bridge sends no keycap for) mark the next paste, within 2 s, as raw. - Click on the chip: the simple expand. Its text goes back into the prompt at the caret as a paste of it, and the chip goes. Its right-click menu has Edit in Prompt and Remove; x removes it. (No Backspace-removes-chip exists for image chips, so none here.) - A pasted chip is kept with the chat's unsent chips like pictures (#23139): kind "pasted" with its text; the host keeps both as they are. - Strings: composer.pastedText, composer.pastedText.edit and the plural composer.pastedText.lines, in the catalog's 21 languages. - Gallery: agent-pane.composer#text-pasted. composer-paste-end now pastes 15 lines, under the chip limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): pasted text reaches every harness whole and expands exactly (red) Review of #23396: - uniichat-agent and the CLI agent with --last-block send only the last text block, so a typed question and a pasted log reached the agent as the log alone. A pasted block (_meta.cmux.pasted) now goes with the typed text, a blank line between them (cli_agent.rs, uniichat_agent.rs tests). - A chip's click must put its text back exactly as a raw paste does: blank lines kept, none added between lines. - Paste as Plain Text by the key's position (KeyV on a non-Latin layout) keeps the paste raw; one that pasted only a file arms nothing for later. - The bubble shows a pasted chip's text as acpmux's echo will (text blocks joined by a newline), and Retry/Edit, which send the echoed text, drop the pasted chip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: pasted text reaches every harness whole; a chip expands exactly Review of #23396: - acpmux's CLI agent (--last-block) and uniichat-agent sent only the last text block, so "why does this fail?" with a pasted log reached the agent as the log alone. A block marked _meta.cmux.pasted now always goes with the typed text, after a blank line (cli_agent::prompt_text, which uniichat-agent now uses); prompts without a paste go as before. - A chip's click put its text back one paragraph per line, blank lines dropped. MarkdownField's pasteText now pastes with the text as the clipboard's plain text, so it takes a person's paste path (Milkdown's markdown paste) and the prompt holds the text exactly as pasted. - The bubble and queue row show a pasted chip's text as acpmux's echo will (text blocks joined by a newline), so a send of only a paste is not an empty bubble and nothing jumps when the echo lands. - Retry and Edit send the echoed text, which has each paste: the pasted chip is no longer sent with it again (promptAttachments). - Paste as Plain Text: Cmd-Shift-V by the key's position (KeyV) counts on a non-Latin layout, and any paste, one of only files too, ends its turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: the pasted chip's edit hint is a title, not aria-description Oxlint's jsx-a11y role-supports-aria-props warns on aria-description on a button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): the paste-chip test waits for the editor, not a fixed time check-fixed-waits (new on base) counts every fixed setTimeout wait. Opening waits for the combobox with until(), and a paste yields one macrotask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: the paste chip counts and names as the transcript's, and the bubble has its chip at once The composer's chip uses promptCollapse.ts pasteLines/pasteName and transcript.pastedLines (#23431) in place of its own copies. The optimistic bubble carries the paste ranges user_message.pasted will name (#23407), so the transcript chip shows from the send on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): paste-chip jsdom tests start from an empty prompt and measure no layout Every test in composer-paste-text opens chat "chat-a", and the composer keeps a chat's unsent prompt in the page's localStorage remount cache (composerDraft.ts). The jsdom window is shared across the file, so the first test's typed "why does this fail?" (and later pastes) came back in the next tests' prompts: the 2,000-character and x tests saw that text, and the send test sent it joined to its own. afterEach now clears localStorage once the root is unmounted. A chip's click puts its text back as a paste, and ProseMirror scrolls a paste into view by measuring a Range. jsdom's Range has no getClientRects, so the click threw inside scrollToSelection, which also left act() overlapping and the next test's handle null. The test gives Range an empty box, as markdown-comments.test.ts does; the product path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): the long-paste browser tests paste under the chip's limit and still overflow A paste over 20 lines or 2,000 characters is now a Pasted text chip, so composer-scroll-fade's 40-line paste never reached the prompt (each test timed out waiting for its last line), and composer-paste-end's 15 short lines, which this PR chose to stay raw, did not overflow the field: the prompt shows pasted lines joined as one paragraph, and 15 short ones wrap to about three rows, under the field's eight-line cap. The small-paste path is the one on base (take() returns false under the limit and the field pastes as before); only the fixtures were too small. Both now paste long lines under both limits (15 and 20 lines of 70 to 90 characters), which wrap well past eight rows at the tests' 900 px viewport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s the same Pasted chip (#23622) * test(acpmux): a huge paste into the composer is a Pasted text chip (red) A paste over 20 lines or 2,000 characters becomes a "Pasted text" chip (first words, line count) and the prompt keeps what was typed; a smaller paste goes in as text; Paste as Plain Text (Cmd-Shift-V) keeps the raw paste; x removes the chip; a click puts its text back in the prompt; and the text goes to the agent as its own text block after the prompt's, with _meta.cmux.pasted. Fails on the base: no chip exists, every paste lands in the prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: a huge paste is a Pasted text chip, sent as its own text block Leo 2026-10-21, as in Claude: a paste of more than 20 lines or 2,000 characters into the composer no longer lands as pages of prompt. It becomes a "Pasted text" attachment chip in the chips row (its first words over "Pasted text · N lines"), and the prompt keeps what was typed and its caret. - pastedText.ts holds the limits (PASTE_CHIP_LINES, PASTE_CHIP_CHARS), the label and line count, and the one wire-shape helper, pastedTextBlocks: each paste goes to the agent as its own ACP text block after the prompt's, with _meta {"cmux":{"pasted":true}} (promptBlocks). The bubble and queue text stay the typed prompt. - The field reports a paste to onBeforeInput as such (state.paste, with the clipboard's plain text as copied); the composer makes the chip there (composer/pastedChips.ts). A paste under the limit goes in as before and still scrolls to its end (#22789). - Paste as Plain Text keeps the raw paste: the prompt menu's item and the Cmd-Shift-V key (Paste and Match Style, a system menu key the bridge sends no keycap for) mark the next paste, within 2 s, as raw. - Click on the chip: the simple expand. Its text goes back into the prompt at the caret as a paste of it, and the chip goes. Its right-click menu has Edit in Prompt and Remove; x removes it. (No Backspace-removes-chip exists for image chips, so none here.) - A pasted chip is kept with the chat's unsent chips like pictures (#23139): kind "pasted" with its text; the host keeps both as they are. - Strings: composer.pastedText, composer.pastedText.edit and the plural composer.pastedText.lines, in the catalog's 21 languages. - Gallery: agent-pane.composer#text-pasted. composer-paste-end now pastes 15 lines, under the chip limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): pasted text reaches every harness whole and expands exactly (red) Review of #23396: - uniichat-agent and the CLI agent with --last-block send only the last text block, so a typed question and a pasted log reached the agent as the log alone. A pasted block (_meta.cmux.pasted) now goes with the typed text, a blank line between them (cli_agent.rs, uniichat_agent.rs tests). - A chip's click must put its text back exactly as a raw paste does: blank lines kept, none added between lines. - Paste as Plain Text by the key's position (KeyV on a non-Latin layout) keeps the paste raw; one that pasted only a file arms nothing for later. - The bubble shows a pasted chip's text as acpmux's echo will (text blocks joined by a newline), and Retry/Edit, which send the echoed text, drop the pasted chip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: pasted text reaches every harness whole; a chip expands exactly Review of #23396: - acpmux's CLI agent (--last-block) and uniichat-agent sent only the last text block, so "why does this fail?" with a pasted log reached the agent as the log alone. A block marked _meta.cmux.pasted now always goes with the typed text, after a blank line (cli_agent::prompt_text, which uniichat-agent now uses); prompts without a paste go as before. - A chip's click put its text back one paragraph per line, blank lines dropped. MarkdownField's pasteText now pastes with the text as the clipboard's plain text, so it takes a person's paste path (Milkdown's markdown paste) and the prompt holds the text exactly as pasted. - The bubble and queue row show a pasted chip's text as acpmux's echo will (text blocks joined by a newline), so a send of only a paste is not an empty bubble and nothing jumps when the echo lands. - Retry and Edit send the echoed text, which has each paste: the pasted chip is no longer sent with it again (promptAttachments). - Paste as Plain Text: Cmd-Shift-V by the key's position (KeyV) counts on a non-Latin layout, and any paste, one of only files too, ends its turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: the pasted chip's edit hint is a title, not aria-description Oxlint's jsx-a11y role-supports-aria-props warns on aria-description on a button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): the paste-chip test waits for the editor, not a fixed time check-fixed-waits (new on base) counts every fixed setTimeout wait. Opening waits for the combobox with until(), and a paste yields one macrotask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: the paste chip counts and names as the transcript's, and the bubble has its chip at once The composer's chip uses promptCollapse.ts pasteLines/pasteName and transcript.pastedLines (#23431) in place of its own copies. The optimistic bubble carries the paste ranges user_message.pasted will name (#23407), so the transcript chip shows from the send on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): paste-chip jsdom tests start from an empty prompt and measure no layout Every test in composer-paste-text opens chat "chat-a", and the composer keeps a chat's unsent prompt in the page's localStorage remount cache (composerDraft.ts). The jsdom window is shared across the file, so the first test's typed "why does this fail?" (and later pastes) came back in the next tests' prompts: the 2,000-character and x tests saw that text, and the send test sent it joined to its own. afterEach now clears localStorage once the root is unmounted. A chip's click puts its text back as a paste, and ProseMirror scrolls a paste into view by measuring a Range. jsdom's Range has no getClientRects, so the click threw inside scrollToSelection, which also left act() overlapping and the next test's handle null. The test gives Range an empty box, as markdown-comments.test.ts does; the product path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): the long-paste browser tests paste under the chip's limit and still overflow A paste over 20 lines or 2,000 characters is now a Pasted text chip, so composer-scroll-fade's 40-line paste never reached the prompt (each test timed out waiting for its last line), and composer-paste-end's 15 short lines, which this PR chose to stay raw, did not overflow the field: the prompt shows pasted lines joined as one paragraph, and 15 short ones wrap to about three rows, under the field's eight-line cap. The small-paste path is the one on base (take() returns false under the limit and the field pastes as before); only the fixtures were too small. Both now paste long lines under both limits (15 and 20 lines of 70 to 90 characters), which wrap well past eight rows at the tests' 900 px viewport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * webviews: a long paste round-trips composer to transcript as the same Pasted chip A JSDOM test runs the real composer (type, paste 2,300 lines, Send), the real AcpmuxDirectClient against a fake acpmux that records the sent blocks as hub/session.rs prompt_text_parts does (text blocks joined with a newline, UTF-16 ranges of the _meta.cmux.pasted ones), and the real UserBubble. It checks the typed text comes first and the chip after, and that the pending bubble drawn at send matches the one drawn from acpmux's user_message echo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba3-d09a-7940-9fd6-158e10477c0e&host=cosmic-gray * Take feat-cmux-next's Localizable.xcstrings; this PR adds no strings The base merge's string-catalog driver reordered the catalog; this branch changes no strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba3-d09a-7940-9fd6-158e10477c0e&host=cosmic-gray --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…past (#23626) * test(acpmux): a huge paste into the composer is a Pasted text chip (red) A paste over 20 lines or 2,000 characters becomes a "Pasted text" chip (first words, line count) and the prompt keeps what was typed; a smaller paste goes in as text; Paste as Plain Text (Cmd-Shift-V) keeps the raw paste; x removes the chip; a click puts its text back in the prompt; and the text goes to the agent as its own text block after the prompt's, with _meta.cmux.pasted. Fails on the base: no chip exists, every paste lands in the prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: a huge paste is a Pasted text chip, sent as its own text block Leo 2026-10-21, as in Claude: a paste of more than 20 lines or 2,000 characters into the composer no longer lands as pages of prompt. It becomes a "Pasted text" attachment chip in the chips row (its first words over "Pasted text · N lines"), and the prompt keeps what was typed and its caret. - pastedText.ts holds the limits (PASTE_CHIP_LINES, PASTE_CHIP_CHARS), the label and line count, and the one wire-shape helper, pastedTextBlocks: each paste goes to the agent as its own ACP text block after the prompt's, with _meta {"cmux":{"pasted":true}} (promptBlocks). The bubble and queue text stay the typed prompt. - The field reports a paste to onBeforeInput as such (state.paste, with the clipboard's plain text as copied); the composer makes the chip there (composer/pastedChips.ts). A paste under the limit goes in as before and still scrolls to its end (#22789). - Paste as Plain Text keeps the raw paste: the prompt menu's item and the Cmd-Shift-V key (Paste and Match Style, a system menu key the bridge sends no keycap for) mark the next paste, within 2 s, as raw. - Click on the chip: the simple expand. Its text goes back into the prompt at the caret as a paste of it, and the chip goes. Its right-click menu has Edit in Prompt and Remove; x removes it. (No Backspace-removes-chip exists for image chips, so none here.) - A pasted chip is kept with the chat's unsent chips like pictures (#23139): kind "pasted" with its text; the host keeps both as they are. - Strings: composer.pastedText, composer.pastedText.edit and the plural composer.pastedText.lines, in the catalog's 21 languages. - Gallery: agent-pane.composer#text-pasted. composer-paste-end now pastes 15 lines, under the chip limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): pasted text reaches every harness whole and expands exactly (red) Review of #23396: - uniichat-agent and the CLI agent with --last-block send only the last text block, so a typed question and a pasted log reached the agent as the log alone. A pasted block (_meta.cmux.pasted) now goes with the typed text, a blank line between them (cli_agent.rs, uniichat_agent.rs tests). - A chip's click must put its text back exactly as a raw paste does: blank lines kept, none added between lines. - Paste as Plain Text by the key's position (KeyV on a non-Latin layout) keeps the paste raw; one that pasted only a file arms nothing for later. - The bubble shows a pasted chip's text as acpmux's echo will (text blocks joined by a newline), and Retry/Edit, which send the echoed text, drop the pasted chip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: pasted text reaches every harness whole; a chip expands exactly Review of #23396: - acpmux's CLI agent (--last-block) and uniichat-agent sent only the last text block, so "why does this fail?" with a pasted log reached the agent as the log alone. A block marked _meta.cmux.pasted now always goes with the typed text, after a blank line (cli_agent::prompt_text, which uniichat-agent now uses); prompts without a paste go as before. - A chip's click put its text back one paragraph per line, blank lines dropped. MarkdownField's pasteText now pastes with the text as the clipboard's plain text, so it takes a person's paste path (Milkdown's markdown paste) and the prompt holds the text exactly as pasted. - The bubble and queue row show a pasted chip's text as acpmux's echo will (text blocks joined by a newline), so a send of only a paste is not an empty bubble and nothing jumps when the echo lands. - Retry and Edit send the echoed text, which has each paste: the pasted chip is no longer sent with it again (promptAttachments). - Paste as Plain Text: Cmd-Shift-V by the key's position (KeyV) counts on a non-Latin layout, and any paste, one of only files too, ends its turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: the pasted chip's edit hint is a title, not aria-description Oxlint's jsx-a11y role-supports-aria-props warns on aria-description on a button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): the paste-chip test waits for the editor, not a fixed time check-fixed-waits (new on base) counts every fixed setTimeout wait. Opening waits for the combobox with until(), and a paste yields one macrotask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: the paste chip counts and names as the transcript's, and the bubble has its chip at once The composer's chip uses promptCollapse.ts pasteLines/pasteName and transcript.pastedLines (#23431) in place of its own copies. The optimistic bubble carries the paste ranges user_message.pasted will name (#23407), so the transcript chip shows from the send on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): paste-chip jsdom tests start from an empty prompt and measure no layout Every test in composer-paste-text opens chat "chat-a", and the composer keeps a chat's unsent prompt in the page's localStorage remount cache (composerDraft.ts). The jsdom window is shared across the file, so the first test's typed "why does this fail?" (and later pastes) came back in the next tests' prompts: the 2,000-character and x tests saw that text, and the send test sent it joined to its own. afterEach now clears localStorage once the root is unmounted. A chip's click puts its text back as a paste, and ProseMirror scrolls a paste into view by measuring a Range. jsdom's Range has no getClientRects, so the click threw inside scrollToSelection, which also left act() overlapping and the next test's handle null. The test gives Range an empty box, as markdown-comments.test.ts does; the product path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * test(acpmux): the long-paste browser tests paste under the chip's limit and still overflow A paste over 20 lines or 2,000 characters is now a Pasted text chip, so composer-scroll-fade's 40-line paste never reached the prompt (each test timed out waiting for its last line), and composer-paste-end's 15 short lines, which this PR chose to stay raw, did not overflow the field: the prompt shows pasted lines joined as one paragraph, and 15 short ones wrap to about three rows, under the field's eight-line cap. The small-paste path is the one on base (take() returns false under the limit and the field pastes as before); only the fixtures were too small. Both now paste long lines under both limits (15 and 20 lines of 70 to 90 characters), which wrap well past eight rows at the tests' 900 px viewport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ba2-63d6-7533-b232-21f35d99d2b2&host=cosmic-gray * cmux NEXT composer: test that the attachment row fades at its scrolled sides A row of chips wider than the composer scrolls sideways. The test expects the side with chips past it to fade like the prompt's edges: data-fade-left / data-fade-right, an alpha mask by the prompt's band (--acpmux-md-fade-band), none at a side with nothing past it, and the composer behind showing through the faded side. Chromium and WebKit. Red until the row fades. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bf1-fdc7-79b0-b869-8ac78b0edd90&host=cosmic-gray * cmux NEXT composer: the attachment row fades at the sides it scrolls past A row of chips wider than the composer scrolls sideways. Its side with chips past it is now marked (data-fade-left / data-fade-right, followScrollEdges across) and faded with an alpha mask on the row itself, as the prompt's edges are, so the composer's glass shows through. The band is the prompt's: MarkdownField.tsx exports FADE_BAND_PX and the row sets --acpmux-md-fade-band from it. Reduce Transparency keeps plain sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bf1-fdc7-79b0-b869-8ac78b0edd90&host=cosmic-gray --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Leo asked that a chat's unsent draft (its text, pictures and attached files) survive reopening the chat, and archiving or deleting it and then restoring it, not only a quit (#22999).
acpmux already keeps the draft's text in the session's meta, so this adds the chips:
AgentPaneDraftAttachments):agent-draft-attachments/<sha256 of session id>/. Each chip's bytes are their own file, listed indraft.json. The file helpers are shared with the relaunch wake (AgentPaneAttachmentFiles).AgentPageProvider:chat.draftAttachments(write; answers the ids whose bytes the host holds) andchat.draftAttachments.get.keptAttachments.ts):pagehide, on hidden, and on unmount.Tests:
webviews/test/composer-kept-attachments.test.tsxcovers:ChatDraftAttachmentsTestscovers:ChatDraftAttachmentsTests,AgentPaneRelaunchWakeTestsandChatScrollRestoreTestsat this head.Known limits:
Independent subagent review: PASS (the five findings from round one are fixed; two nits are fixed here: a refused write goes again, and the page's JSON is checked before encoding).
🤖 Generated with Claude Code
Agent session: cosmic-gray
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Keeps a chat's unsent pictures and files (its composer chips) across reopening the chat, archiving/deleting and restoring it, and relaunches. Previously only the draft text survived; now the chips are stored per chat on disk and restored when the chat is shown again.
AgentPaneDraftAttachments): stores each chat's chips underagent-draft-attachments/<hashed session id>/; chip contents are separate files listed indraft.json.keptAttachments.ts): writes chips 300 ms after they settle, flushes on page close/hide, and clears them on send so sent chips never return.chat.draftAttachmentsandchat.draftAttachments.getops onAgentPageProvider.AgentPaneAttachmentFiles).AgentPaneRelaunchWaketo use the shared attachment file helpers.Known limits
Written for commit d84da19. Summary will update on new commits.