Skip to content

feat(sessions): scoped preview refresh + multi-target live editors + pipeline preview#10006

Merged
rubenfiszel merged 16 commits into
mainfrom
glm/session-multi-target
Jul 8, 2026
Merged

feat(sessions): scoped preview refresh + multi-target live editors + pipeline preview#10006
rubenfiszel merged 16 commits into
mainfrom
glm/session-multi-target

Conversation

@Guilhem-lm

@Guilhem-lm Guilhem-lm commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Three related session-preview improvements, shipped together. Supersedes #9999 (its commit is the base of this branch).

1 — Smart, scoped preview-tab refresh (was #9999)

After a chat round we used to reload every open preview tab. Now refresh is scoped to what a tool actually touched: toolReloadEffect(name, args) returns { scopes, reloadPages } (an allowlist of preview-affecting tools + the items they edited by path), and reloadTabs reloads only the matching item tabs — plus page tabs (Runs/Schedules/Resources/…) when a workspace mutation warrants it. Debounced across a round.

2 — Multi-target preview: every editable tab is its own live editor

Dropped the single session.target. A session now holds N live editors at once, one abstraction per tab: script / flow / raw_app always mount their in-process builder, never an iframe (iframes remain only for real pages — runs / schedules / home).

  • P0 — removed the dead SessionWrapper editor pane + single-target machinery; scoped raw-app reload to args.path.
  • P1 — replaced the 3 per-kind singleton stores/slots with per-(kind,path) editor cells (flowCell/scriptCell/rawAppCell); public interface preserved via a temporary shim.
  • P2 — behavioural flip: resolvePreviewTab(url) drops the target param → any editable route mounts its own editor; each editor binds its own cell; draft codecs close over the cell's store (no cross-write); tab open/navigate dedupe by (kind,path).
  • P3pruneEditorCells evicts cells no open tab references (wired to a new onTabsChanged); removed the dead warm-editor LRU.
  • P4 — retired session.target (field + setter + reads); preview hydrates from persisted tabs alone.

3 — Data-pipeline preview restored as a live editor

The P0 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 + system prompt still make it the first step of pipeline authoring. Restored inside the tab model:

  • A /pipeline/<folder> tab now mounts the in-process PipelineEditorView (never an iframe). parsePipelineRoute + resolvePreviewTab map the folder to a pipeline editor slot; previewTargetForSessionTarget('pipeline') returns the folder route (was undefined).
  • Single pipeline tab. All pipeline tabs share one runtime.pipelineEditorState, so open() keeps at most one pipeline tab and retargets it to the requested folder rather than opening a second editor that would fight over the shared state.
  • PipelineEditorView gained an active prop; AI-helper registration + the live-badge poll gate on isActiveSession && active (only the foreground tab of the foreground session).
  • Manager wiring fix. The view registers build_pipeline_node / edit_pipeline_node on runtime.manager directly, not via getAiChatManager(). PreviewTabHost mounts it in the preview panel — outside the SessionWrapper subtree that provides the scoped aiChatManager context — so a context lookup fell back to the app-wide singleton and the tools never reached the session chat (the model then fell back to write_script, whose draft never lands on the canvas). Using the runtime's own manager fixes it.

Sync with main

Merged origin/main twice:

Validation

  • npm run check0 errors.
  • 155/155 session unit tests pass (incl. the pipeline-routing and shared-tab reorder suites).
  • Browser-verified: two simultaneous live Monaco editors with distinct content; closing one leaves the other intact; no iframe fallback for items; reload hydrates editors from persisted tabs; the pipeline graph mounts in-panel (not an iframe) and a second folder retargets the single pipeline tab; build_pipeline_node adds a node live to the canvas; after the feat: shared tab system, universal markdown code blocks, subtle scrollbars #10003 merge the DraggableTabs strip drives the editor hosts and tab-switching works.

Screenshots

The Playwright screenshot tool times out on the Monaco/graph-heavy preview pane (5s MCP cap), so no static image is attached — but each behaviour above was exercised live via the accessibility tree (two editors with distinct content and no iframe fallback; the pipeline canvas with its nodes; single-tab retarget crm → crm_analytics).

🤖 Generated with Claude Code

Guilhem-lm and others added 9 commits July 8, 2026 09:23
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…r 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…story

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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploying windmill with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7e48ae6
Status: ✅  Deploy successful!
Preview URL: https://bd3a0ed9.windmill.pages.dev
Branch Preview URL: https://glm-session-multi-target.windmill.pages.dev

View logs

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) <noreply@anthropic.com>
@Guilhem-lm Guilhem-lm changed the title feat(sessions): multi-target preview — every editable tab is its own live editor feat(sessions): scoped preview refresh + multi-target live editors Jul 8, 2026
Guilhem-lm and others added 2 commits July 8, 2026 13:42
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/<folder> 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) <noreply@anthropic.com>
@Guilhem-lm Guilhem-lm changed the title feat(sessions): scoped preview refresh + multi-target live editors feat(sessions): scoped preview refresh + multi-target live editors + pipeline preview Jul 8, 2026
Guilhem-lm and others added 3 commits July 8, 2026 14:21
…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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@Guilhem-lm Guilhem-lm marked this pull request as ready for review July 8, 2026 13:17
@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Guilhem-lm's task in 5m 41s —— View job


