Files
windmill/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte
T
hugocasaandClaude Opus 5 f037c73d10 feat(frontend): group the agent form and edit saved agents as drafts (#10880)
* feat(frontend): group the AI agent step form and edit saved agents in a modal

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: edit a saved AI agent through its own resource draft

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: drop the agent fork-for-edit session now that edits live in a draft

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: edit ai_agent resources from the resources page with the agent editor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: send a standalone agent's brain from the module when testing a step

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: keep the agent draft faithful to the resource it deploys to

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: add the sqlx cache entry for the eval subject rename

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: share the module insert between the graph and the agent editor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): open evals inside the agent editor, actions in its header

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): add tools from the agent editor and lighten its test pane

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): open an ai_agent deep link in the agent editor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): drop the failed result badge on a step that never ran

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): head the agent editor's levels with a back control

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): drop connect and fill inputs from the agent editor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): lighten the agent editor's run panel

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): stop a nested agent tool's config reading as AI-filled

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): offer only AI or static on an agent tool's inputs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): give a saved agent's tool editor a static-only surface

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): open an agent tool in a drawer beside the agent

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): hide unset agent config in the run form

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(frontend): share the input forms' pickers and s3 lookup

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(frontend): drop a dead agent-editor export and fix two stale comments

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): reach an ai_agent's resource-level settings and copilot

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): open an ai_agent's resource view as JSON, not the generic form

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): address review findings on the agent editor's draft and streaming

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): close the agent editor on a version restore, as the resource editor does

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): stop the provider picker auto-writing a kind, and clear review nits

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(frontend): drop the fork-for-edit leftovers from the agent card

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): mount the agent editor in the dev flow editor and guard the deep-link race

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): deploy the agent config that was submitted, and refuse one no run could use

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(frontend): build the agent editor's rows from the design-system button

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): keep a draft-only agent's draft, and let a blank MCP summary deploy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): guard read-only agents, incomplete MCP tools and duplicate editor mounts

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read-only agent editor, linked-card refresh, atomic eval rename

* fix: eval rename needs the privileged pool, per-workspace write access

* fix(frontend): drop the agent editor target when its mount goes away

* refactor: drop the agent rename work from this PR, unban the bindable defaults

* fix(frontend): refuse a renaming deploy and drop the copilot from static-only fields

* fix(frontend): mirror the worker's streaming rule and scope agent writes to their target

* fix(frontend): read runtime streaming as off and reset the drawer's json view

* fix(frontend): read an unsettled output_type as non-streaming too

* fix(frontend): let the showing modal claim an agent opened from inside it

* fix(frontend): keep in-flight edits, tool replacements and every linked step in sync

* fix(frontend): keep attachments in the run form and bind the agent ref to its tools

* fix(frontend): preview the agent as authored and re-evaluate step args on run

* fix(frontend): scope agent-editor ownership to the flow's workspace

* fix(frontend): drop the tool drill-in where there is no graph to select on

* fix(frontend): require a provider kind and keep one resource editor open at a time

* fix(frontend): keep legacy nulls, static-only text literal, and the handover anchor

* test(worker): pin the agent streaming default

* fix(frontend): let an AI-fillable input be switched to static

* fix(frontend): report agent editor background failures instead of floating them

* fix(frontend): keep the version pane's path alive while the editor closes

* fix(frontend): clear the anchor-keep flag at the start of each drawer session

* fix(frontend): preview the agent without its synthetic path, refresh the baseline on external writes

* refactor(frontend): drop the unverifiable baseline refresh, state the synthetic-path rule

* fix(frontend): keep the synthetic path out of agent tool test runs too

* refactor(frontend): host the agent editor under the agent's own path

* fix(frontend): mark an agent editor's host explicitly instead of inferring it from the path

* fix(frontend): discard linked-agent responses from before a deploy

* fix(frontend): keep a flow mount from claiming an agent editor's nested target

* feat(frontend): keep an agent used as a tool inside the agent being edited

* fix(frontend): reserve the agent editor's root module id

* docs(frontend): record why the agent editor previews under the agent's path

* fix(frontend): refuse to open or deploy a resource that is not an agent

