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

Merged
TE
15d ago
+939-62

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).