From 32c398f27de8cd5b1478ef60d247c13705b6b50f Mon Sep 17 00:00:00 2001 From: Guilhem Date: Wed, 8 Jul 2026 16:34:13 +0200 Subject: [PATCH] feat(sessions): scoped preview refresh + multi-target live editors + pipeline preview (#10006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(sessions): scope preview-tab refresh to items a chat tool touched Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(sessions): drop dead editor pane, scope raw-app reload by path Multi-target migration P0. SessionWrapper's inline editor pane was dead (the sessions page always mounts it with hideEditor); remove it and the single-target machinery (setSessionTarget/pickEditorTarget/target-keyed editor views). Scope the raw-app file/runnable preview reload to args.path (the app's workspace path) instead of the session target. Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(sessions): back editor state with per-(kind,path) cells Multi-target migration P1. Replace the three per-kind singleton stores/slots with per-(kind,path) cell maps, created on demand and kept (eviction deferred to P3). The runtime's public interface is unchanged: the flowStore/scriptStore/savedScript/rawApp/... getters and slot(kind) now forward to the 'active cell' per kind (a single-target shim, tracked by activePath, removed in P2 when the UI mounts one editor per tab). loadFlow/loadScript/loadRawApp and syncPreviewWithDeployed operate on the resolved cell; load logic and semantics are otherwise unchanged, so loading one item no longer clobbers another's state. Co-Authored-By: Claude Opus 4.8 (1M context) * feat(sessions): mount every editable preview tab as its own live editor Multi-target migration P2 — the behavioral flip. resolvePreviewTab no longer takes a target: any editable route (script/flow/raw_app) resolves to an in-process editor, so several items are live at once (iframes remain only for real pages and regular non-raw apps). Each editor binds its own per-(kind,path) cell; the draft codecs close over that cell's store so two editors never cross-write. The single-target shim (activePath + the flowStore/scriptStore/... getters + slot(kind)) is removed; runtime exposes flowCell/scriptCell/rawAppCell(path). Tab open/navigate dedupe by (kind,path) and no longer setTarget. setLiveEditorDraft is gated on the visible tab (isActiveTab) so N editors don't clobber the one-per-(workspace,kind) live-draft slot (path re-key deferred to P4). Co-Authored-By: Claude Opus 4.8 (1M context) * perf(sessions): evict unreferenced editor cells; drop dead warm-editor LRU Multi-target migration P3. Bound the per-(kind,path) editor cell maps: pruneEditorCells drops every cell no open preview tab still references, wired to a new onTabsChanged adapter callback fired on each tab-set change — so closing or navigating a tab away from an item reclaims its cell (dedupe keeps <=1 editor tab per item, so a pruned item has no live editor to strand). Also remove the now-dead editorWarmIds/promoteEditorWarm/MAX_WARM_EDITORS warm-editor LRU: its only reader (SessionWrapper.mountEditor) was removed in P0, and mounted editors are already capped per-tab by mountedTabKeys. Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(sessions): retire session.target; preview is fully tab-driven Multi-target migration P4 (final). Remove the session.target field and setSessionTarget: the preview is driven entirely by the tab model now (P2). hydratePreviewTabs no longer seeds a tab from target (saved previewTabs only); openEditorInSession seeds the preview via resetSessionPreviewTabs; normalizeLegacySession drops the retired target field from old records. The setLiveEditorDraft focus gate (isActiveTab, one-per-(workspace,kind)) is kept as-is; a per-path re-key is a possible future refinement, not needed for correctness. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(sessions): describe editor cells as-is, not by their refactor history Address standards review: AGENTS.md requires comments describe the code as it is, not its drafting history. Drop the 'used to be per-kind singletons' / 'pre-refactor empty editor' / 'now' phrasings from the cell comments. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(sessions): update stale runtime.rawApp.val comments to cell.store Address spec review: two comments still referenced the removed runtime.rawApp.val accessor; the live code uses the per-cell store now. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(sessions): fix editor-cell comments after main merge Main's #9993 added svelte-ignore comments describing the old runtime.savedFlow.val / runtime.rawApp.val singleton bindings. The multi-target refactor binds each tab's own editor cell (cell.store / cell.saved), so update the comment text to match; the ownership_invalid_binding directives themselves remain correct (the targets are still runtime-owned). Co-Authored-By: Claude Opus 4.8 (1M context) * feat(sessions): restore data-pipeline preview as a live editor tab The multi-target refactor removed the old single-target editor pane — PipelineEditorView's only mount point — so open_preview(kind="pipeline") opened nothing, even though the chat tool and system prompt still make it the first step of pipeline authoring. Route a /pipeline/ preview tab to the in-process graph editor: - previewRouter: parsePipelineRoute + resolvePreviewTab map the folder to a pipeline editor slot; PreviewSlot.editorKind gains 'pipeline'. - previewTargetForSessionTarget('pipeline') returns the folder route target (was undefined); open() keeps a single pipeline tab and retargets it to the requested folder, since all pipeline tabs share one runtime.pipelineEditorState. - PreviewTabHost mounts PipelineEditorView for the pipeline slot. - PipelineEditorView gains an `active` prop; AI-helper registration and the live-badge poll now gate on isActiveSession && active. Register the pipeline tools on the session's own chat, not the singleton: PreviewTabHost mounts the view outside the SessionWrapper subtree that provides the scoped aiChatManager context, so getAiChatManager() fell back to the app-wide singleton — build_pipeline_node / edit_pipeline_node never reached the session chat and the model fell back to write_script (whose draft never appears on the canvas). Use runtime.manager directly instead. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(sessions): scope list-page preview refresh to the page each tool changes The scoped-refresh pass reloaded every open list-page preview tab on any workspace mutation (reloadPages: boolean), so creating a schedule also refreshed the Resources / Variables tabs. Replace the blanket flag with the specific page paths each tool can change: write_schedule → /schedules, write_resource → /resources, write_variable → /variables, create_folder → /folders, write_trigger → the trigger kind's page; delete/deploy/discard/rebase map their `type` to its page (none for script/flow/app). Item-editor writes now reload no pages — their live editor self-syncs. reloadTabs refreshes a list-page tab only when its own path is in the touched set. Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(sessions): drop the inert item-reload path; extract a tested previewReload module Post multi-target, every editable item is a live editor whose reload() no-ops, and the one iframe item kind (legacy drag-drop apps) is never emitted as a scope — so the whole `scopes` half of the preview-reload machinery could never fire. Remove it (PreviewKind, PreviewScope, scopeKey, itemTypeToPreviewKind, pendingScopes, and the item-route branch of reloadTabs); the `pages` path already covers every real reload. Lift the surviving pure logic out of the 900-line route component into previewReload.ts — toolReloadEffect(name,args) -> {pages} and a new tabsToReload(tabs,pages) mirroring selectPreviewTabsToClose — and cover it with previewReload.test.ts (per-tool page mapping, item kinds reload nothing, the unknown/local-tool silent-stale guard, loc-over-url matching). Also clear session.target leftovers: delete the unread EDITOR_TARGET_KINDS export and rewrite five comments that still described the removed single-target pane / target-record write. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(sessions): state the preview-reload self-sync invariant once Consolidate the "live editors self-sync, only list pages reload" rationale to previewReload.ts and drop the drafting-history phrasings the review flagged: the update_user_instructions incident and the "(not the runtime)" contrast in sessionDraftCodecs. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(sessions): follow the editor cell when a live tab retargets Address PR review findings on the multi-target preview. P1 (Codex) — draft sync stayed bound to the old cell after an in-place tab retarget. useUserDraftSync captured `codec` once, but navigate() re-points a live editor tab (script/flow/raw_app) to another item without remounting, so path/workspace/ready followed the new item while the codec still read/wrote the previous cell's store — cross-writing drafts. Make `codec` a reactive getter like the hook's other inputs; SessionEditorTarget rebuilds it per path. P2 (Claude) — navigate() now enforces the single-pipeline-tab invariant that open() does: retargeting to a /pipeline/ route focuses and re-points the existing pipeline tab instead of turning the active tab into a second editor racing the shared pipelineEditorState. P2 (Claude) — the deploy-in-session handler peeked an editor slot via the create-on-miss cell accessors, allocating an empty cell for items with no open tab. Add a non-creating runtime.loadedEditorPath(kind, path) and use it. P2 (Claude) — correct a SessionPicker comment left stale by the session.target removal (the preview no longer seeds from a target). Tests: two navigate() pipeline-invariant cases. npm run check 0 errors; 167 session unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../components/sessions/FlowEditorView.svelte | 31 +- .../sessions/PipelineEditorView.svelte | 43 +- .../components/sessions/PreviewTabHost.svelte | 31 +- .../sessions/RawAppEditorView.svelte | 41 +- .../sessions/ScriptEditorView.svelte | 24 +- .../sessions/SessionEditorTarget.svelte | 44 +- .../components/sessions/SessionPicker.svelte | 4 +- .../components/sessions/SessionWrapper.svelte | 170 +------- .../lib/components/sessions/appDraftCodec.ts | 6 +- .../components/sessions/previewReload.test.ts | 87 ++++ .../lib/components/sessions/previewReload.ts | 75 ++++ .../components/sessions/previewRouter.test.ts | 41 +- .../lib/components/sessions/previewRouter.ts | 34 +- .../sessions/sessionDraftCodecs.test.ts | 15 +- .../components/sessions/sessionDraftCodecs.ts | 36 +- .../sessions/sessionPreviewTabs.svelte.ts | 134 ++++--- .../sessions/sessionPreviewTabs.test.ts | 127 +++--- .../sessions/sessionRuntime.svelte.ts | 379 ++++++++++-------- .../sessions/sessionState.svelte.ts | 40 +- .../sessions/sessionStateIndexedDb.test.ts | 2 - .../sessions/sessionSwitch.svelte.ts | 17 +- .../sessions/useUserDraftSync.svelte.ts | 18 +- .../(root)/(logged)/sessions/+page.svelte | 78 ++-- 23 files changed, 789 insertions(+), 688 deletions(-) create mode 100644 frontend/src/lib/components/sessions/previewReload.test.ts create mode 100644 frontend/src/lib/components/sessions/previewReload.ts diff --git a/frontend/src/lib/components/sessions/FlowEditorView.svelte b/frontend/src/lib/components/sessions/FlowEditorView.svelte index 45163cc416..fe1ae268d7 100644 --- a/frontend/src/lib/components/sessions/FlowEditorView.svelte +++ b/frontend/src/lib/components/sessions/FlowEditorView.svelte @@ -15,7 +15,8 @@ path, workspaceId, onNavigate, - isActiveSession = true + isActiveSession = true, + active = true }: { runtime: SessionRuntime path: string @@ -24,8 +25,12 @@ /** Forwarded to SessionEditorTarget — only the visible session claims the * workspace's single live-editor slot. */ isActiveSession?: boolean + /** Whether this is the visible preview tab (forwarded as isActiveTab). */ + active?: boolean } = $props() + // This tab's own flow cell; each open flow editor binds its own store. + const cell = $derived(runtime.flowCell(path)) let selectedId = $state('settings-metadata') let diffDrawer: DiffDrawer | undefined = $state() @@ -35,7 +40,7 @@ // baseline — useUserDraftSync's inbound effect then syncs the editor preview. // Mirrors ScriptEditorView. async function restoreDeployed() { - const saved = runtime.savedFlow.val + const saved = cell.saved.val if (!saved) { sendUserToast('Could not restore to deployed', true) return @@ -61,7 +66,7 @@ } -{#if runtime.savedFlow.val} +{#if cell.saved.val} {/if} runtime.flowStore.val?.path ?? path} + isActiveTab={active} + effectivePath={() => cell.store.val?.path ?? path} > {#snippet editor()} - +