mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 00:06:06 +00:00
* fix(frontend): keep nested template literals intact in template inputs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: fail a flow step with an unresolvable $args tag instead of hanging Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): surface input expression errors when running a step test Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): treat an escaped \${ as literal text when escaping backticks Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: accept the string "null" as a tag component, reject only JSON null Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: leave a same_worker step's inert tag alone, log an unresolved flow tag Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): escape every backtick when the template walk desynchronizes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: leave a dedicated runnable's inert step tag alone Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: reroute a step only when its own tag is what failed to resolve Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: name the inert-tag guard step_is_pulled_by_tag Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: reject a tag only when it interpolates to nothing at all Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): validate the template walk instead of trusting a balanced stack Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: describe what an unresolvable tag actually interpolates to Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(frontend): decide template escaping with a real parser, not a hand-rolled scan Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: name is_flow_step on push now that it is load-bearing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): heal an expression escaped before nested templates were handled Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: reroute a step whose tag reads args that failed to evaluate Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: never hand a job that failed before running to a dedicated runner Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: reroute only a step whose args failed, leave other tags untouched Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: drop the post-preprocessor tag fallback, leaving tag resolution untouched Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: leave interpolate_args exactly as it was Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: use a generic example in the template literal tests Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): show an expression escaped by the old rule as it was authored Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): surface input expression errors from every step-run entry point Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: state what is_dedicated_worker actually reads Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): heal only text whose backticks were all escaped by the old rule Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): match the old rule textually so an authored backslash still heals Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(frontend): heal only expressions the old rule broke, never ones that parse Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
254 lines
7.0 KiB
Svelte
254 lines
7.0 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
ScriptService,
|
|
type AiAgent,
|
|
type FlowModule,
|
|
type JavascriptTransform,
|
|
type Job
|
|
} from '$lib/gen'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import { getScriptByPath } from '$lib/scripts'
|
|
import { getContext, untrack } from 'svelte'
|
|
import type { FlowEditorContext } from './flows/types'
|
|
import JobLoader, { type Callbacks } from './JobLoader.svelte'
|
|
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
|
|
import { loadSchemaFromModule } from './flows/flowInfers'
|
|
|
|
interface Props {
|
|
mod: FlowModule
|
|
testJob?: Job | undefined
|
|
testIsLoading?: boolean
|
|
noEditor?: boolean
|
|
scriptProgress?: any
|
|
onJobDone?: () => void
|
|
}
|
|
|
|
let {
|
|
mod,
|
|
testJob = $bindable(undefined),
|
|
testIsLoading = $bindable(false),
|
|
noEditor = false,
|
|
scriptProgress = $bindable(undefined),
|
|
onJobDone
|
|
}: Props = $props()
|
|
|
|
const {
|
|
flowStore,
|
|
flowStateStore,
|
|
pathStore,
|
|
stepsInputArgs,
|
|
previewArgs,
|
|
modulesTestStates,
|
|
devTempScriptRefs,
|
|
opWorkspace
|
|
} = getContext<FlowEditorContext>('FlowEditorContext')
|
|
|
|
// Acting workspace when the flow editor runs in an AI session; else the nav workspace.
|
|
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
|
|
|
|
let jobLoader: JobLoader | undefined = $state(undefined)
|
|
let jobProgressReset: () => void = () => {}
|
|
let stepHistoryLoader = getStepHistoryLoaderContext()
|
|
|
|
// Every explicit run re-evaluates the args with errors surfaced. The reactive evaluations
|
|
// that follow each flow edit stay quiet, so without this a failing expression is silently
|
|
// `undefined` in what the run is built from. Manually edited args are preserved across the
|
|
// refresh by `initializeFromSchema`.
|
|
export function runTestWithStepArgs() {
|
|
stepsInputArgs?.updateStepArgs(
|
|
mod.id,
|
|
flowStateStore.val,
|
|
flowStore?.val,
|
|
previewArgs?.val,
|
|
true
|
|
)
|
|
runTest(stepsInputArgs.getStepArgs(mod.id))
|
|
}
|
|
|
|
// A step's timeout is an InputTransform. Only a static numeric value can be applied
|
|
// to a single-step preview; dynamic expressions are evaluated server-side and only
|
|
// take effect when running the full flow.
|
|
function staticTimeout(timeout: FlowModule['timeout']): number | undefined {
|
|
if (timeout?.type === 'static' && typeof timeout.value === 'number') {
|
|
return timeout.value
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export async function runTest(args: any) {
|
|
// Not defined if JobProgressBar not loaded
|
|
if (jobProgressReset) jobProgressReset()
|
|
if (modulesTestStates.states[mod.id]) {
|
|
modulesTestStates.states[mod.id].cancel = async () => {
|
|
await jobLoader?.cancelJob()
|
|
modulesTestStates.states[mod.id].testJob = undefined
|
|
}
|
|
modulesTestStates.runTestCb?.(mod.id)
|
|
}
|
|
|
|
const val = mod.value
|
|
const timeout = staticTimeout(mod.timeout)
|
|
// let jobId: string | undefined = undefined
|
|
let callbacks: Callbacks = {
|
|
done: (x) => {
|
|
jobDone(x)
|
|
}
|
|
}
|
|
if (val.type == 'rawscript') {
|
|
await jobLoader?.runPreview(
|
|
val.path ?? ($pathStore ?? '') + '/' + mod.id,
|
|
val.content,
|
|
val.language,
|
|
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
|
|
flowStore?.val?.tag ?? val.tag,
|
|
undefined,
|
|
undefined,
|
|
callbacks,
|
|
$pathStore,
|
|
undefined,
|
|
devTempScriptRefs?.(),
|
|
timeout
|
|
)
|
|
} else if (val.type == 'script') {
|
|
const script = val.hash
|
|
? await ScriptService.getScriptByHash({ workspace: opWs!, hash: val.hash })
|
|
: await getScriptByPath(val.path, opWs)
|
|
await jobLoader?.runPreview(
|
|
val.path,
|
|
script.content,
|
|
script.language,
|
|
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
|
|
flowStore?.val?.tag ?? (val.tag_override ? val.tag_override : script.tag),
|
|
script.lock,
|
|
val.hash ?? script.hash,
|
|
callbacks,
|
|
$pathStore,
|
|
undefined,
|
|
undefined,
|
|
timeout
|
|
)
|
|
} else if (val.type == 'flow') {
|
|
await jobLoader?.runFlowByPath(val.path, args, callbacks)
|
|
} else if (val.type == 'aiagent') {
|
|
const { schema } = await loadSchemaFromModule(mod, opWs)
|
|
|
|
const inputTransforms: { [key: string]: JavascriptTransform } = Object.fromEntries(
|
|
Object.keys(args).map((key) => [
|
|
key,
|
|
{
|
|
expr: `flow_input.${key}`,
|
|
type: 'javascript'
|
|
}
|
|
])
|
|
)
|
|
|
|
const agentVal = val
|
|
|
|
await jobLoader?.runFlowPreview(
|
|
args,
|
|
{
|
|
value: {
|
|
modules: [
|
|
{
|
|
id: mod.id,
|
|
// A linked step has no tools of its own: the resource's tools are resolved
|
|
// server-side from `agent`. `tool_inputs` goes in either way — a step forked
|
|
// for editing has no `agent` yet still carries the flow's bindings, which the
|
|
// runtime overlays, so the preview must test against them too.
|
|
value: {
|
|
type: 'aiagent',
|
|
...(agentVal.agent ? { agent: agentVal.agent } : { tools: agentVal.tools ?? [] }),
|
|
tool_inputs: agentVal.tool_inputs,
|
|
input_transforms: inputTransforms as AiAgent['input_transforms']
|
|
} as Extract<FlowModule['value'], { type: 'aiagent' }>
|
|
}
|
|
]
|
|
},
|
|
summary: '',
|
|
schema
|
|
},
|
|
callbacks,
|
|
$pathStore
|
|
)
|
|
} else {
|
|
throw Error('Not supported module type')
|
|
}
|
|
}
|
|
|
|
function jobDone(testJob: Job & { result?: any }) {
|
|
if (testJob && !testJob.canceled && testJob.type == 'CompletedJob') {
|
|
if (flowStateStore.val[mod.id]) {
|
|
flowStateStore.val[mod.id] = {
|
|
...flowStateStore.val[mod.id],
|
|
previewResult: testJob.result,
|
|
previewSuccess: testJob.success,
|
|
previewJobId: testJob.id,
|
|
previewLogs: testJob['logs']
|
|
}
|
|
}
|
|
stepHistoryLoader?.resetInitial(mod.id)
|
|
}
|
|
if (modulesTestStates.states[mod.id]) {
|
|
modulesTestStates.states[mod.id].testJob = testJob
|
|
}
|
|
onJobDone?.()
|
|
}
|
|
|
|
export function cancelJob() {
|
|
modulesTestStates.states[mod.id]?.cancel?.()
|
|
}
|
|
|
|
$effect(() => {
|
|
// Update testIsLoading to read the state from parent components
|
|
testIsLoading = modulesTestStates.states?.[mod.id]?.loading ?? false
|
|
})
|
|
|
|
$effect(() => {
|
|
// Update testJob to read the state from parent components
|
|
testJob = modulesTestStates.states?.[mod.id]?.testJob
|
|
})
|
|
|
|
modulesTestStates.states[untrack(() => mod).id] = {
|
|
...(modulesTestStates.states?.[untrack(() => mod).id] ?? { loading: false }),
|
|
loading: testIsLoading,
|
|
testJob: testJob
|
|
}
|
|
</script>
|
|
|
|
<JobLoader
|
|
noCode={true}
|
|
toastError={noEditor}
|
|
workspaceOverride={opWs}
|
|
bind:scriptProgress
|
|
bind:this={jobLoader}
|
|
bind:isLoading={
|
|
() => modulesTestStates.states[mod.id]?.loading ?? false,
|
|
(v) => {
|
|
let newLoading = v ?? false
|
|
if (modulesTestStates.states && modulesTestStates.states?.[mod.id]?.loading !== newLoading) {
|
|
modulesTestStates.states[mod.id] = {
|
|
...(modulesTestStates.states?.[mod.id] ?? {}),
|
|
loading: newLoading,
|
|
hiddenInGraph: false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
bind:job={
|
|
() => modulesTestStates.states[mod.id]?.testJob,
|
|
(v) => modulesTestStates.states[mod.id] && (modulesTestStates.states[mod.id].testJob = v)
|
|
}
|
|
loadPlaceholderJobOnStart={{
|
|
type: 'QueuedJob',
|
|
id: '',
|
|
running: false,
|
|
canceled: false,
|
|
job_kind: 'preview',
|
|
permissioned_as: '',
|
|
is_flow_step: false,
|
|
email: '',
|
|
visible_to_owner: true,
|
|
tag: ''
|
|
}}
|
|
/>
|