fix: keep a staged flow path across reloads and follow moves in session tabs (#11967)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-10-23 14:47:03 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 72c6b10821
commit 6b56656bd6
6 changed files with 38 additions and 26 deletions
+10 -11
View File
@@ -135,8 +135,8 @@
let {
initialPath = $bindable(''),
/** The draft row's own path (the URL path). Unlike `initialPath`, which the
* route re-seeds from `draft_path` so the topbar shows the pending name,
* this stays where the item actually is. */
* route re-seeds from a draft-only flow's `draft_path` so the topbar shows
* the pending name, this stays where the item actually is. */
userDraftPath = '',
onTakeLatest = undefined,
pathStoreInit = undefined,
@@ -1436,18 +1436,17 @@
readFieldsRecursively(flowStore.val)
}
})
// Sync `$pathStore` from `flowStore.val.path` (which `initFlow` populates
// from the loaded flow — including the draft's rename, when there is one).
// This effect only tracks `flowStore.val.path`, so popover edits that go
// straight to `$pathStore` don't trigger it and aren't overwritten.
// Replaces the previous `$pathStore = initialPath` push (added in #2536 for
// the VSCode extension), which silently dropped any draft-renamed path
// because `initialPath` is the URL, not the loaded path.
// Sync `$pathStore` from the loaded flow: its staged `draft_path` when the
// draft has one, else its `path`. Seeding from `path` alone would make the
// effect below drop a staged move on every reload, and deploy would then
// create at a taken path. Only `flowStore.val.path` is tracked, so popover
// edits that go straight to `$pathStore` aren't overwritten.
$effect.pre(() => {
// `flowStore.val` is typed `OpenFlow` here but `initFlow` actually puts a
// `Flow` (with `path`) in it.
const p = (flowStore.val as Flow | undefined)?.path
if (p) untrack(() => ($pathStore = p))
const flow = flowStore.val as (Flow & { draft_path?: string }) | undefined
const p = flow?.path
if (p) untrack(() => ($pathStore = flow?.draft_path || p))
})
// Persist the user-typed path into the draft JSON as `draft_path`
+3 -3
View File
@@ -10,9 +10,9 @@ import type { WorkspaceItem } from './workspacePicker'
export type FlowBuilderProps = {
initialPath?: string
/** The draft row's own path (the URL path). `initialPath` is re-seeded from the
* draft's `draft_path` so the topbar shows the pending name, so it can't be used
* to resolve what is actually deployed. */
/** The draft row's own path (the URL path). For a draft-only flow `initialPath` is
* re-seeded from the draft's `draft_path` so the topbar shows the pending name, so
* it can't be used to resolve what is actually deployed. */
userDraftPath?: string
/** Moves the draft's base to the head and keeps its content; offered in the diff
* drawer while the draft is behind, and called with the version the drawer shows
@@ -131,10 +131,10 @@
onDetails={onSeeDetails}
condensedHeader={true}
customUi={{ topBar: { aiBuilder: false } }}
onDeploy={() => {
onDeploy={(e) => {
// FlowBuilder has no deploy toast and the session stays put, so toast here.
sendUserToast('Deployed')
runtime.itemDeployed(workspaceId, 'flow', path)
runtime.itemDeployed(workspaceId, 'flow', path, e.path)
}}
/>
{/snippet}
@@ -148,7 +148,15 @@
if (dp !== undefined) {
surfacedDraftPath = true
if (val.draft_path !== dp) val.draft_path = dp
} else if (surfacedDraftPath && val.draft_path !== undefined) {
} else if (
surfacedDraftPath &&
val.draft_path !== undefined &&
// Equal to the saved path, it is not stale: a deploy just moved the app
// there. Clearing it would schedule a save of this cell at the old path
// that lands after the deploy discarded it, and the moved draft row
// would come back on the new path.
val.draft_path !== cell.saved.val?.path
) {
val.draft_path = undefined
}
})
@@ -124,7 +124,7 @@
// Fires on every deploy (primary, "Deploy & Stay here", and lib — we
// ignore e.stay since the session always stays).
sendUserToast('Deployed')
runtime.itemDeployed(workspaceId, 'script', e.path)
runtime.itemDeployed(workspaceId, 'script', path, e.path)
}}
/>
{/if}
@@ -339,7 +339,6 @@
v = undefined
}
if (tok !== loadFlowToken) return
version = v
const backendFlow = await FlowService.getFlowByPath({
workspace: $workspaceStore!,
@@ -376,6 +375,9 @@
// Layer the draft (`.draft`, if any) over the deployed payload at the field
// level. See /scripts/edit's loader for the rationale.
const { draft: draftFromBackend, ...deployedFlow } = backendFlow as any
// Set together with the base below: across the await above, a new head beside
// the previous load's base reads as stale, and the stale prompt latches open.
version = v
// The flow_version the draft forked from; undefined when editing the deployed
// flow directly (no draft) or for a draft never forked from a deploy.
draftBaseVersion = backendFlow.draft_base
@@ -395,13 +397,16 @@
deployedBaseline = backendFlow.no_deployed
? undefined
: (structuredClone($state.snapshot(deployedFlow)) as Flow)
// Surface the saved `draft_path` to the Path widget so the topbar shows the
// pending name, not the `draft_{uuid}` URL. Else the widget seeds from the
// URL, the first edit clobbers `draft_path`, and the friendly name is lost.
// Otherwise the URL, set on every load: this route instance is reused when the
// editor follows its draft to a moved item, and the path it opened on is where
// deploy would look for the flow.
const renderedDraftPath = (effectiveFlow as any).draft_path as string | undefined
// A draft-only flow surfaces its saved `draft_path` to the Path widget so the
// topbar shows the pending name, not the `draft_{uuid}` URL. Else the widget
// seeds from the URL, the first edit clobbers `draft_path`, and the friendly
// name is lost. A deployed flow keeps the URL even with a staged move: it is
// where deploy updates the flow and where its triggers live (FlowBuilder shows
// the staged path). Set on every load: this route instance is reused when the
// editor follows its draft to a moved item.
const renderedDraftPath = backendFlow.no_deployed
? ((effectiveFlow as any).draft_path as string | undefined)
: undefined
flowInitialPath = renderedDraftPath || (page.params.path ?? '')
// "Load another user's draft" handoff: render their value over the deployed