Skip to content

fix(chat): single-source composer draft state; drop the pin picker - #1054

Merged
selfcontained merged 2 commits into
mainfrom
agt_29017e45a209/chat-composer
Sep 4, 2026
Merged

fix(chat): single-source composer draft state; drop the pin picker#1054
selfcontained merged 2 commits into
mainfrom
agt_29017e45a209/chat-composer

Conversation

@selfcontained

Copy link
Copy Markdown
Owner

What

Two things from live use, both in the composer. Net 230 lines deleted.

The attachment chip flickered after a send

The composer kept two lists of attachments — live File objects in React state and descriptors in the persisted draft — with an effect mirroring each into the other. After a send cleared one while the other still held the file, they ping-ponged: the chip appeared and vanished repeatedly until a keystroke settled the race.

Now the persisted draft is the only source of what chips exist. Live File bytes sit in a ref keyed by name, size and type; a chip renders live when the ref has the file (or the entry is pasted text) and as the "needs re-attaching" placeholder otherwise — which is exactly the desired cross-tab and post-reload behaviour, for free. Both mirroring effects, reconcileDraftFiles, restoreDraftFiles, consumePlaceholders and the list-equality helpers are gone; every mutation is one draft write plus a ref update, and no effect writes the draft.

The pin picker is gone

Attaching an already-pinned thing to a message doesn't make sense. The picker, its chip, and pinIds in the draft are removed; a stored legacy draft containing pinIds is read tolerantly and never restored, sent, or written back. Agents can still attach pins to posts they write, and the feed renders those unchanged.

Notes

  • mediaId deliberately stays in a ref rather than the persisted descriptor, so a same-name file re-attached after a reload can't send a stale media row.
  • Re-attaching a placeholder's file now fills its slot in place instead of moving to the end.

Checks

pnpm run check, prettier, finalize:web, web vitest (1749), E2E (195 passed). New regression test: a send with a file attachment writes storage exactly once, the chip is gone, and typing afterwards doesn't bring it back.

🤖 Generated with Claude Code

selfcontained and others added 2 commits September 4, 2026 07:38
Users no longer attach pins to chat messages — "I don't need to include
already pinned things in a message." User attachments are files and links
only. Agent-side pin attachments (dispatch_chat_post) are untouched and
still render in the feed; the server's accepted schema is unchanged.

- chat-composer: remove the pin button/popover, `pins` prop, pinIds
  state and the pin branch of the send payload.
- chat-composer-attachments: remove attachablePins, PinChip,
  PinPickerButton.
- chat-pane: stop passing pins to the composer (the feed keeps them).
- chat-draft: drop `pinIds` from the persisted shape and the size
  accounting. Legacy drafts that still carry `pinIds` validate, are read
  without it (`readChatComposerDraft`) and never write it back.
- tests: rewrite the pin cases; add legacy-draft coverage.
- docs: chat-surface-plan states user attachments are files and links.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
After sending a message with an image the attachment chip flickered back
and forth, and typing then cleared it: two effects mirrored local
files/placeholders into the draft atom and the draft's file list back into
local state, and a send that cleared the draft while local state still
held the File set them ping-ponging.

Now the persisted draft (chatDraftAtomFamily) is the only source of truth
for what chips exist — it holds the descriptors — and live File objects
sit in a ref keyed by draftFileKey (name:size:mime), never in React state
and never written back by an effect. Chips render straight from
draft.files: an entry with a File in the ref (or pasted text in the draft)
is live, anything else is the "Needs re-attaching" placeholder. Cross-tab
follows for free: another tab's descriptors render as placeholders here.

- Deleted reconcileDraftFiles, restoreDraftFiles, consumePlaceholders,
  sameFiles/sameDescriptor/sameList, the describe and reconcile effects,
  filesRef/placeholdersRef and the files/placeholders useStates.
- Every mutation (attach, paste, drop, remove, re-attach into a
  placeholder's slot, keep-inline, clear-on-send) is one updateDraft write
  plus a ref update. The only file effect prunes refs for entries the
  draft no longer lists; it never writes the draft.
- Behaviour kept: paste/drop/paperclip, image previews and revocation,
  upload-at-send with the mediaId cache, retry-on-failure keeping the
  draft, placeholders holding Send, text typed during an in-flight send
  surviving the clear, the 64 KB persisted cap.
- A re-attached file now fills the placeholder's slot in place instead of
  moving to the end.
- Tests: regression for a stable composer after a file send (one write,
  no chip returns); cross-tab test kept against the new model.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@selfcontained
selfcontained merged commit 41bbc5b into main Sep 4, 2026
1 check passed
@selfcontained
selfcontained deleted the agt_29017e45a209/chat-composer branch September 4, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant