fix(chat): single-source composer draft state; drop the pin picker - #1054
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Fileobjects 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
Filebytes 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,consumePlaceholdersand 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
pinIdsin the draft are removed; a stored legacy draft containingpinIdsis 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
mediaIddeliberately 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.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