cc @Guilhem-lm

Mergeable, but should ideally address nits: stray editor-cell allocation in deployedInSessionHandler, navigate() doesn't enforce the pipeline single-tab invariant that open() does, and one stale comment in SessionPicker.svelte.

Summary

Large, cleanly-executed refactor: drops the single session.target in favor of per-(kind,path) editor cells, scopes preview reloads to the pages a tool actually touches, and restores the pipeline preview as an in-process editor. The pure-logic surfaces (previewReload.ts, previewRouter.ts, tab model, draft codecs) are well-covered by unit tests, and the removal of session.target / setSessionTarget / editorWarmIds / hideEditor is complete — no dangling references remain in app code.

I verified the central correctness claim of part 1: every mutating global-chat tool maps correctly through toolReloadEffect. All 19 mutating tools in global/core.ts (write_schedule/trigger/resource/variable, create_folder, deploy_/delete_workspace_item, discard_local_draft, rebase_draft, and the item-editor writes) resolve to either the right list page or NO_RELOAD. Arg field names line up too — write_trigger uses kind (schema line 408) and the item tools use { type, trigger_kind } (core.ts:4395/4729/5048), matching toolReloadEffect/pagesForItemType. No tool falls through the allowlist today.

No P0/P1 issues. The findings below are all minor.

Findings

P2 — deployedInSessionHandler allocates a stray cell just to read a slot  (sessionRuntime.svelte.ts:936-942)
The deploy-notification handler now does runtime.flowCell(path).slot (and scriptCell/rawAppCell) purely to check loadedPath. Those accessors create a cell on miss, so a deploy for an item with no open editor tab allocates an empty cell that lingers until the next pruneEditorCells (next tab-set change). Harmless and self-healing, but the old slot(kind) read didn't allocate. A non-creating peek (e.g. flowCells.get(path)?.slot.loadedPath) would avoid it. Fix this →

P2 — navigate() doesn't enforce the pipeline single-tab invariant  (sessionPreviewTabs.svelte.ts:213-232)
open() keeps at most one pipeline tab (finds the existing pipeline tab and re-points it) precisely because all pipeline tabs share runtime.pipelineEditorState. navigate() only has the (kind,path) editor dedup and no equivalent pipeline branch, so re-pointing the active non-pipeline tab to a /pipeline/<folder> while another pipeline tab exists would mount two editors racing the shared state — the exact hazard the open() comment warns about. It's latent today (the three navigate() call sites only pass page targets for runs/schedules filters and item targets, never a pipeline route), so this is a defensive/symmetry nit, not a live bug.

P2 — stale comment after session.target removal  (SessionPicker.svelte:314-315, outside the diff)
The comment still says the preview "seeds from the session's editor target (or stays empty)". hydratePreviewTabs no longer seeds from a target (the field is gone), so this now only ever "stays empty". Worth a one-line correction since this PR is what made it inaccurate.

AGENTS.md compliance

  • No crypto.randomUUID() — the tab model imports randomUUID from $lib/utils/uuid. ✓
  • No $bindable(default_value) on optional props introduced. ✓
  • Comments largely follow the "record constraints, not narration" rule and state invariants once (the self-sync invariant in previewReload.ts, the shared-pipelineEditorState rationale in open()). The stale SessionPicker comment above is the one lapse.

Test coverage

