From c0af331c03a7cff44307279b179d07a9ece2ac81 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:31:36 +0800 Subject: [PATCH] fix(editor): don't wipe in-flight uploads on external content sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a brand-new chat's first file upload triggers lazy session creation, `setActiveSession(null → uuid)` flips ChatInput's draft key mid-upload, which changes `defaultValue` to the new (empty) session draft. ContentEditor's "sync external defaultValue" effect then ran `setContent` over a document that still held the `uploading` image/fileCard node, wiping it — so the upload's finalize could no longer find the node. The file vanished and the draft was left with an empty `!file[name]()`. The editor was never remounted (instance stays alive); the node was removed by the content-sync effect. An uploading node is local state an external sync must not overwrite, exactly like the existing dirty/focused guards. Add a guard that bails the sync while any `uploading` node is present. Pure frontend; affects only the first upload in a new chat (subsequent uploads hit an existing session, so no draft-key flip). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/views/editor/content-editor.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/views/editor/content-editor.tsx b/packages/views/editor/content-editor.tsx index 7e3d6c35c..c709991f4 100644 --- a/packages/views/editor/content-editor.tsx +++ b/packages/views/editor/content-editor.tsx @@ -384,6 +384,21 @@ const ContentEditor = forwardRef( useEffect(() => { if (!editor || editor.isDestroyed) return; + // Guard 0: never clobber an in-flight upload. An external `defaultValue` + // change can arrive mid-upload — e.g. chat lazy-creates a session on the + // first file upload, which flips `activeSessionId` → the draft key → + // `defaultValue`. If we `setContent` over a document that still holds an + // `uploading` image/fileCard node, that node is wiped and the upload's + // finalize can no longer find it (the file vanishes, leaving an empty + // `!file[name]()`). Like the dirty guards below, an uploading node is + // local state that an external sync must not overwrite. + let hasUploadingNode = false; + editor.state.doc.descendants((node) => { + if (node.attrs.uploading) hasUploadingNode = true; + return !hasUploadingNode; + }); + if (hasUploadingNode) return; + const current = stripBlobUrls(editor.getMarkdown()).trimEnd(); // "Dirty" = user has local edits not yet flushed through the debounced // `onUpdate`. `lastEmittedRef` is advanced only after a debounce fire,