From d6388514ca81df71bce7e94a7586117aa3fe1c03 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Sun, 6 Sep 2026 21:04:12 +0200 Subject: [PATCH] fix(sessions): keep "View runs" inside the session, and record released cells only where they are read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The schedule editor's "View runs" anchor navigated the top-level document, which from a session tab means leaving the session — and onto the navigation workspace, not the acting one. A host that is a page of its own now takes the destination instead and re-points its tab at the Runs page, like the way back to the list. The released-cell record is scoped to the kinds whose settle reads it back: elsewhere it retained a whole flow or app draft, or a variable's decrypted secret that the release is otherwise the end of. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 --- .../components/sessions/PreviewTabHost.svelte | 17 +++++++++++++++++ .../sessions/ScheduleEditorView.svelte | 6 +++++- .../schedules/ScheduleEditorInner.svelte | 11 +++++++++-- frontend/src/lib/userDraft.svelte.ts | 15 ++++++++------- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/components/sessions/PreviewTabHost.svelte b/frontend/src/lib/components/sessions/PreviewTabHost.svelte index 893e85e798..282815a165 100644 --- a/frontend/src/lib/components/sessions/PreviewTabHost.svelte +++ b/frontend/src/lib/components/sessions/PreviewTabHost.svelte @@ -22,6 +22,7 @@ import type { EntityEditorKind } from './previewRouter' import type { EntityToolEffect } from './previewReload' import { withMenuHidden } from './sessionMode.svelte' + import { pageHref, RUNS_PATH } from './previewPaths' import ArtifactViewer from '../copilot/chat/artifacts/ArtifactViewer.svelte' import { setOverlayHost } from '../common/overlayHost.svelte' @@ -193,6 +194,21 @@ : undefined ) + // "View runs" from a hosted editor: the Runs page is a preview page like any + // other, so it belongs in the preview rather than in the top-level document the + // anchor would navigate — and the frame gives it the acting workspace. Re-points + // this tab, like the way back to the list: the runs are where the editor was. + const viewRuns = $derived( + runtime + ? (query: string) => + runtime.previewTabs.navigate({ + type: 'page', + href: `${pageHref(RUNS_PATH)}?${query}`, + label: 'Runs' + }) + : undefined + ) + // What an editor reports is about an item, not about this tab: other warm // sessions can hold the same one, and by the time the write it awaited returns // this tab may hold something else. The page resolves both by matching on the @@ -413,6 +429,7 @@ onBack={backToList} onRemoved={(from, ws) => reportRemoved?.('trigger_schedule', from, ws)} onSavedTo={(to, from, ws) => reportSaved?.('trigger_schedule', to, from, ws)} + onViewRuns={viewRuns} /> {/await} {:else if slot.entityKind === 'resource'} diff --git a/frontend/src/lib/components/sessions/ScheduleEditorView.svelte b/frontend/src/lib/components/sessions/ScheduleEditorView.svelte index d9836e1182..b350889961 100644 --- a/frontend/src/lib/components/sessions/ScheduleEditorView.svelte +++ b/frontend/src/lib/components/sessions/ScheduleEditorView.svelte @@ -10,7 +10,8 @@ workspaceId, onBack, onRemoved, - onSavedTo + onSavedTo, + onViewRuns }: { /** The schedule this tab edits (the row its location deep-links). */ path: string @@ -29,6 +30,8 @@ * ACTIVE PREVIEW and the draft key all do — and a plain save moves none but * leaves the others holding a baseline the deploy has replaced. */ onSavedTo?: (newPath: string, fromPath: string, fromWorkspace: string) => void + /** Show this schedule's runs, given the Runs page's filter query. */ + onViewRuns?: (query: string) => void } = $props() // Captured at init, so it must read the current prop rather than close over it. @@ -70,6 +73,7 @@ useDrawer={false} showDraftBanner {onRemoved} + {onViewRuns} onUpdate={(saved: string | undefined, from: string, fromWs: string, keptEdit: boolean) => { if (from === path && !keptEdit) generation++ // A draft-only schedule opens with its path editable (saving CREATEs), so diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index e6beb46430..beaf2b5c89 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -76,7 +76,12 @@ // The schedule is gone — a draft-only one whose draft the banner discarded — // with the workspace and path it was showing. A host addressing it by those // (a session tab) has to stop showing it. - onRemoved = undefined + onRemoved = undefined, + // Where "View runs" goes, given the filter query for this schedule. A host + // mounted inside a page of its own (a session tab) takes it: the plain + // anchor navigates the whole app, which from there means leaving the session + // — and lands on the navigation workspace rather than the acting one. + onViewRuns = undefined } = $props() let optionTabSelected: @@ -893,13 +898,15 @@ > {#snippet extra()} {#if !drawerLoading && edit} + {@const runsQuery = `schedule_path=${path}&job_trigger_kind=schedule&show_future_jobs=true`}
diff --git a/frontend/src/lib/userDraft.svelte.ts b/frontend/src/lib/userDraft.svelte.ts index 7624cbf756..d7794c2b0c 100644 --- a/frontend/src/lib/userDraft.svelte.ts +++ b/frontend/src/lib/userDraft.svelte.ts @@ -143,13 +143,12 @@ function noteMarker(set: Set, key: string): void { } } /** - * What a cell held when its last holder let go. An editor stays editable while - * its save is in flight, so a tab closed in that window releases a cell that may - * carry an edit newer than the write — and once released there is no handle left - * to read it from. Same cap and reason as the marker sets; taken by whoever asks - * (`settleDraftAfterWrite`), and dropped when the key is acquired again, since a - * live entry answers for itself. + * What a cell held when its last holder let go: an editor stays editable while its + * save is in flight, so a tab left in that window releases a cell carrying an edit + * newer than the write. Only for the kinds whose settle reads it back — elsewhere + * it would retain a whole flow or app draft, or a variable's decrypted secret. */ +const RELEASE_RECORDED_KINDS: readonly UserDraftItemKind[] = ['resource', 'variable'] const releasedValues = new Map() /** Read once: what it describes is settled by the caller that reads it. */ function takeReleasedValue( @@ -1060,7 +1059,9 @@ function releaseEntry(mk: string): void { // only here, once, at refcount 0. This is what lets multiple holders (warm // session previews + the nav editor) share the entry and drop in any order. if (entry.count <= 0) { - noteReleasedValue(mk, snapshotDraftValue(entry.state.val)) + if (RELEASE_RECORDED_KINDS.includes(entry.itemKind)) { + noteReleasedValue(mk, snapshotDraftValue(entry.state.val)) + } // The live entry was authoritative while mounted; once gone, drop any // cached write for this key so a later read falls back to the server // rather than a value the editor may have changed in the meantime.