fix(sessions): send a write's settled draft before the list frame re-reads it

The settle that follows a write parks its draft delete on the autosave debounce,
so the list frame reloaded straight after read the row's `*` and kept it: its
markers come from a store inside the iframe, and nothing reloads it a second
time when the delete lands. The fan-out sends what is parked for the key first.

That needed the variable save to report after settling rather than before —
reporting first meant there was nothing parked yet to send, and the flush passed
over a cell about to queue a delete. Every editor now resolves its cell before
saying so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit is contained in:
Diego Imbert
2026-09-06 13:51:09 +02:00
co-authored by Claude Opus 5
parent d27eec52ab
commit 08386433aa
3 changed files with 28 additions and 8 deletions
@@ -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)
+15 -1
View File
@@ -290,6 +290,20 @@ export function settleDraftAfterWrite<V>(
}
}
/**
* 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<void> {
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<boolean> {
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'
@@ -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