From 0a7a71c549851ea04fef8d67553eceaa2fd4db91 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:00:57 +0530 Subject: [PATCH 1/2] fix(web): avoid false draft attachment persistence warnings Flush pending composer draft writes before verifying persisted attachments so image drafts are not incorrectly marked as unsaved due to the debounce window. Made-with: Cursor --- apps/web/src/composerDraftStore.test.ts | 37 +++++++++++++ apps/web/src/composerDraftStore.ts | 74 ++++++++++++++++--------- 2 files changed, 85 insertions(+), 26 deletions(-) diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index 927a16060..28707ea42 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -364,6 +364,43 @@ describe("composerDraftStore codex fast mode", () => { }); }); +describe("composerDraftStore persisted attachments", () => { + const threadId = ThreadId.makeUnsafe("thread-persisted-attachments"); + + beforeEach(() => { + useComposerDraftStore.setState({ + draftsByThreadId: {}, + draftThreadsByThreadId: {}, + projectDraftThreadIdByProjectId: {}, + }); + localStorage.clear(); + }); + + it("verifies attachments after flushing the debounced storage write", async () => { + const image = makeImage({ + id: "img-persisted", + previewUrl: "blob:persisted", + }); + + useComposerDraftStore.getState().addImage(threadId, image); + useComposerDraftStore.getState().syncPersistedAttachments(threadId, [ + { + id: image.id, + name: image.name, + mimeType: image.mimeType, + sizeBytes: image.sizeBytes, + dataUrl: "data:image/png;base64,AQ==", + }, + ]); + + await Promise.resolve(); + + const draft = useComposerDraftStore.getState().draftsByThreadId[threadId]; + expect(draft?.persistedAttachments.map((attachment) => attachment.id)).toEqual([image.id]); + expect(draft?.nonPersistedImageIds).toEqual([]); + }); +}); + describe("composerDraftStore setModel", () => { const threadId = ThreadId.makeUnsafe("thread-model"); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 2af920527..f9869b813 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -487,6 +487,53 @@ function readPersistedAttachmentIdsFromStorage(threadId: ThreadId): string[] { } } +function verifyPersistedAttachments( + threadId: ThreadId, + attachments: PersistedComposerImageAttachment[], + set: ( + partial: + | ComposerDraftStoreState + | Partial + | (( + state: ComposerDraftStoreState, + ) => ComposerDraftStoreState | Partial), + replace?: false, + ) => void, +): void { + let persistedIdSet = new Set(); + try { + composerDebouncedStorage.flush(); + persistedIdSet = new Set(readPersistedAttachmentIdsFromStorage(threadId)); + } catch { + persistedIdSet = new Set(); + } + set((state) => { + const current = state.draftsByThreadId[threadId]; + if (!current) { + return state; + } + const imageIdSet = new Set(current.images.map((image) => image.id)); + const persistedAttachments = attachments.filter( + (attachment) => imageIdSet.has(attachment.id) && persistedIdSet.has(attachment.id), + ); + const nonPersistedImageIds = current.images + .map((image) => image.id) + .filter((imageId) => !persistedIdSet.has(imageId)); + const nextDraft: ComposerThreadDraftState = { + ...current, + persistedAttachments, + nonPersistedImageIds, + }; + const nextDraftsByThreadId = { ...state.draftsByThreadId }; + if (shouldRemoveDraft(nextDraft)) { + delete nextDraftsByThreadId[threadId]; + } else { + nextDraftsByThreadId[threadId] = nextDraft; + } + return { draftsByThreadId: nextDraftsByThreadId }; + }); +} + function hydreatePersistedComposerImageAttachment( attachment: PersistedComposerImageAttachment, ): File | null { @@ -1116,32 +1163,7 @@ export const useComposerDraftStore = create()( return { draftsByThreadId: nextDraftsByThreadId }; }); Promise.resolve().then(() => { - const persistedIdSet = new Set(readPersistedAttachmentIdsFromStorage(threadId)); - set((state) => { - const current = state.draftsByThreadId[threadId]; - if (!current) { - return state; - } - const imageIdSet = new Set(current.images.map((image) => image.id)); - const persistedAttachments = attachments.filter( - (attachment) => imageIdSet.has(attachment.id) && persistedIdSet.has(attachment.id), - ); - const nonPersistedImageIds = current.images - .map((image) => image.id) - .filter((imageId) => !persistedIdSet.has(imageId)); - const nextDraft: ComposerThreadDraftState = { - ...current, - persistedAttachments, - nonPersistedImageIds, - }; - const nextDraftsByThreadId = { ...state.draftsByThreadId }; - if (shouldRemoveDraft(nextDraft)) { - delete nextDraftsByThreadId[threadId]; - } else { - nextDraftsByThreadId[threadId] = nextDraft; - } - return { draftsByThreadId: nextDraftsByThreadId }; - }); + verifyPersistedAttachments(threadId, attachments, set); }); }, clearComposerContent: (threadId) => { From a015d4e262392d32dd63c3f79b5c02bce72cabf1 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:27:06 +0530 Subject: [PATCH 2/2] test not needed --- apps/web/src/composerDraftStore.test.ts | 37 ------------------------- 1 file changed, 37 deletions(-) diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index 28707ea42..927a16060 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -364,43 +364,6 @@ describe("composerDraftStore codex fast mode", () => { }); }); -describe("composerDraftStore persisted attachments", () => { - const threadId = ThreadId.makeUnsafe("thread-persisted-attachments"); - - beforeEach(() => { - useComposerDraftStore.setState({ - draftsByThreadId: {}, - draftThreadsByThreadId: {}, - projectDraftThreadIdByProjectId: {}, - }); - localStorage.clear(); - }); - - it("verifies attachments after flushing the debounced storage write", async () => { - const image = makeImage({ - id: "img-persisted", - previewUrl: "blob:persisted", - }); - - useComposerDraftStore.getState().addImage(threadId, image); - useComposerDraftStore.getState().syncPersistedAttachments(threadId, [ - { - id: image.id, - name: image.name, - mimeType: image.mimeType, - sizeBytes: image.sizeBytes, - dataUrl: "data:image/png;base64,AQ==", - }, - ]); - - await Promise.resolve(); - - const draft = useComposerDraftStore.getState().draftsByThreadId[threadId]; - expect(draft?.persistedAttachments.map((attachment) => attachment.id)).toEqual([image.id]); - expect(draft?.nonPersistedImageIds).toEqual([]); - }); -}); - describe("composerDraftStore setModel", () => { const threadId = ThreadId.makeUnsafe("thread-model");