mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-15 16:03:02 +00:00
feat: agent settings fields and cog, completing the rename
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5d261fab6b
commit
954d8b0212
@@ -14,7 +14,7 @@ export interface AgentEditorTarget {
|
||||
workspace?: string
|
||||
toolId?: string
|
||||
/** A level of the editor that is not the form. Mutually exclusive with `toolId`. */
|
||||
view?: 'evals' | 'path'
|
||||
view?: 'evals' | 'settings'
|
||||
/** Where to re-resolve a graph's tool nodes after a deploy, when opened from a flow step. Only a
|
||||
* real flow sets it: the agent editor offers no way to open a second editor from inside itself. */
|
||||
host?: { flowPath: string; moduleId: string }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
ChevronRight,
|
||||
FlaskConical,
|
||||
FolderOpen,
|
||||
Settings,
|
||||
FormInput,
|
||||
History,
|
||||
MessageSquare,
|
||||
@@ -36,7 +36,7 @@
|
||||
import { publishLinkedAgentTools } from '../flowState'
|
||||
import { linkedModulesForAgent, linkedToolsScope } from '../linkedAgentToolsStore.svelte'
|
||||
import AgentEditorHost from './AgentEditorHost.svelte'
|
||||
import AgentPathField from './AgentPathField.svelte'
|
||||
import AgentSettings from './AgentSettings.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
@@ -91,7 +91,7 @@
|
||||
let ws = $derived(target?.workspace ?? $operatingWorkspace)
|
||||
let host = $state<ReturnType<typeof AgentEditorHost> | undefined>(undefined)
|
||||
let versionDrawer: Drawer | undefined = $state(undefined)
|
||||
let pathDrawer: Drawer | undefined = $state(undefined)
|
||||
let settingsDrawer: Drawer | undefined = $state(undefined)
|
||||
let saving = $state(false)
|
||||
|
||||
// Counted per agent, so a deploy that leaves the editor on the same agent still refetches the
|
||||
@@ -124,8 +124,8 @@
|
||||
// the dialog carries only the refusal the host renders.
|
||||
let refused = $derived(draft?.refusal != null)
|
||||
let inEvals = $derived(target?.view === 'evals' && !refused)
|
||||
// A level of its own in the dialog; the page layout opens the same field in a drawer instead.
|
||||
let inPath = $derived(target?.view === 'path' && !refused)
|
||||
// A level of its own in the dialog; the page layout opens the same settings in a drawer instead.
|
||||
let inSettings = $derived(target?.view === 'settings' && !refused)
|
||||
let readOnly = $derived(draft ? !draft.canWrite : false)
|
||||
let draftOnly = $derived(draft?.noDeployed ?? false)
|
||||
// A never-deployed agent is stored at a minted `draft_<uuid>` path, so it is named by the path
|
||||
@@ -146,7 +146,7 @@
|
||||
|
||||
let root = $derived<ModalTrailSegment>({
|
||||
label: shownPath ?? 'Agent',
|
||||
onclick: inEvals || inPath ? () => showAgentEditorView(undefined) : undefined
|
||||
onclick: inEvals || inSettings ? () => showAgentEditorView(undefined) : undefined
|
||||
})
|
||||
let trail = $derived<ModalTrailSegment[]>(
|
||||
inEvals
|
||||
@@ -155,17 +155,17 @@
|
||||
{ label: 'Evals', onclick: evalsLocation ? evalsLocation.back : undefined },
|
||||
...(evalsLocation ? [{ label: evalsLocation.label }] : [])
|
||||
]
|
||||
: inPath
|
||||
? [root, { label: 'Path' }]
|
||||
: inSettings
|
||||
? [root, { label: 'Settings' }]
|
||||
: [root]
|
||||
)
|
||||
// The root's alone: below it the header's second line is the way back, and what a level is for
|
||||
// belongs to that level rather than to the dialog's own name.
|
||||
let description = $derived(inEvals || inPath || refused ? undefined : AGENT_DESCRIPTION)
|
||||
let description = $derived(inEvals || inSettings || refused ? undefined : AGENT_DESCRIPTION)
|
||||
|
||||
function openPath() {
|
||||
if (layout === 'modal') showAgentEditorView('path')
|
||||
else pathDrawer?.openDrawer()
|
||||
function openSettings() {
|
||||
if (layout === 'modal') showAgentEditorView('settings')
|
||||
else settingsDrawer?.openDrawer()
|
||||
}
|
||||
|
||||
/** The unsaved edits, in the shape the server builds from a deployed config
|
||||
@@ -357,7 +357,7 @@
|
||||
{#if !inEvals && !refused}
|
||||
<!-- Switches the editor's right-hand pane, and is drawn only once the agent has
|
||||
loaded and the pane has picked its first mode. -->
|
||||
{#if testPane?.mode && !inPath}
|
||||
{#if testPane?.mode && !inSettings}
|
||||
<ToggleButtonGroup
|
||||
bind:selected={
|
||||
() => testPane?.mode,
|
||||
@@ -392,16 +392,15 @@
|
||||
Read only
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if !inPath}
|
||||
{#if !inSettings}
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
startIcon={{ icon: FolderOpen }}
|
||||
title="Where the agent is saved"
|
||||
on:click={openPath}
|
||||
>
|
||||
Path
|
||||
</Button>
|
||||
startIcon={{ icon: Settings }}
|
||||
iconOnly
|
||||
title="Settings"
|
||||
on:click={openSettings}
|
||||
/>
|
||||
{/if}
|
||||
<!-- Evals run against the deployed agent, and a draft-only one has none: the
|
||||
backend's `require_agent` would reject every run. -->
|
||||
@@ -457,10 +456,10 @@
|
||||
and evals answers them for its own levels. -->
|
||||
<PagedContent
|
||||
class="flex-1 min-h-0"
|
||||
current={inEvals ? 'evals' : inPath ? 'path' : 'agent'}
|
||||
current={inEvals ? 'evals' : inSettings ? 'settings' : 'agent'}
|
||||
pages={[
|
||||
{ key: 'agent', content: agentPage },
|
||||
{ key: 'path', content: pathPage },
|
||||
{ key: 'settings', content: settingsPage },
|
||||
{ key: 'evals', content: evalsPage }
|
||||
]}
|
||||
/>
|
||||
@@ -483,9 +482,9 @@
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet pathField()}
|
||||
{#snippet settingsFields()}
|
||||
{#if draft}
|
||||
<AgentPathField
|
||||
<AgentSettings
|
||||
{draft}
|
||||
path={target?.path ?? ''}
|
||||
workspace={ws}
|
||||
@@ -494,8 +493,8 @@
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet pathPage()}
|
||||
<div class="max-w-2xl py-6">{@render pathField()}</div>
|
||||
{#snippet settingsPage()}
|
||||
<div class="max-w-2xl py-6">{@render settingsFields()}</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet evalsPage()}
|
||||
@@ -508,9 +507,9 @@
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<Drawer bind:this={pathDrawer} size="600px">
|
||||
<DrawerContent title="Path" on:close={() => pathDrawer?.closeDrawer()}>
|
||||
{@render pathField()}
|
||||
<Drawer bind:this={settingsDrawer} size="600px">
|
||||
<DrawerContent title="Settings" on:close={() => settingsDrawer?.closeDrawer()}>
|
||||
{@render settingsFields()}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { resource } from 'runed'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import LabelsInput from '$lib/components/LabelsInput.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import ResourceDescriptionField from '$lib/components/ResourceDescriptionField.svelte'
|
||||
import ResourcePathHint from '$lib/components/ResourcePathHint.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import type { AgentDraftHandle } from '../agentDraft.svelte'
|
||||
|
||||
/** An agent's own settings, laid out as the top of the resource editor: what it is saved as
|
||||
* rather than what it does. */
|
||||
interface Props {
|
||||
draft: AgentDraftHandle
|
||||
/** The path the editor opened, which a first deploy of a new agent replaces. */
|
||||
@@ -16,30 +23,46 @@
|
||||
let { draft, path, workspace, error = $bindable() }: Props = $props()
|
||||
|
||||
let readOnly = $derived(!draft.canWrite)
|
||||
// Only a workspace that deploys somewhere has anything to keep an agent out of.
|
||||
const deployTo = resource(
|
||||
() => workspace,
|
||||
async (ws) =>
|
||||
ws ? (await WorkspaceService.getDeployTo({ workspace: ws })).deploy_to : undefined
|
||||
)
|
||||
</script>
|
||||
|
||||
<Label label="Path">
|
||||
<Path
|
||||
bind:path={
|
||||
() => draft.state?.path,
|
||||
(v) => {
|
||||
if (draft.state && v !== undefined) draft.state.path = v
|
||||
}
|
||||
}
|
||||
bind:error
|
||||
initialPath={draft.noDeployed ? '' : path}
|
||||
checkInitialPathExistence={draft.noDeployed}
|
||||
namePlaceholder="agent"
|
||||
kind="resource"
|
||||
workspaceOverride={workspace}
|
||||
autofocus={false}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</Label>
|
||||
<!-- An agent runs as whoever runs it, so sharing one through a folder shares nothing it uses. -->
|
||||
{#if !readOnly && draft.state?.path?.startsWith('f/')}
|
||||
<Alert type="info" size="xs" title="Shared through its folder" class="mt-2">
|
||||
Anyone who runs this agent needs access to its AI resource, and to the resources and workspace
|
||||
scripts its tools use.
|
||||
</Alert>
|
||||
{#if draft.state}
|
||||
<div class="flex flex-col gap-6">
|
||||
<Label label="Path">
|
||||
<ResourcePathHint />
|
||||
<Path
|
||||
bind:path={
|
||||
() => draft.state?.path,
|
||||
(v) => {
|
||||
if (draft.state && v !== undefined) draft.state.path = v
|
||||
}
|
||||
}
|
||||
bind:error
|
||||
initialPath={draft.noDeployed ? '' : path}
|
||||
checkInitialPathExistence={draft.noDeployed}
|
||||
namePlaceholder="agent"
|
||||
kind="resource"
|
||||
workspaceOverride={workspace}
|
||||
autofocus={false}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</Label>
|
||||
<LabelsInput bind:labels={draft.state.labels} {workspace} class="-mt-4" />
|
||||
{#if deployTo.current}
|
||||
<Label label="Workspace specific" tooltip="Keeps this agent out of deploys to prod/staging.">
|
||||
<Toggle bind:checked={draft.state.wsSpecific} disabled={readOnly} />
|
||||
</Label>
|
||||
{/if}
|
||||
<ResourceDescriptionField
|
||||
bind:description={draft.state.description}
|
||||
label="Description"
|
||||
placeholder="Describe what this agent does"
|
||||
canWrite={!readOnly}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user