feat: add workspace agents for reusable agent definitions across patterns

Introduce a workspace-level agent library that allows users to define
agents once and reference them from multiple orchestration patterns.

Domain model:
- Add WorkspaceAgentDefinition type with name, model, instructions, etc.
- Extend PatternAgentDefinition with optional workspaceAgentId and overrides
- Add resolution helpers that merge workspace agent base with per-pattern overrides
- Extend WorkspaceSettings with agents array and normalize on load

IPC & main process:
- Add saveWorkspaceAgent/deleteWorkspaceAgent IPC channels and handlers
- Resolve workspace agent references in buildEffectivePattern before
  sending to sidecar (no C# changes needed)

Settings UI:
- Add 'Agents' tab under Orchestration in the Settings panel
- Create WorkspaceAgentEditor component using ToolingEditorShell
- Show usage count (which patterns reference each agent)

Pattern editor integration:
- Add agent picker dropdown: 'New inline agent' or 'From library'
- Show linked badge (chain icon) on referenced agent graph nodes
- Show linked workspace agent banner in the inspector
- Add 'Save to Agent Library' action to promote inline agents
- Add 'Unlink' action to convert referenced agents back to inline

Tests:
- Add unit tests for resolution helpers (resolvePatternAgent,
  resolvePatternAgents, findWorkspaceAgentUsages, normalize)
- Update existing tooling test for new agents field

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-02 17:09:54 +02:00
co-authored by Copilot
parent 10316a2871
commit e39fffaf3b
17 changed files with 808 additions and 17 deletions
+96 -8
View File
@@ -3,14 +3,18 @@ import {
AlertCircle,
ArrowLeftRight,
CheckCircle,
ChevronDown,
ChevronLeft,
GitFork,
Library,
Link2,
ListOrdered,
Lock,
MessageSquare,
Plus,
ShieldCheck,
Trash2,
Unlink,
Users,
type LucideIcon,
} from 'lucide-react';
@@ -35,7 +39,9 @@ import {
type RuntimeToolDefinition,
type WorkspaceToolingSettings,
} from '@shared/domain/tooling';
import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
import { useClickOutside } from '@renderer/hooks/useClickOutside';
import { ToggleSwitch } from '@renderer/components/ui';
import { PatternGraphCanvas } from './pattern-graph/PatternGraphCanvas';
import { PatternGraphInspector } from './pattern-graph/PatternGraphInspector';
@@ -46,6 +52,8 @@ interface PatternEditorProps {
isBuiltin: boolean;
toolingSettings: WorkspaceToolingSettings;
runtimeTools?: ReadonlyArray<RuntimeToolDefinition>;
workspaceAgents: WorkspaceAgentDefinition[];
onSaveWorkspaceAgent: (agent: WorkspaceAgentDefinition) => Promise<void>;
onChange: (pattern: PatternDefinition) => void;
onDelete?: () => void;
onSave: () => void;
@@ -134,12 +142,16 @@ export function PatternEditor({
isBuiltin,
toolingSettings,
runtimeTools,
workspaceAgents,
onSaveWorkspaceAgent,
onChange,
onDelete,
onSave,
onBack,
}: PatternEditorProps) {
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const [addAgentMenuOpen, setAddAgentMenuOpen] = useState(false);
const addAgentMenuRef = useClickOutside<HTMLDivElement>(() => setAddAgentMenuOpen(false), addAgentMenuOpen);
const issues = validatePatternDefinition(pattern);
const graph = resolvePatternGraph(pattern);
@@ -166,6 +178,47 @@ export function PatternEditor({
};
const updatedGraph = addAgentToGraph(graph, pattern.mode, newAgent);
onChange({ ...pattern, agents: [...pattern.agents, newAgent], graph: updatedGraph });
setAddAgentMenuOpen(false);
}
function addAgentFromLibrary(wa: WorkspaceAgentDefinition) {
const newAgent: PatternAgentDefinition = {
id: `agent-${crypto.randomUUID()}`,
name: wa.name,
description: wa.description,
instructions: wa.instructions,
model: wa.model,
reasoningEffort: wa.reasoningEffort,
copilot: wa.copilot,
workspaceAgentId: wa.id,
};
const updatedGraph = addAgentToGraph(graph, pattern.mode, newAgent);
onChange({ ...pattern, agents: [...pattern.agents, newAgent], graph: updatedGraph });
setAddAgentMenuOpen(false);
}
async function promoteAgent(agentId: string) {
const agent = pattern.agents.find((a) => a.id === agentId);
if (!agent || agent.workspaceAgentId) return;
const timestamp = new Date().toISOString();
const workspaceAgent: WorkspaceAgentDefinition = {
id: `agent-${crypto.randomUUID()}`,
name: agent.name,
description: agent.description,
instructions: agent.instructions,
model: agent.model,
reasoningEffort: agent.reasoningEffort,
copilot: agent.copilot,
createdAt: timestamp,
updatedAt: timestamp,
};
await onSaveWorkspaceAgent(workspaceAgent);
updateAgent(agentId, { workspaceAgentId: workspaceAgent.id, overrides: undefined });
}
function unlinkAgent(agentId: string) {
updateAgent(agentId, { workspaceAgentId: undefined, overrides: undefined });
}
function updateAgent(agentId: string, patch: Partial<PatternAgentDefinition>) {
@@ -313,14 +366,46 @@ export function PatternEditor({
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
Topology
</h4>
<button
className="flex items-center gap-1.5 rounded-lg px-2.5 py-1 text-[12px] font-medium text-[var(--color-text-secondary)] transition-all duration-200 hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]"
onClick={addAgent}
type="button"
>
<Plus className="size-3" />
Add agent
</button>
<div className="relative" ref={addAgentMenuRef}>
<button
className="flex items-center gap-1.5 rounded-lg px-2.5 py-1 text-[12px] font-medium text-[var(--color-text-secondary)] transition-all duration-200 hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]"
onClick={() => workspaceAgents.length > 0 ? setAddAgentMenuOpen(!addAgentMenuOpen) : addAgent()}
type="button"
>
<Plus className="size-3" />
Add agent
{workspaceAgents.length > 0 && <ChevronDown className="size-3" />}
</button>
{addAgentMenuOpen && (
<div className="absolute right-0 z-50 mt-1 w-56 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] py-1 shadow-xl">
<button
className="flex w-full items-center gap-2 px-3 py-2 text-left text-[12px] text-[var(--color-text-secondary)] transition hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]"
onClick={addAgent}
type="button"
>
<Plus className="size-3.5" />
New inline agent
</button>
<div className="my-1 border-t border-[var(--color-border)]" />
<div className="px-3 py-1.5">
<span className="text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
From library
</span>
</div>
{workspaceAgents.map((wa) => (
<button
className="flex w-full items-center gap-2 px-3 py-2 text-left text-[12px] text-[var(--color-text-secondary)] transition hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]"
key={wa.id}
onClick={() => addAgentFromLibrary(wa)}
type="button"
>
<Library className="size-3.5 text-[var(--color-accent)]" />
<span className="truncate">{wa.name}</span>
</button>
))}
</div>
)}
</div>
</div>
<div className="min-h-[300px] flex-1 px-5 py-3">
@@ -473,8 +558,11 @@ export function PatternEditor({
graph={graph}
mode={pattern.mode}
selectedNodeId={selectedNodeId}
workspaceAgents={workspaceAgents}
onAgentChange={updateAgent}
onAgentRemove={removeAgent}
onAgentPromote={promoteAgent}
onAgentUnlink={unlinkAgent}
onGraphChange={emitGraphChange}
/>
</div>
+121 -2
View File
@@ -1,11 +1,12 @@
import { useEffect, useState, type ReactNode } from 'react';
import { ChevronLeft, ChevronRight, CircleCheck, Code, Cpu, FolderOpen, Palette, Plus, RefreshCw, Server, TriangleAlert, Workflow, Wrench } from 'lucide-react';
import { ChevronLeft, ChevronRight, CircleCheck, Code, Cpu, FolderOpen, Palette, Plus, RefreshCw, Server, TriangleAlert, UserCircle, Workflow, Wrench } from 'lucide-react';
import { CopilotStatusCard } from '@renderer/components/CopilotStatusCard';
import { PatternEditor } from '@renderer/components/PatternEditor';
import { ToggleSwitch } from '@renderer/components/ui';
import { LspProfileEditor } from '@renderer/components/settings/LspProfileEditor';
import { McpServerEditor } from '@renderer/components/settings/McpServerEditor';
import { WorkspaceAgentEditor } from '@renderer/components/settings/WorkspaceAgentEditor';
import type { SidecarCapabilities, QuotaSnapshot } from '@shared/contracts/sidecar';
import type { DiscoveredMcpServer, DiscoveredToolingState } from '@shared/domain/discoveredTooling';
import { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
@@ -20,6 +21,7 @@ import {
type McpServerDefinition,
type WorkspaceToolingSettings,
} from '@shared/domain/tooling';
import { normalizeWorkspaceAgentDefinition, findWorkspaceAgentUsages, type WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
interface SettingsPanelProps {
availableModels: ReadonlyArray<ModelDefinition>;
@@ -41,6 +43,10 @@ interface SettingsPanelProps {
onSaveLspProfile: (profile: LspProfileDefinition) => Promise<void>;
onDeleteLspProfile: (profileId: string) => Promise<void>;
onNewLspProfile: () => LspProfileDefinition;
onSaveWorkspaceAgent: (agent: WorkspaceAgentDefinition) => Promise<void>;
onDeleteWorkspaceAgent: (agentId: string) => Promise<void>;
onNewWorkspaceAgent: () => WorkspaceAgentDefinition;
workspaceAgents: WorkspaceAgentDefinition[];
onSetTheme: (theme: AppearanceTheme) => void;
notificationsEnabled: boolean;
onSetNotificationsEnabled: (enabled: boolean) => void;
@@ -54,7 +60,7 @@ interface SettingsPanelProps {
onGetQuota?: () => Promise<Record<string, QuotaSnapshot>>;
}
export type SettingsSection = 'appearance' | 'connection' | 'patterns' | 'mcp-servers' | 'lsp-profiles' | 'troubleshooting';
export type SettingsSection = 'appearance' | 'connection' | 'patterns' | 'agents' | 'mcp-servers' | 'lsp-profiles' | 'troubleshooting';
interface NavItem {
id: SettingsSection;
@@ -84,6 +90,7 @@ const navGroups: NavGroup[] = [
label: 'Orchestration',
items: [
{ id: 'patterns', label: 'Patterns', icon: <Workflow className="size-3.5" /> },
{ id: 'agents', label: 'Agents', icon: <UserCircle className="size-3.5" /> },
],
},
{
@@ -126,6 +133,10 @@ export function SettingsPanel({
onSaveLspProfile,
onDeleteLspProfile,
onNewLspProfile,
onSaveWorkspaceAgent,
onDeleteWorkspaceAgent,
onNewWorkspaceAgent,
workspaceAgents,
onSetTheme,
notificationsEnabled,
onSetNotificationsEnabled,
@@ -142,6 +153,7 @@ export function SettingsPanel({
const [editingPattern, setEditingPattern] = useState<PatternDefinition | null>(null);
const [editingMcpServer, setEditingMcpServer] = useState<McpServerDefinition | null>(null);
const [editingLspProfile, setEditingLspProfile] = useState<LspProfileDefinition | null>(null);
const [editingWorkspaceAgent, setEditingWorkspaceAgent] = useState<WorkspaceAgentDefinition | null>(null);
if (editingPattern) {
const isBuiltin = editingPattern.id.startsWith('pattern-');
@@ -167,6 +179,8 @@ export function SettingsPanel({
pattern={editingPattern}
runtimeTools={sidecarCapabilities?.runtimeTools}
toolingSettings={toolingSettings}
workspaceAgents={workspaceAgents}
onSaveWorkspaceAgent={onSaveWorkspaceAgent}
/>
</div>
);
@@ -222,6 +236,33 @@ export function SettingsPanel({
);
}
if (editingWorkspaceAgent) {
const exists = workspaceAgents.some((a) => a.id === editingWorkspaceAgent.id);
return (
<div className="fixed inset-0 z-50 flex flex-col bg-[var(--color-surface-0)]">
<WorkspaceAgentEditor
agent={editingWorkspaceAgent}
availableModels={availableModels}
onBack={() => setEditingWorkspaceAgent(null)}
onChange={setEditingWorkspaceAgent}
onDelete={
exists
? async () => {
await onDeleteWorkspaceAgent(editingWorkspaceAgent.id);
setEditingWorkspaceAgent(null);
}
: undefined
}
onSave={async () => {
await onSaveWorkspaceAgent(normalizeWorkspaceAgentDefinition(editingWorkspaceAgent));
setEditingWorkspaceAgent(null);
}}
patterns={patterns}
/>
</div>
);
}
return (
<div className="overlay-slide-enter fixed inset-0 z-50 flex flex-col bg-[var(--color-surface-0)]">
<div className="drag-region flex items-center gap-3 border-b border-[var(--color-border)] px-5 pb-3 pt-3">
@@ -298,6 +339,14 @@ export function SettingsPanel({
patterns={patterns}
/>
)}
{activeSection === 'agents' && (
<WorkspaceAgentsSection
agents={workspaceAgents}
patterns={patterns}
onEditAgent={(agent) => setEditingWorkspaceAgent(structuredClone(agent))}
onNewAgent={() => setEditingWorkspaceAgent(onNewWorkspaceAgent())}
/>
)}
{activeSection === 'mcp-servers' && (
<McpServersSection
onEditServer={(server) => setEditingMcpServer(structuredClone(server))}
@@ -554,6 +603,76 @@ function PatternsSection({
);
}
function WorkspaceAgentsSection({
agents,
patterns,
onEditAgent,
onNewAgent,
}: {
agents: WorkspaceAgentDefinition[];
patterns: PatternDefinition[];
onEditAgent: (agent: WorkspaceAgentDefinition) => void;
onNewAgent: () => void;
}) {
return (
<div>
<SectionHeader
description="Define reusable agents that can be shared across multiple patterns"
title="Workspace Agents"
>
<SectionAction label="New Agent" onClick={onNewAgent} />
</SectionHeader>
{agents.length === 0 ? (
<div className="rounded-xl border border-dashed border-[var(--color-border)] px-6 py-10 text-center">
<UserCircle className="mx-auto size-8 text-[var(--color-text-muted)]" />
<p className="mt-3 text-[13px] font-medium text-[var(--color-text-secondary)]">
No workspace agents yet
</p>
<p className="mt-1 text-[12px] text-[var(--color-text-muted)]">
Create agents here and reference them in multiple patterns.
Changes to a workspace agent automatically propagate to all linked patterns.
</p>
</div>
) : (
<div className="space-y-1">
{agents.map((agent) => {
const usageCount = findWorkspaceAgentUsages(agent.id, patterns).length;
return (
<button
className="group flex w-full items-center gap-3 rounded-xl border border-transparent px-4 py-3 text-left transition-all duration-200 hover:border-[var(--color-border)] hover:bg-[var(--color-surface-1)]"
key={agent.id}
onClick={() => onEditAgent(agent)}
type="button"
>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-[13px] font-medium text-[var(--color-text-primary)]">{agent.name}</span>
<span className="rounded-full bg-[var(--color-surface-3)] px-2 py-0.5 text-[10px] font-medium text-[var(--color-text-muted)]">
{agent.model}
</span>
</div>
{agent.description && (
<p className="mt-0.5 truncate text-[12px] text-[var(--color-text-muted)]">{agent.description}</p>
)}
</div>
<div className="flex items-center gap-2">
{usageCount > 0 && (
<span className="text-[12px] text-[var(--color-text-muted)]">
{usageCount} pattern{usageCount === 1 ? '' : 's'}
</span>
)}
<ChevronRight className="size-4 text-[var(--color-text-muted)]" />
</div>
</button>
);
})}
</div>
)}
</div>
);
}
function McpServersSection({
servers,
onEditServer,
@@ -1,6 +1,6 @@
import { memo } from 'react';
import { Handle, Position, type NodeProps } from '@xyflow/react';
import { CircleUser, Shuffle, Layers, Radio, Bot } from 'lucide-react';
import { CircleUser, Link2, Shuffle, Layers, Radio, Bot } from 'lucide-react';
import type { GraphNodeData } from '@renderer/lib/patternGraph';
import type { PatternGraphNodeKind } from '@shared/domain/pattern';
@@ -56,6 +56,11 @@ function GraphNodeContent({ data, selected }: { data: GraphNodeData; selected: b
SYS
</span>
)}
{data.isLinked && (
<span className="ml-1 flex size-4 items-center justify-center rounded bg-[var(--color-accent)]/15" title="Linked workspace agent">
<Link2 className="size-2.5 text-[var(--color-accent)]" />
</span>
)}
</div>
);
}
@@ -1,4 +1,4 @@
import { Bot, ChevronDown, ChevronUp, CircleUser, Layers, Radio, Shuffle, Trash2 } from 'lucide-react';
import { Bot, ChevronDown, ChevronUp, CircleUser, Layers, Library, Link2, Radio, Shuffle, Trash2, Unlink } from 'lucide-react';
import {
findModel,
@@ -12,6 +12,7 @@ import type {
PatternGraph,
PatternGraphNodeKind,
} from '@shared/domain/pattern';
import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
import {
canMoveSequential,
findAgentForNode,
@@ -25,8 +26,11 @@ interface PatternGraphInspectorProps {
graph: PatternGraph;
mode: OrchestrationMode;
selectedNodeId: string | null;
workspaceAgents: WorkspaceAgentDefinition[];
onAgentChange: (agentId: string, patch: Partial<PatternAgentDefinition>) => void;
onAgentRemove: (agentId: string) => void;
onAgentPromote: (agentId: string) => void;
onAgentUnlink: (agentId: string) => void;
onGraphChange: (graph: PatternGraph) => void;
}
@@ -118,8 +122,11 @@ function AgentNodeInspector({
mode,
graph,
nodeId,
workspaceAgents,
onAgentChange,
onAgentRemove,
onAgentPromote,
onAgentUnlink,
onGraphChange,
}: {
agent: PatternAgentDefinition;
@@ -127,17 +134,45 @@ function AgentNodeInspector({
mode: OrchestrationMode;
graph: PatternGraph;
nodeId: string;
workspaceAgents: WorkspaceAgentDefinition[];
onAgentChange: (agentId: string, patch: Partial<PatternAgentDefinition>) => void;
onAgentRemove: (agentId: string) => void;
onAgentPromote: (agentId: string) => void;
onAgentUnlink: (agentId: string) => void;
onGraphChange: (graph: PatternGraph) => void;
}) {
const model = findModel(agent.model, availableModels);
const showReorder = mode === 'sequential' || mode === 'single' || mode === 'magentic';
const canUp = showReorder && canMoveSequential(graph, nodeId, 'up');
const canDown = showReorder && canMoveSequential(graph, nodeId, 'down');
const isLinked = Boolean(agent.workspaceAgentId);
const linkedWorkspaceAgent = isLinked
? workspaceAgents.find((wa) => wa.id === agent.workspaceAgentId)
: undefined;
return (
<div className="space-y-4">
{/* Linked workspace agent banner */}
{isLinked && (
<div className="flex items-center justify-between rounded-lg border border-[var(--color-accent)]/20 bg-[var(--color-accent)]/5 px-3 py-2">
<div className="flex items-center gap-2">
<Link2 className="size-3.5 text-[var(--color-accent)]" />
<span className="text-[11px] text-[var(--color-text-secondary)]">
Linked to <strong className="text-[var(--color-text-primary)]">{linkedWorkspaceAgent?.name ?? 'unknown agent'}</strong>
</span>
</div>
<button
className="flex items-center gap-1 rounded px-2 py-1 text-[11px] text-[var(--color-text-muted)] transition hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-secondary)]"
onClick={() => onAgentUnlink(agent.id)}
title="Unlink from workspace agent (convert to inline)"
type="button"
>
<Unlink className="size-3" />
Unlink
</button>
</div>
)}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="flex size-8 items-center justify-center rounded-lg bg-[var(--color-surface-2)]">
@@ -218,6 +253,19 @@ function AgentNodeInspector({
placeholder="System prompt for this agent..."
value={agent.instructions}
/>
{/* Promote / Unlink action */}
{!isLinked && (
<button
className="flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-[var(--color-border)] px-3 py-2 text-[12px] text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)]/30 hover:bg-[var(--color-accent)]/5 hover:text-[var(--color-accent)]"
onClick={() => onAgentPromote(agent.id)}
title="Save this agent to the workspace library for reuse across patterns"
type="button"
>
<Library className="size-3.5" />
Save to Agent Library
</button>
)}
</div>
);
}
@@ -228,8 +276,11 @@ export function PatternGraphInspector({
graph,
mode,
selectedNodeId,
workspaceAgents,
onAgentChange,
onAgentRemove,
onAgentPromote,
onAgentUnlink,
onGraphChange,
}: PatternGraphInspectorProps) {
if (!selectedNodeId) {
@@ -268,8 +319,11 @@ export function PatternGraphInspector({
mode={mode}
graph={graph}
nodeId={selectedNodeId}
workspaceAgents={workspaceAgents}
onAgentChange={onAgentChange}
onAgentRemove={onAgentRemove}
onAgentPromote={onAgentPromote}
onAgentUnlink={onAgentUnlink}
onGraphChange={onGraphChange}
/>
</div>
@@ -0,0 +1,138 @@
import { FormField, TextInput, TextareaInput } from '@renderer/components/ui';
import { ModelSelect, ReasoningEffortSelect } from '@renderer/components/AgentConfigFields';
import { findModel, type ModelDefinition } from '@shared/domain/models';
import { resolveReasoningEffort } from '@shared/domain/models';
import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
import type { PatternDefinition } from '@shared/domain/pattern';
import { findWorkspaceAgentUsages } from '@shared/domain/workspaceAgent';
import { ToolingEditorShell } from './ToolingEditorShell';
import { Link2, Workflow } from 'lucide-react';
function validateWorkspaceAgent(agent: WorkspaceAgentDefinition): string | undefined {
if (!agent.name.trim()) return 'Agent name is required.';
if (!agent.model.trim()) return 'Model is required.';
return undefined;
}
export function WorkspaceAgentEditor({
agent,
onChange,
onBack,
onSave,
onDelete,
availableModels,
patterns,
}: {
agent: WorkspaceAgentDefinition;
onChange: (agent: WorkspaceAgentDefinition) => void;
onBack: () => void;
onSave: () => Promise<void>;
onDelete?: () => Promise<void>;
availableModels: ReadonlyArray<ModelDefinition>;
patterns: PatternDefinition[];
}) {
const validationError = validateWorkspaceAgent(agent);
const usages = findWorkspaceAgentUsages(agent.id, patterns);
return (
<ToolingEditorShell
disableSave={Boolean(validationError)}
error={validationError}
onBack={onBack}
onDelete={onDelete}
onSave={onSave}
subtitle="Reusable agent definition"
title={agent.name || 'Untitled Agent'}
>
<section className="space-y-4">
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
General
</h4>
<FormField label="Name" required>
<TextInput
onChange={(value) => onChange({ ...agent, name: value })}
placeholder="e.g. Code Reviewer, Architect, QA Agent"
value={agent.name}
/>
</FormField>
</section>
<section className="space-y-4">
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
AI Configuration
</h4>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<ModelSelect
models={availableModels}
onChange={(model) => {
const m = findModel(model, availableModels);
onChange({
...agent,
model,
reasoningEffort: resolveReasoningEffort(m, agent.reasoningEffort),
});
}}
value={agent.model}
/>
</div>
<div>
<ReasoningEffortSelect
onChange={(value) => onChange({ ...agent, reasoningEffort: value })}
supportedEfforts={findModel(agent.model, availableModels)?.supportedReasoningEfforts}
value={agent.reasoningEffort}
/>
</div>
</div>
</section>
<section className="space-y-4">
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
Agent Identity
</h4>
<FormField label="Description">
<TextareaInput
onChange={(value) => onChange({ ...agent, description: value })}
placeholder="A short description of this agent's role and purpose"
rows={2}
value={agent.description}
/>
</FormField>
<FormField label="Instructions">
<TextareaInput
onChange={(value) => onChange({ ...agent, instructions: value })}
placeholder="System instructions that define this agent's behavior"
rows={8}
value={agent.instructions}
/>
</FormField>
</section>
{usages.length > 0 && (
<section className="space-y-3">
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
Used By
</h4>
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-glass)]">
{usages.map((usage) => (
<div
className="flex items-center gap-2.5 border-b border-[var(--color-border)] px-3.5 py-2.5 last:border-b-0"
key={usage.patternId}
>
<Workflow className="size-3.5 shrink-0 text-[var(--color-text-muted)]" />
<span className="text-[13px] text-[var(--color-text-secondary)]">
{usage.patternName}
</span>
<Link2 className="ml-auto size-3 text-[var(--color-accent)]" />
</div>
))}
</div>
<p className="text-[11px] text-[var(--color-text-muted)]">
Referenced by {usages.length} pattern{usages.length === 1 ? '' : 's'}.
Changes to this agent will affect all linked patterns.
</p>
</section>
)}
</ToolingEditorShell>
);
}