mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 16:02:59 +00:00
feat: agent detail page config panel, run page on form runs, chat badge
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
161e19da44
commit
71d12a0cf1
@@ -31710,6 +31710,11 @@ components:
|
||||
(over a deployed row or a synthesized draft-only row).
|
||||
Frontend appends a `*` to the displayed name.
|
||||
type: boolean
|
||||
agent_memory:
|
||||
description: |
|
||||
For an `ai_agent` resource only, its `memory` setting: the one part
|
||||
of the value a listing returns, since it decides whether the agent
|
||||
keeps a conversation.
|
||||
required:
|
||||
- path
|
||||
- resource_type
|
||||
|
||||
@@ -206,6 +206,20 @@ pub struct ListableResource {
|
||||
pub is_draft: Option<bool>,
|
||||
}
|
||||
|
||||
/// A row of the resource listing: the resource as a single read returns it, plus what only the
|
||||
/// listing carries.
|
||||
#[derive(FromRow, Serialize)]
|
||||
pub struct ListedResource {
|
||||
#[sqlx(flatten)]
|
||||
#[serde(flatten)]
|
||||
pub resource: ListableResource,
|
||||
/// An `ai_agent`'s `memory` setting, the one part of its value a listing shows: whether it
|
||||
/// keeps a conversation decides how it is offered. `value` stays unlisted for every type.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(default)]
|
||||
pub agent_memory: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateResource {
|
||||
pub path: String,
|
||||
@@ -322,7 +336,7 @@ async fn list_resources(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<ListableResource>> {
|
||||
) -> JsonResult<Vec<ListedResource>> {
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
|
||||
let mut sqlb = SqlBuilder::select_from("resource")
|
||||
@@ -344,6 +358,7 @@ async fn list_resources(
|
||||
"resource.labels",
|
||||
"folder_labels(resource.workspace_id, resource.path) as inherited_labels",
|
||||
"ws_specific.path IS NOT NULL as ws_specific",
|
||||
"CASE WHEN resource.resource_type = 'ai_agent' THEN resource.value->'memory' END as agent_memory",
|
||||
])
|
||||
// Scalar EXISTS flags the authed user's per-user draft without fanning rows out.
|
||||
.field(
|
||||
@@ -428,11 +443,11 @@ async fn list_resources(
|
||||
let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let allowed = build_scope_path_predicate(&authed, "resources", "read");
|
||||
let mut rows = sqlx::query_as::<_, ListableResource>(&sql)
|
||||
let mut rows = sqlx::query_as::<_, ListedResource>(&sql)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|r| allowed(&r.path))
|
||||
.filter(|r| allowed(&r.resource.path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tx.commit().await?;
|
||||
@@ -503,8 +518,13 @@ async fn list_resources(
|
||||
})
|
||||
});
|
||||
let ws_specific = v.get("wsSpecific").and_then(|x| x.as_bool());
|
||||
let agent_memory = if resource_type == "ai_agent" {
|
||||
value.as_ref().and_then(|a| a.get("memory")).cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
rows.push(ListableResource {
|
||||
let resource = ListableResource {
|
||||
workspace_id: w_id.clone(),
|
||||
path,
|
||||
value,
|
||||
@@ -526,7 +546,8 @@ async fn list_resources(
|
||||
draft_only: Some(true),
|
||||
// Synthesized rows are the authed user's draft.
|
||||
is_draft: Some(true),
|
||||
});
|
||||
};
|
||||
rows.push(ListedResource { resource, agent_memory });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { FileUp, Pen, Shield, Trash } from 'lucide-svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
import ChatFlowBadge from '$lib/components/flows/ChatFlowBadge.svelte'
|
||||
import { keepsManagedMemory } from '$lib/components/flows/agentFormFields'
|
||||
import Row from './Row.svelte'
|
||||
|
||||
/**
|
||||
@@ -59,8 +61,13 @@
|
||||
let rowHref = $derived(agent.draft_only ? editHref : `${base}/agents/get/${agent.path}`)
|
||||
</script>
|
||||
|
||||
{#snippet chatBadge()}
|
||||
<ChatFlowBadge title="Managed memory on: this agent opens as a conversation" />
|
||||
{/snippet}
|
||||
|
||||
<Row
|
||||
href={rowHref}
|
||||
titleBadge={keepsManagedMemory(agent.agent_memory) ? chatBadge : undefined}
|
||||
kind="agent"
|
||||
{keyboardSelected}
|
||||
{marked}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { Badge, Section } from '$lib/components/common'
|
||||
import {
|
||||
AGENT_BRAIN_LABELS,
|
||||
summarizeAgentBrain,
|
||||
type AIAgentConfig
|
||||
} from '../agentResourceUtils'
|
||||
import { toolDisplayName, type AgentTool } from '../agentToolUtils'
|
||||
|
||||
/** A saved agent's configuration, read-only and at a glance: what a detail page shows beside
|
||||
* running it, where the editor's form would be all fields and no summary. */
|
||||
interface Props {
|
||||
config: AIAgentConfig
|
||||
}
|
||||
|
||||
let { config }: Props = $props()
|
||||
|
||||
const SYSTEM_PROMPT_LABEL = AGENT_BRAIN_LABELS['system_prompt']
|
||||
|
||||
let settings = $derived(
|
||||
summarizeAgentBrain(config).filter((r) => r.label !== SYSTEM_PROMPT_LABEL)
|
||||
)
|
||||
let systemPrompt = $derived(
|
||||
typeof config.system_prompt === 'string' && config.system_prompt !== ''
|
||||
? config.system_prompt
|
||||
: undefined
|
||||
)
|
||||
let tools = $derived(Array.isArray(config.tools) ? (config.tools as AgentTool[]) : [])
|
||||
|
||||
function toolKind(tool: AgentTool): string {
|
||||
const value = tool?.value as Record<string, any> | undefined
|
||||
if (value?.tool_type === 'mcp') return 'MCP'
|
||||
if (value?.tool_type === 'websearch') return 'Web search'
|
||||
switch (value?.type) {
|
||||
case 'script':
|
||||
return 'Script'
|
||||
case 'flow':
|
||||
return 'Flow'
|
||||
case 'rawscript':
|
||||
return 'Inline script'
|
||||
case 'aiagent':
|
||||
return 'Agent'
|
||||
default:
|
||||
return 'Tool'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-4">
|
||||
<Section label="Model" small>
|
||||
<dl class="flex flex-col gap-2">
|
||||
{#each settings as row (row.label)}
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<dt class="text-2xs text-secondary">{row.label}</dt>
|
||||
<dd class="text-xs text-primary break-words">{row.value}</dd>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-xs text-secondary">Not configured</span>
|
||||
{/each}
|
||||
</dl>
|
||||
</Section>
|
||||
|
||||
<Section label="System message" small>
|
||||
{#if systemPrompt}
|
||||
<p
|
||||
class="text-xs text-primary whitespace-pre-wrap break-words rounded-md bg-surface-secondary p-2 max-h-80 overflow-auto"
|
||||
>
|
||||
{systemPrompt}
|
||||
</p>
|
||||
{:else}
|
||||
<span class="text-xs text-secondary">None</span>
|
||||
{/if}
|
||||
</Section>
|
||||
|
||||
<Section label="Tools ({tools.length})" small>
|
||||
{#if tools.length === 0}
|
||||
<span class="text-xs text-secondary">No tools</span>
|
||||
{:else}
|
||||
<ul class="flex flex-col gap-2">
|
||||
{#each tools as tool, i (tool?.id ?? i)}
|
||||
<li class="flex items-center justify-between gap-2 min-w-0">
|
||||
<span class="text-xs text-primary truncate">{toolDisplayName(tool) ?? tool?.id}</span>
|
||||
<Badge color="gray" small>{toolKind(tool)}</Badge>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</Section>
|
||||
</div>
|
||||
@@ -11,6 +11,8 @@
|
||||
agentChatPath
|
||||
} from '../conversations/agentEditorChat'
|
||||
import { runFlowPreview } from '../utils.svelte'
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { Flow, FlowModule, InputTransform, Job, OpenFlow } from '$lib/gen'
|
||||
import { emptySchema, type StateStore } from '$lib/utils'
|
||||
@@ -355,6 +357,26 @@
|
||||
)
|
||||
}
|
||||
|
||||
let runLoading = $state(false)
|
||||
let scheduledForStr = $state<string | undefined>(undefined)
|
||||
let invisible_to_owner = $state<boolean | undefined>(undefined)
|
||||
let overrideTag = $state<string | undefined>(undefined)
|
||||
|
||||
/** One run from the form, as the same one-step flow a chat turn runs but outside any
|
||||
* conversation, then onto its run page. */
|
||||
async function runOnce(_scheduledFor: string | undefined, args: Record<string, any>) {
|
||||
if (!agentModule) return
|
||||
runLoading = true
|
||||
try {
|
||||
const flow = agentChatFlow($state.snapshot(agentModule) as FlowModule)
|
||||
flow.value.chat_input_enabled = false
|
||||
const id = await runFlowPreview(args, flow, path, undefined, undefined, undefined, workspace)
|
||||
await goto(`/run/${id}?workspace=${workspace}`)
|
||||
} finally {
|
||||
runLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// What the composer reads for its model button and paperclip.
|
||||
let chatModules = $derived(agentModule ? agentChatFlow(agentModule).value.modules : undefined)
|
||||
let chatGap = $derived(agentChatGap(agentValue?.input_transforms))
|
||||
@@ -446,18 +468,17 @@
|
||||
<!-- Named and positioned so the tool picker's popover can portal here: the `#flow-editor` it
|
||||
otherwise targets is behind this dialog, and does not exist at all on the resources page. -->
|
||||
<div id="agent-editor" class="relative h-full min-h-0">
|
||||
<!-- Resizable as the step panel's config and test are: a long system prompt and a long
|
||||
answer want opposite splits, and only the reader knows which they are on. -->
|
||||
<Splitpanes class="h-full">
|
||||
<!-- Viewing leads with running the agent; editing leads with what is being edited. -->
|
||||
{#if view}
|
||||
<Pane size={62} minSize={30}>{@render runPane()}</Pane>
|
||||
<Pane size={38} minSize={20}>{@render configPane()}</Pane>
|
||||
{:else}
|
||||
{#if view}
|
||||
<!-- A view only runs the agent: the page beside it summarizes the configuration. -->
|
||||
{@render runPane()}
|
||||
{:else}
|
||||
<!-- Resizable as the step panel's config and test are: a long system prompt and a long
|
||||
answer want opposite splits, and only the reader knows which they are on. -->
|
||||
<Splitpanes class="h-full">
|
||||
<Pane size={55} minSize={30}>{@render configPane()}</Pane>
|
||||
<Pane size={45} minSize={20}>{@render runPane()}</Pane>
|
||||
{/if}
|
||||
</Splitpanes>
|
||||
</Splitpanes>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#snippet configPane()}
|
||||
@@ -521,36 +542,54 @@
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<!-- Laid out as the script editor's preview column is: what a run takes above what it
|
||||
produced, both alongside what is being edited. -->
|
||||
<div class="flex-1 min-h-0 {testMode === 'chat' ? 'hidden' : ''}">
|
||||
<Splitpanes horizontal class="h-full">
|
||||
<Pane size={40} minSize={15}>
|
||||
<div class="h-full overflow-auto">
|
||||
<ModulePreview
|
||||
{#if view}
|
||||
<!-- A run of the deployed agent is a run like any other item's, so it opens on its
|
||||
own page rather than as a test result beside the form. -->
|
||||
<div class="flex-1 min-h-0 overflow-auto p-4 {testMode === 'chat' ? 'hidden' : ''}">
|
||||
<RunForm
|
||||
runnable={{ schema: AGENT_CHAT_SCHEMA, path }}
|
||||
runAction={runOnce}
|
||||
schedulable={false}
|
||||
detailed={false}
|
||||
autofocus
|
||||
loading={runLoading}
|
||||
bind:scheduledForStr
|
||||
bind:invisible_to_owner
|
||||
bind:overrideTag
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 min-h-0 {testMode === 'chat' ? 'hidden' : ''}">
|
||||
<Splitpanes horizontal class="h-full">
|
||||
<Pane size={40} minSize={15}>
|
||||
<div class="h-full overflow-auto">
|
||||
<ModulePreview
|
||||
mod={agentModule as FlowModule}
|
||||
schema={flowLocalAgentSchema(schema)}
|
||||
pickableProperties={stepPropPicker?.pickableProperties}
|
||||
runInputKeys={AGENT_EDITOR_RUN_INPUTS}
|
||||
bind:testJob
|
||||
bind:testIsLoading
|
||||
bind:scriptProgress
|
||||
/>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={60} minSize={20}>
|
||||
<ModulePreviewResultViewer
|
||||
lang="deno"
|
||||
editor={undefined}
|
||||
diffEditor={undefined}
|
||||
mod={agentModule as FlowModule}
|
||||
schema={flowLocalAgentSchema(schema)}
|
||||
pickableProperties={stepPropPicker?.pickableProperties}
|
||||
runInputKeys={AGENT_EDITOR_RUN_INPUTS}
|
||||
bind:testJob
|
||||
bind:testIsLoading
|
||||
bind:scriptProgress
|
||||
{testJob}
|
||||
{testIsLoading}
|
||||
{scriptProgress}
|
||||
disableMock
|
||||
disableHistory
|
||||
/>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={60} minSize={20}>
|
||||
<ModulePreviewResultViewer
|
||||
lang="deno"
|
||||
editor={undefined}
|
||||
diffEditor={undefined}
|
||||
mod={agentModule as FlowModule}
|
||||
{testJob}
|
||||
{testIsLoading}
|
||||
{scriptProgress}
|
||||
disableMock
|
||||
disableHistory
|
||||
/>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
{/if}
|
||||
{#if chatMounted}
|
||||
<div class={testMode === 'chat' ? 'flex flex-col flex-1 min-h-0' : 'hidden'}>
|
||||
{#if chatGap?.memory}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChevronRight,
|
||||
FlaskConical,
|
||||
FormInput,
|
||||
@@ -286,14 +285,6 @@
|
||||
{:else}
|
||||
<div class="h-full min-h-0 flex flex-col">
|
||||
<div class="flex items-center gap-2 px-4 py-2 border-b shrink-0">
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
iconOnly
|
||||
startIcon={{ icon: ArrowLeft }}
|
||||
title="Back"
|
||||
onClick={close}
|
||||
/>
|
||||
<div class="min-w-0 flex flex-col">
|
||||
<div class="flex items-center gap-1 min-w-0">
|
||||
{#each trail as segment, i (i)}
|
||||
@@ -450,23 +441,24 @@
|
||||
{/snippet}
|
||||
|
||||
<!-- Mounted under the evals level too: it holds the draft the header's banner and Deploy act on,
|
||||
and the config a draft run is offered on. -->
|
||||
and the config a draft run is offered on. The target is read optionally: a deploy that
|
||||
navigates away clears it while these are still rendered. -->
|
||||
{#snippet agentPage()}
|
||||
<AgentEditorHost
|
||||
bind:this={host}
|
||||
path={target.path}
|
||||
path={target?.path ?? ''}
|
||||
workspace={ws}
|
||||
{enableAi}
|
||||
toolId={target.toolId}
|
||||
toolId={target?.toolId}
|
||||
onSelectTool={(id) => showAgentEditorTool(id)}
|
||||
{onSaved}
|
||||
isNew={target.isNew}
|
||||
isNew={target?.isNew}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet evalsPage()}
|
||||
<EvalsPane
|
||||
agentPath={target.path}
|
||||
agentPath={target?.path ?? ''}
|
||||
opWorkspace={ws}
|
||||
editedConfig={draft?.sync.hasDraft ? editedConfig : undefined}
|
||||
bind:location={evalsLocation}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { Bot, FileUp, FormInput, MessageSquare, Pen, Shield, Trash } from 'lucide-svelte'
|
||||
import { MediaQuery } from 'svelte/reactivity'
|
||||
import {
|
||||
FileUp,
|
||||
FormInput,
|
||||
MessageSquare,
|
||||
PanelRightClose,
|
||||
PanelRightOpen,
|
||||
Pen,
|
||||
Shield,
|
||||
Trash
|
||||
} from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { base } from '$lib/base'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Button, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import SummaryPathDisplay from '$lib/components/SummaryPathDisplay.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
|
||||
import AgentEditorHost from '$lib/components/flows/content/AgentEditorHost.svelte'
|
||||
import AgentConfigSummary from '$lib/components/flows/content/AgentConfigSummary.svelte'
|
||||
import { keepsManagedMemory } from '$lib/components/flows/agentFormFields'
|
||||
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
|
||||
import { ResourceService } from '$lib/gen'
|
||||
@@ -20,19 +33,18 @@
|
||||
import { isDeployable } from '$lib/utils_deployable'
|
||||
|
||||
/**
|
||||
* The deployed agent, as a flow's or a script's page shows theirs: what it is, and a way to run
|
||||
* it. A chat when its memory keeps the conversation, the inputs form otherwise.
|
||||
* The deployed agent, as a flow's or a script's page shows theirs: a way to run it, a chat when
|
||||
* its memory keeps the conversation and the inputs form otherwise, beside what it is.
|
||||
*/
|
||||
let path = $derived(page.params.path ?? '')
|
||||
let ws = $derived($workspaceStore)
|
||||
|
||||
let host = $state<ReturnType<typeof AgentEditorHost> | undefined>(undefined)
|
||||
let toolId = $state<string | undefined>(undefined)
|
||||
let agent = $derived(host?.draftHandle())
|
||||
let testPane = $derived(host?.testPaneHandle())
|
||||
let loaded = $derived(agent?.state != undefined)
|
||||
let chatAvailable = $derived(keepsManagedMemory(agent?.state?.args?.memory))
|
||||
let canEdit = $derived(loaded && (agent?.canWrite ?? false) && !$userStore?.operator)
|
||||
let config = $derived(agent?.state?.args)
|
||||
let chatAvailable = $derived(keepsManagedMemory(config?.memory))
|
||||
let canEdit = $derived(config != undefined && (agent?.canWrite ?? false) && !$userStore?.operator)
|
||||
// An agent runs as a flow preview, and the server refuses a preview to an operator, and to a
|
||||
// non-admin under another user's namespace (`require_path_read_access_for_preview`).
|
||||
let runBlockedReason = $derived(
|
||||
@@ -45,6 +57,12 @@
|
||||
: undefined
|
||||
)
|
||||
|
||||
// Tailwind's `lg`, where the detail pages stop collapsing: the configuration sits beside the run
|
||||
// pane above it, and in a drawer over it below, closed until asked for.
|
||||
const wide = new MediaQuery('(min-width: 1024px)')
|
||||
let panelOpen = $state<boolean | undefined>(undefined)
|
||||
let showPanel = $derived(panelOpen ?? wide.current)
|
||||
|
||||
let shareModal: ShareModal | undefined = $state(undefined)
|
||||
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state(undefined)
|
||||
let deleteOpen = $state(false)
|
||||
@@ -77,102 +95,129 @@
|
||||
</ConfirmationModal>
|
||||
|
||||
<main class="h-screen w-full flex flex-col">
|
||||
<div class="flex items-center gap-3 px-4 py-2 border-b shrink-0 min-h-12">
|
||||
<Bot size={20} class="text-violet-500 shrink-0" />
|
||||
<div class="min-w-0 flex flex-col">
|
||||
<span class="text-sm font-semibold text-emphasis truncate">
|
||||
{agent?.state?.description || path}
|
||||
</span>
|
||||
{#if agent?.state?.description}
|
||||
<span class="text-2xs text-tertiary truncate">{path}</span>
|
||||
{/if}
|
||||
<!-- Laid out as `DetailPageHeader` is for flows and scripts, whose trigger and error handler
|
||||
controls an agent does not have. -->
|
||||
<div class="border-b">
|
||||
<div
|
||||
class="flex w-full flex-wrap md:flex-nowrap justify-end gap-x-2 gap-y-4 items-center min-h-12 py-2 md:py-0"
|
||||
>
|
||||
<div class="grow px-2 inline-flex items-center gap-4 min-w-0">
|
||||
<div class={twMerge('min-w-0', $userStore?.operator ? 'pl-10' : '')}>
|
||||
<SummaryPathDisplay summary={config ? agent?.state?.description : undefined} {path} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1 items-center pr-4">
|
||||
<!-- Only an agent that keeps the conversation can chat: without managed memory every
|
||||
message would be answered alone, so it is run from its inputs with nothing to switch. -->
|
||||
{#if chatAvailable && !runBlockedReason && testPane?.mode}
|
||||
<ToggleButtonGroup
|
||||
bind:selected={
|
||||
() => testPane?.mode,
|
||||
(mode) => {
|
||||
if (testPane) testPane.mode = mode
|
||||
}
|
||||
}
|
||||
noWFull
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
size="md"
|
||||
value="chat"
|
||||
label="Chat"
|
||||
icon={MessageSquare}
|
||||
tooltip="Chat with the agent: each message runs it, and it remembers the conversation"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
size="md"
|
||||
value="form"
|
||||
label="Form"
|
||||
icon={FormInput}
|
||||
tooltip="Run the agent once, on the inputs in a form"
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
{#if config && !$userStore?.operator}
|
||||
<DropdownV2
|
||||
placement="bottom-end"
|
||||
size="md"
|
||||
items={async () => [
|
||||
{
|
||||
displayName: 'Permissions',
|
||||
icon: Shield,
|
||||
disabled: !canEdit,
|
||||
action: () => shareModal?.openDrawer?.(path, 'resource')
|
||||
},
|
||||
...(!agent?.state?.wsSpecific &&
|
||||
isDeployable('resource', path, await getDeployUiSettings())
|
||||
? [
|
||||
{
|
||||
displayName: 'Deploy to prod/staging',
|
||||
icon: FileUp,
|
||||
action: () => deploymentDrawer?.openDrawer(path, 'resource')
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: Trash,
|
||||
type: 'delete' as const,
|
||||
disabled: !canEdit,
|
||||
action: () => (deleteOpen = true)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="md"
|
||||
iconOnly
|
||||
startIcon={{ icon: showPanel ? PanelRightClose : PanelRightOpen }}
|
||||
title={showPanel ? 'Hide configuration' : 'Show configuration'}
|
||||
onClick={() => (panelOpen = !showPanel)}
|
||||
/>
|
||||
{#if canEdit}
|
||||
<Button
|
||||
variant="accent"
|
||||
unifiedSize="md"
|
||||
startIcon={{ icon: Pen }}
|
||||
href="{base}/agents/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<!-- Only an agent that keeps the conversation can chat: without managed memory every message
|
||||
would be answered alone, so it is run from its inputs and nothing is offered to switch. -->
|
||||
{#if loaded && chatAvailable && !runBlockedReason && testPane?.mode}
|
||||
<ToggleButtonGroup
|
||||
bind:selected={
|
||||
() => testPane?.mode,
|
||||
(mode) => {
|
||||
if (testPane) testPane.mode = mode
|
||||
}
|
||||
}
|
||||
noWFull
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value="chat"
|
||||
label="Chat"
|
||||
icon={MessageSquare}
|
||||
tooltip="Chat with the agent: each message runs it, and it remembers the conversation"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
size="sm"
|
||||
value="form"
|
||||
label="Form"
|
||||
icon={FormInput}
|
||||
tooltip="Run the agent once, on the inputs in a form"
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<div class="grow"></div>
|
||||
{/if}
|
||||
{#if loaded && !$userStore?.operator}
|
||||
<DropdownV2
|
||||
items={async () => [
|
||||
{
|
||||
displayName: 'Permissions',
|
||||
icon: Shield,
|
||||
disabled: !canEdit,
|
||||
action: () => shareModal?.openDrawer?.(path, 'resource')
|
||||
},
|
||||
...(!agent?.state?.wsSpecific &&
|
||||
isDeployable('resource', path, await getDeployUiSettings())
|
||||
? [
|
||||
{
|
||||
displayName: 'Deploy to prod/staging',
|
||||
icon: FileUp,
|
||||
action: () => deploymentDrawer?.openDrawer(path, 'resource')
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: Trash,
|
||||
type: 'delete' as const,
|
||||
disabled: !canEdit,
|
||||
action: () => (deleteOpen = true)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
{#if canEdit}
|
||||
<Button
|
||||
variant="accent"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: Pen }}
|
||||
href="{base}/agents/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
{#key `${ws}:${path}`}
|
||||
<AgentEditorHost
|
||||
bind:this={host}
|
||||
{path}
|
||||
workspace={ws}
|
||||
enableAi={$copilotInfo.enabled}
|
||||
{toolId}
|
||||
onSelectTool={(id) => (toolId = id)}
|
||||
view
|
||||
{runBlockedReason}
|
||||
/>
|
||||
{/key}
|
||||
<div class="flex-1 min-h-0 flex">
|
||||
<div class="flex-1 min-w-0">
|
||||
{#key `${ws}:${path}`}
|
||||
<AgentEditorHost
|
||||
bind:this={host}
|
||||
{path}
|
||||
workspace={ws}
|
||||
enableAi={$copilotInfo.enabled}
|
||||
view
|
||||
{runBlockedReason}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{#if wide.current && showPanel && config}
|
||||
<aside class="w-80 shrink-0 border-l overflow-y-auto">
|
||||
<AgentConfigSummary {config} />
|
||||
</aside>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{#if !wide.current}
|
||||
<Drawer placement="right" size="320px" bind:open={() => showPanel, (open) => (panelOpen = open)}>
|
||||
<DrawerContent title="Configuration" on:close={() => (panelOpen = false)} noPadding>
|
||||
{#if config}
|
||||
<AgentConfigSummary {config} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user