Frontend pure-logic only (per policy, Svelte components aren't unit-tested here):

  • previewReload.ts — new sibling previewReload.test.ts covers the allowlist, trigger-kind mapping, item-editor NO_RELOAD, the unknown-tool guard, and tabsToReload's loc-over-url matching. ✓
  • previewRouter.ts — tests updated to drop the target param and add pipeline-route cases. ✓
  • codecs / tab modelsessionDraftCodecs.test.ts and sessionPreviewTabs.test.ts updated for the per-cell store signatures and the removed target seeding. ✓

Manual verification worth doing before merge: open two editable tabs of the same kind (two scripts) and confirm each edits its own draft with no cross-write, then close one and confirm the other's Monaco state survives (exercises the per-cell stores + pruneEditorCells). Separately, drive open_preview(kind="pipeline") for one folder then a second folder and confirm a single pipeline tab retargets rather than opening a second (the shared-state invariant), and that build_pipeline_node reaches this session's chat (the manager-wiring fix) rather than falling back to write_script. The PR body reports these were browser-verified via the accessibility tree.
 ·  branch glm/session-multi-target

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

I have now read the full diff, all modified Svelte components, and the key logic modules. Let me compile the review.

Pi Review

Good to merge

This is a well-executed, frontend-only refactor of the sessions preview system. It ships three related improvements — scoped preview-tab refresh, multi-target live editors via per-(kind,path) cells, and pipeline preview restored as a live editor tab — with thorough test coverage and no remaining dead code.

What I verified

  • No dead references. editorWarmIds, promoteEditorWarm, MAX_WARM_EDITORS, setSessionTarget, EDITOR_TARGET_KINDS, session.target, and the hideEditor prop are all fully removed with no remaining callers or imports. The grep confirms zero hits for the warm-editor LRU and for session.target as a field access.

  • Cell lifecycle is correct. pruneEditorCells is called synchronously from SessionPreviewTabs.#flush() on every open/close/navigate/reset/reorder path, which runs after the tab mutations but before Svelte processes the $state updates. That means by the time PreviewTabHost re-renders from the new tab URL, the old cell is already pruned and the new flowCell(path) / scriptCell(path) / rawAppCell(path) lazily creates a fresh cell. No dangling store references.

  • Draft codec isolation. makeFlowCodec, makeScriptCodec, and makeRawAppCodec now close over individual cell stores ({ val } objects) instead of the full SessionRuntime. Two live editors of the same kind cannot cross-write each other's drafts. The test updates in sessionDraftCodecs.test.ts confirm this — they pass a storeWith(...) stub instead of a full runtime stub.

  • Pipeline editor wiring. PipelineEditorView registers build_pipeline_node / edit_pipeline_node on runtime.manager directly rather than via getAiChatManager() context lookup. Since PreviewTabHost mounts it outside the SessionWrapper subtree (the two live in separate Splitpanes panes), a context lookup would fall back to the app-wide singleton. The runtime.manager reference correctly targets this session's chat. The $effect guards (engaged = isActiveSession && active) ensure only the foreground tab of the foreground session owns the pipeline tools and the live-badge poll.

  • Preview reload scoping. The old MUTATING_TOOL_RE regex reloaded every open tab for any mutating tool, including purely local tools like update_user_instructions. The new toolReloadEffect allowlist emits only the list-page paths (e.g. /schedules, /resources) actually affected, and live-editor/pipeline tabs are excluded from reload entirely because tabsToReload filters by page path — which never matches an item-editor route. The "live editors self-sync" invariant is stated once in previewReload.ts (the module-level doc comment) and enforced by both toolReloadEffect (no item-tool case) and tabsToReload (path-based filter). Good compliance with the AGENTS.md comments rule.

  • No $bindable(default_value) violations. All component props in the modified files use plain defaults (= true) or no defaults at all on optional props. The #key pattern in SessionEditorTarget.svelte handles remounts correctly.

  • 155/155 unit tests pass per the PR body. The test files (previewReload.test.ts, previewRouter.test.ts, sessionDraftCodecs.test.ts, sessionPreviewTabs.test.ts) were all updated to reflect the new APIs and remove dependencies on session.target.

Test coverage

  • Backend: Not touched.
  • Frontend: New pure-logic utility previewReload.ts has a dedicated previewReload.test.ts (87 lines, 6 test cases covering the allowlist, self-sync invariant, tabsToReload). The modified sessionDraftCodecs.test.ts, sessionPreviewTabs.test.ts, and previewRouter.test.ts were all updated to match the new signatures (store stubs instead of runtime stubs, target-less resolvePreviewTab, pipeline routing). Coverage at the layer changed is strong.
  • In-app verification needed: The PR author already browser-verified the key scenarios (two simultaneous Monaco editors, pipeline canvas mounting in-panel, single pipeline tab retargeting, build_pipeline_node adds a node live). No additional manual scenarios beyond what's already described are needed.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Codex Review

cc @Guilhem-lm
Should address issues before merging: draft sync stays bound to old editor cell after tab retarget

[P1] frontend/src/lib/components/sessions/SessionEditorTarget.svelte:102 - useUserDraftSync receives codec: buildCodec() only once for the component lifetime, and useUserDraftSync immediately captures that codec. After this PR, buildCodec() closes over a per-(kind,path) cell, while path can change without remounting the component via owner.navigate({ type: 'item', item }). In that case the hook tracks/saves the new path, but the codec still reads/writes the old cell’s store, so navigating one live tab from item A to item B can apply B’s inbound draft to A or save A’s contents under B’s draft key. The codec needs to follow the current cell/path or the editor target needs to remount when the path changes.

Test coverage
Frontend unit coverage was added for preview reload/router/tab utilities, but I do not see coverage for retargeting a mounted live editor tab between two scripts/flows/apps and then syncing drafts. Manual verification should cover opening a session preview editor, navigating the same tab to a second item via an in-editor link or picker, then confirming edits/chat writes affect only the second item and do not overwrite the first item’s draft.

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/<folder> 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) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Pi Review

