Files
windmill/frontend/src/lib/components/ResourceEditorDrawer.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

231 lines
7.9 KiB
Svelte

<script lang="ts">
import { Button, Drawer } from './common'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import { History, Loader2, Save } from 'lucide-svelte'
import WsSpecificVersions from './WsSpecificVersions.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { isOwner } from '$lib/utils'
import LocalDraftBanner from './LocalDraftBanner.svelte'
import OpenInSessionButton from './sessions/OpenInSessionButton.svelte'
import {
clearPageDrawerAnchor,
pageDrawerSessionSource,
setPageDrawerAnchor
} from './sessions/pageDrawerSession'
import { RESOURCES_PATH } from './sessions/previewPaths'
import ResourceVersionHistory from './ResourceVersionHistory.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import { addResourceTitle } from './resourceTypeDisplay'
let {
workspace = undefined,
disableChatOffset = false,
onRestored = undefined,
onSaved = undefined
}: {
workspace?: string
disableChatOffset?: boolean
onRestored?: () => void
/** Fires after Save has written, for a caller showing state derived from the
* resource — `onRestored` only covers restoring an old version. */
onSaved?: () => void
} = $props()
let drawer: Drawer | undefined = $state()
let historyDrawer: Drawer | undefined = $state()
let canSave = $state(true)
let resource_type: string | undefined = $state(undefined)
let defaultValues: Record<string, any> | undefined = $state(undefined)
let resourceEditor:
| {
save: () => void
localDraftDeployed: () => unknown
localDraftCurrent: () => unknown
discardLocalDraft: () => void
}
| undefined = $state(undefined)
let hasLocalDraft = $state(false)
let canWriteSelected = $state(true)
let path: string | undefined = $state(undefined)
let selected: string | undefined = $state(undefined)
let viewJsonSchema = $state(false)
let effectiveWorkspace = $derived(workspace ?? $workspaceStore!)
// The editor renders whichever workspace-specific variant `selected` points at, so history has
// to follow it too — otherwise a restore would write over the variant the user is not looking at.
let historyWorkspace = $derived(selected ?? effectiveWorkspace)
// Clearing is irreversible and the backend gates it on ownership, not write access. $userStore
// describes the user in the workspace they are signed into, so it can only answer for that one:
// history pointed anywhere else — a ws-specific variant, or an explicit `workspace` prop — gets
// no Clear button rather than a verdict computed from the wrong membership.
let canClearSelected = $derived(
historyWorkspace === $workspaceStore && isOwner(path ?? '', $userStore, $workspaceStore)
)
// A close reaches `on:close` on a later flush, by which point a caller that closed this drawer to
// open another editor has already anchored the new one. Clearing then would strip that anchor.
let keepAnchorOnClose = false
/** Shut this drawer without going through its own close button, for a caller opening the other
* editor over the same list. `keepAnchor` when that caller anchors what it opens instead. */
export function close(opts?: { keepAnchor?: boolean }): void {
keepAnchorOnClose = opts?.keepAnchor ?? false
drawer?.closeDrawer?.()
}
/** `json` opens on the JSON editor instead of the resource type's form. For a type with a
* dedicated editor elsewhere: the generic form would render its configuration field by field,
* and materialize a default into every one the value leaves out. */
export async function initEdit(p: string, opts?: { json?: boolean }): Promise<void> {
// A `close({ keepAnchor })` on an already-closed drawer emits no close event, so the flag
// would still be standing when the next drawer session ends and would swallow that one's
// anchor clear. Every session starts having to clear its own.
keepAnchorOnClose = false
resource_type = undefined
path = p
selected = effectiveWorkspace
viewJsonSchema = opts?.json ?? false
drawer?.openDrawer?.()
setPageDrawerAnchor(RESOURCES_PATH, p)
}
export async function initNew(
resourceType: string,
nDefaultValues?: Record<string, any>
): Promise<void> {
keepAnchorOnClose = false
path = undefined
resource_type = resourceType
defaultValues = nDefaultValues
selected = effectiveWorkspace
// This drawer outlives what it opens on, so the view has to be set by every entry point
// rather than left where the last one put it: a new resource is a typed form, whoever was
// looking at JSON before.
viewJsonSchema = false
drawer?.openDrawer?.()
}
let mode: 'edit' | 'new' = $derived(!path ? 'new' : 'edit')
// `selected`, not `effectiveWorkspace`: WsSpecificVersions re-points this drawer
// at another workspace's version, and the session must act on the one shown.
const sessionSource = $derived(
pageDrawerSessionSource(RESOURCES_PATH, path, selected ?? effectiveWorkspace)
)
</script>
<Drawer
bind:this={drawer}
size="50rem"
{disableChatOffset}
on:close={() => {
if (keepAnchorOnClose) {
keepAnchorOnClose = false
return
}
clearPageDrawerAnchor(RESOURCES_PATH)
}}
>
<DrawerContent
title={mode == 'edit' ? 'Edit ' + path : addResourceTitle(resource_type)}
bannerReserved={mode == 'edit'}
on:close={drawer?.closeDrawer}
>
{#snippet titleExtra()}
{#if mode == 'new' && resource_type}
<IconedResourceType name={resource_type} silent width="20px" height="20px" />
{/if}
{/snippet}
{#await import('./ResourceEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
{path}
{resource_type}
{defaultValues}
{workspace}
on:refresh
bind:this={resourceEditor}
bind:canSave
bind:selected
bind:viewJsonSchema
onDraftStateChange={(v) => (hasLocalDraft = v)}
onCanWriteChange={(v) => (canWriteSelected = v)}
/>
{/await}
{#snippet banner()}
<LocalDraftBanner
show={hasLocalDraft}
reserveSpace={mode == 'edit'}
getDeployed={() => resourceEditor?.localDraftDeployed()}
getCurrent={() => resourceEditor?.localDraftCurrent()}
onDiscard={() => resourceEditor?.discardLocalDraft()}
disabled={!canWriteSelected}
/>
{/snippet}
{#snippet actions()}
<OpenInSessionButton source={sessionSource} />
{#if mode == 'edit' && path && effectiveWorkspace}
<Button
variant="default"
unifiedSize="md"
startIcon={{ icon: History }}
on:click={() => historyDrawer?.openDrawer()}
>
History
</Button>
<WsSpecificVersions
kind="resource"
workspaceId={effectiveWorkspace}
initialPath={path}
bind:selected
/>
{/if}
<Button
variant="accent"
unifiedSize="md"
startIcon={{ icon: Save }}
on:click={async () => {
// Closed before the write is awaited, the way it always was: `save()` toasts its
// own failures and never rejects, so waiting would only add visible lag to every
// caller of this drawer. `onSaved` still fires after the write lands.
const saved = resourceEditor?.save()
drawer?.closeDrawer()
await saved
onSaved?.()
}}
disabled={!canSave}
>
Save
</Button>
{/snippet}
</DrawerContent>
</Drawer>
<Drawer bind:this={historyDrawer} size="1200px">
<DrawerContent title="Versions History" on:close={historyDrawer?.closeDrawer} noPadding>
{#if path && historyWorkspace}
<ResourceVersionHistory
{path}
workspace={historyWorkspace}
canRestore={canWriteSelected}
canClear={canClearSelected}
onRestore={() => {
historyDrawer?.closeDrawer()
// Close the editor too. It holds a baseline captured before the restore, and
// any local draft on top of it, so saving from it afterwards would write the
// pre-restore value straight back over the version just restored.
drawer?.closeDrawer()
// Its own callback rather than the `refresh` event: callers bind that to
// reopening a picker (EditorBar), which a restore should not trigger.
onRestored?.()
}}
/>
{/if}
</DrawerContent>
</Drawer>