Skip to content

Commit 18d2fd7

Browse files
committed
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
1 parent 1310045 commit 18d2fd7

File tree

2 files changed

+85
-26
lines changed

2 files changed

+85
-26
lines changed

apps/web/src/composerDraftStore.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,43 @@ describe("composerDraftStore codex fast mode", () => {
531531
});
532532
});
533533

534+
describe("composerDraftStore persisted attachments", () => {
535+
const threadId = ThreadId.makeUnsafe("thread-persisted-attachments");
536+
537+
beforeEach(() => {
538+
useComposerDraftStore.setState({
539+
draftsByThreadId: {},
540+
draftThreadsByThreadId: {},
541+
projectDraftThreadIdByProjectId: {},
542+
});
543+
localStorage.clear();
544+
});
545+
546+
it("verifies attachments after flushing the debounced storage write", async () => {
547+
const image = makeImage({
548+
id: "img-persisted",
549+
previewUrl: "blob:persisted",
550+
});
551+
552+
useComposerDraftStore.getState().addImage(threadId, image);
553+
useComposerDraftStore.getState().syncPersistedAttachments(threadId, [
554+
{
555+
id: image.id,
556+
name: image.name,
557+
mimeType: image.mimeType,
558+
sizeBytes: image.sizeBytes,
559+
dataUrl: "data:image/png;base64,AQ==",
560+
},
561+
]);
562+
563+
await Promise.resolve();
564+
565+
const draft = useComposerDraftStore.getState().draftsByThreadId[threadId];
566+
expect(draft?.persistedAttachments.map((attachment) => attachment.id)).toEqual([image.id]);
567+
expect(draft?.nonPersistedImageIds).toEqual([]);
568+
});
569+
});
570+
534571
describe("composerDraftStore setModel", () => {
535572
const threadId = ThreadId.makeUnsafe("thread-model");
536573

apps/web/src/composerDraftStore.ts

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,53 @@ function readPersistedAttachmentIdsFromStorage(threadId: ThreadId): string[] {
628628
}
629629
}
630630

631+
function verifyPersistedAttachments(
632+
threadId: ThreadId,
633+
attachments: PersistedComposerImageAttachment[],
634+
set: (
635+
partial:
636+
| ComposerDraftStoreState
637+
| Partial<ComposerDraftStoreState>
638+
| ((
639+
state: ComposerDraftStoreState,
640+
) => ComposerDraftStoreState | Partial<ComposerDraftStoreState>),
641+
replace?: false,
642+
) => void,
643+
): void {
644+
let persistedIdSet = new Set<string>();
645+
try {
646+
composerDebouncedStorage.flush();
647+
persistedIdSet = new Set(readPersistedAttachmentIdsFromStorage(threadId));
648+
} catch {
649+
persistedIdSet = new Set();
650+
}
651+
set((state) => {
652+
const current = state.draftsByThreadId[threadId];
653+
if (!current) {
654+
return state;
655+
}
656+
const imageIdSet = new Set(current.images.map((image) => image.id));
657+
const persistedAttachments = attachments.filter(
658+
(attachment) => imageIdSet.has(attachment.id) && persistedIdSet.has(attachment.id),
659+
);
660+
const nonPersistedImageIds = current.images
661+
.map((image) => image.id)
662+
.filter((imageId) => !persistedIdSet.has(imageId));
663+
const nextDraft: ComposerThreadDraftState = {
664+
...current,
665+
persistedAttachments,
666+
nonPersistedImageIds,
667+
};
668+
const nextDraftsByThreadId = { ...state.draftsByThreadId };
669+
if (shouldRemoveDraft(nextDraft)) {
670+
delete nextDraftsByThreadId[threadId];
671+
} else {
672+
nextDraftsByThreadId[threadId] = nextDraft;
673+
}
674+
return { draftsByThreadId: nextDraftsByThreadId };
675+
});
676+
}
677+
631678
function hydreatePersistedComposerImageAttachment(
632679
attachment: PersistedComposerImageAttachment,
633680
): File | null {
@@ -1404,32 +1451,7 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
14041451
return { draftsByThreadId: nextDraftsByThreadId };
14051452
});
14061453
Promise.resolve().then(() => {
1407-
const persistedIdSet = new Set(readPersistedAttachmentIdsFromStorage(threadId));
1408-
set((state) => {
1409-
const current = state.draftsByThreadId[threadId];
1410-
if (!current) {
1411-
return state;
1412-
}
1413-
const imageIdSet = new Set(current.images.map((image) => image.id));
1414-
const persistedAttachments = attachments.filter(
1415-
(attachment) => imageIdSet.has(attachment.id) && persistedIdSet.has(attachment.id),
1416-
);
1417-
const nonPersistedImageIds = current.images
1418-
.map((image) => image.id)
1419-
.filter((imageId) => !persistedIdSet.has(imageId));
1420-
const nextDraft: ComposerThreadDraftState = {
1421-
...current,
1422-
persistedAttachments,
1423-
nonPersistedImageIds,
1424-
};
1425-
const nextDraftsByThreadId = { ...state.draftsByThreadId };
1426-
if (shouldRemoveDraft(nextDraft)) {
1427-
delete nextDraftsByThreadId[threadId];
1428-
} else {
1429-
nextDraftsByThreadId[threadId] = nextDraft;
1430-
}
1431-
return { draftsByThreadId: nextDraftsByThreadId };
1432-
});
1454+
verifyPersistedAttachments(threadId, attachments, set);
14331455
});
14341456
},
14351457
clearComposerContent: (threadId) => {

0 commit comments

Comments
 (0)