From 984e958a72f413a91d0fbe87da086376f8ab2538 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 14 Sep 2026 18:49:21 +0200 Subject: [PATCH] fix: keep an outside draft written to an editor still loading behind a move Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015vn1jCHUcAdaG9C4NsUehF --- frontend/src/lib/itemStore.svelte.ts | 10 ++++++++-- frontend/src/lib/itemStore.test.ts | 30 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/itemStore.svelte.ts b/frontend/src/lib/itemStore.svelte.ts index 5f945a9258..33cfa84566 100644 --- a/frontend/src/lib/itemStore.svelte.ts +++ b/frontend/src/lib/itemStore.svelte.ts @@ -421,8 +421,14 @@ class Entry { * changed from its own deployed side, so they stay a draft over what this entry deployed. * Compared exactly, as a save is: a field the draft comparison ignores (run-as) is an edit. */ carryEditsOf(old: Entry): void { - if (old.origin !== 'deployed' || old.value === undefined || old.deployed === undefined) return - if (this.value === undefined) return + if (old.value === undefined || this.value === undefined) return + // Not loaded yet (its read waits behind the move): only an outside write can have filled it, + // and nothing has persisted that write but this entry. + if (!old.loaded) { + this.applyExternal(old.value) + return + } + if (old.origin !== 'deployed' || old.deployed === undefined) return const edited = old.value as Record const base = old.deployed as Record const next = snapshot(this.value) as Record diff --git a/frontend/src/lib/itemStore.test.ts b/frontend/src/lib/itemStore.test.ts index 6a8ee6cafa..d36108ef46 100644 --- a/frontend/src/lib/itemStore.test.ts +++ b/frontend/src/lib/itemStore.test.ts @@ -556,6 +556,36 @@ describe('item store: one entry per key', () => { expect(rows.writes.at(-1)).toEqual({ path: 'u/me/b', value: chat }) }) + it('keeps a draft written from outside to an editor that opened during the move', async () => { + const rows = fakeRows() + const store = createItemStore(rows.port) + const gate = deferred() + const b = { ...deployedRes, path: 'u/me/b' } + const temporary = newItemPath() + const { handle: moving } = store.acquire( + { workspace: 'w', kind: 'resource', path: temporary }, + { workspace: 'w', path: temporary, template: b }, + adapter({}, () => gate.promise) + ) + await settle() + moving.value = { ...b, args: { a: 2 } } + const moved = moving.save() + await settle() + const { handle: opened } = store.acquire( + { workspace: 'w', kind: 'resource', path: 'u/me/b' }, + { workspace: 'w', path: 'u/me/b' }, + adapter({ deployed: b }) + ) + const chat = { ...b, description: 'written by the chat meanwhile' } + // The opened editor holds the key, so the store persists it. + expect(store.bridge.seed('w', 'resource', 'u/me/b', chat)).toBe(true) + + gate.resolve() + expect(await moved).toMatchObject({ ok: true, moved: true }) + expect(opened.value).toEqual(chat) + expect(rows.writes.at(-1)).toEqual({ path: 'u/me/b', value: chat }) + }) + it('keeps a change the draft comparison ignores in an editor a move replaces', async () => { type Sched = { path: string; summary: string; permissioned_as?: string } const rows = fakeRows()