From 82cf51960639cebc819ec017dae9f69f09895ba7 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Sat, 5 Sep 2026 20:25:15 +0200 Subject: [PATCH] fix(sessions): record a missed seed when it happens, not after the write Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 --- .../lib/components/sessions/previewReload.ts | 5 ++- frontend/src/lib/userDraft.svelte.ts | 35 ++++++++++++------- .../(root)/(logged)/sessions/+page.svelte | 7 ++-- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/components/sessions/previewReload.ts b/frontend/src/lib/components/sessions/previewReload.ts index 2e84eed59a..69cac9c5b3 100644 --- a/frontend/src/lib/components/sessions/previewReload.ts +++ b/frontend/src/lib/components/sessions/previewReload.ts @@ -64,7 +64,10 @@ export function toolReloadEffect(name: string, args: any): ToolReloadEffect { } function itemPath(args: any): string | undefined { - return typeof args?.path === 'string' && args.path ? args.path : undefined + // write_trigger carries the item under `config` (see writeTriggerDraft), unlike + // every other tool here, which names it at the top level. + const p = args?.path ?? args?.config?.path + return typeof p === 'string' && p ? p : undefined } /** One item mutation from a chat round, as the preview needs to read it back. */ diff --git a/frontend/src/lib/userDraft.svelte.ts b/frontend/src/lib/userDraft.svelte.ts index 03ad738758..b2699818da 100644 --- a/frontend/src/lib/userDraft.svelte.ts +++ b/frontend/src/lib/userDraft.svelte.ts @@ -124,6 +124,9 @@ export type ClearLiveEditorDraftOptions = UserDraftOptions & { } const entries = new Map() +// Cells whose last `seed` found no live entry (see `takeSeedMiss`). Bounded by +// the seeds that missed and not yet been read back, and each entry is one key. +const seedMisses = new Set() const liveEditorDrafts = new Map() /** * Map keys whose entry should start `syncSuspended` on acquire. Lets @@ -395,27 +398,35 @@ export const UserDraft = { * is still needed when a write fans out across components (e.g. an * editor's `initContent` cascading into the bound value). * - * No-op if the entry isn't live yet (acquire via `use`/`useMany` first). + * No-op if the entry isn't live yet (acquire via `use`/`useMany` first) — + * recorded for {@link takeSeedMiss}, since the value then reached the server + * without reaching the editor that will show it. */ - /** - * Whether a mounted editor currently holds this cell — i.e. whether {@link seed} - * would reach one rather than no-op. An editor acquires its cell only once its - * first load resolves, so this is false while one is still loading, and a - * caller that seeded then has to reconcile the editor some other way. - */ - hasLiveEntry(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): boolean { - return entries.has(mapKey(resolveWorkspace(opts), itemKind, path)) - }, - seed(itemKind: UserDraftItemKind, path: string, value: V, opts?: UserDraftOptions): void { const ws = resolveWorkspace(opts) const mk = mapKey(ws, itemKind, path) const entry = entries.get(mk) - if (!entry) return + if (!entry) { + seedMisses.add(mk) + return + } + seedMisses.delete(mk) entry.seedNextWrite = true entry.state.val = snapshotDraftValue(value) }, + /** + * Whether the last {@link seed} for this cell found no live entry, clearing the + * record. An editor acquires its cell only once its first load resolves, so a + * seed during that window reaches nothing and the value lands on the server + * alone — a caller showing that editor has to make it re-read. Recorded at the + * seed rather than asked afterwards: by then the editor may have acquired the + * cell, hiding the very miss this reports. + */ + takeSeedMiss(itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions): boolean { + return seedMisses.delete(mapKey(resolveWorkspace(opts), itemKind, path)) + }, + /** * Currently-mounted live entries for `workspace` (in-tab only — for a * workspace-wide view call `DraftService` directly). diff --git a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte index c52f9f3ac9..291af0f6dd 100644 --- a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte @@ -621,10 +621,11 @@ if (pages.length === 0) return for (const p of pages) pendingPages.add(p) // A write reaches a hosted editor through the draft cell it holds, but an - // editor still loading holds none yet and `seed` no-opped past it — so ask - // whether the cell was live, and refresh the ones the write missed. + // editor still loading holds none yet and the seed no-opped past it. The + // miss is recorded when it happens — asking now would be too late, since + // the editor can have acquired the cell during the write's round-trip. const kind = path ? entityKindForPage(pages[0]) : undefined - const seedReachedEditor = !kind || !path || UserDraft.hasLiveEntry(kind, path, { workspace }) + const seedReachedEditor = !kind || !path || !UserDraft.takeSeedMiss(kind, path, { workspace }) const effect = effectForWrite(entity, seedReachedEditor) if (effect !== 'none') pendingMutations.push({ pages, effect, path, workspace }) clearTimeout(reloadHandle)