Good to merge

I verified the three prior-review findings against the current diff:

  1. P1 from Codex — codec capture in useUserDraftSync: The code uses $derived(buildCodec()) + the thunk codec: () => codec, and useUserDraftSync calls opts.codec() inside every $effect block. When path changes, the $derived re-evaluates to a new codec bound to the new cell's store, and Svelte tracks the read through opts.codec() → the effects re-fire with the current codec. The retargeting path is correctly handled; the P1 was a false positive.

  2. P2 from Claude — stray cell allocation in deployedInSessionHandler: The handler now uses runtime.loadedEditorPath(kind, path), a non-creating peek over the per-kind Maps. The comment explicitly states "Peek without creating a cell." No allocation.

  3. P2 from Claude — navigate() missing pipeline single-tab invariant: navigate() has the exact same pipeline branch as open() (lines ~232-242): it finds the existing pipeline tab via parsePipelineRoute, retargets it, and returns early. The invariant is consistently enforced in both methods and covered by two new tests (retargets the one pipeline tab instead of turning the active tab into a second, retargets the active pipeline tab in place to a new folder).

  4. P2 from Claude — stale comment in SessionPicker.svelte: The comment now reads "so the preview starts empty until the chat opens something."

What I verified independently

  • session.target field removal is complete. SessionSchema no longer carries target. normalizeLegacySession deletes it from legacy records. The IndexedDB test asserts no restored?.target. hydratePreviewTabs no longer seeds from a target. No grep hits for setSessionTarget, editorWarmIds, promoteEditorWarm, or MAX_WARM_EDITORS.

  • Cell lifecycle is correct. pruneEditorCells runs synchronously via onTabsChanged inside #flush()before Svelte re-renders the changed tab state. By the time PreviewTabHost re-evaluates $derived(cell.slot) with the new path, the old cell is already deleted and the new one is lazily created via flowCell(path)/scriptCell(path)/rawAppCell(path). The #key slot.loadedPath in SessionEditorTarget remounts the editor snippet when the new item loads.

  • Pipeline single-tab invariant. editorTargetFor() returns undefined for page-type targets, so a /pipeline/<folder> page target never enters the editorTarget dedupe branch. The pipeline check in both open() and navigate() catches the pipeline URL, finds the single existing pipeline tab, and retargets it. No code path can create two pipeline tabs.

  • Pipeline editor wiring fix. PipelineEditorView now imports runtime.manager directly instead of calling getAiChatManager(). The $effect gates on isActiveSession && active (the engaged derived), so only the foreground tab of the foreground session registers the pipeline tools and runs the live-badge poll.

  • No banned patterns. No crypto.randomUUID() (the tab model imports randomUUID from $lib/utils/uuid). No $bindable(default_value) on optional props — the new active prop uses active = true (plain default).

Test coverage

  • Frontend: New previewReload.test.ts (87 lines, 6 cases covering the tool-to-page allowlist, self-sync invariant, and tabsToReload). previewRouter.test.ts updated to test pipeline routing and the target-less resolvePreviewTab. sessionDraftCodecs.test.ts updated with store-stub helpers. sessionPreviewTabs.test.ts updated with pipeline open()/navigate() tests and target-less assertions. sessionStateIndexedDb.test.ts asserts no restored?.target. No backend changes.

  • Manual verification needed: None beyond what's described in the PR body (155/155 unit tests pass; browser-verified multi-target editors, pipeline retarget, build_pipeline_node live-canvas updates, and DraggableTabs integration).

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Codex Review

Good to merge

No issues found. Checked for bugs, regressions against the prior Codex finding, and AGENTS.md compliance. The previous draft-sync retargeting issue is addressed in this head: SessionEditorTarget now derives the codec from the current path, and useUserDraftSync re-reads it instead of capturing the old editor cell.

Test coverage

Frontend-only diff. The changed pure logic has focused tests for preview reload scoping, preview routing, tab-model behavior including pipeline single-tab retargeting, draft codec store isolation, and session persistence. I did not run the suite locally during this read-only review.

Manual verification before merge should still exercise the actual session UI: keep two live editor tabs open for different items, retarget one via breadcrumb/picker, and confirm draft writes stay scoped to the newly selected item. Also verify a pipeline preview mounts in-panel, retargets to a second folder without opening a second pipeline tab, and the pipeline AI tools update the visible canvas.

@rubenfiszel rubenfiszel merged commit 32c398f into main Jul 8, 2026
6 checks passed
@rubenfiszel rubenfiszel deleted the glm/session-multi-target branch July 8, 2026 14:34
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants