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:
hugocasa
2026-09-30 12:37:52 +02:00
co-authored by Claude Opus 5.5
parent 5d261fab6b
commit 954d8b0212
3 changed files with 77 additions and 55 deletions
@@ -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}