* docs(frontend): put the scope-migration comment on the function it describes

* fix(frontend): refuse an agent path whose resource type is not proven

* fix(frontend): recheck the resource type before deploying, and keep expressions off static-only inputs

* fix(frontend): lazy-load the agent editor and slide its levels like the evals pane

* refactor: drop unreachable non-list tools check from agent deploy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): clear text-only agent fields on image output, reserve the root id

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): keep the agent editor usable for a non-list tools value

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): stop the parked eval run list from taking arrow keys

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): report a non-list tools value on deploy instead of throwing

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): keep temperature editable for image output

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): skip non-object tool entries when rendering an agent

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): guard tool entry reads instead of copying the tool array

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): key tool rows by position so duplicate ids render

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:32:17 +02:00

207 lines
6.6 KiB
Svelte

<script lang="ts">
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import FlowModuleWrapper from './FlowModuleWrapper.svelte'
import { moduleSlot, savedModuleById } from '../moduleSlot'
import FlowSettings from './FlowSettings.svelte'
import FlowInput from './FlowInput.svelte'
import FlowFailureModule from './FlowFailureModule.svelte'
import FlowEnvironmentVariables from './FlowEnvironmentVariables.svelte'
import type { FlowModule, Flow, Job } from '$lib/gen'
import FlowPreprocessorModule from './FlowPreprocessorModule.svelte'
import type { TriggerContext } from '$lib/components/triggers'
import { insertNewPreprocessorModule } from '../flowStateUtils.svelte'
import TriggersEditor from '../../triggers/TriggersEditor.svelte'
import { handleSelectTriggerFromKind, type Trigger } from '$lib/components/triggers/utils'
import { computeMissingInputWarnings } from '../missingInputWarnings'
import FlowResult from './FlowResult.svelte'
import ExpandedSubflowStep from './ExpandedSubflowStep.svelte'
import type { StateStore } from '$lib/utils'
import FlowSelectionPanel from './FlowSelectionPanel.svelte'
import {
resolveSelectedModuleIds,
locateModules,
areContiguousSiblings
} from '../multiSelectUtils'
interface Props {
noEditor?: boolean
enableAi?: boolean
newFlow?: boolean
disabledFlowInputs?: boolean
savedFlow?:
| (Flow & {
draft?: Flow | undefined
})
| undefined
onDeployTrigger?: (trigger: Trigger) => void
forceTestTab?: Record<string, boolean>
highlightArg?: Record<string, string | undefined>
onTestFlow?: (conversationId?: string) => Promise<string | undefined>
job?: Job
isOwner?: boolean
suspendStatus?: StateStore<Record<string, { job: Job; nb: number }>>
onOpenDetails?: () => void
previewOpen?: boolean
flowModuleSchemaMap?: import('../map/FlowModuleSchemaMap.svelte').default
}
let {
noEditor = false,
enableAi = false,
newFlow = false,
disabledFlowInputs = false,
savedFlow = undefined,
onDeployTrigger = () => {},
forceTestTab,
highlightArg,
onTestFlow,
job,
isOwner,
suspendStatus,
onOpenDetails,
previewOpen = false,
flowModuleSchemaMap = undefined
}: Props = $props()
const {
selectionManager,
flowStore,
flowStateStore,
flowInputsStore,
pathStore,
initialPathStore,
fakeInitialPath,
previewArgs,
flowInputEditorState,
stepsInputArgs
} = getContext<FlowEditorContext>('FlowEditorContext')
const selectedId = $derived(selectionManager.getSelectedId())
const { showCaptureHint, triggersState, triggersCount } =
getContext<TriggerContext>('TriggerContext')
function checkDup(modules: FlowModule[]): string | undefined {
let seenModules: string[] = []
for (const m of modules) {
if (seenModules.includes(m.id)) {
console.error(`Duplicate module id: ${m.id}`)
return m.id
}
seenModules.push(m.id)
}
}
$effect(() => {
computeMissingInputWarnings(flowStore, flowStateStore.val, flowInputsStore)
})
// Derived state for multi-select operations in the side panel
let resolvedModuleIds = $derived(
resolveSelectedModuleIds(selectionManager.selectedIds, flowStore.val.value.modules ?? [])
)
let canMoveSelected = $derived(
resolvedModuleIds.length > 0 &&
areContiguousSiblings(locateModules(resolvedModuleIds, flowStore.val.value.modules ?? []))
)
</script>
{#if selectionManager && selectionManager.selectedIds.length > 1}
<FlowSelectionPanel
{selectionManager}
{noEditor}
onDeleteSelected={() => flowModuleSchemaMap?.deleteMultiple(resolvedModuleIds)}
onDuplicateSelected={() => flowModuleSchemaMap?.duplicateMultiple(resolvedModuleIds)}
onMoveSelected={() => flowModuleSchemaMap?.moveMultiple(resolvedModuleIds)}
onCreateGroup={() => flowModuleSchemaMap?.createGroup(selectionManager.selectedIds)}
{canMoveSelected}
resolvedCount={resolvedModuleIds.length}
/>
{:else if selectedId?.startsWith('settings')}
<FlowSettings {enableAi} {noEditor} />
{:else if selectedId === 'Input'}
<FlowInput
{noEditor}
disabled={disabledFlowInputs}
on:openTriggers={(ev) => {
selectionManager.selectId('Trigger', { openPanel: true })
handleSelectTriggerFromKind(triggersState, triggersCount, savedFlow?.path, ev.detail.kind)
showCaptureHint.set(true)
}}
on:applyArgs
{onTestFlow}
{previewOpen}
{flowModuleSchemaMap}
/>
{:else if selectedId === 'Result'}
<FlowResult {noEditor} {job} {isOwner} {suspendStatus} {onOpenDetails} />
{:else if selectedId === 'constants'}
<FlowEnvironmentVariables {noEditor} />
{:else if selectedId === 'failure'}
<FlowFailureModule {noEditor} savedModule={savedFlow?.value.failure_module} />
{:else if selectedId === 'preprocessor'}
<FlowPreprocessorModule {noEditor} savedModule={savedFlow?.value.preprocessor_module} />
{:else if selectedId === 'Trigger'}
<TriggersEditor
on:applyArgs
on:addPreprocessor={async (e) => {
await insertNewPreprocessorModule(flowStore, flowStateStore, {
language: 'bun'
})
stepsInputArgs.setStepArgs('preprocessor', e.detail.args ?? {})
selectionManager.selectId('preprocessor')
}}
on:updateSchema={(e) => {
const { payloadData, redirect } = e.detail
if (payloadData) {
previewArgs.val = JSON.parse(JSON.stringify(payloadData))
}
if (redirect) {
selectionManager.selectId('Input')
$flowInputEditorState.selectedTab = 'captures'
$flowInputEditorState.payloadData = payloadData
}
}}
on:testWithArgs
currentPath={$pathStore}
initialPath={$initialPathStore}
{fakeInitialPath}
{noEditor}
newItem={newFlow}
isFlow={true}
hasPreprocessor={!!flowStore.val.value.preprocessor_module}
canHavePreprocessor={true}
args={previewArgs.val}
isDeployed={savedFlow && !savedFlow?.draft_only}
schema={flowStore.val.schema}
{onDeployTrigger}
/>
{:else if selectedId?.startsWith('subflow:')}
<ExpandedSubflowStep
{selectedId}
onSubflowUpdated={() => flowModuleSchemaMap?.reloadExpandedSubflows()}
/>
{:else}
{@const dup = checkDup(flowStore.val.value.modules)}
{#if dup}
<div class="text-red-600 text-xl p-2">There are duplicate modules in the flow at id: {dup}</div>
{:else}
{#key selectedId}
{#each flowStore.val.value.modules as flowModule, index (flowModule.id ?? index)}
{@const slot = moduleSlot(() => flowStore.val.value.modules, flowModule.id, flowModule)}
<FlowModuleWrapper
{noEditor}
bind:flowModule={slot.get, slot.set}
previousModule={flowStore.val.value.modules[index - 1]}
{enableAi}
savedModule={savedModuleById(savedFlow?.value.modules, flowModule.id)}
{forceTestTab}
{highlightArg}
{flowModuleSchemaMap}
/>
{/each}
{/key}
{/if}
{/if}