cmux NEXT composer: an attachment never loads forever; spinner, cancel and retry

Merged
TE
12d ago
+632-142

Stacked on #24975.

Leo, 2026-10-24: "is this still loading? Is there something going on? Like if it's loading, why don't you put a spinner on the attachment rather than this placeholder that doesn't do anything, right?... I can't even cancel these, like, you know, supposedly in-progress attachments."

Why tiles stayed loading forever. A batch's placeholders were only replaced when the whole batch finished, and the batch waited, with no time limit, on chat.attach.dropped before any image work, then on the chunked upload of every original (chat.upload.begin/chunk/finish). One host call that never answers left every tile as a placeholder with no x, and a prompt sent meanwhile was held behind them.

What changes

  • Each file resolves on its own. An image is its chip as soon as its preview is made. Its original's upload patches the chip's path when it finishes and never holds the chip.
  • chat.attach.dropped runs alongside the image work, with a 3 s deadline (the host answers from memory).
  • Each upload step fails after 30 s with "The upload timed out: the app did not answer." instead of waiting forever.
  • A loading tile shows the pane's spinner (a spinner, never a dot) and an x that cancels it. Whatever its read makes later is dropped.
  • A file that could not be attached stays as a tile that says why (the host's message, or the existing refusal text), with Retry and an x. It no longer only leaves a note above the strip. A failed tile never holds the send.
  • New strings: composer.cancelAttachment, composer.retryAttachment.

Tests (agent-pane-paste-large-screenshots.test.ts, red on #24975 alone, green here):

  • a host that never answers chat.attach.dropped or chat.upload.begin still gives both pasted screenshots their chips.
  • a loading tile has a spinner; its x removes it, and it never comes back when its read ends.
  • a file whose upload fails shows the host's reason; Retry uploads it again and it becomes a chip.

composer-attachment-matrix, cloud-upload-staging, attachment-session-paths, composer-image-fit and agent-pane-attach-* pass. The other attachment browser tests that fail here fail the same way on base.

Gallery: new agent-pane.composer states attachment-loading (a screenshot that is a chip while a zip still uploads with its spinner) and attachment-failed (an image-shaped and a file-shaped failed tile with Retry). The gallery-advisory run on this PR has the before/after.

Risk: low to medium. The intake's batch shape changed, but intakeFiles still returns the same aggregate for its callers, and the scope, carry and held-send behaviour is unchanged.

Agent session: huge-blue

šŸ¤– Generated with Claude Code


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