From df06cc20fcaf0c9d58df080bc77dfe57b7edc5cc Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Sun, 6 Sep 2026 18:14:25 +0200 Subject: [PATCH] fix(drafts): do not report a removal a queued edit has already undone, and track one workspace per save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chat's discard reports its delete as landed once nothing failed or conflicted, but the form stays editable across it: an edit made then queues a write behind the delete and recreates the draft. The removal marker — which sends a hosted editor away from an item it says is gone — is only published when nothing is queued behind the delete, so it describes an item that is actually gone. The variable save's committed flag still followed the acting workspace after the path it pairs with began following the shown one, and its reopen let the editor reselect its default workspace. Both follow the workspace the form is showing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 --- frontend/src/lib/components/VariableEditor.svelte | 14 +++++++++++--- .../src/lib/components/copilot/chat/global/core.ts | 11 +++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/VariableEditor.svelte b/frontend/src/lib/components/VariableEditor.svelte index 234798a68c..391949ee00 100644 --- a/frontend/src/lib/components/VariableEditor.svelte +++ b/frontend/src/lib/components/VariableEditor.svelte @@ -304,7 +304,9 @@ payloads.find((pl) => pl.ws === shownWs)?.s.path ?? payloads.find((pl) => pl.ws === fromWs)?.s.path ?? from - let actingCommitted = false + // Committed by the workspace the form is showing — the one `actingPath` follows, + // so the two cannot disagree about who this editor is tracking. + let shownCommitted = false // Every workspace written, with the path it wrote there. Reported once the loop // is done: per workspace, each carrying its own, so a linked workspace's rename // moves the tabs acting on it and no others. @@ -348,7 +350,7 @@ initialStates[ws] = s existedInitially[ws] = true } - if (ws === fromWs) actingCommitted = true + if (ws === (shownWs ?? fromWs)) shownCommitted = true // The just-saved state is the new deployed baseline; this resets the handle // to it via `discard` (not `remove` — blanking the cell to `undefined` reads // as dirty) and keeps an edit made mid-request. Each workspace settles @@ -385,7 +387,13 @@ // back the values this drawer opened on, which describe neither what the // committed workspace now has deployed nor the draft the failed one still // holds — that one stays at its own path, editable from its workspace. - if (actingCommitted && actingPath && editPath === from) editVariable(actingPath) + if (shownCommitted && actingPath && editPath === from) { + // Reopen on the workspace whose write this is following, not on the one the + // editor defaults to: `editVariable` selects `curWs`, which for a linked + // version would load a different workspace's item at this path. + editVariable(actingPath) + if (shownWs) selected = shownWs + } } } diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index 18dbf9c077..99943c8d2b 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -6182,9 +6182,16 @@ async function discardLocalDraft( // Published only now: the delete above can throw, and the marker is read by the // tool-completion listener, which a throw never reaches — leaving it to be // consumed by some later action on this item, which would send its editor away - // while the item is still there. + // while the item is still there. Nor is it published when a write is queued + // behind the delete: the form stays editable across it, and an edit made then + // recreates the draft, so the item the marker would report as gone is not. if (removesItem && discardedKind) { - UserDraft.recordDraftOnlyDiscard(discardedKind, storagePath, { workspace }) + const settled = + UserDraftDbSyncer.getState({ workspace, itemKind: discardedKind, path: storagePath }) + .state === 'none' + if (settled) { + UserDraft.recordDraftOnlyDiscard(discardedKind, storagePath, { workspace }) + } } // The chat's touch on the item is undone — drop it from the mask so a