fix(editor): don't wipe in-flight uploads on external content sync

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) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing
2026-06-16 17:31:36 +08:00
parent 8ba1ef2dce
commit c0af331c03

View File

@@ -384,6 +384,21 @@ const ContentEditor = forwardRef<ContentEditorRef, ContentEditorProps>(
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,