From 2583569150196936a5ff238d13dc9eda7323db57 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Sun, 6 Sep 2026 15:47:24 +0200 Subject: [PATCH] fix(schedules): take the deployed enabled state from the deployed config alone `loadScheduleCfg` applies the draft overlay as well as the deployed config, so recording the accepted `enabled` there let a draft's own value pass for one the server had taken: saving then settled the form back to it, and a schedule the user had switched on came out off, clean, with nothing said. It is read where the deployed config is the one on the form, and reset for a schedule that has none yet. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 --- .../src/lib/components/ResourceEditorDrawer.svelte | 3 +-- .../triggers/schedules/ScheduleEditorInner.svelte | 8 ++++++-- .../routes/(root)/(logged)/sessions/+page.svelte | 14 +++++++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/components/ResourceEditorDrawer.svelte b/frontend/src/lib/components/ResourceEditorDrawer.svelte index 639ec91b2b..5718811ce7 100644 --- a/frontend/src/lib/components/ResourceEditorDrawer.svelte +++ b/frontend/src/lib/components/ResourceEditorDrawer.svelte @@ -32,8 +32,7 @@ disableChatOffset?: boolean /** A version was restored, at this path and in the workspace the history was * pointed at — which `WsSpecificVersions` can make a linked one rather than the - * acting workspace. Both reported rather than left for the caller to read off - * its own live state, which the restore's await has already outlived. */ + * acting workspace. */ onRestored?: (workspace: string, path: string) => void /** Fires after Save has written, with the path it wrote to — which is not the * one it was opened on when the user renamed it — and the workspace and path diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index de78ebb6d9..e38255439a 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -198,8 +198,11 @@ draftOnly = noDeployed if (!defaultCfg) { // Form holds DEPLOYED here; capture it as `initialConfig` so the - // dirty check / banner fires whenever a saved draft exists. + // dirty check / banner fires whenever a saved draft exists. This is also + // the only point where the deployed `enabled` is on the form — the draft + // overlay below can carry one of its own, which the server has not taken. initialConfig = structuredClone($state.snapshot(getScheduleCfg())) + deployedEnabled = enabled } if (draftOverlay) await loadScheduleCfg(draftOverlay) await draftSync.maybeRestore() @@ -353,6 +356,8 @@ runnable = undefined edit = false draftOnly = false + // Nothing deployed yet, so nothing accepted. + deployedEnabled = false // No deployed baseline for a brand-new schedule. The editor instance // is reused across open() calls, so clear any baseline left by a prior // openEdit — otherwise the "unsaved changes" banner / dirty check would @@ -549,7 +554,6 @@ initialCronVersion = cronVersion isLatestCron = cronVersion == 'v2' enabled = cfg.enabled - deployedEnabled = cfg.enabled schedule = cfg.schedule initialSchedule = schedule timezone = cfg.timezone diff --git a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte index 56ed27870b..7e08c93f19 100644 --- a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte @@ -572,8 +572,12 @@ const tabHosts: Record = {} let reloadHandle: ReturnType | undefined - // Base-stripped list-page paths (e.g. `/schedules`) a chat round touched since - // the last flush — see toolReloadEffect for how tools map to pages. + /** A list page in the workspace it was touched in — see `queuePageReload`. */ + function pageReloadKey(workspace: string, page: string): string { + return `${workspace}\u0000${page}` + } + // Pages touched since the last flush, by whatever touched them: a chat round + // (see toolReloadEffect), a hosted editor's write, a draft landing after one. let pendingPages = new Set() // The same round's mutations, kept whole rather than folded into one verdict: // a hosted entity editor is only affected by a mutation to its own item, in @@ -590,7 +594,7 @@ const workspace = getEffectiveWorkspaceId(s) // The queue names the workspace each page was touched in; this session only // cares about the ones touched in its own. - const prefix = `${workspace}\u0000` + const prefix = pageReloadKey(workspace ?? '', '') const mine = new Set( [...pages].filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length)) ) @@ -668,7 +672,7 @@ // frame its scroll and everything else it holds. function queuePageReload(workspace: string | undefined, page: string) { if (!workspace) return - pendingPages.add(`${workspace}\u0000${page}`) + pendingPages.add(pageReloadKey(workspace, page)) clearTimeout(reloadHandle) reloadHandle = setTimeout(flushReload, 500) } @@ -695,7 +699,7 @@ setToolCompletionListener((name, args, workspace) => { const { pages, entity, path } = toolReloadEffect(name, args) if (pages.length === 0) return - for (const p of pages) pendingPages.add(`${workspace}\u0000${p}`) + for (const p of pages) pendingPages.add(pageReloadKey(workspace, p)) // A write reaches a hosted editor through the draft cell it holds, but an // 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