Skip to content

cmux-next composer: a chat's unsent pictures and files come back when it is reopened or restored - #23139

Merged
teamleaderleo merged 3 commits into
feat-cmux-nextfrom
leo/composer-drafts-per-chat
Oct 21, 2026
Merged

teamleaderleo merged 3 commits into
feat-cmux-nextfrom
leo/composer-drafts-per-chat

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

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:

  • Native (AgentPaneDraftAttachments):
    • Each chat's chips are kept in app support under agent-draft-attachments/<sha256 of session id>/. Each chip's bytes are their own file, listed in draft.json. The file helpers are shared with the relaunch wake (AgentPaneAttachmentFiles).
    • Writes for one chat run in order on a per-session queue.
    • Limits: 100 chats (the oldest are pruned) and 64 MB per chat.
    • The page reaches it through two new ops on AgentPageProvider: chat.draftAttachments (write; answers the ids whose bytes the host holds) and chat.draftAttachments.get.
  • Page (keptAttachments.ts):
    • Reads the chat's chips when the chat is shown.
    • Writes them 300 ms after they stop changing. A chip's bytes go once; after that only its id goes.
    • Flushes on pagehide, on hidden, and on unmount.
    • Sending clears the kept chips, and a write still pending is dropped then, so sent chips never come back.
    • A harness switch carries the chips over.
    • Quick chat keeps nothing.

Tests:

  • webviews/test/composer-kept-attachments.test.tsx covers:
    • reopen;
    • archive/restore;
    • send clearing;
    • paste-then-send inside the wait;
    • a refusal putting chips back;
    • stale held ids;
    • flush on hide.
  • ChatDraftAttachmentsTests covers:
    • a round trip through a fresh store on the same folder (relaunch);
    • clearing on send;
    • the byte cap.
  • The fleet ran ChatDraftAttachmentsTests, AgentPaneRelaunchWakeTests and ChatScrollRestoreTests at this head.

Known limits:

  • A brand-new chat with no session yet keeps nothing on tab close. Its text isn't kept either.
  • After a harness switch, the old chat keeps a copy of the chips that moved.
  • Two pages on one chat are last-write-wins.

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


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with 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.

  • Host side (AgentPaneDraftAttachments): stores each chat's chips under agent-draft-attachments/<hashed session id>/; chip contents are separate files listed in draft.json.
  • Page side (keptAttachments.ts): writes chips 300 ms after they settle, flushes on page close/hide, and clears them on send so sent chips never return.
  • Adds chat.draftAttachments and chat.draftAttachments.get ops on AgentPageProvider.
  • Shares the file-moving logic with the relaunch wake (AgentPaneAttachmentFiles).
  • Refactors AgentPaneRelaunchWake to use the shared attachment file helpers.
  • Limits: 100 chats kept (oldest pruned), 64 MB per chat. Quick chats keep nothing.

Known limits

  • A brand-new chat with no session keeps nothing on tab close.
  • After a harness switch, both old and new chats hold copies.
  • Two pages open on the same chat conflict (last write wins).

Written for commit d84da19. Summary will update on new commits.

View guided diff Turn on auto-fix

teamleaderleo and others added 3 commits October 21, 2026 00:25
…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
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 21, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by projects-34 on cosmic-gray · session

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by projects-34 on cosmic-gray · session

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review d84da19: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr23139 -f ref=leo/composer-drafts-per-chat -f platform=macos -f backend_mode=local

Unclear: web / react-apps-check failed: no known signature; failed step: Check webview test shards.

CI failed on d84da191ac (run 39562296834 attempt 1): 4 code, 1 unknown.
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped.

Job Verdict Why
web / react-apps-check (2) code a web test failed
web / react-apps-check (4) code a web test failed
web / react-apps-check (1) code a web test failed
web / react-apps-check (3) code a web test failed
web / react-apps-check unknown no known signature; failed step: Check webview test shards

Failures:

  • seen on other PRs agent-pane-queue-steer.test.ts queued and steering prompts > the queue takes its own place in the stack: nothing is cut off or draw...: failed (job)
  • seen on other PRs composer-mention.test.ts @ then letters lists the chat's matching files and Return writes the path: failed (job)
  • seen on other PRs composer-mention.test.ts after picking a path markdown escapes, typing on leaves the picker closed: failed (job)
  • seen on other PRs composer-mention.test.ts @proje lists other chats, then folders, then the chat folder's files: failed (job)
  • seen on other PRs composer-mention.test.ts a chat named for a long prompt does not take @app from the file: failed (job)
  • seen on other PRs composer-mention.test.ts a bare @ lists the most recent other chats: failed (job)
  • seen on other PRs composer-mention.test.ts an @ inside a word opens nothing and asks the host for nothing: failed (job)
  • seen on other PRs agent-pane-ctrl-b-background.test.ts with no running call, Ctrl-B stays the composer's and asks nothing: failed (job)
  • seen on other PRs agent-pane-ctrl-b-background.test.ts a Codex chat shows no hint and keeps Ctrl-B: failed (job)
  • seen on other PRs agent-pane-math.test.tsx each delimiter typesets once math appears: failed (job)
  • ... 7 more in the job logs
