diff --git a/frontend/src/lib/components/sessions/PreviewTabHost.svelte b/frontend/src/lib/components/sessions/PreviewTabHost.svelte index 3fae60312e..5317ba5fb8 100644 --- a/frontend/src/lib/components/sessions/PreviewTabHost.svelte +++ b/frontend/src/lib/components/sessions/PreviewTabHost.svelte @@ -179,6 +179,16 @@ : undefined ) + // The item is gone (a draft-only one whose draft was discarded), so the tab has + // the same destination as `backToList` — but bound to this tab by id, not to + // whichever is active: a discard completes asynchronously, and by then the user + // may be looking at another tab, which must not be the one sent to the list. + const returnToList = $derived( + slot.kind === 'entity' && runtime + ? () => runtime.previewTabs.retargetTabTo(tab.id, entityListHref(whereIs(tab))) + : undefined + ) + // Follow a rename: the editor stays mounted and keeps editing the item, but the // tab, its label, the chat's ACTIVE PREVIEW and the draft key all address it by // path — so they have to move with it, or they name an item that no longer exists. @@ -377,6 +387,7 @@ path={slot.path} {workspaceId} onBack={backToList} + onRemoved={returnToList} onRenamed={retargetTo} /> {/await} @@ -388,6 +399,7 @@ path={slot.path} {workspaceId} onBack={backToList} + onRemoved={returnToList} onRenamed={retargetTo} /> {/await} @@ -399,6 +411,7 @@ path={slot.path} {workspaceId} onBack={backToList} + onRemoved={returnToList} onRenamed={retargetTo} /> {/await} diff --git a/frontend/src/lib/components/sessions/ResourceEditorView.svelte b/frontend/src/lib/components/sessions/ResourceEditorView.svelte index 1706d226ce..e6a7f3204b 100644 --- a/frontend/src/lib/components/sessions/ResourceEditorView.svelte +++ b/frontend/src/lib/components/sessions/ResourceEditorView.svelte @@ -6,6 +6,7 @@ path, workspaceId, onBack, + onRemoved, onRenamed }: { /** The resource this tab edits (the row its location deep-links). */ @@ -15,6 +16,10 @@ workspaceId: string /** Back to the list this editor was reached through; the tab replaced it. */ onBack?: () => void + /** The item is gone — a draft-only one whose draft was discarded. Distinct from + * `onBack`, which moves whichever tab is active: a discard can complete after + * the user has moved on, and only this tab is the one to send back. */ + onRemoved?: () => void /** The item was saved under a different path. The tab addresses it by path — * as do its label, the chat's ACTIVE PREVIEW and the draft key — so the tab * has to follow, or all four keep naming an item that no longer exists. */ @@ -38,7 +43,7 @@ useDrawer={false} workspace={workspaceId} {onBack} - onRemoved={onBack} + {onRemoved} onSaved={(saved) => { if (saved && saved !== path) onRenamed?.(saved) }} diff --git a/frontend/src/lib/components/sessions/ScheduleEditorView.svelte b/frontend/src/lib/components/sessions/ScheduleEditorView.svelte index a1c99b02da..b2fadb1f46 100644 --- a/frontend/src/lib/components/sessions/ScheduleEditorView.svelte +++ b/frontend/src/lib/components/sessions/ScheduleEditorView.svelte @@ -9,6 +9,7 @@ path, workspaceId, onBack, + onRemoved, onRenamed }: { /** The schedule this tab edits (the row its location deep-links). */ @@ -18,6 +19,10 @@ workspaceId: string /** Back to the list this editor was reached through; the tab replaced it. */ onBack?: () => void + /** The item is gone — a draft-only one whose draft was discarded. Distinct from + * `onBack`, which moves whichever tab is active: the discard awaits a reload of + * the runnable, by which time the user may be looking at another tab. */ + onRemoved?: () => void /** The item was saved under a different path. The tab addresses it by path — * as do its label, the chat's ACTIVE PREVIEW and the draft key — so the tab * has to follow, or all four keep naming an item that no longer exists. */ @@ -64,7 +69,7 @@ bind:this={editor} useDrawer={false} showDraftBanner - onRemoved={onBack} + {onRemoved} onUpdate={(saved: string | undefined) => { generation++ // A draft-only schedule opens with its path editable (saving CREATEs), diff --git a/frontend/src/lib/components/sessions/VariableEditorView.svelte b/frontend/src/lib/components/sessions/VariableEditorView.svelte index 4c5dce93da..13edc6e467 100644 --- a/frontend/src/lib/components/sessions/VariableEditorView.svelte +++ b/frontend/src/lib/components/sessions/VariableEditorView.svelte @@ -6,6 +6,7 @@ path, workspaceId, onBack, + onRemoved, onRenamed }: { /** The variable this tab edits (the row its location deep-links). */ @@ -15,6 +16,10 @@ workspaceId: string /** Back to the list this editor was reached through; the tab replaced it. */ onBack?: () => void + /** The item is gone — a draft-only one whose draft was discarded. Distinct from + * `onBack`, which moves whichever tab is active: a discard can complete after + * the user has moved on, and only this tab is the one to send back. */ + onRemoved?: () => void /** The item was saved under a different path. The tab addresses it by path — * as do its label, the chat's ACTIVE PREVIEW and the draft key — so the tab * has to follow, or all four keep naming an item that no longer exists. */ @@ -38,7 +43,7 @@ useDrawer={false} workspace={workspaceId} {onBack} - onRemoved={onBack} + {onRemoved} onSaved={(saved) => { if (saved && saved !== path) onRenamed?.(saved) }} diff --git a/frontend/src/lib/userDraft.svelte.ts b/frontend/src/lib/userDraft.svelte.ts index aff6e3c932..1fc0aafaac 100644 --- a/frontend/src/lib/userDraft.svelte.ts +++ b/frontend/src/lib/userDraft.svelte.ts @@ -130,11 +130,9 @@ const seedMisses = new Set() // Cells whose last discard removed the item itself (see `takeDraftOnlyDiscard`). const draftOnlyDiscards = new Set() -// Both sets above are read by the tool-completion listener right after the write -// that wrote them — but only while something is listening, and nothing is when the -// user is not in a session. Capped so an unread marker cannot accumulate: the -// oldest is dropped, since a marker is only ever meaningful to the action that -// immediately follows it. +// Nothing reads either set outside a session, so unread markers would accumulate. +// Capped by dropping the oldest: a marker means something only to the action that +// immediately follows the write that left it. const MAX_WRITE_MARKERS = 64 function noteMarker(set: Set, key: string): void { set.add(key)