From a1b85111de6acfcd56af0844c80bc08671aa9c49 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 15 Sep 2026 22:04:12 +0200 Subject: [PATCH] fix: clear only the mirrors after a clear-all, never discard a second time Co-Authored-By: Claude Opus 5 (1M context) --- .../copilot/chat/global/core.test.ts | 39 +++++++++++++++++++ .../copilot/chat/global/userDraftAdapter.ts | 14 ++++++- .../(logged)/global_drafts/+page.svelte | 14 ++++++- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/copilot/chat/global/core.test.ts b/frontend/src/lib/components/copilot/chat/global/core.test.ts index 4c3b9b2825..72a4cb33b2 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.test.ts @@ -2565,6 +2565,45 @@ describe('global AI tools', () => { } }) + // "Clear all" deletes each row, then drops the in-tab mirrors. That second pass must not go + // back through a live editor's discard: one that kept its draft, because it was changed after + // the discard was asked for, would have that newer edit taken on the way past. + it('does not discard again when clearing the mirrors after a clear-all', async () => { + const path = 'u/admin/cleared_res' + seedBackendDraft('resource', path, { path, value: { a: 2 } }) + let discards = 0 + registerLiveItemBridge({ + seed: () => false, + read: () => ({ value: { path, value: { a: 3 } } }), + refresh: () => undefined, + noteRow: () => {}, + itemDeleted: () => Promise.resolve(), + discard: () => { + discards++ + return Promise.resolve('done' as const) + }, + list: () => [{ workspace: WORKSPACE, itemKind: 'resource', path, value: { a: 3 } }] + }) + try { + const { removed } = await deleteGlobalDraft(WORKSPACE, 'resource', path) + expect(removed).toBe(false) + expect(discards).toBe(1) + // The row is there on purpose, so the mirror pass leaves it alone entirely. + clearGlobalDrafts(WORKSPACE, new Set([path])) + expect(discards).toBe(1) + } finally { + registerLiveItemBridge({ + seed: () => false, + read: () => undefined, + refresh: () => undefined, + noteRow: () => {}, + itemDeleted: () => Promise.resolve(), + discard: () => undefined, + list: () => [] + }) + } + }) + // A failed server delete must surface (throw), not silently report removed — // the same guard the write path got, applied to the delete path. it('deleteGlobalDraft throws when the server delete fails', async () => { diff --git a/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts b/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts index 53ce74a1f2..2b96dd64e0 100644 --- a/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts +++ b/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts @@ -762,8 +762,18 @@ export async function flushGlobalDraftSaves( return { unflushedPaths } } -export function clearGlobalDrafts(workspace: string): void { +/** + * Drop the in-tab mirrors left after the rows themselves have been deleted. Only the mirrors: + * `clear` would route to a live editor's discard, and one that kept its draft — because it was + * changed after that discard was asked for — would have that newer edit taken on this pass, + * undoing exactly what the first one preserved. `keep` names the paths to leave alone. + */ +export function clearGlobalDrafts( + workspace: string, + keep: ReadonlySet = new Set() +): void { for (const draft of UserDraft.list({ workspace, itemKinds: [...GLOBAL_DRAFT_KINDS] })) { - UserDraft.clear(draft.itemKind, draft.path, { workspace }) + if (keep.has(draft.path)) continue + UserDraft.forgetLocal(draft.itemKind, draft.path, { workspace }) } } diff --git a/frontend/src/routes/(root)/(logged)/global_drafts/+page.svelte b/frontend/src/routes/(root)/(logged)/global_drafts/+page.svelte index f645f7e3d6..2052e2ebbb 100644 --- a/frontend/src/routes/(root)/(logged)/global_drafts/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/global_drafts/+page.svelte @@ -60,14 +60,24 @@ // Delete each listed draft from the backend (the source of truth) — the // local clearGlobalDrafts only clears in-tab cells, leaving persisted rows. // Continue past a per-row failure so one bad delete doesn't strand the rest. + // A draft an open editor kept, or one whose delete failed, is still there on purpose or + // still to be dealt with: its mirror stays too, or the page would stop showing it. + const kept = new Set() for (const item of [...drafts]) { try { - await deleteGlobalDraft($workspaceStore, item.type, item.path, item.triggerKind) + const { removed } = await deleteGlobalDraft( + $workspaceStore, + item.type, + item.path, + item.triggerKind + ) + if (!removed) kept.add(item.path) } catch (e) { console.error('Failed to clear draft', item.path, e) + kept.add(item.path) } } - clearGlobalDrafts($workspaceStore) + clearGlobalDrafts($workspaceStore, kept) refreshDrafts() }