Matched log lines
web / react-apps-check (2): (fail) queued and steering prompts > the queue takes its own place in the stack: nothing is cut off or drawn over [1296.33ms]
web / react-apps-check (4): (fail) @ then letters lists the chat's matching files and Return writes the path [2117.45ms]
web / react-apps-check (1): (fail) with no running call, Ctrl-B stays the composer's and asks nothing [1057.83ms]
web / react-apps-check (3): (fail) the chat composer at 420 px: the strip's marks line up with the prompt's edges [1825.77ms]

Not re-run automatically: web / react-apps-check (2), web / react-apps-check (4), web / react-apps-check (1), web / react-apps-check (3), web / react-apps-check are not machine failures.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

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
red: web / Web status; NEW: green on the base 29d8bbf; do not merge until green; see lane-rules
red: web / react-apps-check; no finished base run on 29d8bbf to compare; do not merge until green; see lane-rules
red: web / react-apps-check (1); no finished base run on 29d8bbf to compare; do not merge until green; see lane-rules
red: web / react-apps-check (2); no finished base run on 29d8bbf to compare; do not merge until green; see lane-rules
red: web / react-apps-check (3); no finished base run on 29d8bbf to compare; do not merge until green; see lane-rules
red: web / react-apps-check (4); no finished base run on 29d8bbf to compare; do not merge until green; see lane-rules

CI fast guards failed on d84da191ac (https://github.com/manaflow-ai/cmux/actions/runs/39562296834). It does not block the merge; a red guard merged into main breaks it for every open PR.
The log named no failed step; see the run.

Agents: python3 scripts/ci/guard_attribution.py fix applies the mechanical fixes locally.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-afkzg created for this PR by the cmux coordinator.

@blacksmith-sh

blacksmith-sh Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Found 49 test failures on Blacksmith runners:

Failures

Test View Logs
.github/workflows/
nightly-next-tick.yml: triggers on the display names ['cmux-next', 'cmux-tui artifacts'
] and declares no SOURCE_WORKFLOW_PATHS, so renaming any of those workflows stops this
one from ever triggering and nothing reports it
View Logs
/tmp/tmpczmnhrq1/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmpxaxa_s38/summary-fail.json (live):/summary-fail.json (live): View Logs
main.IOSWiring.test_test_ios_macos_jobs_take_the_runner_jobs_pool/
test_test_ios_macos_jobs_take_the_runner_jobs_pool
View Logs
main.SideLanes.test_a_completed_failed_blacksmith_retry_is_not_watched_again/
test_a_completed_failed_blacksmith_retry_is_not_watched_again
View Logs
main.SideLanes.test_a_real_failure_on_cmux_next_attempt_two_is_not_rescued/
test_a_real_failure_on_cmux_next_attempt_two_is_not_rescued
View Logs
cmux-next.yml job swift-canary step 'Report the build fleet compile verdict' uses the t
oolchain (swift) before scripts/select-ci-xcode.sh runs
View Logs
debug info {'CMUX_SWIFT_SANITIZE': 'address', 'CMUX_SWIFT_TEST_DEBUG_INFO': 'none'}: 's
wift --version' lacks '-debug-info-format none'/
debug info {'CMUX_SWIFT_SANITIZE': 'address', 'CMUX_SWIFT_TEST_DEBUG_INFO': 'none'}: 's
wift --version' lacks '-debug-info-format none'
View Logs
debug info {'CMUX_SWIFT_SANITIZE': 'address', 'CMUX_SWIFT_TEST_DEBUG_INFO': 'none'}: 's
wift package resolve --package-path Packages/macOS/Pkg --scratch-path Packages/macOS/
Pkg/.build-sanitize-address' lacks '-debug-info-format none'
View Logs
debug info {'CMUX_SWIFT_SANITIZE': 'address', 'CMUX_SWIFT_TEST_DEBUG_INFO': 'none'}: wa
nt one build and two suites: ['swift package resolve --package-path Packages/macOS/
Pkg --scratch-path Packages/macOS/Pkg/
.build-sanitize-address', 'swift --version', 'swift build --build-tests -c debug -debug
-info-format none --sanitize=address --scratch-path Packages/macOS/Pkg/
.build-sanitize-address -Xlinker -no_compact_unwind --package-path Packages/macOS/
Pkg', 'swift test -c debug -debug-info-format none --sanitize=address --scratch-path Pa
ckages/macOS/Pkg/
.build-sanitize-address -Xlinker -no_compact_unwind --package-path Packages/macOS/
Pkg --skip-build --filter ATests', 'swift test -c debug -debug-info-format none --sanit
ize=address --scratch-path Packages/macOS/Pkg/
.build-sanitize-address -Xlinker -no_compact_unwind --package-path Packages/macOS/
Pkg --skip-build --filter BTests']
View Logs

...and 39 more test failures. View all on Blacksmith: run 39562295975, run 39562296834

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@teamleaderleo
teamleaderleo merged commit 8697514 into feat-cmux-next Oct 21, 2026
@teamleaderleo
teamleaderleo deleted the leo/composer-drafts-per-chat branch October 21, 2026 08:16
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
…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>
@github-actions

Copy link
Copy Markdown
Contributor

Shipped in:

teamleaderleo added a commit that referenced this pull request Oct 21, 2026
…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
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
* 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>
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
…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>
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant