fix: re-read a live editor before a deploy discards the draft it made

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 14:50:47 +02:00
co-authored by Claude Opus 5
parent 994b8dcae1
commit b4ed4f52c5
5 changed files with 77 additions and 2 deletions
@@ -2324,6 +2324,7 @@ describe('global AI tools', () => {
registerLiveItemBridge({
seed: () => false,
read: () => undefined,
refresh: () => undefined,
// Claims the key and removes nothing, so anything still gone was deleted by the chat.
discard: () => {
discards++
@@ -2339,6 +2340,43 @@ describe('global AI tools', () => {
registerLiveItemBridge({
seed: () => false,
read: () => undefined,
refresh: () => undefined,
discard: () => undefined,
list: () => []
})
}
})
// Deploying a draft writes the item, so an editor open on it is a baseline behind. Its row is
// discarded straight after, which resets it to that baseline — so it has to re-read first.
it('refreshes a live editor before a deploy discards its draft', async () => {
const path = 'u/admin/deployed_res'
seedBackendDraft('resource', path, { path, value: { a: 2 } })
const calls: string[] = []
registerLiveItemBridge({
seed: () => false,
read: () => undefined,
refresh: () => {
calls.push('refresh')
return Promise.resolve()
},
discard: () => {
calls.push('discard')
return Promise.resolve({ removed: true })
},
list: () => []
})
try {
await deleteGlobalDraft(WORKSPACE, 'resource', path, undefined, {
preserveLiveDraft: true,
deployed: true
})
expect(calls).toEqual(['refresh', 'discard'])
} finally {
registerLiveItemBridge({
seed: () => false,
read: () => undefined,
refresh: () => undefined,
discard: () => undefined,
list: () => []
})
@@ -7994,7 +7994,10 @@ async function deployDraft(
// fork comparisons before the fallible draft cleanup below.
invalidateWorkspaceComparison(workspace)
await deleteGlobalDraft(workspace, type, path, triggerKind, { preserveLiveDraft: true })
await deleteGlobalDraft(workspace, type, path, triggerKind, {
preserveLiveDraft: true,
deployed: true
})
// Move the chat's mask entry to the deployed path: a draft-only item's
// synthetic storage key never exists deployed, so the entry would otherwise
@@ -2,7 +2,12 @@ import type { Flow, NewSchedule, NewScript } from '$lib/gen/types.gen'
import { DraftService } from '$lib/gen'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import { DEFAULT_DATA as DEFAULT_RAW_APP_DATA } from '$lib/components/raw_apps/dataTableRefUtils'
import { UserDraft, type UserDraftEntry, type UserDraftItemKind } from '$lib/userDraft.svelte'
import {
refreshLiveItem,
UserDraft,
type UserDraftEntry,
type UserDraftItemKind
} from '$lib/userDraft.svelte'
import { invalidateWorkspaceDrafts } from '$lib/workspaceDrafts.svelte'
import {
getWorkspaceItemKey,
@@ -574,6 +579,10 @@ export async function saveGlobalAppDraft(
type DeleteGlobalDraftOptions = {
preserveLiveDraft?: boolean
/** The deployed item was just written from this draft. An editor open on it is holding the
* baseline from before that, and discarding the draft resets it to exactly that — showing
* the pre-deploy value, clean, ready to be saved back over the deployment. */
deployed?: boolean
}
export async function deleteGlobalDraft(
@@ -586,6 +595,7 @@ export async function deleteGlobalDraft(
const itemKind = itemKindFor(type, triggerKind)
if (!itemKind) return
const storagePath = resolveDraftStoragePath(workspace, itemKind, path)
if (options.deployed) await refreshLiveItem(workspace, itemKind, storagePath)
const liveDraft = UserDraft.getLiveEditorDraft(itemKind, { workspace })
const live =
options.preserveLiveDraft && liveDraft?.storagePath === storagePath
+7
View File
@@ -1089,6 +1089,13 @@ export function createItemStore(ports: ItemRowPort) {
if (!entry || !entry.loaded) return undefined
return { value: entry.dirty && entry.origin !== 'new' ? snapshot(entry.value) : undefined }
},
refresh(
workspace: string,
kind: UserDraftItemKind,
path: string
): Promise<unknown> | undefined {
return find(workspace, kind, path)?.reread()
},
discard(
workspace: string,
kind: UserDraftItemKind,
+17
View File
@@ -141,6 +141,13 @@ export type LiveItemBridge = {
itemKind: UserDraftItemKind,
path: string
): { value: unknown | undefined } | undefined
/** Re-read the live item, for a caller that has just written the deployed item under it.
* `undefined` when no live item holds the key. */
refresh(
workspace: string,
itemKind: UserDraftItemKind,
path: string
): Promise<unknown> | undefined
/** Discard the live item's draft, resolving once its delete has landed. `undefined` when no
* live item holds the key, so the caller issues the delete itself. */
discard(
@@ -153,6 +160,16 @@ export type LiveItemBridge = {
let liveItems: LiveItemBridge | undefined
/** Tell a live item that the deployed item under it has just been written, so its baseline is
* current before anything resets to it. Resolves once it has re-read. */
export function refreshLiveItem(
workspace: string,
itemKind: UserDraftItemKind,
path: string
): Promise<unknown> | undefined {
return liveItems?.refresh(workspace, itemKind, path)
}
export function registerLiveItemBridge(bridge: LiveItemBridge): void {
liveItems = bridge
}