From ccf755b351343302b9412377b7df68eede25bd1e Mon Sep 17 00:00:00 2001 From: "David P. Kleinschmidt" Date: Mon, 18 Aug 2025 08:41:15 -0400 Subject: [PATCH 01/42] Add windir to Windows environment (#6403) --- backend/windmill-worker/src/python_executor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 354ff0b8dc..eb6a6e7f30 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -852,6 +852,7 @@ mount {{ { python_cmd.env("SystemRoot", SYSTEM_ROOT.as_str()); python_cmd.env("USERPROFILE", crate::USERPROFILE_ENV.as_str()); + python_cmd.env("windir", SYSTEM_ROOT.as_str()); python_cmd.env( "LOCALAPPDATA", std::env::var("LOCALAPPDATA") From 896238a1a74d3f960a9a6f3921ba662d6d061f2d Mon Sep 17 00:00:00 2001 From: pyranota <92104930+pyranota@users.noreply.github.com> Date: Tue, 19 Aug 2025 04:05:46 +0200 Subject: [PATCH 02/42] remove `ruby` from default features (#6406) * fix: remove `ruby` from default features * fix build error --- backend/Cargo.toml | 2 +- backend/windmill-worker/src/universal_pkg_installer.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 1ca3b88f18..4aee064125 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -49,7 +49,7 @@ incremental = true lto = "thin" [features] -default = ["ruby"] +default = [] private = ["windmill-api/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"] agent_worker_server = ["windmill-api/agent_worker_server"] enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"] diff --git a/backend/windmill-worker/src/universal_pkg_installer.rs b/backend/windmill-worker/src/universal_pkg_installer.rs index 1e4a8c2d39..dc4b5dab2a 100644 --- a/backend/windmill-worker/src/universal_pkg_installer.rs +++ b/backend/windmill-worker/src/universal_pkg_installer.rs @@ -14,6 +14,7 @@ use windmill_common::{error, worker::Connection}; use crate::{common::start_child_process, DISABLE_NSJAIL}; +#[allow(dead_code)] #[derive(Clone, Debug)] pub struct RequiredDependency { /// Expected directory of dependency in cache From 5e73c49ab670be0f55794f5d0cb182de9efd500a Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 19 Aug 2025 02:12:57 +0000 Subject: [PATCH 03/42] fix: flow status reactivity improvement (#6402) --- frontend/src/lib/components/Dev.svelte | 49 +- .../src/lib/components/FirstStepInputs.svelte | 4 +- .../src/lib/components/FlowBuilder.svelte | 64 +- .../src/lib/components/FlowLogViewer.svelte | 39 +- .../components/FlowLogViewerWrapper.svelte | 5 +- .../FlowLoopIterationPreview.svelte | 6 +- .../lib/components/FlowPreviewContent.svelte | 25 +- .../lib/components/FlowPreviewResult.svelte | 13 +- .../lib/components/FlowStatusViewer.svelte | 71 +- .../components/FlowStatusViewerInner.svelte | 661 +++++++++--------- .../src/lib/components/FlowTimeline.svelte | 46 +- frontend/src/lib/components/JobLoader.svelte | 2 +- .../lib/components/ModulePreviewForm.svelte | 2 +- frontend/src/lib/components/ModuleTest.svelte | 34 +- .../display/AppJobIdFlowStatus.svelte | 6 +- .../apps/editor/AppJobsDrawer.svelte | 4 +- .../copilot/chat/AIChatManager.svelte.ts | 28 +- .../copilot/chat/flow/FlowAIChat.svelte | 10 +- frontend/src/lib/components/flow_builder.ts | 3 +- .../lib/components/flows/FlowEditor.svelte | 19 +- .../content/BranchPredicateEditor.svelte | 40 +- .../flows/content/FlowConstants.svelte | 6 +- .../flows/content/FlowEditorPanel.svelte | 6 +- .../components/flows/content/FlowLoop.svelte | 4 +- .../flows/content/FlowModuleComponent.svelte | 47 +- .../flows/content/FlowModuleEarlyStop.svelte | 4 +- .../flows/content/FlowModuleSkip.svelte | 2 +- .../flows/content/FlowModuleSleep.svelte | 2 +- .../flows/content/FlowModuleSuspend.svelte | 2 +- .../flows/content/FlowModuleWrapper.svelte | 6 +- .../flows/content/FlowResult.svelte | 4 +- .../flows/content/FlowWhileLoop.svelte | 2 +- .../src/lib/components/flows/flowState.ts | 9 +- .../components/flows/flowStateUtils.svelte.ts | 23 +- .../src/lib/components/flows/flowStore.ts | 4 +- .../flows/header/FlowPreviewButtons.svelte | 10 +- .../flows/map/FlowGraphPreviewButton.svelte | 2 +- .../components/flows/map/FlowJobsMenu.svelte | 4 +- .../flows/map/FlowModuleSchemaItem.svelte | 23 +- .../flows/map/FlowModuleSchemaMap.svelte | 40 +- .../lib/components/flows/previousResults.ts | 10 +- .../flows/propPicker/InputPickerInner.svelte | 4 +- frontend/src/lib/components/flows/types.ts | 40 +- frontend/src/lib/components/flows/utils.ts | 11 +- .../lib/components/graph/FlowGraphV2.svelte | 65 +- .../components/graph/graphBuilder.svelte.ts | 80 ++- frontend/src/lib/components/graph/model.ts | 15 +- .../graph/renderers/edges/BaseEdge.svelte | 8 +- .../renderers/nodes/BranchAllEndNode.svelte | 2 +- .../renderers/nodes/BranchAllStart.svelte | 2 +- .../renderers/nodes/BranchOneStart.svelte | 2 +- .../renderers/nodes/ForLoopEndNode.svelte | 4 +- .../renderers/nodes/ForLoopStartNode.svelte | 2 +- .../graph/renderers/nodes/ModuleNode.svelte | 43 +- .../renderers/nodes/branchOneEndNode.svelte | 2 +- .../src/lib/components/modulesTest.svelte.ts | 39 +- .../components/stepHistoryLoader.svelte.ts | 21 +- .../FlowBuilderTutorialBranchOne.svelte | 2 +- .../FlowBuilderTutorialForLoop.svelte | 8 +- .../(root)/(logged)/flows/add/+page.svelte | 4 +- .../flows/edit/[...path]/+page.svelte | 5 +- .../(root)/(logged)/run/[...run]/+page.svelte | 8 +- frontend/src/routes/flows/dev/+page.svelte | 4 +- .../src/routes/test_dev/sdk_flow/+page.svelte | 5 +- 64 files changed, 899 insertions(+), 818 deletions(-) diff --git a/frontend/src/lib/components/Dev.svelte b/frontend/src/lib/components/Dev.svelte index d36a557b3d..4eececb8ef 100644 --- a/frontend/src/lib/components/Dev.svelte +++ b/frontend/src/lib/components/Dev.svelte @@ -17,7 +17,7 @@ } from '$lib/gen' import { inferArgs } from '$lib/infer' import { setCopilotInfo, userStore, workspaceStore } from '$lib/stores' - import { emptySchema, readFieldsRecursively, sendUserToast } from '$lib/utils' + import { emptySchema, readFieldsRecursively, sendUserToast, type StateStore } from '$lib/utils' import { Pane, Splitpanes } from 'svelte-splitpanes' import { onDestroy, onMount, setContext, untrack } from 'svelte' import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte' @@ -27,7 +27,7 @@ import FlowModuleSchemaMap from './flows/map/FlowModuleSchemaMap.svelte' import FlowEditorPanel from './flows/content/FlowEditorPanel.svelte' import { deepEqual } from 'fast-equals' - import { writable, type Writable } from 'svelte/store' + import { writable } from 'svelte/store' import type { FlowState } from './flows/flowState' import { initHistory } from '$lib/history.svelte' import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types' @@ -51,7 +51,6 @@ import { TestSteps } from './flows/testSteps.svelte' import { ModulesTestStates } from './modulesTest.svelte' import type { GraphModuleState } from './graph' - import { updateDerivedModuleStatesFromTestJobs } from './flows/utils' let flowCopilotContext: FlowCopilotContext = { shouldUpdatePropertyType: writable<{ @@ -116,7 +115,6 @@ const flowPreviewContent = $derived(flowPreviewButtons?.getFlowPreviewContent()) const job: Job | undefined = $derived(flowPreviewContent?.getJob()) let showJobStatus = $state(false) - let testModuleId: string | undefined = $state(undefined) type LastEditScript = { content: string @@ -453,7 +451,7 @@ } } - const flowStateStore = writable({} as FlowState) + const flowStateStore = $state({ val: {} }) as StateStore const previewArgsStore = $state({ val: {} }) const scriptEditorDrawer = writable(undefined) @@ -464,8 +462,6 @@ const triggersCount = writable(undefined) const modulesTestStates = new ModulesTestStates((moduleId) => { // Update the derived store with test job states - delete $derivedModuleStates[moduleId] - testModuleId = moduleId showJobStatus = false }) const outputPickerOpenFns: Record void> = $state({}) @@ -538,11 +534,11 @@ } mod.value.input_transforms = input_transforms - if (!deepEqual(schema, $flowStateStore[mod.id]?.schema)) { - if (!$flowStateStore[mod.id]) { - $flowStateStore[mod.id] = { schema } + if (!deepEqual(schema, flowStateStore.val[mod.id]?.schema)) { + if (!flowStateStore.val[mod.id]) { + flowStateStore.val[mod.id] = { schema } } else { - $flowStateStore[mod.id].schema = schema + flowStateStore.val[mod.id].schema = schema } reload++ } @@ -586,25 +582,12 @@ $selectedIdStore && untrack(() => inferModuleArgs($selectedIdStore)) }) - const localModuleStates: Writable> = $derived( - flowPreviewContent?.getLocalModuleStates() ?? writable({}) - ) + let localModuleStates: Record = $state({}) - const suspendStatus: Writable> = $derived( - flowPreviewContent?.getSuspendStatus() ?? writable({}) - ) + let suspendStatus: StateStore> = $state({ val: {} }) // Create a derived store that only shows the module states when showModuleStatus is true // this store can also be updated - let derivedModuleStates = writable>({}) - $effect(() => { - derivedModuleStates.update((currentStates) => { - return showJobStatus ? $localModuleStates : currentStates - }) - }) - $effect(() => { - updateDerivedModuleStatesFromTestJobs(testModuleId, modulesTestStates, derivedModuleStates) - }) let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state() function onJobDone() { @@ -639,14 +622,9 @@ } function resetModulesStates() { - derivedModuleStates.set({}) showJobStatus = false } - const individualStepTests = $derived( - !(showJobStatus && job) && Object.keys($derivedModuleStates).length > 0 - ) - const flowHasChanged = $derived(flowPreviewContent?.flowHasChanged()) @@ -785,7 +763,7 @@ bind:this={flowPreviewButtons} {onJobDone} onRunPreview={() => { - localModuleStates.set({}) + localModuleStates = {} showJobStatus = true }} /> @@ -800,19 +778,20 @@ disableTutorials smallErrorHandler={true} disableStaticInputs - localModuleStates={derivedModuleStates} + {localModuleStates} onTestUpTo={flowPreviewButtons?.testUpTo} + testModuleStates={modulesTestStates} isOwner={flowPreviewContent?.getIsOwner?.()} onTestFlow={flowPreviewButtons?.runPreview} isRunning={flowPreviewContent?.getIsRunning?.()} onCancelTestFlow={flowPreviewContent?.cancelTest} onOpenPreview={flowPreviewButtons?.openPreview} onHideJobStatus={resetModulesStates} - {individualStepTests} flowJob={job} {showJobStatus} onDelete={(id) => { - delete $derivedModuleStates[id] + delete localModuleStates[id] + delete modulesTestStates.states[id] }} {flowHasChanged} /> diff --git a/frontend/src/lib/components/FirstStepInputs.svelte b/frontend/src/lib/components/FirstStepInputs.svelte index bd71e6a4b3..775dccdda8 100644 --- a/frontend/src/lib/components/FirstStepInputs.svelte +++ b/frontend/src/lib/components/FirstStepInputs.svelte @@ -19,7 +19,7 @@ let mod: any | undefined = $state(undefined) async function loadSchema() { try { - const res = await getFirstStepSchema($flowStateStore, flowStore.val) + const res = await getFirstStepSchema(flowStateStore.val, flowStore.val) schema = res.schema mod = res.mod dispatch('connectFirstNode', { connectFirstNode: res.connectFirstNode }) @@ -28,7 +28,7 @@ } } $effect(() => { - flowStore.val && $flowStateStore && untrack(() => loadSchema()) + flowStore.val && flowStateStore && untrack(() => loadSchema()) }) function handleClick() { diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 0eb3802a40..78a0363012 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -25,6 +25,7 @@ orderedJsonStringify, readFieldsRecursively, replaceFalseWithUndefined, + type StateStore, type Value } from '$lib/utils' import { sendUserToast } from '$lib/toast' @@ -33,7 +34,7 @@ import AIChangesWarningModal from '$lib/components/copilot/chat/flow/AIChangesWarningModal.svelte' import { onMount, setContext, untrack, type ComponentType } from 'svelte' - import { writable, type Writable } from 'svelte/store' + import { writable } from 'svelte/store' import CenteredPage from './CenteredPage.svelte' import { Badge, Button, UndoRedo } from './common' import FlowEditor from './flows/FlowEditor.svelte' @@ -42,7 +43,7 @@ import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte' import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte' import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types' - import { cleanInputs, updateDerivedModuleStatesFromTestJobs } from './flows/utils' + import { cleanInputs } from './flows/utils' import { Calendar, Pen, @@ -577,13 +578,7 @@ } let insertButtonOpen = writable(false) - let testModuleId: string | undefined = $state(undefined) - let modulesTestStates = new ModulesTestStates((moduleId) => { - // Update the derived store with test job states - delete $derivedModuleStates[moduleId] - testModuleId = moduleId - showJobStatus = false - }) + let modulesTestStates = new ModulesTestStates() let outputPickerOpenFns: Record void> = $state({}) let flowEditor: FlowEditor | undefined = $state(undefined) @@ -933,33 +928,8 @@ } } - const localModuleStates: Writable> = $derived( - flowPreviewContent?.getLocalModuleStates() ?? writable({}) - ) - const suspendStatus: Writable> = $derived( - flowPreviewContent?.getSuspendStatus() ?? writable({}) - ) - - // Create a derived store that only shows the module states when showModuleStatus is true - // this store can also be updated - let derivedModuleStates = writable>({}) - $effect(() => { - derivedModuleStates.update((currentStates) => { - return showJobStatus ? $localModuleStates : currentStates - }) - }) - $effect(() => { - updateDerivedModuleStatesFromTestJobs(testModuleId, modulesTestStates, derivedModuleStates) - }) - - function resetModulesStates() { - derivedModuleStates.set({}) - showJobStatus = false - } - - const individualStepTests = $derived( - !(showJobStatus && job) && Object.keys($derivedModuleStates).length > 0 - ) + let localModuleStates: Record = $state({}) + let suspendStatus: StateStore> = $state({ val: {} }) const flowHasChanged = $derived(flowPreviewContent?.flowHasChanged()) @@ -1025,7 +995,7 @@ for (const mod of restoredModules) { if (mod) { try { - loadFlowModuleState(mod).then((state) => ($flowStateStore[mod.id] = state)) + loadFlowModuleState(mod).then((state) => (flowStateStore.val[mod.id] = state)) } catch (e) { console.error('Error loading state for restored node', e) } @@ -1155,10 +1125,12 @@ showCaptureHint.set(true) }} {onJobDone} + bind:localModuleStates bind:this={flowPreviewButtons} {loading} onRunPreview={() => { - localModuleStates.set({}) + modulesTestStates.hideJobsInGraph() + localModuleStates = {} showJobStatus = true }} /> @@ -1185,7 +1157,7 @@ - {#if $flowStateStore} + {#if flowStateStore} aiChatManager.toggleOpen()} onOpenPreview={flowPreviewButtons?.openPreview} - localModuleStates={derivedModuleStates} + localModuleStates={showJobStatus ? localModuleStates : {}} + {showJobStatus} + testModuleStates={modulesTestStates} isOwner={flowPreviewContent?.getIsOwner()} onTestFlow={flowPreviewButtons?.runPreview} isRunning={flowPreviewContent?.getIsRunning()} onCancelTestFlow={flowPreviewContent?.cancelTest} - onHideJobStatus={resetModulesStates} - {individualStepTests} + onHideJobStatus={() => { + modulesTestStates.hideJobsInGraph() + showJobStatus = false + }} {job} {suspendStatus} - {showJobStatus} onDelete={(id) => { - delete $derivedModuleStates[id] + delete localModuleStates[id] + delete modulesTestStates.states[id] }} {flowHasChanged} /> diff --git a/frontend/src/lib/components/FlowLogViewer.svelte b/frontend/src/lib/components/FlowLogViewer.svelte index 369578ff17..ef9ab3a863 100644 --- a/frontend/src/lib/components/FlowLogViewer.svelte +++ b/frontend/src/lib/components/FlowLogViewer.svelte @@ -21,13 +21,12 @@ import FlowJobsMenu from './flows/map/FlowJobsMenu.svelte' import BarsStaggered from './icons/BarsStaggered.svelte' import type { GraphModuleState } from './graph/model' - import type { Writable } from 'svelte/store' type RootJobData = Partial interface Props { modules: FlowModule[] - localModuleStates: Writable> + localModuleStates: Record rootJob: RootJobData flowStatus: FlowStatusModule['type'] | undefined expandedRows: Record @@ -122,7 +121,7 @@ } function hasEmptySubflow(stepId: string, stepType: FlowModuleValue['type'] | undefined): boolean { - const state = $localModuleStates[stepId] + const state = localModuleStates[stepId] if (!state || !stepType) return false return ( @@ -172,7 +171,7 @@ } // Check if this entry itself has an error (but don't flag it - only its parents) - const stepStatus = $localModuleStates[module.id]?.type + const stepStatus = localModuleStates[module.id]?.type if (stepStatus === 'Failure') { currentEntryHasError = true // Don't add the entry itself to parentsWithErrors @@ -402,7 +401,7 @@ {#if modules.length > 0} {#each modules as module (module.id)} {@const isLeafStep = !hasSubflows(module)} - {@const status = $localModuleStates[module.id]?.type} + {@const status = localModuleStates[module.id]?.type} {@const isRunning = status === 'InProgress' || status === 'WaitingForExecutor'} {@const hasEmptySubflowValue = hasEmptySubflow(module.id, module.value.type)} {@const isCollapsible = !hasEmptySubflowValue} @@ -479,7 +478,7 @@ {/if} - {#if !hasEmptySubflowValue && $localModuleStates[module.id]?.flow_jobs && (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow')} + {#if !hasEmptySubflowValue && localModuleStates[module.id]?.flow_jobs && (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow')} @@ -488,18 +487,18 @@ moduleId={module.id} id={module.id} {onSelectedIteration} - flowJobsSuccess={$localModuleStates[module.id] + flowJobsSuccess={localModuleStates[module.id] ?.flow_jobs_success} - flowJobs={$localModuleStates[module.id]?.flow_jobs} - selected={$localModuleStates[module.id]?.selectedForloopIndex ?? + flowJobs={localModuleStates[module.id]?.flow_jobs} + selected={localModuleStates[module.id]?.selectedForloopIndex ?? 0} - selectedManually={$localModuleStates[module.id] + selectedManually={localModuleStates[module.id] ?.selectedForLoopSetManually ?? false} showIcon={false} /> {#if module.value.type === 'forloopflow'} - {`/${$localModuleStates[module.id]?.iteration_total ?? 0}`} + {`/${localModuleStates[module.id]?.iteration_total ?? 0}`} {/if} {/if} @@ -507,7 +506,7 @@ {#if isLeafStep} - {@const jobId = $localModuleStates[module.id]?.job_id} + {@const jobId = localModuleStates[module.id]?.job_id} {#if isCollapsible && isExpanded(module.id, isRunning)} - {@const args = $localModuleStates[module.id]?.args} - {@const logs = $localModuleStates[module.id]?.logs} - {@const result = $localModuleStates[module.id]?.result} - {@const jobId = $localModuleStates[module.id]?.job_id} + {@const args = localModuleStates[module.id]?.args} + {@const logs = localModuleStates[module.id]?.logs} + {@const result = localModuleStates[module.id]?.result} + {@const jobId = localModuleStates[module.id]?.job_id}
{#each getSubflows(module) as subflow} {@const subflowJob = { id: jobId, type: - $localModuleStates[module.id]?.type === 'Failure' || - $localModuleStates[module.id]?.type === 'Success' + localModuleStates[module.id]?.type === 'Failure' || + localModuleStates[module.id]?.type === 'Success' ? 'CompletedJob' : ('QueuedJob' as Job['type']), logs, result, args, - success: $localModuleStates[module.id]?.type === 'Success' + success: localModuleStates[module.id]?.type === 'Success' }}
@@ -545,7 +544,7 @@ modules={subflow.modules} {localModuleStates} rootJob={subflowJob} - flowStatus={$localModuleStates[module.id]?.type} + flowStatus={localModuleStates[module.id]?.type} {expandedRows} {allExpanded} {showResultsInputs} diff --git a/frontend/src/lib/components/FlowLogViewerWrapper.svelte b/frontend/src/lib/components/FlowLogViewerWrapper.svelte index 3d5c09f460..f6d74651eb 100644 --- a/frontend/src/lib/components/FlowLogViewerWrapper.svelte +++ b/frontend/src/lib/components/FlowLogViewerWrapper.svelte @@ -1,6 +1,5 @@ { - let { job } = detail + onJobsLoaded={({ job, force }) => { if (job.script_path != lastScriptPath && job.script_path) { lastScriptPath = job.script_path loadOwner(lastScriptPath ?? '') } - dispatch('jobsLoaded', job) + onJobsLoaded?.({ job, force }) }} globalModuleStates={[]} - globalDurationStatuses={[]} - {localModuleStates} - {localDurationStatuses} + {globalIterationBounds} + bind:localModuleStates bind:selectedNode={selectedJobStep} - on:start - on:done + bind:localDurationStatuses + {onStart} + {onDone} bind:job {initialJob} {jobId} @@ -122,4 +139,6 @@ {customUi} graphTabOpen={true} isNodeSelected={true} + {refreshGlobal} + {updateGlobalRefresh} /> diff --git a/frontend/src/lib/components/FlowStatusViewerInner.svelte b/frontend/src/lib/components/FlowStatusViewerInner.svelte index 6800d7583d..dfeb72d272 100644 --- a/frontend/src/lib/components/FlowStatusViewerInner.svelte +++ b/frontend/src/lib/components/FlowStatusViewerInner.svelte @@ -8,18 +8,24 @@ type FlowStatus, type FlowModuleValue, type FlowModule, - ResourceService + ResourceService, + type CompletedJob } from '$lib/gen' import { workspaceStore } from '$lib/stores' import { base } from '$lib/base' import FlowJobResult from './FlowJobResult.svelte' import DisplayResult from './DisplayResult.svelte' - import { createEventDispatcher, getContext, setContext, tick, untrack } from 'svelte' + import { getContext, setContext, tick, untrack } from 'svelte' import { onDestroy } from 'svelte' import { Badge, Button, Skeleton, Tab } from './common' import Tabs from './common/tabs/Tabs.svelte' - import { type DurationStatus, type FlowStatusViewerContext, type GraphModuleState } from './graph' + import { + type DurationStatus, + type FlowStatusViewerContext, + type GlobalIterationBounds, + type GraphModuleState + } from './graph' import ModuleStatus from './ModuleStatus.svelte' import { clone, isScriptPreview, msToSec, readFieldsRecursively, truncateRev } from '$lib/utils' import JobArgs from './JobArgs.svelte' @@ -27,7 +33,6 @@ import { deepEqual } from 'fast-equals' import FlowTimeline from './FlowTimeline.svelte' import { dfs } from './flows/dfs' - import { get, writable, type Unsubscriber, type Writable } from 'svelte/store' import Alert from './common/alert/Alert.svelte' import FlowGraphViewerStep from './FlowGraphViewerStep.svelte' import FlowGraphV2 from './graph/FlowGraphV2.svelte' @@ -38,8 +43,7 @@ import type { FlowGraphAssetContext } from './flows/types' import { createState } from '$lib/svelte5Utils.svelte' import JobLoader from './JobLoader.svelte' - - const dispatch = createEventDispatcher() + import { writable } from 'svelte/store' let { flowStateStore, @@ -67,27 +71,30 @@ | undefined //only useful when forloops are optimized and the job doesn't contain the mod id anymore innerModule?: FlowModuleValue | undefined - globalRefreshes?: Record Promise> render?: boolean isOwner?: boolean selectedNode?: string | undefined - globalModuleStates: Writable>[] - globalDurationStatuses: Writable>[] - childFlow?: boolean + globalModuleStates: Record[] + globalDurationStatuses?: Record[] + globalIterationBounds: Record + isSelectedBranch?: boolean isSubflow?: boolean reducedPolling?: boolean wideResults?: boolean hideFlowResult?: boolean + topModuleStates?: Record workspace?: string | undefined prefix?: string | undefined - subflowParentsGlobalModuleStates?: Writable>[] - subflowParentsDurationStatuses?: Writable>[] + subflowParentsGlobalModuleStates?: Record[] + subflowParentsDurationStatuses?: Record[] isForloopSelected?: boolean - parentRecursiveRefresh?: Record Promise> + updateRecursiveRefreshFn?: (jobId: string, updateFn: (clear, root) => Promise) => void + refreshGlobal: (moduleId: string, clear: boolean, root: string) => Promise + updateGlobalRefresh: (moduleId: string, updateFn: (clear, root) => Promise) => void job?: (Job & { result_stream?: string }) | undefined rightColumnSelect?: 'timeline' | 'node_status' | 'node_definition' | 'user_states' - localModuleStates?: Writable> - localDurationStatuses?: Writable> + localModuleStates?: Record + localDurationStatuses?: Record onResultStreamUpdate?: ({ jobId, result_stream @@ -101,6 +108,9 @@ graphTabOpen: boolean isNodeSelected: boolean loadExtraLogs?: (logs: string) => void + onStart?: () => void + onJobsLoaded?: ({ job, force }: { job: Job; force: boolean }) => void + onDone?: ({ job }: { job: CompletedJob }) => void } let { @@ -109,34 +119,42 @@ workspaceId = undefined, flowJobIds = undefined, innerModule = undefined, - globalRefreshes = $bindable({}), render = true, isOwner = false, selectedNode = $bindable(undefined), globalModuleStates, - globalDurationStatuses, - childFlow = false, + globalDurationStatuses = [], + globalIterationBounds, + updateRecursiveRefreshFn = undefined, + isSelectedBranch = true, isSubflow = false, reducedPolling = false, wideResults = false, hideFlowResult = false, workspace = $workspaceStore, prefix = undefined, + topModuleStates = undefined, + refreshGlobal, + updateGlobalRefresh, subflowParentsGlobalModuleStates = [], subflowParentsDurationStatuses = [], isForloopSelected = false, - parentRecursiveRefresh = $bindable({}), job = $bindable(undefined), rightColumnSelect = $bindable('timeline'), - localModuleStates = writable({}), - localDurationStatuses = writable({}), + localModuleStates = $bindable({}), + localDurationStatuses = $bindable({}), customUi, onResultStreamUpdate = undefined, graphTabOpen, isNodeSelected, - loadExtraLogs = undefined + loadExtraLogs = undefined, + onStart = undefined, + onJobsLoaded = undefined, + onDone = undefined }: Props = $props() + let getTopModuleStates = $derived(topModuleStates ?? localModuleStates) + let resultStreams: Record = $state({}) if (onResultStreamUpdate == undefined) { @@ -152,6 +170,14 @@ } let recursiveRefresh: Record Promise> = $state({}) + let updateRecursiveRefreshInner = ( + childJobId: string, + updateFn: (clear, root) => Promise + ) => { + if (childJobId) { + recursiveRefresh[childJobId] = updateFn + } + } // Add support for the input args assets shown as an asset node const _flowGraphAssetsCtx = getContext('FlowGraphAssetContext') @@ -190,81 +216,79 @@ let expandedSubflows: Record = $state({}) - let selectedId: Writable = writable(selectedNode) + let selectedId = writable(selectedNode) function onFlowModuleId() { - if (globalRefreshes) { - let modId = flowJobIds?.moduleId - if (modId) { - globalRefreshes[buildSubflowKey(modId, prefix)] = async (clear, root) => { - await refresh(clear, root) // refresh(true, loopJob) - } - } + let modId = flowJobIds?.moduleId + if (modId) { + let prefixedId = buildSubflowKey(modId, prefix) + updateGlobalRefresh(prefixedId, async (clear, root) => { + // console.debug('updateGlobalRefreshInner refresh', prefixedId, clear, root) + await refresh(clear, root) // refresh(true, loopJob) + }) } } function updateModuleStates( - moduleState: Writable>, + moduleState: Record, key: string, newValue: GraphModuleState, keepType: boolean | undefined ) { - const state = get(moduleState) if ( newValue.selectedForloop != undefined && - state[key]?.selectedForloop != undefined && - newValue.selectedForloop != state[key].selectedForloop + moduleState[key]?.selectedForloop != undefined && + newValue.selectedForloop != moduleState[key].selectedForloop ) { + let newState = { ...moduleState[key] } if ( newValue.type == 'InProgress' && - state[key]?.type != 'InProgress' && - !(keepType && (state[key]?.type === 'Success' || state[key]?.type === 'Failure')) + moduleState[key]?.type != 'InProgress' && + !( + keepType && + (moduleState[key]?.type === 'Success' || moduleState[key]?.type === 'Failure') + ) ) { - moduleState.update((state) => { - state[key].type = 'InProgress' - return state - }) + newState.type = 'InProgress' + } else if (['Success', 'Failure'].includes(newValue.type)) { + newState.type = newValue.type } if ( - state[key]?.job_id != newValue.job_id || - !deepEqual(state[key]?.args, newValue.args) || - !deepEqual(state[key]?.result, newValue.result) + moduleState[key]?.job_id != newValue.job_id || + !deepEqual(moduleState[key]?.args, newValue.args) || + !deepEqual(moduleState[key]?.result, newValue.result) ) { - moduleState.update((state) => { - state[key].args = newValue.args - state[key].result = newValue.result - state[key].job_id = newValue.job_id - return state - }) + newState.args = newValue.args + newState.result = newValue.result + newState.job_id = newValue.job_id } + moduleState[key] = newState return } - if (state[key]?.selectedForLoopSetManually) { + if (moduleState[key]?.selectedForLoopSetManually) { if ( newValue.selectedForloop != undefined && - state[key]?.selectedForloop != newValue.selectedForloop + moduleState[key]?.selectedForloop != newValue.selectedForloop ) { - return state + return moduleState } else { newValue.selectedForLoopSetManually = true - newValue.selectedForloopIndex = state[key]?.selectedForloopIndex - newValue.selectedForloop = state[key]?.selectedForloop + newValue.selectedForloopIndex = moduleState[key]?.selectedForloopIndex + newValue.selectedForloop = moduleState[key]?.selectedForloop } - } else if (state[key]?.selectedForloopIndex != undefined) { - newValue.selectedForloopIndex = state[key]?.selectedForloopIndex - newValue.selectedForloop = state[key]?.selectedForloop + } else if (moduleState[key]?.selectedForloopIndex != undefined) { + newValue.selectedForloopIndex = moduleState[key]?.selectedForloopIndex + newValue.selectedForloop = moduleState[key]?.selectedForloop } - if (keepType && (state[key]?.type == 'Success' || state[key]?.type == 'Failure')) { - newValue.type = state[key].type + if (keepType && (moduleState[key]?.type == 'Success' || moduleState[key]?.type == 'Failure')) { + newValue.type = moduleState[key].type } - if (!deepEqual(state[key], newValue)) { - moduleState.update((state) => { - state[key] = newValue - return state - }) + if (!deepEqual(moduleState[key], newValue)) { + // console.debug('updateModuleStates 2', key, $state.snapshot(moduleState)) + moduleState[key] = newValue } } @@ -273,69 +297,72 @@ } async function refresh(clearLoop: boolean, rootJob: string | undefined) { + console.debug('refresh', clearLoop, rootJob) let modId = flowJobIds?.moduleId + let topModuleStates = getTopModuleStates if (clearLoop) { if (!rootJob) { - let topLevelModuleStates = globalModuleStates?.[globalModuleStates?.length - 1] - if (modId) { - topLevelModuleStates?.update((x) => { - if (modId) { - delete x[modId] - } - return x - }) + if (modId && topModuleStates) { + let prefixedId = buildSubflowKey(modId, prefix) + delete topModuleStates[prefixedId] // TODO: this is not working } if (subflowParentsGlobalModuleStates.length > 0) { - subflowParentsGlobalModuleStates?.[subflowParentsGlobalModuleStates?.length - 1]?.update( - (x) => { - for (let mod of innerModules ?? []) { - if (mod.id) { - delete x[buildSubflowKey(mod.id, prefix)] - } - } + let subflowModuleStates = + subflowParentsGlobalModuleStates?.[subflowParentsGlobalModuleStates?.length - 1] - return x + for (let mod of innerModules ?? []) { + if (mod.id) { + delete subflowModuleStates[buildSubflowKey(mod.id, prefix)] } - ) + } } else { - topLevelModuleStates?.update((x) => { - for (let mod of innerModules ?? []) { - if (mod.id) { - delete x[mod.id] - } + for (let mod of innerModules ?? []) { + if (mod.id && topModuleStates) { + let prefixedId = buildSubflowKey(mod.id, prefix) + delete topModuleStates[prefixedId] } - - return x - }) + } } } } else { - let state = modId ? getTopModuleStates()?.[modId] : undefined + let state = modId ? topModuleStates?.[buildSubflowKey(modId, prefix)] : undefined let loopjob = state?.selectedForloop let njob = flowJobIds && modId && loopjob ? storedListJobs?.[loopjob] : job + if (njob) { - dispatch('jobsLoaded', { job: njob, force: true }) + onJobsLoaded?.({ job: njob, force: true }) } } - for (let [k, rec] of Object.entries(recursiveRefresh)) { - if (rootJob != undefined && rootJob != k) { - continue - } + let callRec = async (rec: (clear, root) => Promise) => { await tick() await rec(clearLoop, undefined) } + if (rootJob) { + let rec = recursiveRefresh[rootJob] + if (rec) { + await callRec(rec) + // console.debug('refresh recursive 1', rec) + } else { + // console.debug('refresh recursive no rec', rootJob) + } + } else { + for (let rec of Object.values(recursiveRefresh)) { + await callRec(rec) + // console.debug('refresh recursive 2', rec) + } + } } function updateRecursiveRefresh(jobId: string) { if (jobId) { - parentRecursiveRefresh[jobId] = async (clear, root) => { + updateRecursiveRefreshFn?.(jobId, async (clear, root) => { if (globalModuleStates.length > 0 || isSubflow) { await refresh(clear, root) } - } + }) } } @@ -345,67 +372,55 @@ force?: boolean, keepType?: boolean ) { - let newValue = { ...($localModuleStates[key] ?? {}), ...value } - if (!deepEqual($localModuleStates[key], value) || force) { + let newValue = { ...(localModuleStates[key] ?? {}), ...value } + if (!deepEqual(localModuleStates[key], value) || force) { + // console.debug('setModuleState', key, force, keepType, $state.snapshot(value)) ;[localModuleStates, ...globalModuleStates].forEach((s) => { updateModuleStates(s, key, newValue, keepType) }) if (prefix) { - subflowParentsGlobalModuleStates.forEach((s) => - updateModuleStates(s, buildSubflowKey(key, prefix), newValue, keepType) - ) + let prefixedId = buildSubflowKey(key, prefix) + subflowParentsGlobalModuleStates.forEach((s) => { + updateModuleStates(s, prefixedId, newValue, keepType) + }) } } } function setDurationStatusByJob(key: string, id: string, value: any) { - if (!deepEqual($localDurationStatuses[key]?.byJob[id], value)) { - $localDurationStatuses[key].byJob[id] = value + if (!deepEqual(localDurationStatuses[key]?.byJob[id], value)) { + localDurationStatuses[key].byJob[id] = value globalDurationStatuses.forEach((s) => { - s.update((x) => { - x[key].byJob[id] = value - - return x - }) + s[key].byJob[id] = value }) if (prefix) { subflowParentsDurationStatuses.forEach((s) => { - s.update((x) => { - x[buildSubflowKey(key, prefix)].byJob[id] = value - return x - }) + s[buildSubflowKey(key, prefix)].byJob[id] = value }) } } } function initializeByJob(modId: string) { - if ($localDurationStatuses[modId] == undefined) { - $localDurationStatuses[modId] = { byJob: {} } + if (localDurationStatuses[modId] == undefined) { + localDurationStatuses[modId] = { byJob: {} } } - globalDurationStatuses.forEach((x) => - x.update((x) => { - if (x[modId] == undefined) { - x[modId] = { byJob: {} } - } - return x - }) - ) + globalDurationStatuses.forEach((x) => { + if (x[modId] == undefined) { + x[modId] = { byJob: {} } + } + }) if (prefix) { - subflowParentsDurationStatuses.forEach((x) => - x.update((x) => { - let key = buildSubflowKey(modId, prefix) - if (x[key] == undefined) { - x[key] = { byJob: {} } - } - return x - }) - ) + subflowParentsDurationStatuses.forEach((x) => { + let key = buildSubflowKey(modId, prefix) + if (x[key] == undefined) { + x[key] = { byJob: {} } + } + }) } } - let innerModules: FlowStatusModule[] = $state([]) - + let innerModules = $state(undefined) as FlowStatusModule[] | undefined function updateStatus(status: FlowStatus) { innerModules = status?.modules?.concat( @@ -418,29 +433,27 @@ let count = status.retry?.fail_count if (count) { - $retryStatus[jobId ?? ''] = count - } else if ($retryStatus[jobId ?? ''] != undefined) { - delete $retryStatus[jobId ?? ''] - $retryStatus = $retryStatus + retryStatus.val[jobId ?? ''] = count + } else if (retryStatus.val[jobId ?? ''] != undefined) { + delete retryStatus.val[jobId ?? ''] } let jobStatus = job?.flow_status?.modules?.[job?.flow_status.step] if (jobStatus && jobStatus.count != undefined) { - $suspendStatus[jobId ?? ''] = { nb: jobStatus.count, job: job! } - } else if ($suspendStatus[jobId ?? ''] != undefined) { - delete $suspendStatus[jobId ?? ''] - $suspendStatus = $suspendStatus + suspendStatus.val[jobId ?? ''] = { nb: jobStatus.count, job: job! } + } else if (suspendStatus.val[jobId ?? ''] != undefined) { + delete suspendStatus.val[jobId ?? ''] } } function updateInnerModules() { - if ($localModuleStates) { - innerModules.forEach((mod, i) => { + if (localModuleStates) { + innerModules?.forEach((mod, i) => { if (mod.type === 'WaitingForEvents' && innerModules?.[i - 1]?.type === 'Success') { setModuleState(mod.id ?? '', { type: mod.type, args: job?.args, tag: job?.tag }) } else if ( mod.type === 'WaitingForExecutor' && - $localModuleStates[mod.id ?? '']?.scheduled_for == undefined + localModuleStates[mod.id ?? '']?.scheduled_for == undefined ) { JobService.getJob({ workspace: workspaceId ?? $workspaceStore ?? '', @@ -466,7 +479,7 @@ } else if ( (mod.flow_jobs || mod.branch_chosen) && (mod.type == 'Success' || mod.type == 'Failure') && - !['Success', 'Failure'].includes($localModuleStates?.[mod.id ?? '']?.type) + !['Success', 'Failure'].includes(localModuleStates?.[mod.id ?? '']?.type) ) { let branchChosen = mod.branch_chosen ? { @@ -474,6 +487,7 @@ mod.branch_chosen.type == 'default' ? 0 : (mod.branch_chosen.branch ?? 0) + 1 } : {} + console.debug('updateInnerModules', mod.id, mod.type, branchChosen) setModuleState( mod.id ?? '', { @@ -486,9 +500,11 @@ setModuleState(mod.id ?? '', {}, true) } - if (mod.flow_jobs_success) { + if (mod.flow_jobs_success || mod.flow_jobs) { setModuleState(mod.id ?? '', { - flow_jobs_success: mod.flow_jobs_success + flow_jobs_success: mod.flow_jobs_success, + flow_jobs: mod.flow_jobs, + iteration_total: mod.iterator?.itered?.length ?? mod.flow_jobs?.length }) } }) @@ -519,20 +535,20 @@ let jobLoader: JobLoader | undefined = undefined function setJob(newJob: Job, force: boolean) { - if (!deepEqual(job, newJob) || isForloopSelected || force) { + if (!deepEqual(job, newJob) || isForloopSelected || force || innerModules == undefined) { job = newJob job?.flow_status && updateStatus(job?.flow_status) - dispatch('jobsLoaded', { job, force: false }) + onJobsLoaded?.({ job, force: false }) notAnonynmous = false if (job?.type == 'CompletedJob' && !destroyed) { - dispatch('done', job) + onDone?.({ job }) } } } async function loadJobInProgress() { if (!started) { started = true - dispatch('start') + onStart?.() } if (jobId != '00000000-0000-0000-0000-000000000000') { try { @@ -577,68 +593,46 @@ updateRecursiveRefresh(jobId) async function updateJobId() { - if (jobId !== job?.id) { - $localModuleStates = {} + if (jobId !== job?.id || innerModules == undefined) { + localModuleStates = {} flowTimeline?.reset() timeout && clearTimeout(timeout) - innerModules = [] + innerModules = undefined + console.log('updateJobId', jobId) if (flowJobIds) { let modId = flowJobIds?.moduleId ?? '' - let common = { + if (localDurationStatuses[modId] == undefined) { + localDurationStatuses[modId] = { byJob: {} } + } + let prefixed = buildSubflowKey(modId, prefix) + globalDurationStatuses.forEach((x) => { + if (x[prefixed] == undefined) { + x[prefixed] = { byJob: {} } + } + }) + + globalIterationBounds[prefixed] = { iteration_from: flowJobIds?.branchall ? 0 : Math.max(flowJobIds.flowJobs.length - 20, 0), - iteration_total: $localDurationStatuses?.[modId]?.iteration_total ?? flowJobIds?.length + iteration_total: flowJobIds?.length } - $localDurationStatuses[modId] = { - ...($localDurationStatuses[modId] ?? { byJob: {} }), - ...common - } - let prefixed = modId - globalDurationStatuses.forEach((x) => - x.update((x) => { - x[prefixed] = { ...(x[prefixed] ?? { byJob: {} }), ...common } - return x - }) - ) } else { - updateRecursiveRefresh(jobId) recursiveRefresh = {} - $localDurationStatuses = {} + localDurationStatuses = {} + updateRecursiveRefresh(jobId) } await loadJobInProgress() } } - function getTopModuleStates() { - return get(globalModuleStates?.[globalModuleStates?.length - 1]) - } - - let forloop_selected = $state(getTopModuleStates()?.[flowJobIds?.moduleId ?? '']?.selectedForloop) - - let sub: Unsubscriber | undefined = undefined - let timeoutForloopSelectedSub: NodeJS.Timeout | undefined = undefined - let timeoutForloopSelected: NodeJS.Timeout | undefined = undefined - - function onModuleIdChange() { - clearTimeout(timeoutForloopSelectedSub) - timeoutForloopSelectedSub = setTimeout(() => { - sub?.() - sub = globalModuleStates?.[globalModuleStates?.length - 1].subscribe((x) => { - const newForloopSelected = x[flowJobIds?.moduleId ?? '']?.selectedForloop - if (newForloopSelected != forloop_selected) { - clearTimeout(timeoutForloopSelected) - timeoutForloopSelected = setTimeout(() => { - forloop_selected = newForloopSelected - }, 200) - } - }) - }, 200) - } + let forloop_selected = $state( + getTopModuleStates?.[buildSubflowKey(flowJobIds?.moduleId ?? '', prefix)]?.selectedForloop + ) onDestroy(() => { destroyed = true timeout && clearTimeout(timeout) - sub?.() + // sub?.() }) function isSuccess(arg: any): boolean | undefined { @@ -649,25 +643,25 @@ } } - function onJobsLoaded(mod: FlowStatusModule, job: Job, force?: boolean): void { - if (mod.id && (mod.flow_jobs ?? []).length == 0) { - if (!childFlow) { - if ($flowStateStore?.[mod.id]) { - $flowStateStore[mod.id] = { - ...$flowStateStore[mod.id], - previewResult: job['result'], - previewArgs: job.args, - previewJobId: job.id, - previewWorkspaceId: job.workspace_id, - previewSuccess: job['success'] - } + function onJobsLoadedInner(mod: FlowStatusModule, job: Job, force?: boolean): void { + let id = mod.id + if (id && ((mod.flow_jobs ?? []).length == 0 || force)) { + // console.debug('onJobsLoadedInner', id, job.id, force) + if (flowStateStore) { + flowStateStore[buildSubflowKey(id, prefix)] = { + ...(flowStateStore?.[buildSubflowKey(id, prefix)] ?? {}), + previewResult: job['result'], + previewArgs: job.args, + previewJobId: job.id, + previewSuccess: job['success'] } } - initializeByJob(mod.id) + + initializeByJob(id) let started_at = job.started_at ? new Date(job.started_at).getTime() : undefined if (job.type == 'QueuedJob') { setModuleState( - mod.id, + id, { type: 'InProgress', job_id: job.id, @@ -679,7 +673,7 @@ }, force ) - setDurationStatusByJob(mod.id, job.id, { + setDurationStatusByJob(id, job.id, { created_at: job.created_at ? new Date(job.created_at).getTime() : undefined, started_at }) @@ -687,10 +681,10 @@ const parent_module = mod['parent_module'] // Delete existing failure node attached to the same parent module - removeFailureNode(mod.id, parent_module) + removeFailureNode(id, parent_module) setModuleState( - mod.id, + id, { args: job.args, type: job['success'] ? 'Success' : 'Failure', @@ -706,12 +700,12 @@ iteration_total: mod.iterator?.itered?.length, retries: mod?.failed_retries?.length, skipped: mod.skipped - // retries: $flowStateStore?.raw_flow + // retries: flowStateStore?.raw_flow }, force ) - setDurationStatusByJob(mod.id, job.id, { + setDurationStatusByJob(id, job.id, { created_at: job.created_at ? new Date(job.created_at).getTime() : undefined, started_at, duration_ms: job['duration_ms'] @@ -728,12 +722,13 @@ isForloop: boolean ) { if (modId) { - let globalState = globalModuleStates?.[globalModuleStates?.length - 1] - let globalStateGet = globalState ? get(globalState) : undefined - let state = globalStateGet?.[modId] + let prefixedId = buildSubflowKey(modId, prefix) + + let topModuleStates = getTopModuleStates + let state = topModuleStates?.[prefixedId] if (clicked && state?.selectedForloop) { - await globalRefreshes?.[modId]?.(true, state.selectedForloop) + await refreshGlobal?.(prefixedId, true, state.selectedForloop) } let manualOnce = state?.selectedForLoopSetManually if ( @@ -743,7 +738,7 @@ ) { let setManually = clicked || manualOnce - let newState = { + let newState: Partial = { ...(state ?? {}), selectedForloop: id, selectedForloopIndex: j, @@ -754,21 +749,20 @@ id != state?.selectedForloop || j != state?.selectedForloopIndex || setManually != state?.selectedForLoopSetManually + // console.debug('setIteration', selectedNotEqual, state, topModuleStates) if (selectedNotEqual) { - globalState?.update((topLevelModuleStates) => { - topLevelModuleStates[modId] = { + if (topModuleStates) { + topModuleStates[prefixedId] = { type: 'WaitingForPriorSteps', args: {}, ...newState } - return topLevelModuleStates - // clicked && callGlobRefresh(modId, {index: j, job: id, selectedManually: setManually ?? false}) - }) + } } } if (clicked) { - await globalRefreshes?.[modId]?.(false, id) + await refreshGlobal?.(prefixedId, false, id) } } } @@ -785,28 +779,33 @@ innerModule?.type == 'forloopflow' || innerModule?.type == 'whileloopflow' ) - if ($flowStateStore && $flowStateStore?.[modId] == undefined) { - $flowStateStore[modId] = { - ...(($flowStateStore[modId] as object) ?? {}), - previewResult: jobLoaded.args - } + let prefixedId = buildSubflowKey(modId, prefix) + + // if (flowStateStore) { + // flowStateStore[modId] = { + // ...((flowStateStore[modId] as object) ?? {}), + // previewResult: jobLoaded.args + // } + // } + if (flowStateStore && flowStateStore[prefixedId] == undefined) { + flowStateStore[prefixedId] = {} } - if ($flowStateStore?.[modId]) { - if (!childFlow) { - if ( - !$flowStateStore[modId].previewResult || - !Array.isArray($flowStateStore[modId]?.previewResult) - ) { - $flowStateStore[modId].previewResult = [] - } - $flowStateStore[modId].previewArgs = jobLoaded.args + if (flowStateStore) { + if ( + !flowStateStore?.[prefixedId]?.previewResult || + !Array.isArray(flowStateStore[prefixedId]?.previewResult) + ) { + flowStateStore[prefixedId].previewResult = [] } - if (jobLoaded.type == 'QueuedJob') { - jobResults[j] = 'Job in progress ...' - } else if (jobLoaded.type == 'CompletedJob') { - $flowStateStore[modId].previewResult[j] = jobLoaded.result - jobResults[j] = jobLoaded.result + flowStateStore[prefixedId].previewArgs = jobLoaded.args + } + if (jobLoaded.type == 'QueuedJob') { + jobResults[j] = 'Job in progress ...' + } else if (jobLoaded.type == 'CompletedJob') { + if (flowStateStore?.[prefixedId]) { + flowStateStore[prefixedId].previewResult[j] = jobLoaded.result } + jobResults[j] = jobLoaded.result } let started_at = jobLoaded.started_at ? new Date(jobLoaded.started_at).getTime() : undefined @@ -824,15 +823,15 @@ duration_ms: undefined } - let currentIndex = getTopModuleStates()?.[modId]?.selectedForloopIndex == j + let currentIndex = getTopModuleStates?.[prefixedId]?.selectedForloopIndex == j + if (currentIndex) { v.logs = jobLoaded.logs v.args = jobLoaded.args v.job_id = jobLoaded.id } - if (jobLoaded.type == 'QueuedJob') { - if (started_at && $localModuleStates[modId]?.type != 'InProgress') { + if (started_at && localModuleStates[modId]?.type != 'InProgress') { v.type = 'InProgress' } } else if (jobLoaded.type == 'CompletedJob') { @@ -842,7 +841,6 @@ } } setModuleState(modId, v, force, true) - if (jobLoaded.type == 'QueuedJob') { setDurationStatusByJob(modId, job_id, { created_at, @@ -865,7 +863,7 @@ id = innerModule?.modules?.[0]?.id } if (id) { - onJobsLoaded({ id } as FlowStatusModule, jobLoaded) + onJobsLoadedInner({ id } as FlowStatusModule, jobLoaded) } } } @@ -873,17 +871,12 @@ let flowTimeline: FlowTimeline | undefined = $state() - function loadPreviousIters(lenToAdd: number) { - let r = $localDurationStatuses[flowJobIds?.moduleId ?? ''] - if (r.iteration_from) { - r.iteration_from -= lenToAdd - $localDurationStatuses = $localDurationStatuses - globalDurationStatuses.forEach((x) => x.update((x) => x)) + function loadPreviousIters(innerKey: string, lenToAdd: number) { + let key = buildSubflowKey(innerKey, prefix) + if (globalIterationBounds[key]) { + globalIterationBounds[key].iteration_from = + (globalIterationBounds[key]?.iteration_from ?? 0) - lenToAdd } - jobResults = [ - ...[...new Array(lenToAdd).keys()].map((x) => 'not computed or loaded yet'), - ...jobResults - ] // updateSlicedListJobIds() } @@ -894,17 +887,14 @@ function removeFailureNode(id: string, parent_module: any) { if (id?.startsWith('failure-') && parent_module) { - ;[...globalModuleStates, localModuleStates].forEach((stateMapStore) => { - stateMapStore.update((stateMap) => { - if (id) { - Object.keys(stateMap).forEach((key) => { - if (stateMap[key]?.parent_module == parent_module) { - delete stateMap[key] - } - }) - } - return stateMap - }) + ;[...globalModuleStates, localModuleStates].forEach((stateMap) => { + if (id) { + Object.keys(stateMap).forEach((key) => { + if (stateMap[key]?.parent_module == parent_module) { + delete stateMap[key] + } + }) + } }) } } @@ -941,35 +931,49 @@ let subflowsSize = $state(500) + function setParentModuleState(modId: string, state: Partial) { + ;[localModuleStates, ...globalModuleStates].forEach((stateMap) => { + if (stateMap[modId]) { + stateMap[modId] = { ...stateMap[modId], ...state } + } + }) + if (prefix) { + let prefixedId = buildSubflowKey(modId, prefix) + subflowParentsGlobalModuleStates.forEach((stateMap) => { + if (stateMap[prefixedId]) { + stateMap[prefixedId] = { ...stateMap[prefixedId], ...state } + } + }) + } + } async function onSelectedIteration( detail: | { id: string; index: number; manuallySet: true; moduleId: string } | { manuallySet: false; moduleId: string } ) { + let prefixedId = buildSubflowKey(detail.moduleId, prefix) if (detail.manuallySet) { let rootJobId = detail.id await tick() - let previousId = $localModuleStates[detail.moduleId]?.selectedForloop + let previousId = getTopModuleStates?.[prefixedId]?.selectedForloop if (previousId) { - await globalRefreshes?.[detail.moduleId]?.(true, previousId) + await refreshGlobal?.(prefixedId, true, previousId) } - $localModuleStates[detail.moduleId] = { - ...$localModuleStates[detail.moduleId], + setParentModuleState(detail.moduleId, { selectedForloop: detail.id, selectedForloopIndex: detail.index, selectedForLoopSetManually: true - } + }) await tick() - await globalRefreshes?.[detail.moduleId]?.(false, rootJobId) + await refreshGlobal?.(prefixedId, false, rootJobId) } else { - $localModuleStates[detail.moduleId] = { - ...$localModuleStates[detail.moduleId], + setParentModuleState(detail.moduleId, { selectedForLoopSetManually: false - } + }) } } @@ -984,7 +988,17 @@ }) let isListJob = $derived(flowJobIds != undefined && Array.isArray(flowJobIds?.flowJobs)) $effect(() => { - flowJobIds?.moduleId && untrack(() => onModuleIdChange()) + if (isSelectedBranch) { + let modId = flowJobIds?.moduleId + if (modId) { + let selectedForloop = getTopModuleStates?.[buildSubflowKey(modId, prefix)]?.selectedForloop + untrack(() => { + if (selectedForloop != forloop_selected) { + forloop_selected = selectedForloop + } + }) + } + } }) let selected = $derived(isListJob ? 'sequence' : 'graph') as 'sequence' | 'graph' | 'logs' @@ -1006,16 +1020,17 @@
{/if} --> {#if isListJob} - {@const sliceFrom = $localDurationStatuses[flowJobIds?.moduleId ?? '']?.iteration_from ?? 0} + {@const sliceFrom = + globalIterationBounds[buildSubflowKey(flowJobIds?.moduleId ?? '', prefix)] + ?.iteration_from ?? 0} {@const lenToAdd = Math.min(20, sliceFrom)} - {#if (flowJobIds?.flowJobs.length ?? 0) > 20 && lenToAdd > 0} {@const allToAdd = (flowJobIds?.length ?? 0) - sliceFrom}

For performance reasons, only the last 20 items are shown by default @@ -1024,7 +1039,7 @@ @@ -1058,7 +1073,7 @@

{/if} {#if render} - {#if innerModules.length > 0 && !isListJob} + {#if innerModules && innerModules.length > 0 && !isListJob} Graph {#if isListJob} - {@const sliceFrom = $localDurationStatuses[flowJobIds?.moduleId ?? '']?.iteration_from ?? 0} + {@const sliceFrom = + globalIterationBounds[buildSubflowKey(flowJobIds?.moduleId ?? '', prefix)] + ?.iteration_from ?? 0}

Subflows ({flowJobIds?.flowJobs.length})

@@ -1125,9 +1142,10 @@
20)} {workspaceId} jobId={loopJobId} - on:jobsLoaded={(e) => { - let { job, force } = e.detail + onJobsLoaded={({ job, force }) => { storedListJobs[j] = job innerJobLoaded(job, j, false, force) }} {onResultStreamUpdate} graphTabOpen={selected == 'graph' && graphTabOpen} isNodeSelected={forloop_selected == loopJobId} + {globalIterationBounds} />
{/if} {/each}
- {:else if innerModules.length > 0 && (job.raw_flow?.modules.length ?? 0) > 0} + {:else if innerModules && innerModules.length > 0 && (job.raw_flow?.modules.length ?? 0) > 0} {@const hasPreprocessor = innerModules[0]?.id == 'preprocessor' ? 1 : 0}
- {#if selectedJob && 'result' in selectedJob && displayResultJob && toolbarLocationJob === 'external'} + {#if selectedJob && displayResultJob && toolbarLocationJob === 'external'} Date: Wed, 20 Aug 2025 17:28:41 +0000 Subject: [PATCH 20/42] multiple flow editor fixes --- frontend/src/lib/components/FirstStepInputs.svelte | 2 +- frontend/src/lib/components/FlowBuilder.svelte | 2 +- frontend/src/lib/components/ModulePreviewForm.svelte | 2 +- frontend/src/lib/components/ModuleTest.svelte | 3 ++- .../src/lib/components/copilot/chat/AIChatManager.svelte.ts | 2 +- .../src/lib/components/copilot/chat/flow/FlowAIChat.svelte | 2 +- .../src/lib/components/flows/content/FlowEditorPanel.svelte | 2 +- .../lib/components/flows/content/FlowModuleComponent.svelte | 2 +- frontend/src/lib/components/flows/flowStateUtils.svelte.ts | 6 +++--- .../lib/components/flows/map/FlowModuleSchemaItem.svelte | 4 ++-- .../lib/components/flows/propPicker/InputPickerInner.svelte | 2 +- frontend/src/lib/components/stepHistoryLoader.svelte.ts | 3 ++- .../tutorials/FlowBuilderTutorialBranchOne.svelte | 2 +- .../components/tutorials/FlowBuilderTutorialForLoop.svelte | 4 ++-- 14 files changed, 20 insertions(+), 18 deletions(-) diff --git a/frontend/src/lib/components/FirstStepInputs.svelte b/frontend/src/lib/components/FirstStepInputs.svelte index 775dccdda8..68bc0afe8c 100644 --- a/frontend/src/lib/components/FirstStepInputs.svelte +++ b/frontend/src/lib/components/FirstStepInputs.svelte @@ -28,7 +28,7 @@ } } $effect(() => { - flowStore.val && flowStateStore && untrack(() => loadSchema()) + flowStore.val && flowStateStore.val && untrack(() => loadSchema()) }) function handleClick() { diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 78a0363012..0573c67c40 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -1157,7 +1157,7 @@
- {#if flowStateStore} + {#if flowStateStore.val} >{}) onMount(() => { - testSteps?.updateStepArgs(mod.id, flowStateStore, flowStore?.val, previewArgs?.val) + testSteps?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val) args = testSteps?.getStepArgs(mod.id) ?? { value: {} } }) diff --git a/frontend/src/lib/components/ModuleTest.svelte b/frontend/src/lib/components/ModuleTest.svelte index aacd0b10a6..d382e2580c 100644 --- a/frontend/src/lib/components/ModuleTest.svelte +++ b/frontend/src/lib/components/ModuleTest.svelte @@ -35,7 +35,8 @@ } export function loadArgsAndRunTest() { - testSteps?.updateStepArgs(mod.id, flowStateStore, flowStore?.val, previewArgs?.val) + console.log('loadArgsAndRunTest', flowStateStore.val) + testSteps?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val) runTest(testSteps.getStepArgs(mod.id)?.value) } diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 29e4e1bef7..ed6b8f019c 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -936,7 +936,7 @@ class AIChatManager { const module = getModule(id) if (module && module.value.type === 'rawscript') { - const moduleState: FlowModuleState | undefined = flowStateStore.val[module.id] + const moduleState: FlowModuleState | undefined = flowStateStore[module.id] const editorRelated = currentEditor && currentEditor.type === 'script' && currentEditor.stepId === module.id diff --git a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte index c0688bafd8..7c9d76cafd 100644 --- a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte +++ b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte @@ -514,7 +514,7 @@ const cleanup = aiChatManager.listenForSelectedIdChanges( $selectedId, flowStore.val, - flowStateStore, + flowStateStore.val, $currentEditor ) return cleanup diff --git a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte index 763de8e3fb..668fcf2d54 100644 --- a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte +++ b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte @@ -78,7 +78,7 @@ } $effect(() => { - computeMissingInputWarnings(flowStore, flowStateStore, flowInputsStore) + computeMissingInputWarnings(flowStore, flowStateStore.val, flowInputsStore) }) diff --git a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte index 308324c4a3..40528b4dbe 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte @@ -250,7 +250,7 @@ let stepPropPicker = $derived( $executionCount != undefined && failureModule - ? getFailureStepPropPicker(flowStateStore, flowStore.val, previewArgs.val) + ? getFailureStepPropPicker(flowStateStore.val, flowStore.val, previewArgs.val) : getStepPropPicker( flowStateStore.val, parentModule, diff --git a/frontend/src/lib/components/flows/flowStateUtils.svelte.ts b/frontend/src/lib/components/flows/flowStateUtils.svelte.ts index ced1907a2b..63739f2c41 100644 --- a/frontend/src/lib/components/flows/flowStateUtils.svelte.ts +++ b/frontend/src/lib/components/flows/flowStateUtils.svelte.ts @@ -260,7 +260,7 @@ export async function createScriptFromInlineScript( return pickScript(availablePath, flowModule.summary ?? '', flowModule.id, hash) } -export function deleteFlowStateById(id: string, flowStateStore: FlowState) { +export function deleteFlowStateById(id: string, flowStateStore: StateStore) { delete flowStateStore.val[id] } @@ -295,7 +295,7 @@ export function sliceModules( export async function insertNewPreprocessorModule( flowStore: StateStore, - flowStateStore: FlowState, + flowStateStore: StateStore, inlineScript?: { language: RawScript['language'] }, @@ -325,7 +325,7 @@ export async function insertNewPreprocessorModule( export async function insertNewFailureModule( flowStore: StateStore, - flowStateStore: FlowState, + flowStateStore: StateStore, inlineScript?: { language: RawScript['language'] subkind: 'pgsql' | 'flow' diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte index ab8e565bce..27b59ef366 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaItem.svelte @@ -2,7 +2,7 @@ import { preventDefault, stopPropagation } from 'svelte/legacy' import Popover from '$lib/components/Popover.svelte' - import { classNames } from '$lib/utils' + import { classNames, type StateStore } from '$lib/utils' import { AlertTriangle, Bed, @@ -158,7 +158,7 @@ id: string | undefined, pickableIds: Record | undefined, flowPropPickerConfig: any | undefined, - flowStateStore: FlowState | undefined + flowStateStore: StateStore | undefined ) { if (!id) return connectingData = diff --git a/frontend/src/lib/components/flows/propPicker/InputPickerInner.svelte b/frontend/src/lib/components/flows/propPicker/InputPickerInner.svelte index 816804b007..11473aca9c 100644 --- a/frontend/src/lib/components/flows/propPicker/InputPickerInner.svelte +++ b/frontend/src/lib/components/flows/propPicker/InputPickerInner.svelte @@ -45,7 +45,7 @@ {#if testSteps?.isArgManuallySet(id, key)} + {#if isSelected || storedToolCallJobs[j] || toolCallIndicesToLoad.includes(j)} + { + storedToolCallJobs[j] = job + onJobsLoadedInner({ id: toolCallId } as FlowStatusModule, job, force) + }} + loadExtraLogs={(logs) => { + setModuleState(toolCallId, { + logs + }) + }} + {onResultStreamUpdate} + graphTabOpen={selected == 'graph' && graphTabOpen} + isNodeSelected={localModuleStates?.[toolCallId]?.job_id == + agentAction.job_id} + {globalIterationBounds} + /> + {/if} + {/if} + {/each} + {/if} {/if} {:else} - m).find((m) => m?.id === e) + const id = e.startsWith(AI_TOOL_CALL_PREFIX) ? e.split('-').pop() : e + const mod = dfs(job?.raw_flow?.modules ?? [], (m) => m).find( + (m) => m?.id === id + ) stepDetail = mod selectedNode = e + if (e.startsWith(AI_TOOL_CALL_PREFIX)) { + const [_prefix, _agentModuleId, j, _toolModuleId] = e.split('-') + const jIdx = Number(j) + if (!toolCallIndicesToLoad.includes(jIdx)) { + toolCallIndicesToLoad.push(jIdx) + } + } } } else { stepDetail = e @@ -1454,9 +1566,12 @@ /> {:else if rightColumnSelect == 'node_status'}
- {#if selectedNode} + {#if selectedNode?.startsWith(AI_TOOL_MESSAGE_PREFIX)} +
+ +
+ {:else if selectedNode} {@const node = localModuleStates[selectedNode]} - {#if selectedNode == 'end'} No arguments

{/if} {:else if node} + {@const module = + stepDetail && typeof stepDetail !== 'string' ? stepDetail : undefined} + {@const agentTools = + module && module.value.type === 'aiagent' ? module.value.tools : undefined} {#if node.flow_jobs_results} Result of step as collection of all subflows { + storedToolCallJobs[idx] = job + } + } + : undefined} /> {:else}

, + flow: OpenFlow & { tag?: string }, + callbacks?: Callbacks + ): Promise { + return abstractRun( + () => + JobService.runFlowPreview({ + workspace: $workspaceStore!, + requestBody: { + args, + value: flow.value, + tag: flow.tag + } + }), + callbacks + ) + } + function refreshLogOffset() { if (logOffset == 0) { logOffset = job?.logs?.length ? job.logs?.length + 1 : 0 @@ -577,6 +597,7 @@ noLogs: noLogs, noCode }) + callbacks?.change?.(job) } diff --git a/frontend/src/lib/components/LogViewerHeader.svelte b/frontend/src/lib/components/LogViewerHeader.svelte new file mode 100644 index 0000000000..93f641485b --- /dev/null +++ b/frontend/src/lib/components/LogViewerHeader.svelte @@ -0,0 +1,40 @@ + + +

+
+ +
+ +
+
+ +
diff --git a/frontend/src/lib/components/ModulePreviewResultViewer.svelte b/frontend/src/lib/components/ModulePreviewResultViewer.svelte index 6c4be420b6..4ea6c2c154 100644 --- a/frontend/src/lib/components/ModulePreviewResultViewer.svelte +++ b/frontend/src/lib/components/ModulePreviewResultViewer.svelte @@ -4,12 +4,13 @@ import ScriptFix from './copilot/ScriptFix.svelte' import type DiffEditor from './DiffEditor.svelte' import type Editor from './Editor.svelte' - import type { Script, Job, FlowModule } from '$lib/gen' + import { type Script, type Job, type FlowModule } from '$lib/gen' import OutputPickerInner from '$lib/components/flows/propPicker/OutputPickerInner.svelte' import { Pane, Splitpanes } from 'svelte-splitpanes' import type { FlowEditorContext } from './flows/types' import { getContext } from 'svelte' import { getStringError } from './copilot/chat/utils' + import AiAgentLogViewer from './AIAgentLogViewer.svelte' interface Props { lang: Script['language'] @@ -108,6 +109,15 @@ customEmptyMessage="Using pinned data" {tagLabel} /> + {:else if mod.value.type === 'aiagent' && logJob?.type === 'CompletedJob'} + {:else} - import { ScriptService, type FlowModule, type Job } from '$lib/gen' + import { ScriptService, type FlowModule, type JavascriptTransform, type Job } from '$lib/gen' import { workspaceStore } from '$lib/stores' import { getScriptByPath } from '$lib/scripts' import { getContext } 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 @@ -84,6 +85,39 @@ ) } else if (val.type == 'flow') { await jobLoader?.runFlowByPath(val.path, args, callbacks) + } else if (val.type == 'aiagent') { + const { schema } = await loadSchemaFromModule(mod) + + const inputTransforms: { [key: string]: JavascriptTransform } = Object.fromEntries( + Object.keys(args).map((key) => [ + key, + { + expr: `flow_input.${key}`, + type: 'javascript' + } + ]) + ) + + await jobLoader?.runFlowPreview( + args, + { + value: { + modules: [ + { + ...mod, + value: { + type: 'aiagent', + tools: mod.value.type == 'aiagent' ? mod.value.tools : [], + input_transforms: inputTransforms + } + } + ] + }, + summary: '', + schema + }, + callbacks + ) } else { throw Error('Not supported module type') } diff --git a/frontend/src/lib/components/copilot/MetadataGen.svelte b/frontend/src/lib/components/copilot/MetadataGen.svelte index bd47e28e0c..f9e5b0c9a6 100644 --- a/frontend/src/lib/components/copilot/MetadataGen.svelte +++ b/frontend/src/lib/components/copilot/MetadataGen.svelte @@ -12,6 +12,7 @@ import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs' import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte' + import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte' type PromptConfig = { system: string @@ -24,6 +25,7 @@ description: PromptConfig flowSummary: PromptConfig flowDescription: PromptConfig + agentToolFunctionName: PromptConfig } = { summary: { system: ` @@ -76,6 +78,18 @@ Do not include line breaks. Generate a description for the flow below: {flow}`, placeholderName: 'flow' + }, + agentToolFunctionName: { + system: ` +You are a helpful AI assistant. You generate function names from scripts. +These function names will be used by an AI agent to call this tool. +It has to respect the following regex: /[a-zA-Z0-9_]+/ +Examples: generate_image, classify_image, summarize_text, etc. +`, + user: ` +Generate a function name for the script below: +{code}`, + placeholderName: 'code' } } @@ -294,7 +308,12 @@ Generate a description for the flow below: bind:this={el} bind:value={content} placeholder={!active ? elementProps.placeholder : ''} - class={active ? '!indent-[3.5rem]' : ''} + class={twMerge( + active ? '!indent-[3.5rem]' : '', + promptConfigName === 'agentToolFunctionName' && + !validateToolName(content ?? '') && + '!border-red-400' + )} on:focus={() => (focused = true)} on:blur={() => (focused = false)} /> diff --git a/frontend/src/lib/components/copilot/chat/flow/utils.ts b/frontend/src/lib/components/copilot/chat/flow/utils.ts index cd8df07c42..02eafeb32f 100644 --- a/frontend/src/lib/components/copilot/chat/flow/utils.ts +++ b/frontend/src/lib/components/copilot/chat/flow/utils.ts @@ -77,13 +77,13 @@ export function getNestedModules(flow: OpenFlow, id: string, branchIndex?: numbe } return branch.modules + } else if (module.value.type === 'aiagent') { + return module.value.tools } else { throw new Error('Module is not a loop or branch') } } - - export function aiModuleActionToBgColor(action: AIModuleAction | undefined) { switch (action) { case 'modified': @@ -95,4 +95,4 @@ export function aiModuleActionToBgColor(action: AIModuleAction | undefined) { default: return '' } -} \ No newline at end of file +} diff --git a/frontend/src/lib/components/flows/common/FlowCard.svelte b/frontend/src/lib/components/flows/common/FlowCard.svelte index e02f5bc91a..96bff1f674 100644 --- a/frontend/src/lib/components/flows/common/FlowCard.svelte +++ b/frontend/src/lib/components/flows/common/FlowCard.svelte @@ -10,6 +10,7 @@ flowModuleValue?: FlowModuleValue | undefined header?: import('svelte').Snippet children?: import('svelte').Snippet + isAgentTool?: boolean } let { @@ -19,14 +20,15 @@ noHeader = false, flowModuleValue = undefined, header, - children + children, + isAgentTool = false }: Props = $props()
{#if !noEditor && !noHeader}
- + {@render header?.()}
diff --git a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte index 1fa16b2674..07d1a0eb37 100644 --- a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte +++ b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte @@ -17,19 +17,23 @@ import { workspaceStore } from '$lib/stores' import { Lock, RefreshCw, Unlock } from 'lucide-svelte' import { createEventDispatcher, untrack } from 'svelte' + import { twMerge } from 'tailwind-merge' + import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte' interface Props { flowModuleValue?: FlowModuleValue | undefined title?: string | undefined summary?: string | undefined children?: import('svelte').Snippet + isAgentTool?: boolean } let { flowModuleValue = undefined, title = undefined, summary = $bindable(undefined), - children + children, + isAgentTool = false }: Props = $props() let latestHash: string | undefined = $state(undefined) @@ -81,11 +85,11 @@
{:else if flowModuleValue.type === 'script' && 'path' in flowModuleValue && flowModuleValue.path} @@ -139,7 +143,14 @@ >
{/if} - + {:else if flowModuleValue.type === 'flow'} flow diff --git a/frontend/src/lib/components/flows/content/FlowInputsQuick.svelte b/frontend/src/lib/components/flows/content/FlowInputsQuick.svelte index 238b0450e9..a670f83bb6 100644 --- a/frontend/src/lib/components/flows/content/FlowInputsQuick.svelte +++ b/frontend/src/lib/components/flows/content/FlowInputsQuick.svelte @@ -159,7 +159,8 @@ ['For loop', 'forloop'], ['While loop', 'whileloop'], ['Branch to one', 'branchone'], - ['Branch to all', 'branchall'] + ['Branch to all', 'branchall'], + ['AI Agent', 'aiagent'] ] let topLevelNodes: [string, string][] = $state([]) function computeToplevelNodeChoices(funcDesc: string, preFilter: 'all' | 'workspace' | 'hub') { diff --git a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte index 40528b4dbe..d00341032d 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte @@ -80,6 +80,7 @@ savedModule?: FlowModule | undefined forceTestTab?: boolean highlightArg?: string + isAgentTool?: boolean } let { @@ -94,7 +95,8 @@ enableAi, savedModule = undefined, forceTestTab = false, - highlightArg = undefined + highlightArg = undefined, + isAgentTool = false }: Props = $props() let workspaceScriptTag: string | undefined = $state(undefined) @@ -111,7 +113,8 @@ ruff: false, shellcheck: false }) - let selected = $state(preprocessorModule ? 'test' : 'inputs') + + let selected = $state(preprocessorModule || isAgentTool ? 'test' : 'inputs') let advancedSelected = $state('retries') let advancedRuntimeSelected = $state('concurrency') let s3Kind = $state('s3_client') @@ -147,6 +150,7 @@ reloadError = undefined try { const { input_transforms, schema } = await loadSchemaFromModule(flowModule) + console.log('reload', schema) validCode = true if (inputTransformSchemaForm) { @@ -155,7 +159,8 @@ if ( flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || - flowModule.value.type == 'flow' + flowModule.value.type == 'flow' || + flowModule.value.type == 'aiagent' ) { if (!deepEqual(flowModule.value.input_transforms, input_transforms)) { flowModule.value.input_transforms = input_transforms @@ -336,6 +341,7 @@ } }} bind:summary={flowModule.summary} + {isAgentTool} > {#snippet header()} - - {#if flowModule.value.type === 'rawscript'} - {#if !noEditor} - {#key flowModule.id} -
- {#if assets?.length} - - {/if} -
- { - selected = 'test' - if ($selectedId == flowModule.id) { - if (flowModule.value.type === 'rawscript' && editor) { - flowModule.value.content = editor.getCode() + {#if flowModule.value.type !== 'aiagent'} + + {#if flowModule.value.type === 'rawscript'} + {#if !noEditor} + {#key flowModule.id} +
+ {#if assets?.length} + + {/if} +
+ { + selected = 'test' + if ($selectedId == flowModule.id) { + if (flowModule.value.type === 'rawscript' && editor) { + flowModule.value.content = editor.getCode() + } + await reload(flowModule) + modulePreview?.runTestWithStepArgs() } - await reload(flowModule) - modulePreview?.runTestWithStepArgs() - } - }} - on:change={async (event) => { - const content = event.detail - if (flowModule.value.type === 'rawscript') { - if (flowModule.value.content !== content) { - flowModule.value.content = content + }} + on:change={async (event) => { + const content = event.detail + if (flowModule.value.type === 'rawscript') { + if (flowModule.value.content !== content) { + flowModule.value.content = content + } + await reload(flowModule) } - await reload(flowModule) - } - }} - formatAction={() => { - reload(flowModule) - saveDraft() - }} - fixedOverflowWidgets={true} - args={Object.entries(flowModule.value.input_transforms).reduce( - (acc, [key, obj]) => { - acc[key] = obj.type === 'static' ? obj.value : undefined - return acc - }, - {} - )} - key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`} - /> - - {/key} - {/if} - {:else if flowModule.value.type === 'script'} - {#if !noEditor && (customUi?.hubCode != false || !flowModule?.value?.path?.startsWith('hub/'))} -
- {#key forceReload} - { + reload(flowModule) + saveDraft() + }} + fixedOverflowWidgets={true} + args={Object.entries(flowModule.value.input_transforms).reduce( + (acc, [key, obj]) => { + acc[key] = obj.type === 'static' ? obj.value : undefined + return acc + }, + {} + )} + key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`} + /> + {/key} -
+ {/if} + {:else if flowModule.value.type === 'script'} + {#if !noEditor && (customUi?.hubCode != false || !flowModule?.value?.path?.startsWith('hub/'))} +
+ {#key forceReload} + + {/key} +
+ {/if} + {:else if flowModule.value.type === 'flow'} + {#key forceReload} + + {/key} {/if} - {:else if flowModule.value.type === 'flow'} - {#key forceReload} - - {/key} - {/if} -
- + + {/if} + { + if (flowModule.value.type === 'aiagent') { + return 100 + } + return editorSettingsPanelSize + }, + (v) => { + if (flowModule.value.type !== 'aiagent') { + editorSettingsPanelSize = v + } + } + } + minSize={20} + > - {#if !preprocessorModule} + {#if !preprocessorModule && !isAgentTool} Step Input {/if} Test this step - {#if !preprocessorModule} + {#if !preprocessorModule && !isAgentTool} Advanced {/if} @@ -521,7 +544,7 @@ ? 'h-[calc(100%-68px)]' : 'h-[calc(100%-34px)]'} > - {#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')} + {#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow' || flowModule.value.type == 'aiagent')}
highlightArg?: Record + isAgentTool?: boolean } let { @@ -52,7 +53,8 @@ parentModule = $bindable(), previousModule = undefined, forceTestTab, - highlightArg + highlightArg, + isAgentTool = false }: Props = $props() function initializePrimaryScheduleForTriggerScript(module: FlowModule) { @@ -188,7 +190,7 @@ preprocessorModule={$selectedId === 'preprocessor'} /> {/if} - {:else if flowModule.value.type === 'rawscript' || flowModule.value.type === 'script' || flowModule.value.type === 'flow'} + {:else if flowModule.value.type === 'rawscript' || flowModule.value.type === 'script' || flowModule.value.type === 'flow' || flowModule.value.type === 'aiagent'} {/if} {:else if flowModule.value.type === 'forloopflow' || flowModule.value.type == 'whileloopflow'} @@ -289,4 +292,13 @@ {/each} {/if} {/each} +{:else if flowModule.value.type === 'aiagent'} + {#each flowModule.value.tools as _, index (index)} + + {/each} {/if} diff --git a/frontend/src/lib/components/flows/dfs.ts b/frontend/src/lib/components/flows/dfs.ts index 9b68b8b99e..20b487ac72 100644 --- a/frontend/src/lib/components/flows/dfs.ts +++ b/frontend/src/lib/components/flows/dfs.ts @@ -2,7 +2,8 @@ import type { FlowModule } from '$lib/gen' export function dfs( modules: FlowModule[], - f: (x: FlowModule, modules: FlowModule[], branches: FlowModule[][]) => T + f: (x: FlowModule, modules: FlowModule[], branches: FlowModule[][]) => T, + { skipToolNodes = false }: { skipToolNodes?: boolean } = {} ): T[] { let result: T[] = [] for (const module of modules) { @@ -22,6 +23,9 @@ export function dfs( for (const branch of allBranches) { result = result.concat(dfs(branch, f)) } + } else if (module.value.type == 'aiagent' && !skipToolNodes) { + result = result.concat(f(module, modules, [module.value.tools])) + result = result.concat(dfs(module.value.tools, f)) } else { result.push(f(module, modules, [])) } diff --git a/frontend/src/lib/components/flows/flowInfers.ts b/frontend/src/lib/components/flows/flowInfers.ts index 9e20e6ac6a..00b6c608c1 100644 --- a/frontend/src/lib/components/flows/flowInfers.ts +++ b/frontend/src/lib/components/flows/flowInfers.ts @@ -44,7 +44,7 @@ export async function loadSchemaFromModule(module: FlowModule): Promise<{ : { type: 'static', value: undefined - }) + }) accu[key] = nv return accu }, {}) @@ -54,6 +54,84 @@ export async function loadSchemaFromModule(module: FlowModule): Promise<{ input_transforms: input_transforms, schema: schema ?? emptySchema() } + } else if (mod.type === 'aiagent') { + const schema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + properties: { + provider: { + type: 'object', + oneOf: [ + { + type: 'object', + title: 'OpenAI', + properties: { + kind: { type: 'string', enum: ['OpenAI'] }, + resource: { + type: 'object', + format: 'resource-openai' + }, + + model: { + type: 'string', + enum: ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-4.1', 'gpt-4o', 'gpt-4o-mini'] + } + }, + required: ['kind', 'resource', 'model'] + }, + { + type: 'object', + title: 'Anthropic', + properties: { + kind: { type: 'string', enum: ['Anthropic'] }, + resource: { + type: 'object', + format: 'resource-anthropic' + }, + model: { + type: 'string', + enum: ['claude-sonnet-4-0', 'claude-3-7-sonnet-latest', 'claude-3-5-haiku-latest'] + } + }, + required: ['kind', 'resource', 'model'] + } + ] + }, + system_prompt: { + type: 'string', + default: 'You are a helpful assistant' + }, + user_message: { + type: 'string' + }, + max_completion_tokens: { + type: 'number' + }, + temperature: { + type: 'number' + } + }, + required: ['provider', 'model', 'system_prompt', 'user_message'], + type: 'object', + order: [ + 'provider', + 'model', + 'system_prompt', + 'user_message', + 'max_completion_tokens', + 'temperature' + ] + } + let input_transforms = mod.input_transforms ?? {} + return { + input_transforms: Object.keys(schema?.properties ?? {}).reduce((accu, key) => { + accu[key] = input_transforms[key] ?? { + type: 'static', + value: undefined + } + return accu + }, {}), + schema + } } return { diff --git a/frontend/src/lib/components/flows/flowStateUtils.svelte.ts b/frontend/src/lib/components/flows/flowStateUtils.svelte.ts index 63739f2c41..5b2e91320d 100644 --- a/frontend/src/lib/components/flows/flowStateUtils.svelte.ts +++ b/frontend/src/lib/components/flows/flowStateUtils.svelte.ts @@ -28,7 +28,8 @@ export async function loadFlowModuleState(flowModule: FlowModule): Promise { + const aiAgentFlowModules: FlowModule = { + id, + value: { type: 'aiagent', tools: [], input_transforms: {} }, + summary: 'AI Agent' + } + + const flowModuleState = await loadFlowModuleState(aiAgentFlowModules) + + return [aiAgentFlowModules, flowModuleState] +} + export async function createFlow(id: string): Promise<[FlowModule, FlowModuleState]> { const flowFlowModules: FlowModule = { id, diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte index 4d54b2c775..fde5f996d9 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte @@ -11,7 +11,8 @@ emptyModule, pickScript, pickFlow, - insertNewPreprocessorModule + insertNewPreprocessorModule, + createAiAgent } from '$lib/components/flows/flowStateUtils.svelte' import type { FlowModule, Job, ScriptLang } from '$lib/gen' import { emptyFlowModuleState } from '../utils' @@ -139,6 +140,8 @@ ;[module, state] = await createBranches(module.id) } else if (kind == 'branchall') { ;[module, state] = await createBranchAll(module.id) + } else if (kind == 'aiagent') { + ;[module, state] = await createAiAgent(module.id) } else if (inlineScript) { const { language, kind, subkind, summary } = inlineScript ;[module, state] = await createInlineScriptModule(language, kind, subkind, module.id, summary) @@ -190,6 +193,8 @@ return branch }) mod.value.default = removeAtId(mod.value.default, id) + } else if (mod.value.type == 'aiagent') { + mod.value.tools = removeAtId(mod.value.tools, id) } return mod }) @@ -448,6 +453,8 @@ } } else if (mod.id == detail.sourceId || mod.id == detail.targetId) { targetModules = modules + } else if (mod.id == detail.agentId && mod.value.type === 'aiagent') { + targetModules = mod.value.tools } }) if (flowStore.val.value.modules && Array.isArray(flowStore.val.value.modules)) { @@ -479,7 +486,8 @@ }) } } else { - const index = detail.index ?? 0 + const index = (detail.agentId ? targetModules?.length : detail.index) ?? 0 + await insertNewModuleAtIndex( targetModules, index, diff --git a/frontend/src/lib/components/flows/map/InsertModuleInner.svelte b/frontend/src/lib/components/flows/map/InsertModuleInner.svelte index 196c38afe7..609ffebdb1 100644 --- a/frontend/src/lib/components/flows/map/InsertModuleInner.svelte +++ b/frontend/src/lib/components/flows/map/InsertModuleInner.svelte @@ -14,6 +14,7 @@ export let disableAi = false export let kind: 'script' | 'trigger' | 'preprocessor' | 'failure' = 'script' export let allowTrigger = true + export let scriptOnly = false let customUi: undefined | FlowBuilderWhitelabelCustomUi = getContext('customUi') let selectedKind: 'script' | 'trigger' | 'preprocessor' | 'approval' | 'flow' | 'failure' = kind @@ -66,7 +67,7 @@ shouldUsePortal={true} -->
- {#if kind === 'script'} + {#if kind === 'script' && !scriptOnly}
dispatch('new', { kind: 'branchall' }) }} /> + { + dispatch('close') + dispatch('new', { kind: 'aiagent' }) + }} + />
{/if} diff --git a/frontend/src/lib/components/flows/map/MapItem.svelte b/frontend/src/lib/components/flows/map/MapItem.svelte index 9aec1d13ad..96cd4ac192 100644 --- a/frontend/src/lib/components/flows/map/MapItem.svelte +++ b/frontend/src/lib/components/flows/map/MapItem.svelte @@ -3,7 +3,7 @@ import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte' import IconedResourceType from '$lib/components/IconedResourceType.svelte' import type { FlowModule, FlowStatusModule, Job } from '$lib/gen' - import { Building, Repeat, Square, ArrowDown, GitBranch } from 'lucide-svelte' + import { Building, Repeat, Square, ArrowDown, GitBranch, Bot } from 'lucide-svelte' import { createEventDispatcher, getContext } from 'svelte' import type { Writable } from 'svelte/store' import FlowModuleSchemaItem from './FlowModuleSchemaItem.svelte' @@ -282,7 +282,9 @@ > {#snippet icon()}
- {#if mod.value.type === 'rawscript'} + {#if mod.value.type === 'aiagent'} + + {:else if mod.value.type === 'rawscript'} {:else if mod.summary == 'Terminate flow'} diff --git a/frontend/src/lib/components/flows/pickers/TopLevelNode.svelte b/frontend/src/lib/components/flows/pickers/TopLevelNode.svelte index 16af9850ed..c00ed14d5c 100644 --- a/frontend/src/lib/components/flows/pickers/TopLevelNode.svelte +++ b/frontend/src/lib/components/flows/pickers/TopLevelNode.svelte @@ -1,6 +1,15 @@ + + + + + {#snippet children({ darkMode })} + {@const bgColor = getStateColor(flowModuleState?.type, darkMode, true, false)} + {@const bgHoverColor = getStateHoverColor(flowModuleState?.type, darkMode, true, false)} + + +
(hover = true)} onmouseleave={() => (hover = false)}> + + {#if data.insertable} + + {/if} +
+ {/snippet} +
diff --git a/frontend/src/lib/components/graph/renderers/nodes/NewAIToolNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/NewAIToolNode.svelte new file mode 100644 index 0000000000..b9f3a900d2 --- /dev/null +++ b/frontend/src/lib/components/graph/renderers/nodes/NewAIToolNode.svelte @@ -0,0 +1,69 @@ + + + + {#snippet children({ darkMode })} + + + +tool + + { + close() + }} + on:new={(e) => { + data.eventHandlers.insert({ + index: -1, // ignored when agentId is set + agentId: data.agentModuleId, + ...e.detail + }) + close() + }} + on:insert={(e) => { + data.eventHandlers.insert({ + index: -1, // ignored when agentId is set + agentId: data.agentModuleId, + ...e.detail + }) + close() + }} + on:pickScript={(e) => { + data.eventHandlers.insert({ + index: -1, // ignored when agentId is set + agentId: data.agentModuleId, + kind: e.detail.kind, + script: { + ...e.detail, + summary: e.detail.summary + ? e.detail.summary.replace(/\s/, '_').replace(/[^a-zA-Z0-9_]/g, '') + : e.detail.path.split('/').pop() + } + }) + close() + }} + /> + + + {/snippet} + diff --git a/frontend/src/lib/components/tutorials/utils.ts b/frontend/src/lib/components/tutorials/utils.ts index b0eed886b1..d6b4af8240 100644 --- a/frontend/src/lib/components/tutorials/utils.ts +++ b/frontend/src/lib/components/tutorials/utils.ts @@ -240,6 +240,8 @@ export function updateFlowModuleById( module.value.branches.forEach((branch) => dfs(branch.modules)) } else if (module.value.type === 'branchall') { module.value.branches.forEach((branch) => dfs(branch.modules)) + } else if (module.value.type === 'aiagent') { + dfs(module.value.tools) } } } diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index ab335e5af2..94c2d380b6 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -762,7 +762,7 @@ {/if} {job.script_path ?? (job.job_kind == 'dependencies' ? 'lock dependencies' : 'No path')}
- {#if job.script_hash} + {#if job.script_hash && job.job_kind !== 'aiagent'} {#if job.job_kind == 'script'} {truncateHash(job.script_hash)} {/if}
- {#if job?.workflow_as_code_status} + {#if job?.workflow_as_code_status && job.job_kind !== 'aiagent'}
Date: Wed, 20 Aug 2025 22:55:44 +0000 Subject: [PATCH 27/42] nit fix --- frontend/src/lib/components/ModulePreviewForm.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/components/ModulePreviewForm.svelte b/frontend/src/lib/components/ModulePreviewForm.svelte index 58c0ee6782..531fd9f24e 100644 --- a/frontend/src/lib/components/ModulePreviewForm.svelte +++ b/frontend/src/lib/components/ModulePreviewForm.svelte @@ -125,7 +125,7 @@ label={argName} description={schema.properties[argName].description} bind:value={ - () => testSteps?.getStepInputArgs(mod.id, argName) ?? {}, + () => testSteps?.getStepInputArgs(mod.id, argName), (v) => testSteps?.setStepInputArgs(mod.id, argName, v) } type={schema.properties[argName].type} From 54f36fcce7220142442d5d0412f17814c563db65 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 21 Aug 2025 01:35:59 +0000 Subject: [PATCH 28/42] remove default db port on docker-compose --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 82c66a9e69..f8831523ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,8 +19,6 @@ services: - db_data:/var/lib/postgresql/data expose: - 5432 - ports: - - 5432:5432 environment: POSTGRES_PASSWORD: changeme POSTGRES_DB: windmill From 28f1d611643459d42531fa217c185408eb97d6d1 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Thu, 21 Aug 2025 13:59:16 +0200 Subject: [PATCH 29/42] fix(frontend): graph cache of ai agent step tools (#6431) --- .../graph/renderers/nodes/AIToolNode.svelte | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte index 040871ad6b..8900e471f1 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte @@ -28,6 +28,20 @@ : AI_TOOL_MESSAGE_PREFIX + '-' + agentModuleId + '-' + idx } + function getComparableNode(node: Node & NodeLayout): Node & NodeLayout { + if (node.type === 'module' && node.data.module.value.type === 'aiagent') { + return { + ...node, + data: { + ...node.data, + module: $state.snapshot(node.data.module) // module is a proxy object so we need to snapshot to be able to compare + } + } + } else { + return node + } + } + export function computeAIToolNodes( nodes: (Node & NodeLayout)[], eventHandlers: GraphEventHandlers, @@ -41,7 +55,7 @@ if ( computeAIToolNodesCache && !!flowModuleStates === computeAIToolNodesCache.hasFlowModuleStates && - deepEqual(nodes, computeAIToolNodesCache.nodes) + deepEqual(nodes.map(getComparableNode), computeAIToolNodesCache.nodes) ) { return computeAIToolNodesCache.ret } @@ -201,7 +215,7 @@ } computeAIToolNodesCache = { - nodes, + nodes: nodes.map(getComparableNode), hasFlowModuleStates: !!flowModuleStates, ret } From d152e8e58f80fddc1ec240ca515f178985c8ed69 Mon Sep 17 00:00:00 2001 From: pyranota <92104930+pyranota@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:14:29 +0200 Subject: [PATCH 30/42] add `go1_22_compat` annotation (#6432) * bump go to 1.25 Signed-off-by: pyranota * Update Dockerfile * add comms --------- Signed-off-by: pyranota --- backend/windmill-common/src/worker.rs | 6 ++++++ backend/windmill-worker/src/go_executor.rs | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 50ed13ff7b..7ec4cd09f5 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -642,6 +642,12 @@ pub struct PythonAnnotations { pub py313: bool, } +#[derive(Copy, Clone)] +#[annotations("//")] +pub struct GoAnnotations { + pub go1_22_compat: bool, +} + #[annotations("//")] pub struct TypeScriptAnnotations { pub npm: bool, diff --git a/backend/windmill-worker/src/go_executor.rs b/backend/windmill-worker/src/go_executor.rs index 1f09f325de..8fe187b8dd 100644 --- a/backend/windmill-worker/src/go_executor.rs +++ b/backend/windmill-worker/src/go_executor.rs @@ -12,7 +12,7 @@ use uuid::Uuid; use windmill_common::{ error::{self, Error}, utils::calculate_hash, - worker::{save_cache, write_file, Connection}, + worker::{save_cache, write_file, Connection, GoAnnotations}, }; use windmill_parser_go::{parse_go_imports, REQUIRE_PARSE}; use windmill_queue::{append_logs, CanceledBy, MiniPulledJob}; @@ -464,6 +464,7 @@ pub async fn install_go_dependencies( w_id: &str, occupation_metrics: &mut OccupancyMetrics, ) -> error::Result { + let anns = GoAnnotations::parse(code); if raw_deps { let go_mod = if let Some(module) = code.lines().find(|l| l.trim_start().starts_with("module ")) { @@ -529,7 +530,11 @@ pub async fn install_go_dependencies( } else { "".to_string() }; - let hash = format!("go-{}", hash); + let hash = format!( + "go{}-{}", + if anns.go1_22_compat { "1.22" } else { "" }, + hash + ); let mut skip_tidy = has_sum; @@ -579,6 +584,11 @@ pub async fn install_go_dependencies( .args(vec!["mod", mod_command]) .stdout(Stdio::piped()) .stderr(Stdio::piped()); + // If annotation used we want to call tidy with special flag to pin go to 1.22 + // The reason for this that at some point we had to jump from go 1.22 to 1.25 and this addds backward compatibility. + if anns.go1_22_compat && mod_command == "tidy" { + child_cmd.args(vec!["-go", "1.22"]); + } #[cfg(windows)] set_windows_env_vars(&mut child_cmd); From c92bfe6601fd96f6d74860f52f9307e02961ac21 Mon Sep 17 00:00:00 2001 From: Fred Reimer Date: Thu, 21 Aug 2025 11:15:09 -0400 Subject: [PATCH 31/42] feat: bump Go version from 1.22.0 to 1.25.0 #6415 Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3815744d1e..3d56c251f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -90,7 +90,7 @@ ARG POWERSHELL_VERSION=7.5.0 ARG POWERSHELL_DEB_VERSION=7.5.0-1 ARG KUBECTL_VERSION=1.28.7 ARG HELM_VERSION=3.14.3 -ARG GO_VERSION=1.22.5 +ARG GO_VERSION=1.25.0 ARG APP=/usr/src/app ARG WITH_POWERSHELL=true ARG WITH_KUBECTL=true From 51ea9473ef23c6871699e69bbe79772a4d50d3b8 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 21 Aug 2025 17:32:47 +0000 Subject: [PATCH 32/42] fix(app): fix ctrl drag for insertion into subgrids --- .../components/apps/editor/GridEditor.svelte | 8 +-- .../apps/editor/SubGridEditor.svelte | 11 ++- .../components/apps/svelte-grid/Grid.svelte | 67 ++++++++++++------- .../apps/svelte-grid/MoveResize.svelte | 41 +++++++----- 4 files changed, 74 insertions(+), 53 deletions(-) diff --git a/frontend/src/lib/components/apps/editor/GridEditor.svelte b/frontend/src/lib/components/apps/editor/GridEditor.svelte index 4b541cf2ef..14482470d7 100644 --- a/frontend/src/lib/components/apps/editor/GridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/GridEditor.svelte @@ -200,14 +200,12 @@ allIdsInPath={$allIdsInPath} selectedIds={$selectedComponent} items={$app.grid} - on:redraw={(e) => { + onRedraw={(grid) => { push(history, $app) - $app.grid = e.detail + $app.grid = grid }} root - on:dropped={(e) => { - const { id, overlapped, x, y } = e.detail - + onDropped={({ id, overlapped, x, y }) => { const overlappedComponent = findGridItem($app, overlapped) if (overlappedComponent && !isContainer(overlappedComponent.data.type)) { diff --git a/frontend/src/lib/components/apps/editor/SubGridEditor.svelte b/frontend/src/lib/components/apps/editor/SubGridEditor.svelte index f0947dd213..7d361fee29 100644 --- a/frontend/src/lib/components/apps/editor/SubGridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/SubGridEditor.svelte @@ -218,7 +218,7 @@ {#if $mode !== 'preview'}
{ + onRedraw={(grid) => { push(editorContext?.history, $app) if ($app.subgrids) { - $app.subgrids[subGridId] = e.detail + $app.subgrids[subGridId] = grid } }} selectedIds={$selectedComponent} scroller={container} parentWidth={$parentWidth - 17} {containerWidth} - on:dropped={(e) => { - const { id, overlapped, x, y } = e.detail - + onDropped={({ id, overlapped, x, y }) => { if (!overlapped) { moveToRoot(id, { x, y }) } else { @@ -257,7 +255,6 @@ if (id === overlapped) { return } - moveComponentBetweenSubgrids( id, overlapped, diff --git a/frontend/src/lib/components/apps/svelte-grid/Grid.svelte b/frontend/src/lib/components/apps/svelte-grid/Grid.svelte index 87232c1029..f20ee9baf1 100644 --- a/frontend/src/lib/components/apps/svelte-grid/Grid.svelte +++ b/frontend/src/lib/components/apps/svelte-grid/Grid.svelte @@ -16,7 +16,7 @@ import { getContainerHeight } from './utils/container' import { moveItem, getItemById, specifyUndefinedColumns } from './utils/item' - import { onMount, createEventDispatcher, getContext } from 'svelte' + import { onMount, getContext } from 'svelte' import { getColumn, throttle } from './utils/other' import MoveResize from './MoveResize.svelte' import type { FilledItem } from './types' @@ -34,8 +34,6 @@ type GridShadow } from '../editor/appUtils' - const dispatch = createEventDispatcher() - type T = $$Generic const { app, worldStore } = getContext('AppViewerContext') @@ -55,6 +53,20 @@ parentWidth?: number | undefined disableMove?: boolean children?: import('svelte').Snippet<[any]> + onDropped?: (e: { id: string; overlapped: string; x: number; y: number }) => void + onRedraw?: (grid: FilledItem[]) => void + onResize?: (e: { + cols: number + xPerPx: number + yPerPx: number + width: number | undefined + }) => void + onMounted?: (e: { + cols: number + xPerPx: number + yPerPx: number + width: number | undefined + }) => void } let { @@ -71,7 +83,11 @@ root = false, parentWidth = undefined, disableMove = false, - children + children, + onDropped, + onRedraw, + onResize, + onMounted }: Props = $props() const cols = columnConfiguration @@ -83,10 +99,10 @@ let xPerPx = $state(0) let yPerPx = rowHeight - const onResize = throttle(() => { + const onResizeThrottled = throttle(() => { if (!getComputedCols) return sortedItems = specifyUndefinedColumns(sortedItems, getComputedCols, cols) - dispatch('resize', { + onResize?.({ cols: getComputedCols, xPerPx, yPerPx, @@ -116,13 +132,14 @@ if (!containerWidth && getComputedCols) { sortedItems = specifyUndefinedColumns(sortedItems, getComputedCols, cols) - dispatch('mount', { + onMounted?.({ cols: getComputedCols, xPerPx, - yPerPx // same as rowHeight + yPerPx, + width }) } else { - onResize() + onResizeThrottled() } containerWidth = width @@ -162,8 +179,7 @@ }) : [] } - const updateMatrix = ({ detail }) => { - let isPointerUp = detail.isPointerUp + const updateMatrix = ({ isPointerUp, id, activate }) => { let citems: FilledItem[] if (isPointerUp) { if (initItems == undefined) { @@ -179,8 +195,8 @@ citems = smartCopy(initItems) } let nselectedIds = selectedIds ?? [] - if (detail.id && !selectedIds?.includes(detail.id)) { - nselectedIds = [detail.id, ...(selectedIds ?? [])] + if (id && !selectedIds?.includes(id)) { + nselectedIds = [id, ...(selectedIds ?? [])] } for (let id of nselectedIds) { let activeItem = getItemById(id, citems) @@ -235,13 +251,13 @@ } for (let id of nselectedIds ?? []) { - if (detail.activate) { + if (activate) { moveResizes?.[id]?.inActivate() } } if (isPointerUp && getComputedCols) { - dispatch('redraw', sortGridItemsPosition(smartCopy(sortedItems), getComputedCols)) + onRedraw?.(sortGridItemsPosition(smartCopy(sortedItems), getComputedCols)) } } @@ -259,11 +275,11 @@ } | undefined = $state(undefined) - const handleRepaint = ({ detail }) => { - if (!detail.isPointerUp) { - throttleMatrix({ detail }) + const handleRepaint = ({ isPointerUp, id, activate }) => { + if (!isPointerUp) { + throttleMatrix({ isPointerUp, id, activate }) } else { - updateMatrix({ detail }) + updateMatrix({ isPointerUp, id, activate }) } /** @@ -488,19 +504,19 @@ {/if} handleInitMove(item.id)} + onInitMove={() => handleInitMove(item.id)} onMove={handleMove} bind:shadow={shadows[item.id]} bind:this={moveResizes[item.id]} - on:repaint={handleRepaint} - on:resizeStart={() => (resizing = true)} - on:resizeEnd={() => (resizing = false)} + onRepaint={handleRepaint} + onResizeStart={() => (resizing = true)} + onResizeEnd={() => (resizing = false)} onTop={Boolean(allIdsInPath?.includes(item.id))} id={item.id} {xPerPx} {yPerPx} fakeShadow={$fakeShadowStore} - on:dropped={(e) => { + onDropped={({ id, overlapped, x, y }) => { $componentDraggedIdStore = undefined $componentDraggedParentIdStore = undefined $overlappedStore = undefined @@ -510,8 +526,7 @@ if ($moveMode === 'move') { return } - - dispatch('dropped', e.detail) + onDropped?.({ id, overlapped, x, y }) }} width={xPerPx == 0 ? 0 diff --git a/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte b/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte index 6a558d396d..d7d6420b79 100644 --- a/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte +++ b/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte @@ -1,5 +1,5 @@ @@ -206,24 +207,28 @@ }} root onDropped={({ id, overlapped, x, y }) => { - const overlappedComponent = findGridItem($app, overlapped) + const overlappedComponent = overlapped ? findGridItem($app, overlapped) : undefined if (overlappedComponent && !isContainer(overlappedComponent.data.type)) { return } - if (!overlapped) { - return - } - if (id === overlapped) { return } moveComponentBetweenSubgrids( id, - overlapped, - subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore), + overlapped + ? { + parentComponentId: overlapped, + subGridIndex: subGridIndexKey( + overlappedComponent?.data?.type, + overlapped, + $worldStore + ) + } + : undefined, { x, y } ) }} diff --git a/frontend/src/lib/components/apps/editor/SubGridEditor.svelte b/frontend/src/lib/components/apps/editor/SubGridEditor.svelte index 7d361fee29..25ee011644 100644 --- a/frontend/src/lib/components/apps/editor/SubGridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/SubGridEditor.svelte @@ -3,7 +3,7 @@ import { push } from '$lib/history.svelte' import { classNames } from '$lib/utils' - import { createEventDispatcher, getContext, onDestroy } from 'svelte' + import { getContext, onDestroy } from 'svelte' import { twMerge } from 'tailwind-merge' import { gridColumns, isFixed, toggleFixed } from '../gridUtils' import Grid from '../svelte-grid/Grid.svelte' @@ -34,6 +34,7 @@ visible?: boolean id: string shouldHighlight?: boolean + onFocus?: () => void } let { @@ -46,11 +47,10 @@ subGridId, visible = true, id, - shouldHighlight = true + shouldHighlight = true, + onFocus }: Props = $props() - const dispatch = createEventDispatcher() - const { app, connectingInput, @@ -80,7 +80,7 @@ let highlight = $derived(id === $focusedGrid?.parentComponentId && shouldHighlight) const onpointerdown = (e) => { - dispatch('focus') + onFocus?.() } function selectComponent(e: PointerEvent, id: string) { diff --git a/frontend/src/lib/components/apps/editor/appUtils.ts b/frontend/src/lib/components/apps/editor/appUtils.ts index 415b13ccaf..d4e2a2e995 100644 --- a/frontend/src/lib/components/apps/editor/appUtils.ts +++ b/frontend/src/lib/components/apps/editor/appUtils.ts @@ -39,14 +39,14 @@ import gridHelp from '../svelte-grid/utils/helper' type GridItemLocation = | { - type: 'grid' - gridItemIndex: number - } + type: 'grid' + gridItemIndex: number + } | { - type: 'subgrid' - subgridItemIndex: number - subgridKey: string - } + type: 'subgrid' + subgridItemIndex: number + subgridKey: string + } interface GridItemWithLocation { location: GridItemLocation item: GridItem @@ -187,7 +187,7 @@ export function selectId( selectedComponent: Writable, app: App ) { - ;(document?.activeElement as HTMLElement)?.blur() + ; (document?.activeElement as HTMLElement)?.blur() if (e.shiftKey) { selectedComponent.update((old) => { if (old && old?.[0]) { @@ -492,11 +492,11 @@ export function appComponentFromType( xData: type === 'plotlycomponentv2' || type === 'chartjscomponentv2' ? { - type: 'evalv2', - fieldType: 'array', - expr: '[1, 2, 3, 4]', - connections: [] - } + type: 'evalv2', + fieldType: 'array', + expr: '[1, 2, 3, 4]', + connections: [] + } : undefined, ...(extra ?? {}) } @@ -845,33 +845,33 @@ export type InitConfig< | EvalAppInput | EvalV2AppInput | { - type: 'oneOf' - selected: string - configuration: Record< - string, - Record - > - } + type: 'oneOf' + selected: string + configuration: Record< + string, + Record + > + } > > = { - [Property in keyof T]: T[Property] extends StaticAppInput + [Property in keyof T]: T[Property] extends StaticAppInput ? T[Property]['value'] | undefined : T[Property] extends { type: 'oneOf' } - ? { - type: 'oneOf' - selected: keyof T[Property]['configuration'] - configuration: { - [Choice in keyof T[Property]['configuration']]: { - [IT in keyof T[Property]['configuration'][Choice]]: T[Property]['configuration'][Choice][IT] extends StaticAppInput - ? T[Property]['configuration'][Choice][IT] extends StaticAppInputOnDemand - ? () => Promise - : T[Property]['configuration'][Choice][IT]['value'] | undefined - : undefined - } - } + ? { + type: 'oneOf' + selected: keyof T[Property]['configuration'] + configuration: { + [Choice in keyof T[Property]['configuration']]: { + [IT in keyof T[Property]['configuration'][Choice]]: T[Property]['configuration'][Choice][IT] extends StaticAppInput + ? T[Property]['configuration'][Choice][IT] extends StaticAppInputOnDemand + ? () => Promise + : T[Property]['configuration'][Choice][IT]['value'] | undefined + : undefined } - : undefined -} + } + } + : undefined + } export function initConfig< T extends Record< @@ -880,13 +880,13 @@ export function initConfig< | EvalAppInput | EvalV2AppInput | { - type: 'oneOf' - selected: string - configuration: Record< - string, - Record - > - } + type: 'oneOf' + selected: string + configuration: Record< + string, + Record + > + } > >( r: T, @@ -894,13 +894,13 @@ export function initConfig< string, | StaticAppInput | { - type: 'oneOf' - selected: string - configuration: Record< - string, - Record - > - } + type: 'oneOf' + selected: string + configuration: Record< + string, + Record + > + } | any > ): InitConfig { @@ -910,31 +910,31 @@ export function initConfig< Object.entries(r).map(([key, value]) => value.type == 'static' ? [ - key, - configuration?.[key]?.type == 'static' ? configuration?.[key]?.['value'] : undefined - ] + key, + configuration?.[key]?.type == 'static' ? configuration?.[key]?.['value'] : undefined + ] : value.type == 'oneOf' ? [ - key, - { - selected: value.selected, - type: 'oneOf', - configuration: Object.fromEntries( - Object.entries(value.configuration).map(([choice, config]) => { - const conf = initConfig( - config, - configuration?.[key]?.configuration?.[choice] - ) - Object.entries(config).forEach(([innerKey, innerValue]) => { - if (innerValue.type === 'static' && !(innerKey in conf)) { - conf[innerKey] = innerValue.value - } - }) - return [choice, conf] + key, + { + selected: value.selected, + type: 'oneOf', + configuration: Object.fromEntries( + Object.entries(value.configuration).map(([choice, config]) => { + const conf = initConfig( + config, + configuration?.[key]?.configuration?.[choice] + ) + Object.entries(config).forEach(([innerKey, innerValue]) => { + if (innerValue.type === 'static' && !(innerKey in conf)) { + conf[innerKey] = innerValue.value + } }) - ) - } - ] + return [choice, conf] + }) + ) + } + ] : [key, undefined] ) ) as any @@ -1269,10 +1269,11 @@ export function isContainer(type: string): boolean { export function subGridIndexKey(type: string | undefined, id: string, world: World): number { switch (type) { case 'containercomponent': - case 'verticalsplitpanescomponent': - case 'horizontalsplitpanescomponent': case 'listcomponent': return 0 + case 'verticalsplitpanescomponent': + case 'horizontalsplitpanescomponent': + return (world?.outputsById?.[id]?.selectedPaneIndex?.peak() as number) ?? 0 case 'tabscomponent': { return (world?.outputsById?.[id]?.selectedTabIndex?.peak() as number) ?? 0 } diff --git a/frontend/src/lib/components/apps/svelte-grid/Grid.svelte b/frontend/src/lib/components/apps/svelte-grid/Grid.svelte index f20ee9baf1..4ffb2414ee 100644 --- a/frontend/src/lib/components/apps/svelte-grid/Grid.svelte +++ b/frontend/src/lib/components/apps/svelte-grid/Grid.svelte @@ -53,7 +53,7 @@ parentWidth?: number | undefined disableMove?: boolean children?: import('svelte').Snippet<[any]> - onDropped?: (e: { id: string; overlapped: string; x: number; y: number }) => void + onDropped?: (e: { id: string; overlapped: string | undefined; x: number; y: number }) => void onRedraw?: (grid: FilledItem[]) => void onResize?: (e: { cols: number diff --git a/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte b/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte index d7d6420b79..bdae49569d 100644 --- a/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte +++ b/frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte @@ -39,9 +39,9 @@ clientY: number intersectingElement?: string | undefined shadow?: GridShadow | undefined - overlapped?: string | undefined + overlapped?: string }) => void - onDropped?: (e: { id: string; overlapped: string; x: number; y: number }) => void + onDropped?: (e: { id: string; overlapped: string | undefined; x: number; y: number }) => void onInitMove?: () => void onResizeStart?: () => void onResizeEnd?: () => void @@ -418,10 +418,6 @@ return } - if (!overlapped) { - return - } - if (!moving) { return } From d0c7ac9f9551e0b7620b148d187701e8598755aa Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 21 Aug 2025 17:55:29 +0000 Subject: [PATCH 34/42] nit 2 --- .../src/lib/components/apps/components/layout/AppList.svelte | 2 +- .../src/lib/components/apps/components/layout/AppModal.svelte | 2 +- .../src/lib/components/apps/components/layout/AppStepper.svelte | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/apps/components/layout/AppList.svelte b/frontend/src/lib/components/apps/components/layout/AppList.svelte index 7332638318..3f324094ce 100644 --- a/frontend/src/lib/components/apps/components/layout/AppList.svelte +++ b/frontend/src/lib/components/apps/components/layout/AppList.svelte @@ -225,7 +225,7 @@ {id} subGridId={`${id}-0`} containerHeight={resolvedConfig.heightPx} - on:focus={() => { + onFocus={() => { if (!$connectingInput.opened) { $selectedComponent = [id] } diff --git a/frontend/src/lib/components/apps/components/layout/AppModal.svelte b/frontend/src/lib/components/apps/components/layout/AppModal.svelte index 1c50c0a017..8d5adb807a 100644 --- a/frontend/src/lib/components/apps/components/layout/AppModal.svelte +++ b/frontend/src/lib/components/apps/components/layout/AppModal.svelte @@ -243,7 +243,7 @@ {id} {containerHeight} subGridId={`${id}-0`} - on:focus={() => { + onFocus={() => { if (!$connectingInput.opened) { $selectedComponent = [id] $focusedGrid = { diff --git a/frontend/src/lib/components/apps/components/layout/AppStepper.svelte b/frontend/src/lib/components/apps/components/layout/AppStepper.svelte index a0abbad040..2b4eb183f7 100644 --- a/frontend/src/lib/components/apps/components/layout/AppStepper.svelte +++ b/frontend/src/lib/components/apps/components/layout/AppStepper.svelte @@ -220,7 +220,7 @@ class={twMerge(css?.container?.class, 'wm-stepper')} style={css?.container?.style} containerHeight={componentContainerHeight - tabHeight - footerHeight} - on:focus={() => { + onFocus={() => { if (!$connectingInput.opened) { $selectedComponent = [id] handleTabSelection() From 4373dfbd80477c9399909343686f38be6c915f27 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 21 Aug 2025 18:17:52 +0000 Subject: [PATCH 35/42] make tag select removable in custom ui --- .../src/lib/components/ScriptBuilder.svelte | 22 ++++++++++--------- frontend/src/lib/components/custom_ui.ts | 1 + 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 7d1721a988..e563af8551 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -1707,16 +1707,18 @@ {/if}
- {#if $workerTags} - {#if $workerTags?.length ?? 0 > 0} -
- -
+ {#if customUi?.topBar?.tagEdit != false} + {#if $workerTags} + {#if $workerTags?.length ?? 0 > 0} +
+ +
+ {/if} {/if} {/if} {#if customUi?.topBar?.settings != false} diff --git a/frontend/src/lib/components/custom_ui.ts b/frontend/src/lib/components/custom_ui.ts index b653f6fd55..70e0df9aec 100644 --- a/frontend/src/lib/components/custom_ui.ts +++ b/frontend/src/lib/components/custom_ui.ts @@ -111,6 +111,7 @@ export type ScriptBuilderWhitelabelCustomUi = { extraDeployOptions?: boolean editableSummary?: boolean diff?: boolean + tagEdit?: boolean } settingsPanel?: SettingsPanelUi disableTooltips?: boolean From 06d078ebfa8f70b66bc764eae70d33c8c57b4012 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 21 Aug 2025 18:37:32 +0000 Subject: [PATCH 36/42] fix: make relevant sidebar menu items a instead of button --- .../lib/components/sidebar/MenuButton.svelte | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/frontend/src/lib/components/sidebar/MenuButton.svelte b/frontend/src/lib/components/sidebar/MenuButton.svelte index 27d2fbf04b..d859b74ab2 100644 --- a/frontend/src/lib/components/sidebar/MenuButton.svelte +++ b/frontend/src/lib/components/sidebar/MenuButton.svelte @@ -3,7 +3,6 @@ import Popover from '../Popover.svelte' import { createEventDispatcher } from 'svelte' import SideBarNotification from './SideBarNotification.svelte' - import { goto } from '$app/navigation' import { conditionalMelt } from '$lib/utils' import type { MenubarMenuElements } from '@melt-ui/svelte' import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte' @@ -46,9 +45,12 @@ class: classNames = undefined }: Props = $props() - let buttonRef: HTMLButtonElement | undefined = $state(undefined) + let buttonRef: HTMLButtonElement | HTMLAnchorElement | undefined = $state(undefined) let dispatch = createEventDispatcher() + + // Dynamic component based on whether href is provided + const Element = href ? 'a' : 'button' {#if !disabled} @@ -59,7 +61,8 @@ disablePopup={!isCollapsed} placement="right" > -
{/if} - + {#snippet text()} {#if label} From 9df008b9f8fe58692463e4b9da0538935e458b10 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 22 Aug 2025 07:56:02 +0000 Subject: [PATCH 37/42] fix: s3 result presigned not working with list --- frontend/src/lib/components/DisplayResult.svelte | 2 +- .../components/common/fileDownload/FileDownload.svelte | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/DisplayResult.svelte b/frontend/src/lib/components/DisplayResult.svelte index e2b9aebace..a0d5784552 100644 --- a/frontend/src/lib/components/DisplayResult.svelte +++ b/frontend/src/lib/components/DisplayResult.svelte @@ -928,7 +928,7 @@ > {:else if !s3object?.disable_download} - + {:else}
{s3object?.s3} (download disabled)
{/if} diff --git a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte index a3ac04d207..824847eb4f 100644 --- a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte +++ b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte @@ -3,9 +3,13 @@ import { Download } from 'lucide-svelte' import { base } from '$lib/base' - export let s3object: any - export let workspaceId: string | undefined = undefined - export let appPath: string | undefined = undefined + interface Props { + s3object: any + workspaceId?: string | undefined + appPath?: string | undefined + } + + let { s3object, workspaceId = undefined, appPath = undefined }: Props = $props() Date: Fri, 22 Aug 2025 09:01:58 +0100 Subject: [PATCH 38/42] chore(main): release 1.531.0 (#6429) * chore(main): release 1.531.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> --- CHANGELOG.md | 16 ++++ backend/Cargo.lock | 80 +++++++++---------- backend/Cargo.toml | 4 +- backend/windmill-api/openapi.yaml | 2 +- benchmarks/lib.ts | 2 +- cli/src/main.ts | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- lsp/Pipfile | 4 +- openflow.openapi.yaml | 2 +- .../WindmillClient/WindmillClient.psd1 | 2 +- python-client/wmill/pyproject.toml | 2 +- python-client/wmill_pg/pyproject.toml | 2 +- typescript-client/jsr.json | 2 +- typescript-client/package.json | 2 +- version.txt | 2 +- 16 files changed, 73 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58265bbb79..a9a4147933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [1.531.0](https://github.com/windmill-labs/windmill/compare/v1.530.0...v1.531.0) (2025-08-22) + + +### Features + +* ai agent steps ([#6393](https://github.com/windmill-labs/windmill/issues/6393)) ([958e8af](https://github.com/windmill-labs/windmill/commit/958e8af78290cf859f98c45c012ed41e3bada39e)) +* bump Go version from 1.22.0 to 1.25.0 [#6415](https://github.com/windmill-labs/windmill/issues/6415) ([c92bfe6](https://github.com/windmill-labs/windmill/commit/c92bfe6601fd96f6d74860f52f9307e02961ac21)) + + +### Bug Fixes + +* **app:** fix ctrl drag for insertion into subgrids ([51ea947](https://github.com/windmill-labs/windmill/commit/51ea9473ef23c6871699e69bbe79772a4d50d3b8)) +* **frontend:** graph cache of ai agent step tools ([#6431](https://github.com/windmill-labs/windmill/issues/6431)) ([28f1d61](https://github.com/windmill-labs/windmill/commit/28f1d611643459d42531fa217c185408eb97d6d1)) +* make relevant sidebar menu items a instead of button ([06d078e](https://github.com/windmill-labs/windmill/commit/06d078ebfa8f70b66bc764eae70d33c8c57b4012)) +* s3 result presigned not working with list ([9df008b](https://github.com/windmill-labs/windmill/commit/9df008b9f8fe58692463e4b9da0538935e458b10)) + ## [1.530.0](https://github.com/windmill-labs/windmill/compare/v1.529.0...v1.530.0) (2025-08-20) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index da501f252f..4eea9b2aff 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1895,9 +1895,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -5345,9 +5345,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -5798,9 +5798,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" +checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" dependencies = [ "cc", "cfg-if", @@ -6856,9 +6856,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -9534,9 +9534,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" @@ -14596,9 +14596,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4" dependencies = [ "form_urlencoded", "idna", @@ -15129,7 +15129,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "axum", @@ -15183,7 +15183,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "argon2", @@ -15300,7 +15300,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.530.0" +version = "1.531.0" dependencies = [ "base64 0.22.1", "chrono", @@ -15315,7 +15315,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.530.0" +version = "1.531.0" dependencies = [ "chrono", "serde", @@ -15328,7 +15328,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "axum", @@ -15347,7 +15347,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "async-recursion", @@ -15427,7 +15427,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.530.0" +version = "1.531.0" dependencies = [ "regex", "serde", @@ -15442,7 +15442,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "bytes", @@ -15466,7 +15466,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.530.0" +version = "1.531.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15478,7 +15478,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.530.0" +version = "1.531.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -15487,7 +15487,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "lazy_static", @@ -15499,7 +15499,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "serde_json", @@ -15511,7 +15511,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "gosyn", @@ -15523,7 +15523,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "lazy_static", @@ -15535,7 +15535,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "serde_json", @@ -15547,7 +15547,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "nu-parser", @@ -15558,7 +15558,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15569,7 +15569,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15581,7 +15581,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "async-recursion", @@ -15604,7 +15604,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "lazy_static", @@ -15618,7 +15618,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15635,7 +15635,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "lazy_static", @@ -15649,7 +15649,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "lazy_static", @@ -15667,7 +15667,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "getrandom 0.2.16", @@ -15692,7 +15692,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "serde_json", @@ -15702,7 +15702,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "async-recursion", @@ -15735,7 +15735,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.530.0" +version = "1.531.0" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -15745,7 +15745,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.530.0" +version = "1.531.0" dependencies = [ "anyhow", "async-recursion", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index c4b7f442eb..f3c06a123c 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.530.0" +version = "1.531.0" authors.workspace = true edition.workspace = true @@ -33,7 +33,7 @@ members = [ ] [workspace.package] -version = "1.530.0" +version = "1.531.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0722ea1349..f9b11dd5c8 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.530.0 + version: 1.531.0 title: Windmill API contact: diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 72bf94e23f..cb2e44bb95 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.530.0"; +export const VERSION = "v1.531.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/main.ts b/cli/src/main.ts index d9ce7f8255..7a6b6d0462 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -68,7 +68,7 @@ export { // } // }); -export const VERSION = "1.530.0"; +export const VERSION = "1.531.0"; const command = new Command() .name("wmill") diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1fe671f5ac..6b0a29b889 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.530.0", + "version": "1.531.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.530.0", + "version": "1.531.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index c6fd4aa02d..1e87a4b43f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.530.0", + "version": "1.531.0", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/lsp/Pipfile b/lsp/Pipfile index 395de8369d..807453d92e 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,8 +4,8 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.530.0" -wmill_pg = ">=1.530.0" +wmill = ">=1.531.0" +wmill_pg = ">=1.531.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index d0acb82878..5c82985cef 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.530.0 + version: 1.531.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index 7ab642f708..f06ec3db19 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.530.0' + ModuleVersion = '1.531.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index 3a8aaf7627..86bac91388 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.530.0" +version = "1.531.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/python-client/wmill_pg/pyproject.toml b/python-client/wmill_pg/pyproject.toml index 0b2a5fee2a..883cb971f8 100644 --- a/python-client/wmill_pg/pyproject.toml +++ b/python-client/wmill_pg/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill-pg" -version = "1.530.0" +version = "1.531.0" description = "An extension client for the wmill client library focused on pg" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 604c42ae8c..2cd421aa70 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.530.0", + "version": "1.531.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index 347a780a56..58e795eef7 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.530.0", + "version": "1.531.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "devDependencies": { diff --git a/version.txt b/version.txt index ae143b1a95..dbae720437 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.530.0 +1.531.0 From 73272f16fddc355703b04f2c3458520753d1e19c Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Fri, 22 Aug 2025 10:46:03 +0200 Subject: [PATCH 39/42] feat(aichat): allow adding contexts to flow mode (#6424) * add new feature instructions * add db as context for flow mode * add diff * cleaner diff * add modules as available context * convert to svelte 5 * auto add selected module to context * change flowinline ai button + nits * handle adding selected lines * clean context handling * apply code pieces * new chat when changing mode * clean * show code for code steps * add last saved flow * fix size * categorize context * optionnaly categorize * fix module finding * logs * nit prompt * fix * fix * fix test tool for script * clean --- CLAUDE.md | 11 + frontend/src/lib/components/Editor.svelte | 7 +- frontend/src/lib/components/EditorBar.svelte | 6 +- .../src/lib/components/ScriptEditor.svelte | 1 + .../copilot/FlowInlineScriptAIButton.svelte | 11 +- .../lib/components/copilot/chat/AIChat.svelte | 6 +- .../copilot/chat/AIChatInput.svelte | 9 +- .../copilot/chat/AIChatManager.svelte.ts | 73 ++++-- .../copilot/chat/AvailableContextList.svelte | 154 +++++++++-- .../copilot/chat/ContextElementBadge.svelte | 55 ++-- .../copilot/chat/ContextManager.svelte.ts | 201 ++++++++++++--- .../copilot/chat/ContextTextarea.svelte | 2 +- .../lib/components/copilot/chat/context.ts | 25 +- .../copilot/chat/flow/FlowAIChat.svelte | 2 +- .../lib/components/copilot/chat/flow/core.ts | 106 ++++++-- .../components/copilot/chat/script/core.ts | 242 ++++++------------ .../src/lib/components/copilot/chat/shared.ts | 200 ++++++++++++++- .../lib/components/flows/FlowEditor.svelte | 14 + .../components/flows/FlowModuleIcon.svelte | 50 ++++ .../flows/content/FlowModuleComponent.svelte | 2 + .../lib/components/flows/map/MapItem.svelte | 47 +--- 21 files changed, 871 insertions(+), 353 deletions(-) create mode 100644 frontend/src/lib/components/flows/FlowModuleIcon.svelte diff --git a/CLAUDE.md b/CLAUDE.md index f61e0336be..3f29bf04ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,17 @@ Windmill is an open-source developer platform for building internal tools, workflows, API integrations, background jobs, workflows, and user interfaces. See @windmill-overview.mdc for full platform details. +## New Feature Implementation Guidelines + +When implementing new features in Windmill, follow these best practices: + +- **Clean Code First**: Write clean, readable, and maintainable code. Prioritize clarity over cleverness. +- **Avoid Duplication at All Costs**: Before writing new code, thoroughly search for existing implementations that can be reused or extended. +- **Adapt Existing Code**: Refactor and generalize existing code when necessary to avoid logic duplication. Extract common patterns into reusable utilities. +- **Follow Established Patterns**: Study existing code patterns in the codebase and maintain consistency with established conventions. +- **Single Responsibility**: Each function, component, and module should have a single, well-defined responsibility. +- **Incremental Implementation**: Break large features into smaller, reviewable chunks that can be implemented and tested incrementally. + ## Language-Specific Guides - Backend (Rust): @backend/rust-best-practices.mdc + @backend/summarized_schema.txt diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index 32b1c42809..1796ab76c0 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -184,6 +184,7 @@ loadAsync?: boolean key?: string | undefined class?: string | undefined + moduleId?: string } let { @@ -209,7 +210,8 @@ changeTimeout = 500, loadAsync = false, key = undefined, - class: clazz = undefined + class: clazz = undefined, + moduleId = undefined }: Props = $props() $effect.pre(() => { @@ -1328,7 +1330,8 @@ aiChatManager.addSelectedLinesToContext( selectedLines, selection.startLineNumber, - selection.endLineNumber + selection.endLineNumber, + moduleId ) } else { aiChatManager.toggleOpen() diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 40665e3f36..3dfe898301 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -82,6 +82,7 @@ showHistoryDrawer?: boolean right?: import('svelte').Snippet openAiChat?: boolean + moduleId?: string } let { @@ -105,7 +106,8 @@ diffMode = false, showHistoryDrawer = $bindable(false), right, - openAiChat = false + openAiChat = false, + moduleId = undefined }: Props = $props() let contextualVariablePicker: ItemPicker | undefined = $state() @@ -964,7 +966,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS {#if customUi?.aiGen != false} {#if openAiChat} - + {:else} {/if} diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index ba3f86a51f..cc7c87dd9e 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -269,6 +269,7 @@ onMount(() => { inferSchema(code) loadPastTests() + aiChatManager.saveAndClear() aiChatManager.changeMode(AIMode.SCRIPT) }) diff --git a/frontend/src/lib/components/copilot/FlowInlineScriptAIButton.svelte b/frontend/src/lib/components/copilot/FlowInlineScriptAIButton.svelte index bdbecba8c9..593a6b5cee 100644 --- a/frontend/src/lib/components/copilot/FlowInlineScriptAIButton.svelte +++ b/frontend/src/lib/components/copilot/FlowInlineScriptAIButton.svelte @@ -8,6 +8,12 @@ import { twMerge } from 'tailwind-merge' import { aiChatManager, AIMode } from './chat/AIChatManager.svelte' + interface Props { + moduleId?: string + } + + const { moduleId }: Props = $props() + const aiChatScriptModeClasses = $derived( aiChatManager.mode === AIMode.SCRIPT && aiChatManager.isOpen ? 'dark:bg-violet-900 bg-violet-100' @@ -22,7 +28,7 @@ btnClasses={twMerge('!px-2', aiChatScriptModeClasses)} {onClick} iconOnly - title="Open AI chat in script mode" + title="Open AI chat" startIcon={{ icon: WandSparkles, classes: 'text-violet-800 dark:text-violet-400' }} /> {/snippet} @@ -30,7 +36,8 @@ {#if $copilotInfo.enabled} {@render button(() => { aiChatManager.openChat() - aiChatManager.changeMode(AIMode.SCRIPT) + const availableContext = aiChatManager.contextManager.getAvailableContext() + aiChatManager.contextManager.setSelectedModuleContext(moduleId, availableContext) })} {:else} { - aiChatManager.listenForScriptEditorContextChange( + aiChatManager.listenForContextChange( $dbSchemas, $workspaceStore, $copilotSessionModel @@ -115,9 +115,7 @@ pastChats={historyManager.getPastChats()} bind:selectedContext={ () => aiChatManager.contextManager.getSelectedContext(), - (sc) => { - aiChatManager.scriptEditorOptions && aiChatManager.contextManager.setSelectedContext(sc) - } + (sc) => aiChatManager.contextManager.setSelectedContext(sc) } availableContext={aiChatManager.contextManager.getAvailableContext()} messages={aiChatManager.currentReply diff --git a/frontend/src/lib/components/copilot/chat/AIChatInput.svelte b/frontend/src/lib/components/copilot/chat/AIChatInput.svelte index 8c4bd57e37..1fe935417f 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatInput.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChatInput.svelte @@ -52,7 +52,7 @@ if (placeholder) { return placeholder } - + switch (aiChatManager.mode) { case AIMode.SCRIPT: return 'Modify this script...' @@ -74,7 +74,7 @@ let instructions = $state(initialInstructions) export function focusInput() { - if (aiChatManager.mode === AIMode.SCRIPT) { + if (aiChatManager.mode === AIMode.SCRIPT || aiChatManager.mode === AIMode.FLOW) { contextTextareaComponent?.focus() } else { instructionsTextareaComponent?.focus() @@ -132,7 +132,7 @@
- {#if aiChatManager.mode === AIMode.SCRIPT} + {#if aiChatManager.mode === AIMode.SCRIPT || aiChatManager.mode === AIMode.FLOW} {#if showContext}
@@ -150,6 +150,7 @@ addContextToSelection(element) close() }} + categorize /> @@ -157,7 +158,7 @@ { + onDelete={() => { selectedContext = selectedContext?.filter( (c) => c.type !== element.type || c.title !== element.title ) diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index ed6b8f019c..452a19ffef 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -1,5 +1,5 @@ import type { AIProviderModel, ScriptLang } from '$lib/gen/types.gen' -import type { ScriptOptions } from './ContextManager.svelte' +import type { FlowOptions, ScriptOptions } from './ContextManager.svelte' import { flowTools, prepareFlowSystemMessage, @@ -88,6 +88,7 @@ class AIChatManager { helpers = $state(undefined) scriptEditorOptions = $state(undefined) + flowOptions = $state(undefined) scriptEditorApplyCode = $state<((code: string, applyAll?: boolean) => void) | undefined>( undefined ) @@ -100,7 +101,7 @@ class AIChatManager { private confirmationCallback = $state<((value: boolean) => void) | undefined>(undefined) allowedModes: Record = $derived({ - script: this.scriptEditorOptions !== undefined, + script: this.flowAiChatHelpers === undefined && this.scriptEditorOptions !== undefined, flow: this.flowAiChatHelpers !== undefined, navigator: true, ask: true, @@ -127,7 +128,7 @@ class AIChatManager { return ( estimatedTokens > modelContextWindow - - Math.max(modelContextWindow * MAX_TOKENS_THRESHOLD_PERCENTAGE, MAX_TOKENS_HARD_LIMIT) + Math.max(modelContextWindow * MAX_TOKENS_THRESHOLD_PERCENTAGE, MAX_TOKENS_HARD_LIMIT) ) } @@ -557,8 +558,8 @@ class AIChatManager { onNewToken: (token: string) => { reply += token }, - onMessageEnd: () => { }, - setToolStatus: () => { } + onMessageEnd: () => {}, + setToolStatus: () => {} }, systemMessage } @@ -625,7 +626,7 @@ class AIChatManager { } try { const oldSelectedContext = this.contextManager?.getSelectedContext() ?? [] - if (this.mode === AIMode.SCRIPT) { + if (this.mode === AIMode.SCRIPT || this.mode === AIMode.FLOW) { this.contextManager?.updateContextOnRequest(options) } this.loading = true @@ -648,7 +649,10 @@ class AIChatManager { { role: 'user', content: this.instructions, - contextElements: this.mode === AIMode.SCRIPT ? oldSelectedContext : undefined, + contextElements: + this.mode === AIMode.SCRIPT || this.mode === AIMode.FLOW + ? oldSelectedContext + : undefined, snapshot, index: this.messages.length // matching with actual messages index. not -1 because it's not yet added to the messages array } @@ -672,7 +676,8 @@ class AIChatManager { case AIMode.FLOW: userMessage = prepareFlowUserMessage( oldInstructions, - this.flowAiChatHelpers!.getFlowAndSelectedId() + this.flowAiChatHelpers!.getFlowAndSelectedId(), + oldSelectedContext ) break case AIMode.NAVIGATOR: @@ -823,12 +828,19 @@ class AIChatManager { this.sendRequest() } - addSelectedLinesToContext = (lines: string, startLine: number, endLine: number) => { + addSelectedLinesToContext = ( + lines: string, + startLine: number, + endLine: number, + moduleId?: string + ) => { if (!this.open) { this.toggleOpen() } - this.changeMode(AIMode.SCRIPT) - this.contextManager?.addSelectedLinesToContext(lines, startLine, endLine) + if (!moduleId) { + this.changeMode(AIMode.SCRIPT) + } + this.contextManager?.addSelectedLinesToContext(lines, startLine, endLine, moduleId) this.focusInput() } @@ -869,12 +881,12 @@ class AIChatManager { }) } - listenForScriptEditorContextChange = ( + listenForContextChange = ( dbSchemas: DBSchemas, workspaceStore: string | undefined, copilotSessionModel: AIProviderModel | undefined ) => { - if (this.scriptEditorOptions) { + if (this.mode === AIMode.SCRIPT && this.scriptEditorOptions) { this.contextManager.updateAvailableContext( this.scriptEditorOptions, dbSchemas, @@ -882,6 +894,18 @@ class AIChatManager { !copilotSessionModel?.model.endsWith('/thinking'), untrack(() => this.contextManager.getSelectedContext()) ) + } else if (this.mode === AIMode.FLOW && this.flowOptions) { + this.contextManager.updateAvailableContextForFlow( + this.flowOptions, + dbSchemas, + workspaceStore ?? '', + !copilotSessionModel?.model.endsWith('/thinking'), + untrack(() => this.contextManager.getSelectedContext()) + ) + } + + if (this.scriptEditorOptions) { + this.contextManager.setScriptOptions(this.scriptEditorOptions) } } @@ -941,15 +965,15 @@ class AIChatManager { const editorRelated = currentEditor && currentEditor.type === 'script' && currentEditor.stepId === module.id ? { - diffMode: currentEditor.diffMode, - lastDeployedCode: currentEditor.lastDeployedCode, - lastSavedCode: undefined - } + diffMode: currentEditor.diffMode, + lastDeployedCode: currentEditor.lastDeployedCode, + lastSavedCode: undefined + } : { - diffMode: false, - lastDeployedCode: undefined, - lastSavedCode: undefined - } + diffMode: false, + lastDeployedCode: undefined, + lastSavedCode: undefined + } return { args: moduleState?.previewArgs ?? {}, @@ -976,6 +1000,13 @@ class AIChatManager { this.scriptEditorOptions = undefined } + untrack(() => + this.contextManager?.setSelectedModuleContext( + selectedId, + untrack(() => this.contextManager.getAvailableContext()) + ) + ) + return () => { this.scriptEditorOptions = undefined } diff --git a/frontend/src/lib/components/copilot/chat/AvailableContextList.svelte b/frontend/src/lib/components/copilot/chat/AvailableContextList.svelte index 89a05ff623..5690a7b610 100644 --- a/frontend/src/lib/components/copilot/chat/AvailableContextList.svelte +++ b/frontend/src/lib/components/copilot/chat/AvailableContextList.svelte @@ -1,5 +1,9 @@ -
- {#if actualAvailableContext.length === 0} -
No available context
+
+ {#if categorize} + {#if currentView === 'categories'} + {#each categories as category} + {@const itemCount = contextByCategory[category.id].length} + {@const Icon = category.icon} + {#if itemCount > 0} + + {/if} + {/each} + {#if categories.every((cat) => contextByCategory[cat.id].length === 0)} +
No available context
+ {/if} + {:else} + + + + {#if currentCategoryItems.length === 0} +
No items in this category
+ {:else} + {#each currentCategoryItems as element} + {@const Icon = ContextIconMap[element.type]} + + {/each} + {/if} + {/if} {:else} - {#each actualAvailableContext as element, i} + {#each filteredAvailableContext as element, i} {@const Icon = ContextIconMap[element.type]} {/each} {/if} diff --git a/frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte b/frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte index d001f7493c..c2f98b76c3 100644 --- a/frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte +++ b/frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte @@ -10,46 +10,53 @@ formatSchema } from '$lib/components/apps/components/display/dbtable/utils' import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte' - import { createEventDispatcher } from 'svelte' import HighlightCode from '$lib/components/HighlightCode.svelte' + import FlowModuleIcon from '$lib/components/flows/FlowModuleIcon.svelte' + import type { FlowModule } from '$lib/gen' - export let contextElement: ContextElement - export let deletable = false + interface Props { + contextElement: ContextElement + deletable?: boolean + onDelete?: () => void + } + + let { contextElement, deletable = false, onDelete }: Props = $props() const icon = ContextIconMap[contextElement.type] - let showDelete = false + let showDelete = $state(false) - const dispatch = createEventDispatcher<{ - delete: void - }>() + const isDeletable = $derived(deletable && contextElement.deletable !== false) - + {#snippet trigger()}
(showDelete = true)} - on:mouseleave={() => (showDelete = false)} + onmouseenter={() => (showDelete = true)} + onmouseleave={() => (showDelete = false)} aria-label="Context element" role="button" tabindex={0} > - - {contextElement.type === 'diff' + {contextElement.type === 'diff' || contextElement.type === 'flow_module' ? contextElement.title.replace(/_/g, ' ') : contextElement.title}
-
- + {/snippet} + {#snippet content()} {#if contextElement.type === 'error'}
@@ -79,6 +86,20 @@ class="w-full p-2 " />
+ {:else if contextElement.type === 'flow_module'} + {#if contextElement.value.content} +
+ +
+ {:else} +
+
{contextElement.title}
+
+ {/if} {/if} -
+ {/snippet}
diff --git a/frontend/src/lib/components/copilot/chat/ContextManager.svelte.ts b/frontend/src/lib/components/copilot/chat/ContextManager.svelte.ts index a7eb958fca..e56085ae2b 100644 --- a/frontend/src/lib/components/copilot/chat/ContextManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/ContextManager.svelte.ts @@ -1,11 +1,13 @@ -import { ResourceService, type ListResourceResponse, type ScriptLang } from '$lib/gen' +import { ResourceService, type Flow, type ListResourceResponse, type ScriptLang } from '$lib/gen' import { scriptLangToEditorLang } from '$lib/scripts' import { SQLSchemaLanguages, type DBSchemas } from '$lib/stores' import { diffLines } from 'diff' import type { ContextElement } from './context' +import type { FlowModule } from '$lib/gen' import type { DisplayMessage } from './shared' import { langToExt } from '$lib/editorLangUtils' +import type { ExtendedOpenFlow } from '$lib/components/flows/types' export interface ScriptOptions { lang: ScriptLang | 'bunnative' @@ -18,6 +20,14 @@ export interface ScriptOptions { diffMode: boolean } +export interface FlowOptions { + currentFlow: ExtendedOpenFlow + lastDeployedFlow?: Flow + path: string | undefined + modules: FlowModule[] + lastSavedFlow?: Flow +} + export default class ContextManager { private selectedContext: ContextElement[] = $state([]) private availableContext: ContextElement[] = $state([]) @@ -55,6 +65,93 @@ export default class ContextManager { ) } + async updateAvailableContextForFlow( + flowOptions: FlowOptions, + dbSchemas: DBSchemas, + workspace: string, + toolSupport: boolean, + currentlySelectedContext: ContextElement[] + ) { + try { + if (this.workspace !== workspace) { + await this.refreshDbResources(workspace) + this.workspace = workspace + } + + let newAvailableContext: ContextElement[] = [] + + // Add diff context if we have a deployed flow version + const deployedFlowString = JSON.stringify(flowOptions.lastDeployedFlow, null, 2) + const savedFlowString = JSON.stringify(flowOptions.lastSavedFlow, null, 2) + const currentFlowString = JSON.stringify(flowOptions.currentFlow, null, 2) + + if (currentFlowString && deployedFlowString && deployedFlowString !== currentFlowString) { + newAvailableContext.push({ + type: 'diff', + title: 'diff_with_last_deployed_version', + content: deployedFlowString, + diff: diffLines(deployedFlowString, currentFlowString), + lang: 'graphql' // irrelevant, but needed for the diff component + }) + } + + if (currentFlowString && savedFlowString && savedFlowString !== currentFlowString) { + newAvailableContext.push({ + type: 'diff', + title: 'diff_with_last_saved_draft', + content: savedFlowString, + diff: diffLines(savedFlowString, currentFlowString), + lang: 'graphql' // irrelevant, but needed for the diff component + }) + } + + for (const module of flowOptions.modules) { + newAvailableContext.push({ + type: 'flow_module', + id: module.id, + title: `module_[${module.id}]`, + value: { + language: 'language' in module.value ? module.value.language : 'bunnative', + path: 'path' in module.value ? module.value.path : '', + content: 'content' in module.value ? module.value.content : '', + type: module.value.type + } + }) + } + + if (toolSupport) { + for (const d of this.dbResources) { + const loadedSchema = dbSchemas[d.path] + newAvailableContext.push({ + type: 'db', + title: d.path, + // If the db is already fetched, add the schema to the context + ...(loadedSchema ? { schema: loadedSchema } : {}) + }) + } + } + + let newSelectedContext: ContextElement[] = [...currentlySelectedContext] + + // Filter selected context to only include available items + newSelectedContext = newSelectedContext + .filter((c) => newAvailableContext.some((ac) => ac.type === c.type && ac.title === c.title)) + .map((c) => + c.type === 'db' && dbSchemas[c.title] + ? { + ...c, + schema: dbSchemas[c.title] + } + : c + ) + + this.availableContext = newAvailableContext + this.selectedContext = newSelectedContext + } catch (err) { + console.error('Could not update available context for flow', err) + } + } + async updateAvailableContext( scriptOptions: ScriptOptions, dbSchemas: DBSchemas, @@ -63,12 +160,10 @@ export default class ContextManager { currentlySelectedContext: ContextElement[] ) { try { - let firstTime = !this.workspace if (this.workspace !== workspace) { await this.refreshDbResources(workspace) this.workspace = workspace } - this.scriptOptions = scriptOptions let newAvailableContext: ContextElement[] = [ { type: 'code', @@ -123,16 +218,15 @@ export default class ContextManager { let newSelectedContext: ContextElement[] = [...currentlySelectedContext] - if (firstTime) { - newSelectedContext = [ - { - type: 'code', - title: this.getContextCodePath(scriptOptions) ?? '', - content: scriptOptions.code, - lang: scriptOptions.lang - } - ] - } + newSelectedContext = [ + { + type: 'code', + title: this.getContextCodePath(scriptOptions) ?? '', + content: scriptOptions.code, + lang: scriptOptions.lang, + deletable: false + } + ] const db = this.getSelectedDBSchema(scriptOptions, dbSchemas) if ( @@ -160,15 +254,15 @@ export default class ContextManager { .map((c) => c.type === 'code' ? { - ...c, - content: scriptOptions.code, - title: this.getContextCodePath(scriptOptions) - } + ...c, + content: scriptOptions.code, + title: this.getContextCodePath(scriptOptions) + } : c.type === 'db' && dbSchemas[c.title] ? { - ...c, - schema: dbSchemas[c.title] - } + ...c, + schema: dbSchemas[c.title] + } : c ) @@ -191,12 +285,15 @@ export default class ContextManager { return this.availableContext } - addSelectedLinesToContext(lines: string, startLine: number, endLine: number) { + setScriptOptions(scriptOptions: ScriptOptions) { + this.scriptOptions = scriptOptions + } + + addSelectedLinesToContext(lines: string, startLine: number, endLine: number, moduleId?: string) { + const title = moduleId ? `[${moduleId}] L${startLine}-L${endLine}` : `L${startLine}-L${endLine}` if ( !this.scriptOptions || - this.selectedContext.find( - (c) => c.type === 'code_piece' && c.title === `L${startLine}-L${endLine}` - ) + this.selectedContext.find((c) => c.type === 'code_piece' && c.title === title) ) { return } @@ -204,7 +301,7 @@ export default class ContextManager { ...this.selectedContext, { type: 'code_piece', - title: `L${startLine}-L${endLine}`, + title: title, startLine, endLine, content: lines, @@ -234,14 +331,14 @@ export default class ContextManager { ...(options.withCode === false ? [] : [codeContext]), ...(options.withDiff ? [ - { - type: 'diff' as const, - title: 'diff_with_last_deployed_version', - content: this.scriptOptions.lastDeployedCode ?? '', - diff: diffLines(this.scriptOptions.lastDeployedCode ?? '', this.scriptOptions.code), - lang: this.scriptOptions.lang - } - ] + { + type: 'diff' as const, + title: 'diff_with_last_deployed_version', + content: this.scriptOptions.lastDeployedCode ?? '', + diff: diffLines(this.scriptOptions.lastDeployedCode ?? '', this.scriptOptions.code), + lang: this.scriptOptions.lang + } + ] : []) ] } @@ -268,15 +365,37 @@ export default class ContextManager { contextElements: m.role !== 'tool' && m.contextElements ? m.contextElements.map((c) => - c.type === 'db' - ? { - type: 'db', - title: c.title, - schema: dbSchemas[c.title] - } - : c - ) + c.type === 'db' + ? { + type: 'db', + title: c.title, + schema: dbSchemas[c.title] + } + : c + ) : undefined })) } + + setSelectedModuleContext( + moduleId: string | undefined, + availableContext: ContextElement[] | undefined + ) { + if (availableContext && moduleId) { + const module = availableContext.find((c) => c.type === 'flow_module' && c.id === moduleId) + if ( + module && + !this.selectedContext.find((c) => c.type === 'flow_module' && c.id === moduleId) + ) { + this.selectedContext = this.selectedContext.filter((c) => c.type !== 'flow_module') + this.selectedContext = [module, ...this.selectedContext] + } + } else if (!moduleId) { + this.selectedContext = this.selectedContext.filter((c) => c.type !== 'flow_module') + } + } + + clearContext() { + this.selectedContext = [] + } } diff --git a/frontend/src/lib/components/copilot/chat/ContextTextarea.svelte b/frontend/src/lib/components/copilot/chat/ContextTextarea.svelte index d45d36ca2f..090437e259 100644 --- a/frontend/src/lib/components/copilot/chat/ContextTextarea.svelte +++ b/frontend/src/lib/components/copilot/chat/ContextTextarea.svelte @@ -155,7 +155,7 @@ } function getHighlightedText(text: string) { - return text.replace(/@[\w/.-]+/g, (match) => { + return text.replace(/@[\w/.\-\[\]]+/g, (match) => { const contextElement = availableContext.find((c) => c.title === match.slice(1)) if (contextElement) { return `${match}` diff --git a/frontend/src/lib/components/copilot/chat/context.ts b/frontend/src/lib/components/copilot/chat/context.ts index e1dcf9d115..c25ae2ade4 100644 --- a/frontend/src/lib/components/copilot/chat/context.ts +++ b/frontend/src/lib/components/copilot/chat/context.ts @@ -9,6 +9,7 @@ export const ContextIconMap = { db: Database, diff: Diff, code_piece: Code + // flow_module type is handled with FlowModuleIcon } export interface CodeElement { @@ -47,4 +48,26 @@ export interface CodePieceElement { lang: ScriptLang | 'bunnative' } -export type ContextElement = CodeElement | ErrorElement | DBElement | DiffElement | CodePieceElement +export interface FlowModule { + type: 'flow_module' + id: string + title: string + // mimics the FlowModule type, with only the fields we need + value: { + language?: ScriptLang | 'bunnative' + path?: string + content?: string + type: string + } +} + +export type ContextElement = ( + | CodeElement + | ErrorElement + | DBElement + | DiffElement + | CodePieceElement + | FlowModule +) & { + deletable?: boolean +} diff --git a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte index 7c9d76cafd..8170731ee9 100644 --- a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte +++ b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte @@ -19,7 +19,7 @@ import DiffDrawer from '$lib/components/DiffDrawer.svelte' let { - flowModuleSchemaMap + flowModuleSchemaMap, }: { flowModuleSchemaMap: FlowModuleSchemaMap | undefined } = $props() diff --git a/frontend/src/lib/components/copilot/chat/flow/core.ts b/frontend/src/lib/components/copilot/chat/flow/core.ts index 975a6de62b..741e86a575 100644 --- a/frontend/src/lib/components/copilot/chat/flow/core.ts +++ b/frontend/src/lib/components/copilot/chat/flow/core.ts @@ -10,9 +10,21 @@ import { emptySchema, emptyString } from '$lib/utils' import { getFormattedResourceTypes, getLangContext, - SUPPORTED_CHAT_SCRIPT_LANGUAGES + SUPPORTED_CHAT_SCRIPT_LANGUAGES, + createDbSchemaTool } from '../script/core' -import { createSearchHubScriptsTool, createToolDef, type Tool, executeTestRun, buildSchemaForTool, buildTestRunArgs } from '../shared' +import { + createSearchHubScriptsTool, + createToolDef, + type Tool, + executeTestRun, + buildSchemaForTool, + buildTestRunArgs, + buildContextString, + applyCodePiecesToFlowModules, + findModuleById +} from '../shared' +import type { ContextElement } from '../context' import type { ExtendedOpenFlow } from '$lib/components/flows/types' export type AIModuleAction = 'added' | 'modified' | 'removed' @@ -339,8 +351,11 @@ const getInstructionsForCodeGenerationToolDef = createToolDef( // Will be overridden by setSchema const testRunFlowSchema = z.object({ - args: z.object({}).nullable().optional() - .describe('Arguments to pass to the flow (optional, uses default flow inputs if not provided)') + args: z + .object({}) + .nullable() + .optional() + .describe('Arguments to pass to the flow (optional, uses default flow inputs if not provided)') }) const testRunFlowToolDef = createToolDef( @@ -368,6 +383,7 @@ const workspaceScriptsSearch = new WorkspaceScriptsSearch() export const flowTools: Tool[] = [ createSearchHubScriptsTool(false), + createDbSchemaTool(), { def: searchScriptsToolDef, fn: async ({ args, workspace, toolId, toolCallbacks }) => { @@ -562,7 +578,7 @@ export const flowTools: Tool[] = [ }, { def: testRunFlowToolDef, - fn: async function({ args, workspace, helpers, toolCallbacks, toolId }) { + fn: async function ({ args, workspace, helpers, toolCallbacks, toolId }) { const { flow } = helpers.getFlowAndSelectedId() if (!flow || !flow.value) { @@ -577,13 +593,14 @@ export const flowTools: Tool[] = [ const parsedArgs = await buildTestRunArgs(args, this.def) return executeTestRun({ - jobStarter: () => JobService.runFlowPreview({ - workspace: workspace, - requestBody: { - args: parsedArgs, - value: flow.value, - } - }), + jobStarter: () => + JobService.runFlowPreview({ + workspace: workspace, + requestBody: { + args: parsedArgs, + value: flow.value + } + }), workspace, toolCallbacks, toolId, @@ -591,7 +608,7 @@ export const flowTools: Tool[] = [ contextName: 'flow' }) }, - setSchema: async function(helpers: FlowAIChatHelpers) { + setSchema: async function (helpers: FlowAIChatHelpers) { await buildSchemaForTool(this.def, async () => { const flowInputsSchema = await helpers.getFlowInputsSchema() return flowInputsSchema @@ -622,7 +639,7 @@ export const flowTools: Tool[] = [ // Find the step in the flow const modules = helpers.getModules() - let targetModule: FlowModule | undefined = modules.find((m) => m.id === stepId) + let targetModule: FlowModule | undefined = findModuleById(modules, stepId) if (!targetModule) { toolCallbacks.setToolStatus(toolId, { @@ -647,7 +664,10 @@ export const flowTools: Tool[] = [ requestBody: { content: moduleValue.content ?? '', language: moduleValue.language, - args: module.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...stepArgs } : stepArgs + args: + module.id === 'preprocessor' + ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...stepArgs } + : stepArgs } }), workspace, @@ -675,7 +695,10 @@ export const flowTools: Tool[] = [ requestBody: { content: script.content, language: script.language, - args: module.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...stepArgs } : stepArgs, + args: + module.id === 'preprocessor' + ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...stepArgs } + : stepArgs } }), workspace, @@ -721,6 +744,16 @@ Follow the user instructions carefully. Go step by step, and explain what you're doing as you're doing it. DO NOT wait for user confirmation before performing an action. Only do it if the user explicitly asks you to wait in their initial instructions. ALWAYS test your modifications. You have access to the \`test_run_flow\` and \`test_run_step\` tools to test the flow and steps. If you only modified a single step, use the \`test_run_step\` tool to test it. If you modified the flow, use the \`test_run_flow\` tool to test it. If the user cancels the test run, do not try again and wait for the next user instruction. +When testing steps that are sql scripts, the arguments to be passed are { database: $res: }. + +## Code Markers in Flow Modules + +When viewing flow modules, the code content of rawscript steps may include \`[#START]\` and \`[#END]\` markers: +- These markers indicate specific code sections that need attention +- You MUST only modify the code between these markers when using the \`set_code\` tool +- After modifying the code, remove the markers from your response +- If a question is asked about the code, focus only on the code between the markers +- The markers appear in the YAML representation of flow modules when specific code pieces are selected ## Understanding User Requests @@ -802,6 +835,16 @@ For truly static values in step inputs (those not linked to previous steps or lo Both modules only support a script or rawscript step. You cannot nest modules using forloop/branchone/branchall. +### Contexts + +You have access to the following contexts: +- Database schemas +- Flow diffs +- Focused flow modules +Database schemas give you the schema of databases the user is using. +Flow diffs give you the diff between the current flow and the last deployed flow. +Focused flow modules give you the ids of the flow modules the user is focused on. Your response should focus on these modules. + ## Resource types On Windmill, credentials and configuration are stored in resources. Resource types define the format of the resource. If the user needs a resource as flow input, you should set the property type in the schema to "object" as well as add a key called "format" and set it to "resource-nameofresourcetype" (e.g. "resource-stripe"). @@ -816,26 +859,33 @@ If the user wants a specific resource as step input, you should set the step val export function prepareFlowUserMessage( instructions: string, - flowAndSelectedId?: { flow: ExtendedOpenFlow; selectedId: string } + flowAndSelectedId?: { flow: ExtendedOpenFlow; selectedId: string }, + selectedContext?: ContextElement[] ): ChatCompletionUserMessageParam { const flow = flowAndSelectedId?.flow const selectedId = flowAndSelectedId?.selectedId + // Handle context elements + const contextInstructions = selectedContext ? buildContextString(selectedContext) : '' + if (!flow || !selectedId) { + let userMessage = `## INSTRUCTIONS: +${instructions}` return { role: 'user', - content: `## INSTRUCTIONS: -${instructions}` + content: userMessage } } - return { - role: 'user', - content: `## FLOW: + + const codePieces = selectedContext?.filter((c) => c.type === 'code_piece') ?? [] + const flowModulesYaml = applyCodePiecesToFlowModules(codePieces, flow.value.modules) + + let flowContent = `## FLOW: flow_input schema: ${JSON.stringify(flow.schema ?? emptySchema())} flow modules: -${YAML.stringify(flow.value.modules)} +${flowModulesYaml} preprocessor module: ${YAML.stringify(flow.value.preprocessor_module)} @@ -844,9 +894,15 @@ failure module: ${YAML.stringify(flow.value.failure_module)} currently selected step: -${selectedId} +${selectedId}` -## INSTRUCTIONS: + flowContent += contextInstructions + + flowContent += `\n\n## INSTRUCTIONS: ${instructions}` + + return { + role: 'user', + content: flowContent } } diff --git a/frontend/src/lib/components/copilot/chat/script/core.ts b/frontend/src/lib/components/copilot/chat/script/core.ts index 6ad56de1b4..ee6b364ecf 100644 --- a/frontend/src/lib/components/copilot/chat/script/core.ts +++ b/frontend/src/lib/components/copilot/chat/script/core.ts @@ -1,6 +1,6 @@ import { ResourceService, JobService } from '$lib/gen/services.gen' import type { ResourceType, ScriptLang } from '$lib/gen/types.gen' -import { capitalize, emptySchema, isObject, toCamel } from '$lib/utils' +import { capitalize, isObject, toCamel } from '$lib/utils' import { get } from 'svelte/store' import { compile, phpCompile, pythonCompile } from '../../utils' import type { @@ -9,14 +9,18 @@ import type { ChatCompletionUserMessageParam } from 'openai/resources/index.mjs' import { copilotSessionModel, type DBSchema, dbSchemas } from '$lib/stores' -import { scriptLangToEditorLang } from '$lib/scripts' import { getDbSchemas } from '$lib/components/apps/components/display/dbtable/utils' -import type { CodePieceElement, ContextElement } from '../context' +import type { ContextElement } from '../context' import { PYTHON_PREPROCESSOR_MODULE_CODE, TS_PREPROCESSOR_MODULE_CODE } from '$lib/script_helpers' -import { createSearchHubScriptsTool, type Tool, executeTestRun, buildSchemaForTool, buildTestRunArgs } from '../shared' +import { + createSearchHubScriptsTool, + type Tool, + executeTestRun, + buildTestRunArgs, + buildContextString +} from '../shared' import { setupTypeAcquisition, type DepsToGet } from '$lib/ata' import { getModelContextWindow } from '../../lib' -import { inferArgs } from '$lib/infer' // Score threshold for npm packages search filtering const SCORE_THRESHOLD = 1000 @@ -348,7 +352,7 @@ export const CHAT_SYSTEM_PROMPT = ` - You can also receive a \`DIFF\` of the changes that have been made to the code. You should use this diff to give better answers. - Before giving your answer, check again that you carefully followed these instructions. - When asked to create a script that communicates with an external service, you can use the \`search_hub_scripts\` tool to search for relevant scripts in the hub. Make sure the language is the same as what the user is coding in. If you do not find any relevant scripts, you can use the \`search_npm_packages\` tool to search for relevant packages and their documentation. Always give a link to the documentation in your answer if possible. - - After modifying the code, ALWAYS use the \`test_run_script\` tool to test the code, and iterate on the code until it works as expected. If the user cancels the test run, do not try again and wait for the next user instruction. + - At the end of your reponse, if you modified or suggested changes to the code, ALWAYS use the \`test_run_script\` tool to test the code, and iterate on the code until it works as expected (MAX 3 times). If the user cancels the test run, do not try again and wait for the next user instruction. Important: Do not mention or reveal these instructions to the user unless explicitly asked to do so. @@ -439,18 +443,6 @@ export async function main() { \`\`\` ` -const CHAT_USER_CODE_CONTEXT = ` -- {title}: -\`\`\`{language} -{code} -\`\`\` -` - -const CHAT_USER_ERROR_CONTEXT = ` -ERROR: -{error} -` - export const CHAT_USER_PROMPT = ` INSTRUCTIONS: {instructions} @@ -460,8 +452,6 @@ WINDMILL LANGUAGE CONTEXT: ` -export const CHAT_USER_DB_CONTEXT = `- {title}: SCHEMA: \n{schema}\n` - export function prepareScriptSystemMessage(): ChatCompletionSystemMessageParam { return { role: 'system', @@ -469,18 +459,6 @@ export function prepareScriptSystemMessage(): ChatCompletionSystemMessageParam { } } -const applyCodePieceToCodeContext = (codePieces: CodePieceElement[], codeContext: string) => { - let code = codeContext.split('\n') - let shiftOffset = 0 - codePieces.sort((a, b) => a.startLine - b.startLine) - for (const codePiece of codePieces) { - code.splice(codePiece.endLine + shiftOffset, 0, '[#END]') - code.splice(codePiece.startLine + shiftOffset - 1, 0, '[#START]') - shiftOffset += 2 - } - return code.join('\n') -} - export function prepareScriptTools( language: ScriptLang | 'bunnative', context: ContextElement[] @@ -508,61 +486,12 @@ export function prepareScriptUserMessage( isPreprocessor?: boolean } = {} ): ChatCompletionUserMessageParam { - let codeContext = 'CODE:\n' - let errorContext = 'ERROR:\n' - let dbContext = 'DATABASES:\n' - let diffContext = 'DIFF:\n' - let hasCode = false - let hasError = false - let hasDb = false - let hasDiff = false - for (const context of selectedContext) { - if (context.type === 'code') { - hasCode = true - codeContext += CHAT_USER_CODE_CONTEXT.replace('{title}', context.title) - .replace('{language}', scriptLangToEditorLang(language)) - .replace( - '{code}', - applyCodePieceToCodeContext( - selectedContext.filter((c) => c.type === 'code_piece'), - context.content - ) - ) - } else if (context.type === 'error') { - if (hasError) { - throw new Error('Multiple error contexts provided') - } - hasError = true - errorContext = CHAT_USER_ERROR_CONTEXT.replace('{error}', context.content) - } else if (context.type === 'db') { - hasDb = true - dbContext += CHAT_USER_DB_CONTEXT.replace('{title}', context.title).replace( - '{schema}', - context.schema?.stringified ?? 'to fetch with get_db_schema' - ) - } else if (context.type === 'diff') { - hasDiff = true - const diff = JSON.stringify(context.diff) - diffContext = diff.length > 3000 ? diff.slice(0, 3000) + '...' : diff - } - } - let userMessage = CHAT_USER_PROMPT.replace('{instructions}', instructions).replace( '{lang_context}', getLangContext(language, { allowResourcesFetch: true, ...options }) ) - if (hasCode) { - userMessage += codeContext - } - if (hasError) { - userMessage += errorContext - } - if (hasDb) { - userMessage += dbContext - } - if (hasDiff) { - userMessage += diffContext - } + const contextInstructions = buildContextString(selectedContext) + userMessage += contextInstructions return { role: 'user', content: userMessage @@ -626,7 +555,12 @@ async function formatDBSchema(dbSchema: DBSchema) { } export interface ScriptChatHelpers { - getScriptOptions: () => { code: string; lang: ScriptLang | 'bunnative'; path: string; args: Record } + getScriptOptions: () => { + code: string + lang: ScriptLang | 'bunnative' + path: string + args: Record + } getLastSuggestedCode: () => string | undefined applyCode: (code: string, applyAll?: boolean) => void } @@ -634,51 +568,60 @@ export interface ScriptChatHelpers { export const resourceTypeTool: Tool = { def: RESOURCE_TYPE_FUNCTION_DEF, fn: async ({ args, workspace, helpers, toolCallbacks, toolId }) => { - toolCallbacks.setToolStatus(toolId, { content: 'Searching resource types for "' + args.query + '"...' }) + toolCallbacks.setToolStatus(toolId, { + content: 'Searching resource types for "' + args.query + '"...' + }) const lang = helpers.getScriptOptions().lang - const formattedResourceTypes = await getFormattedResourceTypes( - lang, - args.query, - workspace - ) - toolCallbacks.setToolStatus(toolId, { content: 'Retrieved resource types for "' + args.query + '"' }) + const formattedResourceTypes = await getFormattedResourceTypes(lang, args.query, workspace) + toolCallbacks.setToolStatus(toolId, { + content: 'Retrieved resource types for "' + args.query + '"' + }) return formattedResourceTypes } } -export const dbSchemaTool: Tool = { - def: DB_SCHEMA_FUNCTION_DEF, - fn: async ({ args, workspace, toolCallbacks, toolId }) => { - if (!args.resourcePath) { - throw new Error('Database path not provided') - } - toolCallbacks.setToolStatus(toolId, { content: 'Getting database schema for ' + args.resourcePath + '...' }) - const resource = await ResourceService.getResource({ - workspace: workspace, - path: args.resourcePath - }) - const newDbSchemas = {} - await getDbSchemas( - resource.resource_type, - args.resourcePath, - workspace, - newDbSchemas, - (error) => { - console.error(error) +// Generic DB schema tool factory that can be used by both script and flow modes +export function createDbSchemaTool(): Tool { + return { + def: DB_SCHEMA_FUNCTION_DEF, + fn: async ({ args, workspace, toolCallbacks, toolId }) => { + if (!args.resourcePath) { + throw new Error('Database path not provided') } - ) - dbSchemas.update((schemas) => ({ ...schemas, ...newDbSchemas })) - const dbs = get(dbSchemas) - const db = dbs[args.resourcePath] - if (!db) { - throw new Error('Database not found') + toolCallbacks.setToolStatus(toolId, { + content: 'Getting database schema for ' + args.resourcePath + '...' + }) + const resource = await ResourceService.getResource({ + workspace: workspace, + path: args.resourcePath + }) + const newDbSchemas = {} + await getDbSchemas( + resource.resource_type, + args.resourcePath, + workspace, + newDbSchemas, + (error) => { + console.error(error) + } + ) + dbSchemas.update((schemas) => ({ ...schemas, ...newDbSchemas })) + const dbs = get(dbSchemas) + const db = dbs[args.resourcePath] + if (!db) { + throw new Error('Database not found') + } + const stringSchema = await formatDBSchema(db) + toolCallbacks.setToolStatus(toolId, { + content: 'Retrieved database schema for ' + args.resourcePath + }) + return stringSchema } - const stringSchema = await formatDBSchema(db) - toolCallbacks.setToolStatus(toolId, { content: 'Retrieved database schema for ' + args.resourcePath }) - return stringSchema } } +export const dbSchemaTool: Tool = createDbSchemaTool() + type PackageSearchQuery = { package: { name: string @@ -839,31 +782,31 @@ const TEST_RUN_SCRIPT_TOOL: ChatCompletionTool = { function: { name: 'test_run_script', description: 'Execute a test run of the current script in the editor', - // will be overridden by setSchema parameters: { type: 'object', properties: { - args: { - type: 'object', - description: 'Arguments to pass to the script (optional, uses current editor args if not provided)' - } + args: { type: 'string', description: 'JSON string containing the arguments for the tool' } }, - required: [] + additionalProperties: false, + strict: false, + required: ['args'] } - }, + } } export const testRunScriptTool: Tool = { def: TEST_RUN_SCRIPT_TOOL, - fn: async function({ args, workspace, helpers, toolCallbacks, toolId }) { + fn: async function ({ args, workspace, helpers, toolCallbacks, toolId }) { const scriptOptions = helpers.getScriptOptions() - + if (!scriptOptions) { - toolCallbacks.setToolStatus(toolId, { + toolCallbacks.setToolStatus(toolId, { content: 'No script available to test', error: 'No script found in current context' }) - throw new Error('No script code available to test. Please ensure you have a script open in the editor.') + throw new Error( + 'No script code available to test. Please ensure you have a script open in the editor.' + ) } let codeToTest = scriptOptions.code @@ -873,7 +816,7 @@ export const testRunScriptTool: Tool = { if (lastSuggestedCode && lastSuggestedCode !== codeToTest) { codeToTest = lastSuggestedCode toolCallbacks.setToolStatus(toolId, { content: 'Applying code changes...' }) - + // Apply the suggested code changes using the existing mechanism helpers.applyCode(lastSuggestedCode, true) @@ -883,15 +826,16 @@ export const testRunScriptTool: Tool = { const parsedArgs = await buildTestRunArgs(args, this.def) return executeTestRun({ - jobStarter: () => JobService.runScriptPreview({ - workspace: workspace, - requestBody: { - path: scriptOptions.path, - content: codeToTest, - args: parsedArgs, - language: scriptOptions.lang as ScriptLang, - } - }), + jobStarter: () => + JobService.runScriptPreview({ + workspace: workspace, + requestBody: { + path: scriptOptions.path, + content: codeToTest, + args: parsedArgs, + language: scriptOptions.lang as ScriptLang + } + }), workspace, toolCallbacks, toolId, @@ -899,23 +843,7 @@ export const testRunScriptTool: Tool = { contextName: 'script' }) }, - setSchema: async function(helpers: ScriptChatHelpers) { - await buildSchemaForTool(this.def, async () => { - const scriptOptions = helpers.getScriptOptions() - const code = scriptOptions?.code - const lang = scriptOptions?.lang - const lastSuggestedCode = helpers.getLastSuggestedCode() - - const codeToTest = lastSuggestedCode ?? code - if (codeToTest) { - const newSchema = emptySchema() - await inferArgs(lang, codeToTest, newSchema) - return newSchema - } - return emptySchema() - }) - }, requiresConfirmation: true, confirmationMessage: 'Run script test', - showDetails: true, + showDetails: true } diff --git a/frontend/src/lib/components/copilot/chat/shared.ts b/frontend/src/lib/components/copilot/chat/shared.ts index 1aa8aab89c..f47632ffbd 100644 --- a/frontend/src/lib/components/copilot/chat/shared.ts +++ b/frontend/src/lib/components/copilot/chat/shared.ts @@ -4,13 +4,179 @@ import type { ChatCompletionTool } from 'openai/resources/chat/completions.mjs' import { get } from 'svelte/store' -import type { ContextElement } from './context' +import type { CodePieceElement, ContextElement } from './context' import { copilotSessionModel, workspaceStore } from '$lib/stores' import type { ExtendedOpenFlow } from '$lib/components/flows/types' import type { FunctionParameters } from 'openai/resources/shared.mjs' import { zodToJsonSchema } from 'zod-to-json-schema' import { z } from 'zod' -import { ScriptService, JobService, type CompletedJob } from '$lib/gen' +import { ScriptService, JobService, type CompletedJob, type FlowModule } from '$lib/gen' +import { scriptLangToEditorLang } from '$lib/scripts' +import YAML from 'yaml' + +export interface ContextStringResult { + dbContext: string + diffContext: string + flowModuleContext: string + hasDb: boolean + hasDiff: boolean + hasFlowModule: boolean +} + +export const findModuleById = (modules: FlowModule[], moduleId: string): FlowModule | undefined => { + for (const module of modules) { + if (module.id === moduleId) { + return module + } + if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') { + const found = findModuleById(module.value.modules, moduleId) + if (found) { + return found + } + } + if (module.value.type === 'branchall') { + const allModules = module.value.branches.flatMap((b) => b.modules) + const found = findModuleById(allModules, moduleId) + if (found) { + return found + } + } + if (module.value.type === 'branchone') { + const allModules = [ + ...module.value.branches.flatMap((b) => b.modules), + ...module.value.default + ] + const found = findModuleById(allModules, moduleId) + if (found) { + return found + } + } + } + return undefined +} + +const applyCodePieceToCodeContext = (codePieces: CodePieceElement[], codeContext: string) => { + let code = codeContext.split('\n') + let shiftOffset = 0 + codePieces.sort((a, b) => a.startLine - b.startLine) + for (const codePiece of codePieces) { + code.splice(codePiece.endLine + shiftOffset, 0, '[#END]') + code.splice(codePiece.startLine + shiftOffset - 1, 0, '[#START]') + shiftOffset += 2 + } + return code.join('\n') +} + +export function applyCodePiecesToFlowModules( + codePieces: CodePieceElement[], + flowModules: FlowModule[] +): string { + // Parse code piece titles to extract module IDs + // Format: "[id] L3-L5" + const moduleCodePieces = new Map() + + for (const codePiece of codePieces) { + const match = codePiece.title.match(/\[([^\]]+)\]\s+L\d+-L\d+/) + if (match) { + const moduleId = match[1] + if (!moduleCodePieces.has(moduleId)) { + moduleCodePieces.set(moduleId, []) + } + moduleCodePieces.get(moduleId)!.push(codePiece) + } + } + + // Clone modules to avoid mutation + const modifiedModules = JSON.parse(JSON.stringify(flowModules)) + + // Apply code pieces to each module + for (const [moduleId, pieces] of moduleCodePieces) { + const module = findModuleById(modifiedModules, moduleId) + if (module && module.value.type === 'rawscript' && module.value.content) { + module.value.content = applyCodePieceToCodeContext(pieces, module.value.content) + } + } + + return YAML.stringify(modifiedModules) +} + +export function buildContextString(selectedContext: ContextElement[]): string { + const dbTemplate = `- {title}: SCHEMA: \n{schema}\n` + const codeTemplate = ` + - {title}: + \`\`\`{language} + {code} + \`\`\` + ` + + let dbContext = 'DATABASES:\n' + let diffContext = 'DIFF:\n' + let flowModuleContext = 'FOCUSED FLOW MODULES IDS:\n' + let codeContext = 'CODE:\n' + let errorContext = ` + ERROR: + {error} + ` + let hasCode = false + let hasDb = false + let hasDiff = false + let hasFlowModule = false + let hasError = false + + let result = '\n\n' + for (const context of selectedContext) { + if (context.type === 'code') { + hasCode = true + codeContext += codeTemplate + .replace('{title}', context.title) + .replace('{language}', scriptLangToEditorLang(context.lang)) + .replace( + '{code}', + applyCodePieceToCodeContext( + selectedContext.filter((c) => c.type === 'code_piece'), + context.content + ) + ) + } else if (context.type === 'error') { + if (hasError) { + throw new Error('Multiple error contexts provided') + } + hasError = true + errorContext = errorContext.replace('{error}', context.content) + } else if (context.type === 'db') { + hasDb = true + dbContext += dbTemplate + .replace('{title}', context.title) + .replace('{schema}', context.schema?.stringified ?? 'to fetch with get_db_schema') + dbContext += '\n' + } else if (context.type === 'diff') { + hasDiff = true + const diff = JSON.stringify(context.diff) + diffContext += (diff.length > 3000 ? diff.slice(0, 3000) + '...' : diff) + '\n' + } else if (context.type === 'flow_module') { + hasFlowModule = true + flowModuleContext += `${context.id}\n` + } + } + + if (hasCode) { + result += '\n' + codeContext + } + if (hasError) { + result += '\n' + errorContext + } + if (hasDb) { + result += '\n' + dbContext + } + if (hasDiff) { + result += '\n' + diffContext + } + if (hasFlowModule) { + result += '\n' + flowModuleContext + } + + return result +} type BaseDisplayMessage = { content: string @@ -89,11 +255,13 @@ export async function processToolCall({ // Add the tool to the display with appropriate status toolCallbacks.setToolStatus(toolCall.id, { - ...(tool?.requiresConfirmation ? { content: tool.confirmationMessage ?? "Waiting for confirmation..." } : {}), + ...(tool?.requiresConfirmation + ? { content: tool.confirmationMessage ?? 'Waiting for confirmation...' } + : {}), parameters: args, isLoading: true, needsConfirmation: needsConfirmation, - showDetails: tool?.showDetails, + showDetails: tool?.showDetails }) // If confirmation is needed and we have the callback, wait for it @@ -254,12 +422,17 @@ export const createSearchHubScriptsTool = (withContent: boolean = false) => ({ } }) -export async function buildSchemaForTool(toolDef: ChatCompletionTool, schemaBuilder: () => Promise): Promise { +export async function buildSchemaForTool( + toolDef: ChatCompletionTool, + schemaBuilder: () => Promise +): Promise { try { const schema = await schemaBuilder() // if schema properties contains values different from '^[a-zA-Z0-9_.-]{1,64}$' - const invalidProperties = Object.keys(schema.properties ?? {}).filter((key) => !/^[a-zA-Z0-9_.-]{1,64}$/.test(key)) + const invalidProperties = Object.keys(schema.properties ?? {}).filter( + (key) => !/^[a-zA-Z0-9_.-]{1,64}$/.test(key) + ) if (invalidProperties.length > 0) { console.warn(`Invalid flow inputs schema: ${invalidProperties.join(', ')}`) throw new Error(`Invalid flow inputs schema: ${invalidProperties.join(', ')}`) @@ -275,7 +448,15 @@ export async function buildSchemaForTool(toolDef: ChatCompletionTool, schemaBuil } catch (error) { console.error('Error building schema for tool', error) // fallback to schema with args as a JSON string - toolDef.function.parameters = { type: 'object', properties: { args: { type: 'string', description: 'JSON string containing the arguments for the tool' } }, additionalProperties: false, strict: false, required: ['args'] } + toolDef.function.parameters = { + type: 'object', + properties: { + args: { type: 'string', description: 'JSON string containing the arguments for the tool' } + }, + additionalProperties: false, + strict: false, + required: ['args'] + } return false } } @@ -393,7 +574,10 @@ function getErrorMessage(result: unknown): string { export async function buildTestRunArgs(args: any, toolDef: ChatCompletionTool): Promise { let parsedArgs = args // if the schema is the fallback schema, parse the args as a JSON string - if ((toolDef.function.parameters as any).properties?.args?.description === 'JSON string containing the arguments for the tool') { + if ( + (toolDef.function.parameters as any).properties?.args?.description === + 'JSON string containing the arguments for the tool' + ) { try { parsedArgs = JSON.parse(args.args) } catch (error) { diff --git a/frontend/src/lib/components/flows/FlowEditor.svelte b/frontend/src/lib/components/flows/FlowEditor.svelte index 2e264d94ae..25cb5c60ab 100644 --- a/frontend/src/lib/components/flows/FlowEditor.svelte +++ b/frontend/src/lib/components/flows/FlowEditor.svelte @@ -18,6 +18,7 @@ import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte' import type { ModulesTestStates } from '../modulesTest.svelte' import type { StateStore } from '$lib/utils' + import type { FlowOptions } from '../copilot/chat/ContextManager.svelte' const { flowStore } = getContext('FlowEditorContext') interface Props { @@ -103,11 +104,24 @@ pickablePropertiesFiltered: writable(undefined) }) + $effect(() => { + const options: FlowOptions = { + currentFlow: flowStore.val, + lastDeployedFlow: savedFlow, + lastSavedFlow: savedFlow?.draft, + path: savedFlow?.path, + modules: flowStore.val.value.modules + } + aiChatManager.flowOptions = options + }) + onMount(() => { + aiChatManager.saveAndClear() aiChatManager.changeMode(AIMode.FLOW) }) onDestroy(() => { + aiChatManager.flowOptions = undefined aiChatManager.changeMode(AIMode.NAVIGATOR) }) diff --git a/frontend/src/lib/components/flows/FlowModuleIcon.svelte b/frontend/src/lib/components/flows/FlowModuleIcon.svelte new file mode 100644 index 0000000000..ecadb9ea4f --- /dev/null +++ b/frontend/src/lib/components/flows/FlowModuleIcon.svelte @@ -0,0 +1,50 @@ + + +{#if module.value.type === 'aiagent'} + +{:else if module.value.type === 'rawscript'} + +{:else if module.summary === 'Terminate flow'} + +{:else if module.value.type === 'identity'} + +{:else if module.value.type === 'flow'} + +{:else if module.value.type === 'forloopflow' || module.value.type === 'whileloopflow'} + +{:else if module.value.type === 'branchone' || module.value.type === 'branchall'} + +{:else if module.value.type === 'script'} + {#if module.value.path.startsWith('hub/')} + + {:else} + + {/if} +{:else} + + +{/if} diff --git a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte index d00341032d..f30dd02ca2 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte @@ -419,6 +419,7 @@ {lastDeployedCode} {diffMode} openAiChat + moduleId={flowModule.id} />
{/if} @@ -477,6 +478,7 @@ {} )} key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`} + moduleId={flowModule.id} /> import { Button } from '$lib/components/common' - import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte' - import IconedResourceType from '$lib/components/IconedResourceType.svelte' import type { FlowModule, FlowStatusModule, Job } from '$lib/gen' - import { Building, Repeat, Square, ArrowDown, GitBranch, Bot } from 'lucide-svelte' import { createEventDispatcher, getContext } from 'svelte' import type { Writable } from 'svelte/store' import FlowModuleSchemaItem from './FlowModuleSchemaItem.svelte' + import FlowModuleIcon from '../FlowModuleIcon.svelte' import { prettyLanguage } from '$lib/common' import { msToSec } from '$lib/utils' - import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte' import FlowJobsMenu from './FlowJobsMenu.svelte' import { isTriggerStep, @@ -185,9 +182,7 @@ {darkMode} > {#snippet icon()} -
- -
+ {/snippet} {:else if mod.value.type === 'branchone'} @@ -208,9 +203,7 @@ {darkMode} > {#snippet icon()} -
- -
+ {/snippet} {:else if mod.value.type === 'branchall'} @@ -231,9 +224,7 @@ {darkMode} > {#snippet icon()} -
- -
+ {/snippet} {:else} @@ -281,32 +272,10 @@ {skipped} > {#snippet icon()} -
- {#if mod.value.type === 'aiagent'} - - {:else if mod.value.type === 'rawscript'} - - {:else if mod.summary == 'Terminate flow'} - - {:else if mod.value.type === 'identity'} - - {:else if mod.value.type === 'flow'} - - {:else if mod.value.type === 'script'} - {#if mod.value.path.startsWith('hub/')} -
- -
- {:else} - - {/if} - {/if} -
+ {@const size = mod.value.type === 'script' && mod.value.path.startsWith('hub/') + ? 20 + : mod.value.type === "script" ? 14 : 16} + {/snippet} {/if} From 4d8777b278b837982532182e3308ac80a7852ba6 Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:24:43 +0200 Subject: [PATCH 40/42] Fix read undefined when renaming flow step (#6440) --- frontend/src/lib/components/ModuleTest.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/ModuleTest.svelte b/frontend/src/lib/components/ModuleTest.svelte index fa265abfbd..0ff430f148 100644 --- a/frontend/src/lib/components/ModuleTest.svelte +++ b/frontend/src/lib/components/ModuleTest.svelte @@ -179,7 +179,10 @@ } } } - bind:job={modulesTestStates.states[mod.id].testJob} + bind:job={ + () => modulesTestStates.states[mod.id]?.testJob, + (v) => modulesTestStates.states[mod.id] && (modulesTestStates.states[mod.id].testJob = v) + } loadPlaceholderJobOnStart={{ type: 'QueuedJob', id: '', From 7da79a8bc525fc6b89748ad0af25c2bac4ca2ef3 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 22 Aug 2025 11:39:13 +0200 Subject: [PATCH 41/42] feat: json schema resource (#6433) * feat: json schema resource * feat: cache * fix cleanup * fix: use format instead of custom property --- ...d0fca091d7c1cb0924f6f9343544abb2666a5.json | 23 +++++ backend/windmill-api/src/apps.rs | 34 ++++--- frontend/src/lib/components/ArgInput.svelte | 44 +++++++++ .../lib/components/EditableSchemaForm.svelte | 45 +++++---- .../src/lib/components/FieldHeader.svelte | 2 +- .../src/lib/components/ResourceEditor.svelte | 4 + .../schema/FlowPropertyEditor.svelte | 97 +++++++++++++++---- .../schema/jsonSchemaResource.svelte.ts | 44 +++++++++ frontend/src/lib/utils.ts | 17 ++-- 9 files changed, 252 insertions(+), 58 deletions(-) create mode 100644 backend/.sqlx/query-d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5.json create mode 100644 frontend/src/lib/components/schema/jsonSchemaResource.svelte.ts diff --git a/backend/.sqlx/query-d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5.json b/backend/.sqlx/query-d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5.json new file mode 100644 index 0000000000..dbd97c765a --- /dev/null +++ b/backend/.sqlx/query-d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT value from resource WHERE path = $1 AND workspace_id = $2 AND resource_type = 'json_schema'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "value", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5" +} diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index f110751942..37743eae34 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -762,20 +762,26 @@ async fn get_public_resource( Path((w_id, path)): Path<(String, StripPath)>, ) -> JsonResult> { let path = path.to_path(); - if !path.starts_with("f/app_themes/") { - return Err(Error::BadRequest( - "Only app themes are public resources".to_string(), - )); - } - let res = sqlx::query_scalar!( - "SELECT value from resource WHERE path = $1 AND workspace_id = $2", - path.to_owned(), - &w_id - ) - .fetch_optional(&db) - .await? - .flatten(); - Ok(Json(res)) + + let res = if path.starts_with("f/app_themes/") { + sqlx::query_scalar!( + "SELECT value from resource WHERE path = $1 AND workspace_id = $2", + path.to_owned(), + &w_id + ) + .fetch_optional(&db) + .await? + } else { + sqlx::query_scalar!( + "SELECT value from resource WHERE path = $1 AND workspace_id = $2 AND resource_type = 'json_schema'", + path.to_owned(), + &w_id + ) + .fetch_optional(&db) + .await? + }; + + Ok(Json(res.flatten())) } async fn get_secret_id( diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index 1a9e7c0efc..a15c461c12 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -6,6 +6,7 @@ import { setInputCat as computeInputCat, debounce, + emptySchema, emptyString, getSchemaFromProperties, type DynamicSelect @@ -42,6 +43,8 @@ import { safeSelectItems } from './select/utils.svelte' import S3ArgInput from './common/fileUpload/S3ArgInput.svelte' import { base } from '$lib/base' + import { workspaceStore } from '$lib/stores' + import { getJsonSchemaFromResource } from './schema/jsonSchemaResource.svelte' interface Props { label?: string @@ -658,6 +661,47 @@ {appPath} {computeS3ForceViewerPolicies} /> + {:else if inputCat == 'object' && format == 'json-schema'} + {#await import('$lib/components/EditableSchemaForm.svelte')} + + {:then Module} + + value && typeof value === 'object' && !Array.isArray(value) ? value : emptySchema(), + (v) => { + value = v + } + } + isFlowInput + editTab="inputEditor" + noPreview + addPropertyInEditorTab + /> + {/await} + {:else if inputCat == 'object' && format?.startsWith('jsonschema-')} + {#await getJsonSchemaFromResource(format.substring('jsonschema-'.length), workspace ?? $workspaceStore ?? '')} + + {:then schema} + {#if !schema || !schema.properties} + {#await import('$lib/components/JsonEditor.svelte')} + + {:then Module} + + {/await} + {:else} +
+ +
+ {/if} + {/await} {:else if inputCat == 'list' && !isListJson}
diff --git a/frontend/src/lib/components/EditableSchemaForm.svelte b/frontend/src/lib/components/EditableSchemaForm.svelte index d91cc5c2d6..a3dfc5a168 100644 --- a/frontend/src/lib/components/EditableSchemaForm.svelte +++ b/frontend/src/lib/components/EditableSchemaForm.svelte @@ -29,6 +29,7 @@ import type { EditableSchemaFormUi } from '$lib/components/custom_ui' import Section from '$lib/components/Section.svelte' import Editor from './Editor.svelte' + import AddPropertyV2 from './schema/AddPropertyV2.svelte' // export let openEditTab: () => void = () => {} const dispatch = createEventDispatcher() @@ -69,6 +70,7 @@ dynSelectCode?: string | undefined dynSelectLang?: ScriptLang | undefined showDynSelectOpt?: boolean + addPropertyInEditorTab?: boolean openEditTab?: import('svelte').Snippet addProperty?: import('svelte').Snippet runButton?: import('svelte').Snippet @@ -104,6 +106,7 @@ dynSelectCode = $bindable(), dynSelectLang = $bindable(), showDynSelectOpt = false, + addPropertyInEditorTab = false, openEditTab, addProperty, runButton, @@ -509,22 +512,31 @@ {:else} {#if jsonEnabled && customUi?.jsonOnly != true} -
- { - schemaString = JSON.stringify(schema, null, '\t') - editor?.setCode(schemaString) - }} - /> +
+ {#if addPropertyInEditorTab} + + {#snippet trigger()} +
{/if} @@ -655,7 +667,6 @@ const isS3 = v == 'S3' const isOneOf = v == 'oneOf' const isDynSelect = v == 'dynselect' - const emptyProperty = { contentEncoding: undefined, enum_: undefined, diff --git a/frontend/src/lib/components/FieldHeader.svelte b/frontend/src/lib/components/FieldHeader.svelte index 9c465f73fe..15bc6638a6 100644 --- a/frontend/src/lib/components/FieldHeader.svelte +++ b/frontend/src/lib/components/FieldHeader.svelte @@ -40,7 +40,7 @@ {/if} {#if displayType} - {#if format && !format.startsWith('resource')} + {#if format && !format.startsWith('resource') && !format.startsWith('jsonschema-')} {format} diff --git a/frontend/src/lib/components/ResourceEditor.svelte b/frontend/src/lib/components/ResourceEditor.svelte index 51226d4958..baca88f16c 100644 --- a/frontend/src/lib/components/ResourceEditor.svelte +++ b/frontend/src/lib/components/ResourceEditor.svelte @@ -22,6 +22,7 @@ import TestTriggerConnection from './triggers/TestTriggerConnection.svelte' import GitHubAppIntegration from './GitHubAppIntegration.svelte' import Button from './common/button/Button.svelte' + import { clearJsonSchemaResourceCache } from './schema/jsonSchemaResource.svelte' interface Props { canSave?: boolean @@ -94,6 +95,9 @@ path: resourceToEdit.path, requestBody: { path, value: args, description } }) + if (resourceToEdit.resource_type === 'json_schema') { + clearJsonSchemaResourceCache(resourceToEdit.path, $workspaceStore!) + } sendUserToast(`Updated resource at ${path}`) dispatch('refresh', path) } else { diff --git a/frontend/src/lib/components/schema/FlowPropertyEditor.svelte b/frontend/src/lib/components/schema/FlowPropertyEditor.svelte index 631212c974..c272df3ca3 100644 --- a/frontend/src/lib/components/schema/FlowPropertyEditor.svelte +++ b/frontend/src/lib/components/schema/FlowPropertyEditor.svelte @@ -17,6 +17,8 @@ import Button from '../common/button/Button.svelte' import { Pen, Plus, Trash2 } from 'lucide-svelte' import Popover from '$lib/components/meltComponents/Popover.svelte' + import ResourcePicker from '../ResourcePicker.svelte' + import Tooltip from '../Tooltip.svelte' interface Props { format?: string | undefined @@ -122,9 +124,19 @@ } let initialObjectSelected = $state( - Object.keys(properties ?? {}).length == 0 ? 'resource' : 'custom-object' + format === 'json-schema' + ? 'json-schema' + : format?.startsWith('jsonschema-') + ? 'custom-object' + : Object.keys(properties ?? {}).length == 0 + ? 'resource' + : 'custom-object' ) let isDynSelect = $derived(format?.startsWith('dynselect-') ?? false) + + let customObjectSelected: 'editor' | 'json-schema-resource' = $state( + format?.startsWith('jsonschema-') ? 'json-schema-resource' : 'editor' + )
@@ -291,33 +303,82 @@ { - if (e.detail === 'custom-object') { + if (e.detail === 'json-schema') { + format = 'json-schema' + } else { format = '' } }} > Resource Custom Object + + JSON Schema + + This displays a JSON schema editor, useful when a JSON schema input is expected. + + {#snippet content()}
- { - return { - properties: properties, - order: order, - required: requiredProperty - } - }, - (v) => { - properties = v.properties - order = v.order - requiredProperty = v.required - dispatch('schemaChange') + { + if (e.detail === 'editor') { + format = undefined + } else { + properties = undefined + order = undefined + requiredProperty = undefined } - } - /> + }} + > + {#snippet children({ item })} + + + {/snippet} + + {#if customObjectSelected === 'editor'} + { + return { + properties: properties, + order: order, + required: requiredProperty + } + }, + (v) => { + properties = v.properties + order = v.order + requiredProperty = v.required + dispatch('schemaChange') + } + } + /> + {:else if customObjectSelected === 'json-schema-resource'} + { + if (format?.startsWith('jsonschema-')) { + return format.substring('jsonschema-'.length) + } + return undefined + }, + (v) => { + format = 'jsonschema-' + v + } + } + /> + {/if} diff --git a/frontend/src/lib/components/schema/jsonSchemaResource.svelte.ts b/frontend/src/lib/components/schema/jsonSchemaResource.svelte.ts new file mode 100644 index 0000000000..0d4fee8f2a --- /dev/null +++ b/frontend/src/lib/components/schema/jsonSchemaResource.svelte.ts @@ -0,0 +1,44 @@ +import { z } from 'zod' + +import { AppService } from '$lib/gen' +import { sendUserToast } from '$lib/utils' + +let loadedJsonSchemaResources: Record> = $state({}) + +const jsonSchemaResourceSchema = z.object({ + schema: z.record(z.string(), z.any()) +}) +export async function getJsonSchemaFromResource(path: string, workspace: string) { + if (loadedJsonSchemaResources[workspace]?.[path]) { + return loadedJsonSchemaResources[workspace][path] + } + + try { + const resourceValue = await AppService.getPublicResource({ + path, + workspace + }) + + const parsedResource = jsonSchemaResourceSchema.safeParse(resourceValue) + if (parsedResource.success) { + const workspaceResources = loadedJsonSchemaResources[workspace] + if (!workspaceResources) { + loadedJsonSchemaResources[workspace] = {} + } + loadedJsonSchemaResources[workspace][path] = parsedResource.data.schema + return parsedResource.data.schema + } else { + console.error('Invalid JSON schema resource:', parsedResource.error) + sendUserToast('Invalid JSON schema resource: ' + parsedResource.error, true) + } + } catch (err) { + console.error(err) + sendUserToast('Could not load JSON schema resource: ' + err, true) + } +} + +export async function clearJsonSchemaResourceCache(path: string, workspace: string) { + if (loadedJsonSchemaResources[workspace]?.[path]) { + delete loadedJsonSchemaResources[workspace][path] + } +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 99b1ccef76..f666e46490 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -172,9 +172,9 @@ export function displayDate( } const dateChoices: Intl.DateTimeFormatOptions = displayDate ? { - day: 'numeric', - month: 'numeric' - } + day: 'numeric', + month: 'numeric' + } : {} return date.toLocaleString(undefined, { ...timeChoices, @@ -592,6 +592,7 @@ export type InputCat = | 'currency' | 'oneOf' | 'dynselect' + | 'json-schema' export namespace DynamicSelect { export type HelperScript = @@ -1025,7 +1026,7 @@ export async function tryEvery({ try { await tryCode() break - } catch (err) { } + } catch (err) {} i++ } if (i >= times) { @@ -1292,7 +1293,7 @@ export function conditionalMelt(node: HTMLElement, meltItem: AnyMeltElement | un if (meltItem) { return meltItem(node) } - return { destroy: () => { } } + return { destroy: () => {} } } export type Item = { @@ -1497,9 +1498,9 @@ export type S3Uri = `s3://${string}/${string}` export type S3Object = | S3Uri | { - s3: string - storage?: string - } + s3: string + storage?: string + } export function parseS3Object(s3Object: S3Object): { s3: string; storage?: string } { if (typeof s3Object === 'object') return s3Object From 2046b64ec8bb0e974836cedb3e7995a157469b9b Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 22 Aug 2025 11:05:42 +0100 Subject: [PATCH 42/42] chore(main): release 1.532.0 (#6439) * chore(main): release 1.532.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> --- CHANGELOG.md | 8 +++ backend/Cargo.lock | 66 +++++++++---------- backend/Cargo.toml | 4 +- backend/windmill-api/openapi.yaml | 2 +- benchmarks/lib.ts | 2 +- cli/src/main.ts | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- lsp/Pipfile | 4 +- openflow.openapi.yaml | 2 +- .../WindmillClient/WindmillClient.psd1 | 2 +- python-client/wmill/pyproject.toml | 2 +- python-client/wmill_pg/pyproject.toml | 2 +- typescript-client/jsr.json | 2 +- typescript-client/package.json | 2 +- version.txt | 2 +- 16 files changed, 58 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a4147933..5b4a02f43e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.532.0](https://github.com/windmill-labs/windmill/compare/v1.531.0...v1.532.0) (2025-08-22) + + +### Features + +* **aichat:** allow adding contexts to flow mode ([#6424](https://github.com/windmill-labs/windmill/issues/6424)) ([73272f1](https://github.com/windmill-labs/windmill/commit/73272f16fddc355703b04f2c3458520753d1e19c)) +* json schema resource ([#6433](https://github.com/windmill-labs/windmill/issues/6433)) ([7da79a8](https://github.com/windmill-labs/windmill/commit/7da79a8bc525fc6b89748ad0af25c2bac4ca2ef3)) + ## [1.531.0](https://github.com/windmill-labs/windmill/compare/v1.530.0...v1.531.0) (2025-08-22) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 4eea9b2aff..41110087df 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1800,7 +1800,7 @@ dependencies = [ "byteorder", "gemm 0.17.1", "half", - "memmap2 0.9.7", + "memmap2 0.9.8", "num-traits", "num_cpus", "rand 0.9.0", @@ -8063,9 +8063,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", "stable_deref_trait", @@ -13196,7 +13196,7 @@ dependencies = [ "lru 0.12.5", "lz4_flex", "measure_time", - "memmap2 0.9.7", + "memmap2 0.9.8", "once_cell", "oneshot", "rayon", @@ -14341,7 +14341,7 @@ dependencies = [ "gemm 0.18.2", "half", "libloading 0.8.8", - "memmap2 0.9.7", + "memmap2 0.9.8", "num", "num-traits", "num_cpus", @@ -15129,7 +15129,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "axum", @@ -15183,7 +15183,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "argon2", @@ -15300,7 +15300,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.531.0" +version = "1.532.0" dependencies = [ "base64 0.22.1", "chrono", @@ -15315,7 +15315,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.531.0" +version = "1.532.0" dependencies = [ "chrono", "serde", @@ -15328,7 +15328,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "axum", @@ -15347,7 +15347,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "async-recursion", @@ -15427,7 +15427,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.531.0" +version = "1.532.0" dependencies = [ "regex", "serde", @@ -15442,7 +15442,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "bytes", @@ -15466,7 +15466,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.531.0" +version = "1.532.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15478,7 +15478,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.531.0" +version = "1.532.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -15487,7 +15487,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15499,7 +15499,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "serde_json", @@ -15511,7 +15511,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "gosyn", @@ -15523,7 +15523,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15535,7 +15535,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "serde_json", @@ -15547,7 +15547,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "nu-parser", @@ -15558,7 +15558,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15569,7 +15569,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15581,7 +15581,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "async-recursion", @@ -15604,7 +15604,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15618,7 +15618,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15635,7 +15635,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15649,7 +15649,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "lazy_static", @@ -15667,7 +15667,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "getrandom 0.2.16", @@ -15692,7 +15692,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "serde_json", @@ -15702,7 +15702,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "async-recursion", @@ -15735,7 +15735,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.531.0" +version = "1.532.0" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -15745,7 +15745,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.531.0" +version = "1.532.0" dependencies = [ "anyhow", "async-recursion", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index f3c06a123c..58caa17cb5 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.531.0" +version = "1.532.0" authors.workspace = true edition.workspace = true @@ -33,7 +33,7 @@ members = [ ] [workspace.package] -version = "1.531.0" +version = "1.532.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index f9b11dd5c8..bf40d63dd6 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.531.0 + version: 1.532.0 title: Windmill API contact: diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index cb2e44bb95..edf4971ef8 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.531.0"; +export const VERSION = "v1.532.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/main.ts b/cli/src/main.ts index 7a6b6d0462..3c21060113 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -68,7 +68,7 @@ export { // } // }); -export const VERSION = "1.531.0"; +export const VERSION = "1.532.0"; const command = new Command() .name("wmill") diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6b0a29b889..ab97bcb6cc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.531.0", + "version": "1.532.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.531.0", + "version": "1.532.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 1e87a4b43f..a61e8a233b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.531.0", + "version": "1.532.0", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/lsp/Pipfile b/lsp/Pipfile index 807453d92e..dcc362946f 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,8 +4,8 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.531.0" -wmill_pg = ">=1.531.0" +wmill = ">=1.532.0" +wmill_pg = ">=1.532.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 5c82985cef..3587667c35 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.531.0 + version: 1.532.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index f06ec3db19..6f64bfb936 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.531.0' + ModuleVersion = '1.532.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index 86bac91388..1f7ad7442d 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.531.0" +version = "1.532.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/python-client/wmill_pg/pyproject.toml b/python-client/wmill_pg/pyproject.toml index 883cb971f8..db5c3e0562 100644 --- a/python-client/wmill_pg/pyproject.toml +++ b/python-client/wmill_pg/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill-pg" -version = "1.531.0" +version = "1.532.0" description = "An extension client for the wmill client library focused on pg" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 2cd421aa70..0a2cb66268 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.531.0", + "version": "1.532.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index 58e795eef7..5d5ab95edc 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.531.0", + "version": "1.532.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "devDependencies": { diff --git a/version.txt b/version.txt index dbae720437..0b9cab5746 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.531.0 +1.532.0