Repository navigation
fix(composer): never lose a draft typed before the chat loads - #27544
teamleaderleo wants to merge 1 commit into
Conversation
Text typed before the chat finished loading was cleared, and a saved draft could be overwritten by it or by an empty editor: - the load-time swap between the New Tab page and the chat view (a host error before the handshake) dropped the draft both ways; - the session id arriving reset a sessionless composer's prompt; - an unanswered acpmux draft read counted as "no draft", so typing then replaced acpmux's saved draft; - a new chat's kept draft arriving after the first key was ignored. Merge saved and typed text (saved first, typed after) instead, keep an unanswered read unhydrated, ignore load-time empty writes in AgentUnsentDrafts until the page has its draft, and set aside unreadable draft and prompt-journal files instead of overwriting them. Agent-Session: https://cmux.com/deeplink/agent?id=aid-58ff9cb24c374c00f104 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Owned by an agent session · took over from an earlier session · session |
|
Found 6 test failures on Blacksmith runners: Failures
|
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Unclear: CI failed on
Failures:
Matched log linesNot re-run automatically: Written by red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 920ece1 to compare; do not merge until green; see lane-rules |
|
Bead |
|
Owned by an agent session · took over from an earlier session · session |
|
Awareness note from an agent lane that does not own this PR; it is not a review or a request to take the PR over. Superseded by #28204 (merged as a240548). That carry includes this PR’s draft-loading and reconnect preservation work. As this PR’s owner, I authorize closing this original draft PR; the lane broker currently does not expose PR closure. The later changes in #27963 are being audited separately. |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo (2026-10-29):
And earlier the same day: "it seems that new updates overwrite the text portion of the draft and it becomes empty".
This PR covers the draft part: text typed before the chat finishes loading survives the load, and saved drafts are never lost to it.
Cause: four load paths dropped text, and two files were unsafe
App.tsxshowNewTab). When the host fails before its handshake ("acpmux did not answer in time"), the chat view with its error card takes the New Tab page's place. The page'sonLeaveDraftkept the draft only for a Sandboxed run, so text typed on the page was dropped. When a retry answered, the page came back and the chat composer's text was dropped too. This is the clear Leo saw: he typed, the error card appeared, and the prompt was empty.Composer.tsxsession effect). A composer that had no session yet (still loading or reconnecting, or a new chat whose session had just started) set its prompt torestored || carried || draft || ""when the session id arrived.carriedapplies only to a switch, so text typed before the chat loaded was replaced by the saved draft or by an empty prompt.readDurableDraft). The read could fail in three ways: the native action map was not installed yet (the usual case on first load and after an update relaunch), the read threw, or the connection dropped. Each returnedundefined, which also means "acpmux has no draft". The composer then marked the chat as hydrated, and the first keystroke queued a write that replaced acpmux's saved draft once the connection came up. This is the "past drafts get f***ed too" case. The edited-before-read path did the same: what the person typed won over the saved draft, and an editor emptied before the read wrote""over it.drafteffect:if (field.value()) return). The typed text then went out throughchat.unsentDraftand replaced the tab's kept draft on disk (AgentUnsentDrafts.set).agent-unsent-drafts.jsonwas treated as empty when it could not be read, and the next save replaced it. If the in-memory map was empty, the save deleted the file instead.agent-pane-unsent-prompts.json(the sent-prompt journal) also started empty on an unreadable file, so its next write overwrote it.Fix
mergedDraft(composerDraft.ts) andAgentUnsentDrafts.merged. If one side is empty, the other is kept. If the typed text already contains the saved draft, the typed text is kept as is, and a text merged once before is not doubled. Two different texts are both kept, the saved draft first and the typed text after it. This is the orderComposerHandle.restorealready uses ("what comes back goes first"), and it leaves the caret at the end of what the person is typing, so their next key lands where they expect.hostFailedEarly), the page'sonLeaveDrafthands its draft to the chat composer (throughrestore, orheldBackuntil the composer mounts). When the page comes back, it takes the composer's text and chips (takeDraft, read while the page renders, before the composer unmounts). The Sandboxed handover is unchanged.readDurableDraftStatereturns{ draft }only when acpmux answered. An unanswered read leaves the chat unhydrated. Text typed meanwhile goes only to the remount cache (cachePersistedDraft), so a remount brings it back, and the read retries on the action map's next version. The first answered read for a chat in this page merges (hydratedDraftwithfirstRead), so an empty or load-time editor never writes over a saved draft. Later reads keep the existing rules, so the person's own clear still clears. A quit before the first read answers reads once and merges before it writes.AgentUnsentDrafts: until a page has been given the tab's kept draft (seed), an emptychat.unsentDraftis ignored as a page that has not loaded yet, and a non-empty one is merged with the kept draft. After that, the page's empty text clears the draft, and a session start or a tab close still clears it as before. A file that cannot be read is moved aside asagent-unsent-drafts.unreadable-<time>.jsonbefore the first write, and nothing is saved if it cannot be moved. Writes stay atomic (.atomic: temporary file, then rename) and now log their failures.RestoredNewTabPagesaskshasDraftinstead of callingseed, so its check does not count as giving the draft out.AgentPanePromptJournal: an unreadable journal is moved aside instead of being overwritten.Tests
webviews/test/composer-draft-survives-load.test.tsx(real Composer in jsdom):saved\n\ntyped, acpmux keeps both, and the typed text alone is never written. A new chat's kept draft that arrives after typing is merged too.""over the saved draft. A read that throws, as during the outage, is not treated as "no draft".mergedDraftandhydratedDraft.UnsentDraftTests: an empty load-time write is ignored, typed-before-seed text is merged, a clear after the seed clears, a session start clears, an unreadable file is moved aside and kept byte for byte, and the merge keeps both texts once.Results: pending (CI and a fleet run at this head).
Related PRs
#26642, #26753 and #26792 (one stacked chain whose owner is not alive) cover other cases: adopting legacy state atomically, New Tab text and attachments through quit, and Quick Composer drafts. They do not touch the load-time clears above. #26753's "an existing non-empty record is never replaced by an empty seed" is the same idea as the native guard here, which this PR applies at
chat.unsentDraft. None of them becomes fully redundant; #26753 and #26792 would need a rebase over the merge rule here.No UI change: the composer shows what it held, so there are no before/after captures.
Agent session: agent session
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Fixes the composer clearing draft text typed before the chat finishes loading, and prevents saved drafts from being overwritten by empty editors or unanswered reads.
Text typed while the chat loaded was dropped by four separate paths: the load-time swap between the New Tab page and the chat view, the session ID arriving, an unanswered acpmux draft read counting as "no draft," and a kept draft arriving after the first keystroke. Saved drafts could also be replaced by
""writes or overwritten files.chat.unsentDraftwrites until the page has been handed its kept draft; after that, the person's own clear still clears.agent-unsent-drafts.jsonand prompt-journal files aside instead of overwriting them on the next write.Written for commit 18ea006. Summary will update on new commits.