diff --git a/frontend/src/lib/components/VariableEditor.svelte b/frontend/src/lib/components/VariableEditor.svelte index 7b78bd2216..c3e0c422d5 100644 --- a/frontend/src/lib/components/VariableEditor.svelte +++ b/frontend/src/lib/components/VariableEditor.svelte @@ -339,11 +339,6 @@ existedInitially[ws] = true } if (ws === fromWs) actingCommitted = true - // Per workspace, as each write lands. Each carries its own workspace and - // the path it wrote there, so a linked workspace's rename moves the tabs - // acting on it and no others — and a workspace written before a later one - // threw is still reported, because it is deployed. - onSaved?.(s.path, from, ws) // 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 @@ -351,6 +346,12 @@ settleDraftAfterWrite('variable', s, states[ws]?.draft, from ?? '', s.path, { workspace: ws }) + // Reported after settling, so what hears about the write sees a cell that + // has already been resolved. Per workspace, each carrying its own and the + // path it wrote there, so a linked workspace's rename moves the tabs acting + // on it and no others — and a workspace written before a later one threw is + // still reported, because it is deployed. + onSaved?.(s.path, from, ws) // Path now exists server-side — drop the autocomplete cache so // it shows up immediately instead of after the 60s TTL. invalidateWorkspacePaths(ws) diff --git a/frontend/src/lib/userDraft.svelte.ts b/frontend/src/lib/userDraft.svelte.ts index f64d6986ae..8e7585dfb7 100644 --- a/frontend/src/lib/userDraft.svelte.ts +++ b/frontend/src/lib/userDraft.svelte.ts @@ -290,6 +290,20 @@ export function settleDraftAfterWrite( } } +/** + * Send whatever this cell has parked on the autosave debounce, now. For a reader + * that cannot be corrected once it has read — a list page inside an iframe, whose + * draft markers come from its own store — and would otherwise render a row the + * debounce has not caught up with. + */ +export async function flushDraftWrites( + itemKind: UserDraftItemKind, + path: string, + opts?: UserDraftOptions +): Promise { + await UserDraftDbSyncer.flush({ workspace: resolveWorkspace(opts), itemKind, path }) +} + /** * Wait for a queued draft delete to land, reporting whether the draft is gone. A * caller acts on this — a host leaves an editor whose item the discard removed — @@ -304,7 +318,7 @@ export async function flushDraftDelete( opts?: UserDraftOptions ): Promise { const query = { workspace: resolveWorkspace(opts), itemKind, path } - await UserDraftDbSyncer.flush(query) + await flushDraftWrites(itemKind, path, opts) return ( UserDraftDbSyncer.lastLandedWasDelete(query) && UserDraftDbSyncer.getState(query).state === 'none' diff --git a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte index 09e6c70fd0..3b55a02f9c 100644 --- a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte @@ -90,7 +90,7 @@ type WorkspaceItemKind } from '$lib/components/workspacePicker' import { splitterPointerCapture } from '$lib/utils/splitterPointerCapture' - import { UserDraft } from '$lib/userDraft.svelte' + import { UserDraft, flushDraftWrites } from '$lib/userDraft.svelte' const globalEnabled = isGlobalAiEnabled() @@ -614,7 +614,7 @@ // A hosted entity editor wrote its item. Every tab on it hears, across warm // sessions: they share the one draft cell and the one server row, so one left // behind edits a path that is gone or discards over a value already deployed. - function entityWritten(ev: { + async function entityWritten(ev: { kind: EntityEditorKind path: string workspace: string @@ -625,6 +625,11 @@ }) { const page = entityListPage(ev.kind)?.path if (!page) return + // The settle that follows a write parks its draft delete on the autosave + // debounce, and a list frame reads its `*` markers from a store of its own + // — read before that lands, it keeps the row's marker until something else + // reloads it, and nothing does. + await flushDraftWrites(ev.kind, ev.path, { workspace: ev.workspace }) for (const s of warmSessions) { const owner = getRuntime(s.id)?.previewTabs if (!owner || getEffectiveWorkspaceId(s) !== ev.workspace) continue