Files
windmill/frontend/src/lib/components/copilot/StepInputGen.svelte
T
GuilhemandClaude Opus 4.8 09c8f3b1f3 feat: redesign flow step, loop and branch settings panels (#10026)
* feat: responsive modal step panel for the flow editor in sessions

On narrow layouts the flow editor's step-details pane opens as a modal
(double-click a graph node) instead of a split pane, with a dock/float
toggle. Scoped to sessions via allowModalPanel; the full-page editor is
unchanged.

- FlowEditor: modal/docked modes gated by mount width + allowModalPanel,
  small header (step-id Badge + subtle dock/close), standing
  double-click hint, and a per-step hint in the name tooltip
- selectionManager: onSelectIntent hook so flow-level panels (settings,
  input, triggers…) open the modal on single click
- PropPickerWrapper: collapse the prop picker until connect and animate
  it in via AnimatedPane (runs-page pattern), no blue connect ring in
  modal mode
- StepInputGen: drop the TAB/Wand autocompletion button + spinner
  (feature still works via focus + Tab)
- InputTransformForm: decouple the Help dropdown from the AI suggestion
- FlowModuleHeader: move 'Save to workspace' into an ellipsis dropdown

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: loop editor rendering and nested splitpanes splitters in the sessions modal

- Loop iterator/parallelism: keep the picker split pane (forceExpanded) so
  the editor fills its box and the picker shows; the collapse-until-connect
  mode stays for the step inputs
- Remove the intrusive AI TAB/Wand autocompletion button from IteratorGen
  (generation still runs headless via focus + Tab)
- Size the iterator connect plug and restyle the loop header/labels/toggles
- Scope the global `.splitter-hidden` splitter-hiding rule to direct children
  so it no longer leaks into nested Splitpanes under the sessions preview

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: redesign flow step advanced settings as a single toggle-first column

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: taller step test pane by default and restyle advanced section titles

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: show flow run-settings params disabled when a setting is toggled off

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: single-column for-loop panel reusing the run-settings accordion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: single-column while-loop panel reusing the run-settings accordion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: single-column branch panels reusing the run-settings accordion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: auto-open modal panel when creating an AI agent tool

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: redesign branch panels with card layout and shared predicate editor

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: remove per-setting status badges from flow map nodes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: sync package-lock after windmill-utils-internal bump

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style: polish prop-picker plug button and branch panel layouts

* fix: persist skip-if-stopped toggles in early stop settings

* fix: open the step panel modal on demand and cap its width

* fix: restore graph step setting badges, strip panel header chips instead

* feat: docked panel header with detach action and open-details step menu

* feat: width-based panel mode on every surface with inline detach action

* refactor: single source for flow step settings and their defaults

* docs: pin flow editor vocabulary in CONTEXT.md

* fix: open the trigger panel on double click or a specific trigger

* fix: keep module pickers inside their pane and dismissable

* fix: drop the misleading chevron on the MCP tool entry

* fix: resolve flow approvals against the job's workspace, not the nav one

* refactor: derive the approval workspace from the job, not from callers

* fix: restore S3 snippets and gate params while their setting is off

* fix: restore branch mock controls and address review findings

* chore: drop stray debug log from the flow map item

* feat: pinned output section for loop and branch panels

* fix: open the panel for deliberate navigation from the flow header

* perf: mount branch predicate editors on demand

* fix: skip predicate picker previews the previous step's result

* fix: flow-level graph nodes open their panel on a single click

* fix: open the step panel for AI chat selections, not for undo

* chore: drop dead console.log and duplicated modalPanel doc

* fix: re-sync expression editors and scope error-handler settings

* fix: match the failure module exactly and ignore unselectable nodes

* fix: keep concurrency editable, honour module cache_ttl, tighten panel ids

* fix: open panel from indirect selections, use presence for value-driven toggles

* fix: don't open settings on error-handler delete, flush editors on unmount

* fix: guard editor destroy flush, keep retry kind reachable

* refactor: name the run settings panel after the domain vocabulary

* fix: only write editor flushes to the step they belong to

* fix: bind step panels by id so a delete can't retarget editor writes

* fix: don't let the trigger picker's escape close the drawer beneath it

* docs: condense two comments to the constraint they record

* fix: arbitrate escape through the overlay stack instead of deferring to it

* fix: key nested step blocks by identity so anchored bindings can't go stale

* fix: untrack the overlay-stack push and drop the frozen branch binding

* chore: state the escape rationale once, key branch lists, format

* fix: let the topmost overlay own escape instead of the graph

* fix: keep the dynamic-input help box out of static template fields

* fix: restore the graph connect on the for-loop iterator

* fix: end connect mode with the modal and keep it to docked panels

* fix: never enter graph connect mode from the modal panel

* fix: reveal inserted steps, restore editor pane size, unleak the drawer stack

* fix: keep the enable-AI popover reachable in session panes

* feat: add the connect policy and its single armed slot

* refactor: one picker for every expression input

* refactor: route every connect through one armed slot

* fix: give every connect button the same footprint

* fix: keep the connect ring from showing through the button

* fix: keep flow card actions right-aligned beside the detach button

* fix: give the connect ring an opaque ground to mask against

* feat: dock the panel back without reopening it

* feat: dock the panel from the graph control bar

* style: round the graph control bar and size its glyphs

* style: customize the graph controls through their supported api

* style: build the graph control bar from lucide icons

* fix: use the graph's tooltip component in the zoom controls

* style: pad the graph controls and enlarge their glyphs

* style: pad the graph controls and put dock at the bar's end

* refactor: give settings rows the same popover picker as other expressions

* fix: pass the wrapper's pickable properties to nested inputs

* refactor: stack step settings and render every expression through the step input form

* feat: split loop panels into tabs and rework the approval form

* feat: anchor drawers to their host pane and give them a size floor

* fix: mark the loop iterator expression as required

* refactor: badge ee-only toggles instead of a warning line

* fix: flag an empty loop iterator expression as an error

* refactor: pick the early-stop flow status from one toggle group

* fix: keep parallel loops uncapped unless a limit is opted into

* fix: scope the overlay stack to its host and disarm connect on dismissal

* fix: anchor the trigger picker to its host pane

* feat: move diff into the menu when the top bar is narrow

* fix: gate the result logs toggle to the graph popover

* feat: raise the modal-panel breakpoint to 1280

* fix: anchor flow editor popovers and fullscreen to their host pane

* fix: anchor overlays to their host pane and mute them when hidden

* fix: portal hosted modals and menus into the pane they anchor to

* fix: keep non-listening dialogs off the overlay stack

* fix: drop the topmost gate from confirmation dialogs

* fix: silence overlays in a collapsed preview panel

* feat: rework the branch panels with tabs, reordering and add/delete

* refactor: fold the detached-panel chrome into the card header

* fix: give every flow panel a titled card header

* fix: stop the step panel oscillating on an auto-height editor

* feat: consolidate script panel actions and restore branch predicate AI

* fix: restore the logs toggle on the flow result popover

* fix: collapse the idle property picker in modal step panels

* fix: stop the docked pane scrolling alongside its panel

* fix: space the last settings row off the panel bottom

* revert: always show the property picker pane in step panels

* chore: keep the inline script AI button identical to main

* fix: ask for AI input suggestions on click, not on hover

* fix: keep graph connects armed and remount the parallelism input

* style: reveal the predicate AI button on row hover

* style: give branch cards a handle and delete column

* refactor: arbitrate flow overlay escape through Disposable

* fix: give the popover picker its results and re-narrow the EE badge

* docs: correct loopSubset and guard the modal width measurement

* fix: insert picked properties at the cursor in expression inputs

* fix: give the expanded-subflow panel the shared header chrome

* style: rename the suspend setting to Suspend until approval/resume

* feat: open a step's modal when clicking the step already selected

* feat: add an auto/attached/detached toggle for the step panel

* refactor: pick the step panel's placement from one named menu

* refactor: keep the panel-mode module's exports to what is consumed

* feat: show each configured setting's value on its badge

* fix: carry the suspend rename into the step settings registry

* docs: name both gestures in the step explore hint

* test: pin where the step panel goes for a given width and preference

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-05 12:25:25 +02:00

241 lines
7.9 KiB
Svelte

<script lang="ts">
import { getNonStreamingMetadataCompletion } from './lib'
import { sendUserToast } from '$lib/toast'
import type { Flow } from '$lib/gen'
import { createEventDispatcher, getContext } from 'svelte'
import type { FlowEditorContext } from '../flows/types'
import type { PickableProperties } from '../flows/previousResults'
import YAML from 'yaml'
import { sliceModules } from '../flows/flowStateUtils.svelte'
import { dfs } from '../flows/dfs'
import { yamlStringifyExceptKeys } from './utils'
import type { FlowCopilotContext } from './flow'
import { stepInputCompletionEnabled } from '$lib/stores'
import type { SchemaProperty } from '$lib/common'
import FlowCopilotInputsModal from './FlowCopilotInputsModal.svelte'
import { copilotInfo } from '$lib/aiStore'
import Button from '../common/button/Button.svelte'
import { AIBtnClasses } from './chat/AIButtonStyle'
import { Check, Wand2 } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
let generatedContent = $state('')
let loading = $state(false)
interface Props {
/** Whether the input this belongs to has focus — a suggestion is only worth keeping
* while the user is still on that input. */
focused?: boolean
schemaProperty: SchemaProperty
pickableProperties?: PickableProperties | undefined
argName: string
}
let { focused = false, schemaProperty, pickableProperties = undefined, argName }: Props = $props()
/** The button takes focus when clicked, which blurs the input — without tracking that,
* asking for a suggestion would immediately look like leaving the field. */
let btnFocused = $state(false)
let abortController = new AbortController()
let newFlowInput = $state('')
const { flowStore, selectionManager } = getContext<FlowEditorContext>('FlowEditorContext')
const { generatedExprs } = getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
function createFlowInput() {
if (!newFlowInput) {
return
}
const properties = {
...(flowStore.val.schema?.properties as Record<string, SchemaProperty> | undefined),
[newFlowInput]: schemaProperty
}
const required = [
...((flowStore.val.schema?.required as string[] | undefined) ?? []),
newFlowInput
]
flowStore.val.schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties,
required,
type: 'object'
}
}
async function generateStepInput() {
if (generatedContent.length > 0 || loading) {
return
}
abortController = new AbortController()
loading = true
const flow: Flow = JSON.parse(JSON.stringify(flowStore.val))
const idOrders = dfs(flow.value.modules, (x) => x.id)
const upToIndex = idOrders.indexOf(selectionManager.getSelectedId())
if (upToIndex === -1) {
throw new Error('Could not find the selected id in the flow')
}
const flowDetails =
'Take into account the following information for never tested results:\n<flowDetails>\n' +
yamlStringifyExceptKeys(sliceModules(flow.value.modules, upToIndex, idOrders), ['lock']) +
'</flowDetails>'
try {
const availableData = {
results: pickableProperties?.priorIds,
flow_input: pickableProperties?.flow_input
}
const isInsideLoop = availableData.flow_input && 'iter' in availableData.flow_input
const user = `I'm building a workflow which is a DAG of script steps.
The current step is ${selectionManager.getSelectedId()}, you can find the details for the step and previous ones below:
${flowDetails}
Determine for the input "${argName}", what to pass either from the previous results or the flow inputs.
All possibles inputs either start with results. or flow_input. and are followed by the key of the input.
${
isInsideLoop
? 'As the step is in a loop, the iterator value is accessible as flow_input.iter.value.'
: 'As the step is not in a loop, flow_input.iter.value is not available.'
}
Here's a summary of the available data:
<available>
${YAML.stringify(availableData)}</available>
${
isInsideLoop
? 'Favor results and flow_input.iter.value over flow inputs.'
: 'Favor results over flow inputs'
}
If none of the available results are appropriate, are already used or are more appropriate for other inputs, you can also imagine new flow_input properties which we will create programmatically based on what you provide.
Reply with the most probable answer, do not explain or discuss.
Use javascript object dot notation to access the properties.
Only return the expression without any wrapper.`
generatedContent = await getNonStreamingMetadataCompletion(
[
{
role: 'user',
content: user
}
],
abortController
)
if (
pickableProperties &&
generatedContent.startsWith('flow_input.') &&
generatedContent.split('.')[1] &&
!(generatedContent.split('.')[1] in pickableProperties.flow_input)
) {
newFlowInput = generatedContent.split('.')[1]
} else {
newFlowInput = ''
}
} catch (err) {
if (!abortController.signal.aborted) {
sendUserToast('Could not generate step input: ' + err, true)
}
} finally {
loading = false
}
}
export function onKeyUp(event: KeyboardEvent) {
if (!$copilotInfo.enabled || !$stepInputCompletionEnabled) {
return
}
if (event.key === 'Tab') {
if (!loading && generatedContent) {
event.preventDefault()
dispatch('setExpr', generatedContent)
if (newFlowInput) {
openInputsModal = true
}
generatedContent = ''
}
} else {
cancel()
}
}
const dispatch = createEventDispatcher()
function cancel() {
abortController.abort()
generatedContent = ''
}
// Drop a suggestion once the user has moved on, so the accept button can't sit there armed
// against an input nobody is editing. Deferred because focus moves through nothing on its
// way from the input to the button, and left alone while loading: the click that asked for
// the suggestion is itself what blurred the input.
$effect(() => {
if (focused || btnFocused) return
const timer = setTimeout(() => {
if (!focused && !btnFocused && !loading) {
cancel()
}
}, 150)
return () => clearTimeout(timer)
})
$effect(() => {
dispatch('showExpr', generatedContent)
})
$effect(() => {
dispatch('showExpr', $generatedExprs?.[argName] || '')
})
let openInputsModal = $state(false)
/** A suggestion is waiting to be accepted, rather than waiting to be asked for. */
let ready = $derived(!loading && generatedContent.length > 0)
function accept() {
dispatch('setExpr', generatedContent)
if (newFlowInput) {
openInputsModal = true
}
generatedContent = ''
}
// A suggestion costs a model call, so nothing generates on its own — this button is the
// only trigger, and the same control then accepts what it produced. Blur must not cancel:
// clicking here takes focus out of the input, which is the gesture that started the call.
function onClick() {
if (loading) cancel()
else if (ready) accept()
else generateStepInput()
}
</script>
{#if $copilotInfo.enabled && $stepInputCompletionEnabled}
<FlowCopilotInputsModal
on:confirmed={async () => {
createFlowInput()
}}
bind:open={openInputsModal}
inputs={[newFlowInput]}
/>
<!-- Sized to match FlowPlugConnect: it shares the control row with the connect plug. -->
<Button
variant="default"
size="xs3"
iconOnly
{loading}
clickableWhileLoading
title={loading ? 'Cancel' : ready ? 'Accept the suggestion' : 'Suggest an expression with AI'}
startIcon={{ icon: ready ? Check : Wand2 }}
btnClasses={twMerge(
AIBtnClasses(ready ? 'green' : 'default'),
'bg-surface overflow-clip flex p-0'
)}
wrapperClasses={twMerge(
'h-5 w-8 p-0 group-hover:opacity-100 transition-opacity',
// Same reveal-on-hover as the connect plug beside it, but a request in flight or a
// suggestion waiting to be accepted has to stay reachable once the pointer leaves.
loading || ready ? '' : 'opacity-0'
)}
on:click={onClick}
on:focus={() => (btnFocused = true)}
on:blur={() => (btnFocused = false)}
/>
{/if}