mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 08:16:49 +00:00
* feat: open a flow linking an agent from its menu Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: toast when the flow handoff cannot be stored Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: turn an agent into a flow from its editor, and into a chat app from its page Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: let sandboxed apps run agents, and keep the flow handoff race-free Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: save the new flow as a draft before letting the agent draft go Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: offer the chat app only to those who may build apps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: give the agent page's chat app section a tab on small screens Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show the flow handoff as an info alert, framed around context and output Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep the flow handoff from operators and say why the chat app is locked Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: collapse the flow handoff hint to its title and button Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show the flow handoff as a corner hint, as the flow editor shows its own Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: name the agent on the app button and say why it is disabled Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: ask whether the agent needs steps before or after it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: name what a flow adds around the agent in its hint Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: say a never-deployed agent turns into a flow Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * revert: leave agent runs and the raw-app guidance to the chat SDK's runnable transport Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: show the agent page's sections as tabs at every width, as a chat-mode flow's page does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: one webhook panel for scripts, flows and agents, with an AI prompt and chat-mode examples Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: describe the preprocessor's event argument instead of the v1 wm_trigger Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: give the flow chat's preprocessor example the v2 event signature Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: group a webhook's request choices and examples under an HTTP tab, beside the AI prompt and chat SDK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: drop a comment about guidance this branch no longer adds Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: say where the AI prompt expects the webhook token Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: tell the chat-flow prompt that memory_id is required Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep the webhook tabs next to their content Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep the webhook's REST tab name Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: build an agent's chat app from a template, not an AI session Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: don't send the agent chat app's message on an IME confirmation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: agent API tab with one row of call examples and a copy-prompt button Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: open build app in an AI session when AI sessions are on Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: app conversations through the memory_id query parameter, chat-mode flows included Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: regenerate the mcp session instructions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: type backendAsync's memoryId option in the eval diagnostics Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: pin git-sync hub scripts to windmill-cli 1.849.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep build app in the editor when windmill ai is off Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: webhook panel with http and chat sdk tabs over toggle settings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: memoryId option on the sync backend call too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: bump the ui builder artifact for the memoryId option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep the webhook panel's http and chat sdk tabs close to their content Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: shared-utils 1.0.14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: cli takes shared-utils 1.0.14 for the memoryId option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: pin the app conversation scoping and the memoryId signatures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: assert the ownership refusal, not any refusal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: match the memoryId option in the cli wmill.ts signature tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: show the webhook request formats in a card with their toggle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: put the chat sdk example in the same card as the http request Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: add a windmill app tab to chat-mode flow pages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: title the webhook example cards and label the ai prompt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: offer the chat app from the conversations sidebar instead of a tab Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: raise the chat app's chat-sdk floor to 1.851.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: render the agent's chat app card only once the workspace is known Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: gate the flow's build chat app menu item like the sidebar card Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: build the agent's chat app through the page's navigation, for app-only authors too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep an imported app in the editor inside a session preview Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: build a form app from an agent without managed memory Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: cover the agent form app's generated source Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
345 lines
11 KiB
Svelte
345 lines
11 KiB
Svelte
<script lang="ts">
|
|
import { moduleSlot, savedModuleById } from '../moduleSlot'
|
|
import FlowModuleWrapper from './FlowModuleWrapper.svelte'
|
|
import { type FlowModule } from '$lib/gen'
|
|
import { getContext } from 'svelte'
|
|
|
|
import { stepSettingDefaults } from '$lib/components/flows/flowStepSettings'
|
|
import { emptyString } from '$lib/utils'
|
|
import type { FlowEditorContext } from '../types'
|
|
import FlowLoop from './FlowLoop.svelte'
|
|
import FlowModuleComponent from './FlowModuleComponent.svelte'
|
|
import FlowBranchAllWrapper from './FlowBranchAllWrapper.svelte'
|
|
import FlowBranchOneWrapper from './FlowBranchOneWrapper.svelte'
|
|
import {
|
|
createInlineScriptModule,
|
|
pickFlow,
|
|
pickScript
|
|
} from '$lib/components/flows/flowStateUtils.svelte'
|
|
import FlowInputs from './FlowInputs.svelte'
|
|
import { Alert } from '$lib/components/common'
|
|
import FlowInputsFlow from './FlowInputsFlow.svelte'
|
|
import FlowBranchesAllWrapper from './FlowBranchesAllWrapper.svelte'
|
|
import FlowBranchesOneWrapper from './FlowBranchesOneWrapper.svelte'
|
|
import FlowWhileLoop from './FlowWhileLoop.svelte'
|
|
import FlowCard from '../common/FlowCard.svelte'
|
|
import type { TriggerContext } from '$lib/components/triggers'
|
|
import { formatCron } from '$lib/utils'
|
|
import AgentToolWrapper from './AgentToolWrapper.svelte'
|
|
const { selectionManager, flowStateStore, opWorkspace } =
|
|
getContext<FlowEditorContext>('FlowEditorContext')
|
|
const selectedId = $derived(selectionManager.getSelectedId())
|
|
|
|
const { triggersState, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
|
|
|
let scriptKind: 'script' | 'trigger' | 'approval' | 'preprocessor' = $state('script')
|
|
let scriptTemplate: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' = $state('script')
|
|
|
|
// These pointers are used to easily access previewArgs of parent module, and previous module
|
|
|
|
interface Props {
|
|
flowModule: FlowModule
|
|
noEditor?: boolean
|
|
enableAi?: boolean
|
|
savedModule?: FlowModule | undefined
|
|
// Pointer to parent module, only defined within Branches or Loops.
|
|
parentModule?: FlowModule | undefined
|
|
// Pointer to previous module, for easy access to testing results
|
|
previousModule?: FlowModule | undefined
|
|
forceTestTab?: Record<string, boolean>
|
|
highlightArg?: Record<string, string | undefined>
|
|
isAgentTool?: boolean
|
|
/** Lets an agent step add a tool through the same path as the graph's `+ Tool`. */
|
|
flowModuleSchemaMap?: import('../map/FlowModuleSchemaMap.svelte').default
|
|
}
|
|
|
|
let {
|
|
flowModule = $bindable(),
|
|
noEditor = false,
|
|
enableAi = false,
|
|
savedModule = undefined,
|
|
parentModule = $bindable(),
|
|
previousModule = undefined,
|
|
forceTestTab,
|
|
highlightArg,
|
|
isAgentTool = false,
|
|
flowModuleSchemaMap = undefined
|
|
}: Props = $props()
|
|
|
|
function initializePrimaryScheduleForTriggerScript(module: FlowModule) {
|
|
const primaryIndex = triggersState.triggers.findIndex((t) => t.isPrimary)
|
|
if (primaryIndex === -1) {
|
|
const primaryCfg = {
|
|
summary: 'Scheduled poll of flow',
|
|
args: {},
|
|
schedule: formatCron('0 */15 * * *'),
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
enabled: true,
|
|
is_flow: true
|
|
}
|
|
triggersState.addDraftTrigger(triggersCount, 'schedule', undefined, primaryCfg)
|
|
} else if (triggersState.triggers[primaryIndex].draftConfig) {
|
|
//If there is a primary schedule draft update it
|
|
const newCfg = { ...triggersState.triggers[primaryIndex].draftConfig }
|
|
let updated = false
|
|
if (!newCfg.schedule) {
|
|
newCfg.schedule = formatCron('0 */15 * * *')
|
|
updated = true
|
|
}
|
|
if (!newCfg.enabled) {
|
|
newCfg.enabled = true
|
|
updated = true
|
|
}
|
|
if (updated) {
|
|
triggersState.triggers[primaryIndex].draftConfig = newCfg
|
|
}
|
|
}
|
|
|
|
module.stop_after_if = stepSettingDefaults('early-stop', 'trigger')
|
|
}
|
|
async function createModuleFromScript(
|
|
path: string,
|
|
summary: string,
|
|
kind: string,
|
|
hash: string | undefined
|
|
) {
|
|
const [module, state] = await pickScript(
|
|
path,
|
|
summary,
|
|
flowModule.id,
|
|
hash,
|
|
undefined,
|
|
opWorkspace?.()
|
|
)
|
|
|
|
if (kind == 'approval') {
|
|
module.suspend = stepSettingDefaults('suspend')
|
|
}
|
|
|
|
if (kind == 'trigger') {
|
|
initializePrimaryScheduleForTriggerScript(module)
|
|
}
|
|
|
|
flowModule = module
|
|
flowStateStore.val[module.id] = state
|
|
}
|
|
</script>
|
|
|
|
{#if flowModule.id === selectedId}
|
|
{#if flowModule.value.type === 'forloopflow'}
|
|
<FlowLoop {noEditor} bind:mod={flowModule} {parentModule} {previousModule} {enableAi} />
|
|
{:else if flowModule.value.type === 'whileloopflow'}
|
|
<FlowWhileLoop {noEditor} bind:mod={flowModule} {previousModule} {parentModule} />
|
|
{:else if flowModule.value.type === 'branchone'}
|
|
<FlowBranchesOneWrapper {noEditor} {previousModule} {parentModule} {enableAi} bind:flowModule />
|
|
{:else if flowModule.value.type === 'branchall'}
|
|
<FlowBranchesAllWrapper {noEditor} {previousModule} {parentModule} bind:flowModule />
|
|
{:else if flowModule.value.type === 'identity'}
|
|
{#if selectedId == 'failure'}
|
|
<div class="p-4">
|
|
<Alert type="info" title="Error handlers are triggered upon non recovered errors">
|
|
If defined, the error handler will take the error as input.
|
|
</Alert>
|
|
</div>
|
|
{:else if selectedId == 'preprocessor'}
|
|
<div class="p-4">
|
|
<Alert
|
|
type="info"
|
|
title="Preprocessor is called when the flow is triggered by API or email"
|
|
>
|
|
It prepares arguments for the flow. It receives an `event` argument: the trigger's `kind`
|
|
(such as `webhook` or `email`) and what the trigger received, a webhook's `body` and
|
|
`headers` for instance.
|
|
</Alert>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if flowModule.value.flow}
|
|
<FlowInputsFlow
|
|
on:pick={async ({ detail }) => {
|
|
const { path, summary } = detail
|
|
const [module, state] = await pickFlow(path, summary, flowModule.id, opWorkspace?.())
|
|
|
|
flowModule = module
|
|
flowStateStore.val[module.id] = state
|
|
}}
|
|
/>
|
|
{:else}
|
|
<FlowInputs
|
|
{noEditor}
|
|
summary={flowModule.summary}
|
|
shouldDisableTriggerScripts={parentModule !== undefined ||
|
|
previousModule !== undefined ||
|
|
selectedId == 'failure' ||
|
|
selectedId == 'preprocessor'}
|
|
on:pick={async ({ detail }) => {
|
|
const { path, summary, kind, hash } = detail
|
|
// The picked script's summary is a default: anything already typed on the step
|
|
// was the user's choice and outranks it.
|
|
createModuleFromScript(
|
|
path,
|
|
emptyString(flowModule.summary) ? summary : flowModule.summary,
|
|
kind,
|
|
hash
|
|
)
|
|
}}
|
|
on:new={async ({ detail }) => {
|
|
const { language, kind, subkind, summary } = detail
|
|
|
|
const [module, state] = await createInlineScriptModule(
|
|
language,
|
|
kind,
|
|
subkind,
|
|
flowModule.id,
|
|
summary
|
|
)
|
|
scriptKind = kind
|
|
scriptTemplate = subkind
|
|
|
|
if (kind == 'trigger') {
|
|
initializePrimaryScheduleForTriggerScript(module)
|
|
}
|
|
|
|
if (kind == 'approval') {
|
|
module.suspend = stepSettingDefaults('suspend')
|
|
}
|
|
|
|
flowModule = module
|
|
flowStateStore.val[module.id] = state
|
|
}}
|
|
failureModule={selectedId === 'failure'}
|
|
preprocessorModule={selectedId === 'preprocessor'}
|
|
/>
|
|
{/if}
|
|
{:else if flowModule.value.type === 'rawscript' || flowModule.value.type === 'script' || flowModule.value.type === 'flow' || flowModule.value.type === 'aiagent' || flowModule.value.type === 'aidecision'}
|
|
<FlowModuleComponent
|
|
{noEditor}
|
|
bind:flowModule
|
|
{parentModule}
|
|
{previousModule}
|
|
failureModule={selectedId === 'failure'}
|
|
preprocessorModule={selectedId === 'preprocessor'}
|
|
{scriptKind}
|
|
{scriptTemplate}
|
|
{enableAi}
|
|
{savedModule}
|
|
forceTestTab={forceTestTab?.[flowModule.id]}
|
|
highlightArg={highlightArg?.[flowModule.id]}
|
|
{isAgentTool}
|
|
{flowModuleSchemaMap}
|
|
/>
|
|
{/if}
|
|
{:else if flowModule.value.type === 'forloopflow' || flowModule.value.type == 'whileloopflow'}
|
|
{#each flowModule.value.modules as child, index (child.id ?? index)}
|
|
{@const slot = moduleSlot(
|
|
() => (flowModule.value as { modules: FlowModule[] }).modules,
|
|
child.id,
|
|
child
|
|
)}
|
|
<FlowModuleWrapper
|
|
{flowModuleSchemaMap}
|
|
{noEditor}
|
|
bind:flowModule={slot.get, slot.set}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.modules[index - 1]}
|
|
savedModule={savedModuleById(savedModule, child.id)}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{:else if flowModule.value.type === 'branchone'}
|
|
{#if selectedId === `${flowModule?.id}-branch-default`}
|
|
<div class="h-full flex flex-col">
|
|
<FlowCard {noEditor} title="Default branch">
|
|
<div class="p-4">
|
|
<p class="text-xs text-tertiary">
|
|
Nothing to configure — this branch runs when none of the predicates match.
|
|
</p>
|
|
</div>
|
|
</FlowCard>
|
|
</div>
|
|
{:else}
|
|
{#each flowModule.value.default as child, index (child.id ?? index)}
|
|
{@const slot = moduleSlot(
|
|
() => (flowModule.value as { default: FlowModule[] }).default,
|
|
child.id,
|
|
child
|
|
)}
|
|
<FlowModuleWrapper
|
|
{flowModuleSchemaMap}
|
|
{noEditor}
|
|
bind:flowModule={slot.get, slot.set}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.default[index - 1]}
|
|
savedModule={savedModuleById(savedModule, child.id)}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{#each flowModule.value.branches as branch, branchIndex (branch)}
|
|
{#if selectedId === `${flowModule?.id}-branch-${branchIndex}`}
|
|
<FlowBranchOneWrapper
|
|
{noEditor}
|
|
{branch}
|
|
parentModule={flowModule}
|
|
{previousModule}
|
|
{enableAi}
|
|
/>
|
|
{:else}
|
|
{#each branch.modules as child, index (child.id ?? index)}
|
|
{@const slot = moduleSlot(() => branch.modules, child.id, child)}
|
|
<FlowModuleWrapper
|
|
{flowModuleSchemaMap}
|
|
{noEditor}
|
|
bind:flowModule={slot.get, slot.set}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.branches[branchIndex].modules[index - 1]}
|
|
savedModule={savedModuleById(savedModule, child.id)}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{/each}
|
|
{:else if flowModule.value.type === 'branchall'}
|
|
{#each flowModule.value.branches as branch, branchIndex (branch)}
|
|
{#if selectedId === `${flowModule?.id}-branch-${branchIndex}`}
|
|
<FlowBranchAllWrapper {noEditor} {branch} />
|
|
{:else}
|
|
{#each branch.modules as child, index (child.id ?? index)}
|
|
{@const slot = moduleSlot(() => branch.modules, child.id, child)}
|
|
<FlowModuleWrapper
|
|
{flowModuleSchemaMap}
|
|
{noEditor}
|
|
bind:flowModule={slot.get, slot.set}
|
|
bind:parentModule={flowModule}
|
|
previousModule={flowModule.value.branches[branchIndex].modules[index - 1]}
|
|
{enableAi}
|
|
savedModule={savedModuleById(savedModule, child.id)}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
{/each}
|
|
{:else if flowModule.value.type === 'aiagent'}
|
|
{#each flowModule.value.tools ?? [] as tool, toolIndex (toolIndex)}
|
|
{#if selectedId === tool.id}
|
|
<AgentToolWrapper
|
|
{noEditor}
|
|
bind:tool={flowModule.value.tools![toolIndex]}
|
|
parentModule={flowModule}
|
|
{previousModule}
|
|
{enableAi}
|
|
{forceTestTab}
|
|
{highlightArg}
|
|
siblingToolNames={flowModule.value.tools!.map((t) => t.summary ?? '')}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|