diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte index 59644af31e..28bd90a5ff 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte @@ -143,7 +143,8 @@ writes: { kind: AssetWithAltAccessType['kind']; path: string }[] // Body-inferred reads, so the parent's draft keeps its input // lineage while inactive (no live inference runs for it). - reads: { kind: AssetWithAltAccessType['kind']; path: string }[] + // Undefined when nothing was inferred: keep what the draft has. + reads?: { kind: AssetWithAltAccessType['kind']; path: string }[] // Full edited script — set when the source is a persisted // script (needed to seed a brand-new draft entry). script?: Script @@ -303,7 +304,13 @@ // its editor. `onRunInputs` runs it with them, drafts included, and // `onShowScript` leaves this view for the script. inputsOnly?: boolean - onRunInputs?: (path: string, args: Record) => Promise + /** `content` is the pane's live buffer, newer than the stored draft when it was + * edited before switching to the inputs view. */ + onRunInputs?: ( + path: string, + args: Record, + content?: string + ) => Promise onShowScript?: () => void } let { @@ -518,6 +525,15 @@ // alongside `liveBodyAssets` and forwarded so the live graph can show // inferred column lineage on the edited script before it deploys. let liveColumnLineage = $state(undefined) + // The script `liveBodyAssets` was inferred for: they stay bound after its editor + // unmounts, so a script with no editor of its own (the inputs-only view) must + // not report or persist them as its lineage. Not reset on a switch: the + // previous script's save-on-switch cleanup still reads its own. + let liveAssetsOwner: string | undefined = undefined + $effect.pre(() => { + const assets = liveBodyAssets + untrack(() => (liveAssetsOwner = assets === undefined ? undefined : script?.path)) + }) // Bumped when the runs panel reports a watched job has reached a // terminal state. Drives S3FilePreview's refreshKey so the preview @@ -633,18 +649,25 @@ b: Array<{ kind: string; path: string }> ) => a.length === b.length && a.every((x, i) => x.kind === b[i]?.kind && x.path === b[i]?.path) return () => { - const writes = (liveBodyAssets ?? []) + // No editor inferred this script (the inputs-only view): keep its stored + // lineage rather than persisting it as empty. + const inferred = liveBodyAssets !== undefined && liveAssetsOwner === captured.path + const writes = !inferred + ? (writesAtRegister ?? []) + : (liveBodyAssets ?? []) .filter((a) => { const t = a.access_type ?? a.alt_access_type return t === 'w' || t === 'rw' }) .map((a) => ({ kind: a.kind, path: a.path })) - const reads = (liveBodyAssets ?? []) - .filter((a) => { - const t = a.access_type ?? a.alt_access_type - return t === 'r' || t === 'rw' - }) - .map((a) => ({ kind: a.kind, path: a.path })) + const reads = !inferred + ? readsAtRegister + : (liveBodyAssets ?? []) + .filter((a) => { + const t = a.access_type ?? a.alt_access_type + return t === 'r' || t === 'rw' + }) + .map((a) => ({ kind: a.kind, path: a.path })) // Draft runs: emit only when content or lineage changed in THIS clone. // An unconditional emit ping-pongs forever when the entry is rewritten // externally while the pane stays mounted (rename rekey, AI edit): @@ -654,6 +677,7 @@ (captured.content ?? '') === contentAtRegister && refsEq(writesAtRegister ?? [], writes) && readsAtRegister != undefined && + reads != undefined && refsEq(readsAtRegister, reads) ) return @@ -694,6 +718,25 @@ // variable: only read inside the untracked cleanup). let deployedFromPane: { path: string; content: string } | undefined = undefined let isDraft = $derived(draftScript != undefined) + // A draft's stored schema is empty until an editor infers it, and the inputs + // view mounts none: infer from the body here so its fields (the file picker) + // show up. + const inputsSchema = resource( + [() => (inputsOnly && isDraft ? script?.content : undefined), () => script?.language], + async ([content, language]) => { + if (content === undefined || !language) return undefined + const schema = emptySchema() + try { + await inferArgs(language, content, schema as Schema) + } catch { + return undefined + } + return schema + } + ) + let inputsScript = $derived( + script && inputsSchema.current ? { ...script, schema: inputsSchema.current } : undefined + ) // Single trash-bin button opens one modal that exposes both Archive // (always available) and Delete permanently (admin-only). Archive is @@ -800,7 +843,7 @@ onAnnotationsChange?.(script?.path, liveAnnotations) }) $effect(() => { - if (readOnly) return + if (readOnly || (inputsOnly && liveAssetsOwner !== script?.path)) return onAssetsChange?.(script?.path, liveBodyAssets ?? [], liveColumnLineage) }) $effect(() => { @@ -1348,13 +1391,13 @@ {:else if script && inputsOnly} {#key script.path} onRunInputs(path, args, script?.content))} + script={inputsScript ?? script} {runsRefreshKey} {runsPendingJobId} initialArgs={runFormInitialArgs} diff --git a/frontend/src/lib/components/assets/AssetGraph/PipelineGraphEditor.svelte b/frontend/src/lib/components/assets/AssetGraph/PipelineGraphEditor.svelte index 95c5a435f3..061a939808 100644 --- a/frontend/src/lib/components/assets/AssetGraph/PipelineGraphEditor.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/PipelineGraphEditor.svelte @@ -218,7 +218,11 @@ onRunFormArgsChange?: (path: string, args: Record, isValid: boolean) => void /** See AssetGraphDetailsPane's `inputsOnly`. */ inputsOnly?: boolean - onRunInputs?: (path: string, args: Record) => Promise + onRunInputs?: ( + path: string, + args: Record, + content?: string + ) => Promise onShowScript?: () => void // Data-upload entry scripts whose staged upload is ready (green node). readyDataUploadPaths?: Set diff --git a/frontend/src/lib/components/sessions/PipelineEditorView.svelte b/frontend/src/lib/components/sessions/PipelineEditorView.svelte index 61612f8191..695613b7f3 100644 --- a/frontend/src/lib/components/sessions/PipelineEditorView.svelte +++ b/frontend/src/lib/components/sessions/PipelineEditorView.svelte @@ -263,10 +263,12 @@ let runsRefreshKey = $state(0) let runsPendingJobId = $state(undefined) + // `content` overrides the stored draft body with a newer one (an open editor's). async function runNode( nodePath: string, args: Record = {}, - cascade = false + cascade = false, + content?: string ): Promise { const draft = pe.drafts.get(nodePath) activeRunnables.arm(`script:${nodePath}`) @@ -278,7 +280,7 @@ workspace: workspaceId, requestBody: { path: nodePath, - content: draft.script.content, + content: content ?? draft.script.content, language: draft.script.language, args } @@ -511,7 +513,7 @@ onRunByPath={(path, args) => runNode(path, args)} inputsOnly={uploadInputsPath !== undefined && (pe.openScriptPath === undefined || uploadInputsPath === pe.openScriptPath)} - onRunInputs={(path, args) => runNode(path, args)} + onRunInputs={(path, args, content) => runNode(path, args, false, content)} onShowScript={() => (uploadInputsPath = undefined)} canRunByPath onTestStateChange={(running) => { diff --git a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte index 9795f4396a..1203ca3f6e 100644 --- a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte @@ -2504,19 +2504,23 @@ let uploadInputsPath = $state(undefined) // Runs the open node with its form inputs: a draft previews its own content, a // deployed script runs for real (in view mode, cascading like any legit run). + // `content` is the pane's live buffer, newer than the stored draft when it was + // edited before switching to the inputs view. async function runUploadInputs( path: string, - args: Record + args: Record, + content?: string ): Promise { const draft = mode === 'edit' ? pe.drafts.get(path) : undefined if (!draft) return mode === 'edit' ? runDeployedWithArgs(path, args) : runByPathLegit(path, args) - if (!$workspaceStore || !draft.script.content || !draft.script.language) return undefined + const body = content ?? draft.script.content + if (!$workspaceStore || !body || !draft.script.language) return undefined activeRunnables.arm(`script:${path}`) try { const jobId = await JobService.runScriptPreview({ workspace: $workspaceStore, requestBody: { - content: draft.script.content, + content: body, language: draft.script.language, path, args: { ...args, _wmill_skip_asset_dispatch: true }