mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 05:28:46 +02:00
feat: allow saved workspace agents in the workflow editor
Add the ability to select from saved workspace agents when configuring agent nodes in the workflow editor. Previously, adding an agent node always created a new inline agent with empty fields. Changes: - Thread workspaceAgents prop through SettingsPanel → WorkflowEditor → WorkflowGraphInspector → AgentNodeInspector - Add Inline/Saved Agent source toggle in the agent node inspector - Add workspace agent picker dropdown with stale-agent detection - Add override-aware fields that show inherited values from the saved agent with per-field reset buttons and visual indicators - Support clean bidirectional switching between inline and linked modes - Show saved workspace agents as directly-addable items in the node palette with link icon, separated from the generic New Agent button - Add handleAddWorkspaceAgentNode to create pre-linked agent nodes The underlying data model (AgentNodeConfig.workspaceAgentId, overrides) and resolution logic (resolveWorkflowAgentNode) already existed but were never exposed in the UI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -199,6 +199,7 @@ export function SettingsPanel({
|
||||
}}
|
||||
workflow={editingWorkflow}
|
||||
workflows={workflows}
|
||||
workspaceAgents={workspaceAgents}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
SubWorkflowConfig,
|
||||
WorkflowStateScope,
|
||||
} from '@shared/domain/workflow';
|
||||
import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
|
||||
import { validateWorkflowDefinition, isBuilderBasedMode, syncBuilderModeEdgeIterations } from '@shared/domain/workflow';
|
||||
import { createId } from '@shared/utils/ids';
|
||||
import { ToggleSwitch } from '@renderer/components/ui';
|
||||
@@ -25,6 +26,7 @@ import { OrchestrationModePanel } from './workflow/OrchestrationModePanel';
|
||||
|
||||
interface WorkflowEditorProps {
|
||||
availableModels: ReadonlyArray<ModelDefinition>;
|
||||
workspaceAgents: ReadonlyArray<WorkspaceAgentDefinition>;
|
||||
workflow: WorkflowDefinition;
|
||||
workflows: ReadonlyArray<WorkflowDefinition>;
|
||||
onChange: (workflow: WorkflowDefinition) => void;
|
||||
@@ -152,6 +154,7 @@ function createMinimalInlineWorkflow(): WorkflowDefinition {
|
||||
|
||||
export function WorkflowEditor({
|
||||
availableModels,
|
||||
workspaceAgents,
|
||||
workflow,
|
||||
workflows,
|
||||
onChange,
|
||||
@@ -251,6 +254,36 @@ export function WorkflowEditor({
|
||||
setSelectedEdgeId(null);
|
||||
}
|
||||
|
||||
function handleAddWorkspaceAgentNode(agentId: string) {
|
||||
const agent = workspaceAgents.find((a) => a.id === agentId);
|
||||
if (!agent) return;
|
||||
const nodeId = createId('wf-agent');
|
||||
const config: AgentNodeConfig = {
|
||||
kind: 'agent',
|
||||
id: createId('agent'),
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
instructions: agent.instructions,
|
||||
model: agent.model,
|
||||
reasoningEffort: agent.reasoningEffort,
|
||||
copilot: agent.copilot,
|
||||
workspaceAgentId: agent.id,
|
||||
};
|
||||
const newNode: WorkflowNode = {
|
||||
id: nodeId,
|
||||
kind: 'agent',
|
||||
label: agent.name,
|
||||
position: { x: 300, y: 200 },
|
||||
config,
|
||||
};
|
||||
emitGraphChange({
|
||||
...activeWorkflow.graph,
|
||||
nodes: [...activeWorkflow.graph.nodes, newNode],
|
||||
});
|
||||
setSelectedNodeId(nodeId);
|
||||
setSelectedEdgeId(null);
|
||||
}
|
||||
|
||||
function handleNodeChange(nodeId: string, patch: Partial<WorkflowNode>) {
|
||||
emitGraphChange({
|
||||
...activeWorkflow.graph,
|
||||
@@ -486,7 +519,12 @@ export function WorkflowEditor({
|
||||
<div className="flex min-h-0 flex-1">
|
||||
{/* Left palette */}
|
||||
<div className="w-40 shrink-0 overflow-y-auto border-r border-[var(--color-border)] bg-[var(--color-surface-1)]">
|
||||
<WorkflowNodePalette disabledKinds={disabledPaletteKinds} onAddNode={handleAddNode} />
|
||||
<WorkflowNodePalette
|
||||
disabledKinds={disabledPaletteKinds}
|
||||
onAddNode={handleAddNode}
|
||||
onAddWorkspaceAgentNode={handleAddWorkspaceAgentNode}
|
||||
workspaceAgents={workspaceAgents}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Center column: validation + canvas + settings */}
|
||||
@@ -551,6 +589,7 @@ export function WorkflowEditor({
|
||||
validationIssues={issues}
|
||||
workflow={activeWorkflow}
|
||||
workflows={workflows}
|
||||
workspaceAgents={workspaceAgents}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { AlertCircle, Bot, FunctionSquare, GitBranch, Info, Radio, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { AlertCircle, Bot, FunctionSquare, GitBranch, Info, Link2, Radio, RotateCcw, Trash2, Unlink } from 'lucide-react';
|
||||
|
||||
import {
|
||||
findModel,
|
||||
@@ -16,8 +16,10 @@ import type {
|
||||
WorkflowOrchestrationMode,
|
||||
WorkflowValidationIssue,
|
||||
AgentNodeConfig,
|
||||
WorkflowAgentOverrides,
|
||||
} from '@shared/domain/workflow';
|
||||
import { isBuilderBasedMode } from '@shared/domain/workflow';
|
||||
import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
|
||||
import { ModelSelect, ReasoningEffortSelect } from '@renderer/components/AgentConfigFields';
|
||||
import { InvokeFunctionInspector } from '@renderer/components/workflow/InvokeFunctionInspector';
|
||||
import { ConditionEditor } from '@renderer/components/workflow/ConditionEditor';
|
||||
@@ -26,6 +28,7 @@ import { SubWorkflowInspector } from '@renderer/components/workflow/SubWorkflowI
|
||||
|
||||
interface WorkflowGraphInspectorProps {
|
||||
availableModels: ReadonlyArray<ModelDefinition>;
|
||||
workspaceAgents: ReadonlyArray<WorkspaceAgentDefinition>;
|
||||
workflow: WorkflowDefinition;
|
||||
workflows: ReadonlyArray<WorkflowDefinition>;
|
||||
selectedNodeId: string | null;
|
||||
@@ -39,6 +42,12 @@ interface WorkflowGraphInspectorProps {
|
||||
onDrillIntoSubWorkflow: (node: WorkflowNode) => void;
|
||||
}
|
||||
|
||||
const inputBaseClass =
|
||||
'w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-3 py-2 text-[13px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none transition focus:border-[var(--color-accent)]/50';
|
||||
|
||||
const selectClass =
|
||||
'w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-3 py-2 text-[13px] text-[var(--color-text-primary)] outline-none transition focus:border-[var(--color-accent)]/50';
|
||||
|
||||
function InputField({
|
||||
label,
|
||||
value,
|
||||
@@ -52,21 +61,19 @@ function InputField({
|
||||
multiline?: boolean;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const base =
|
||||
'w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-3 py-2 text-[13px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none transition focus:border-[var(--color-accent)]/50';
|
||||
return (
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">{label}</span>
|
||||
{multiline ? (
|
||||
<textarea
|
||||
className={`${base} min-h-20 resize-y`}
|
||||
className={`${inputBaseClass} min-h-20 resize-y`}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
className={base}
|
||||
className={inputBaseClass}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
@@ -76,37 +83,176 @@ function InputField({
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Overridable field ─────────────────────────────────────── */
|
||||
|
||||
function OverridableField({
|
||||
label,
|
||||
baseValue,
|
||||
overrideValue,
|
||||
onChange,
|
||||
onReset,
|
||||
multiline,
|
||||
placeholder,
|
||||
}: {
|
||||
label: string;
|
||||
baseValue: string;
|
||||
overrideValue: string | undefined;
|
||||
onChange: (value: string) => void;
|
||||
onReset: () => void;
|
||||
multiline?: boolean;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const isOverridden = overrideValue !== undefined;
|
||||
const displayValue = overrideValue ?? baseValue;
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">{label}</span>
|
||||
{isOverridden && (
|
||||
<button
|
||||
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-accent)] transition hover:bg-[var(--color-accent)]/10"
|
||||
onClick={onReset}
|
||||
title="Reset to saved agent value"
|
||||
type="button"
|
||||
>
|
||||
<RotateCcw className="size-2.5" />
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{multiline ? (
|
||||
<textarea
|
||||
className={`${inputBaseClass} min-h-20 resize-y ${isOverridden ? 'border-[var(--color-accent)]/30' : ''}`}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder ?? baseValue}
|
||||
value={displayValue}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
className={`${inputBaseClass} ${isOverridden ? 'border-[var(--color-accent)]/30' : ''}`}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder ?? baseValue}
|
||||
value={displayValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Agent node inspector ──────────────────────────────────── */
|
||||
|
||||
function AgentNodeInspector({
|
||||
node,
|
||||
availableModels,
|
||||
workspaceAgents,
|
||||
onNodeChange,
|
||||
onNodeConfigChange,
|
||||
onNodeRemove,
|
||||
}: {
|
||||
node: WorkflowNode;
|
||||
availableModels: ReadonlyArray<ModelDefinition>;
|
||||
workspaceAgents: ReadonlyArray<WorkspaceAgentDefinition>;
|
||||
onNodeChange: (nodeId: string, patch: Partial<WorkflowNode>) => void;
|
||||
onNodeConfigChange: (nodeId: string, config: WorkflowNodeConfig) => void;
|
||||
onNodeRemove: (nodeId: string) => void;
|
||||
}) {
|
||||
const config = node.config as AgentNodeConfig;
|
||||
const model = findModel(config.model, availableModels);
|
||||
const isLinked = !!config.workspaceAgentId;
|
||||
const baseAgent = useMemo(
|
||||
() => isLinked ? workspaceAgents.find((a) => a.id === config.workspaceAgentId) : undefined,
|
||||
[isLinked, config.workspaceAgentId, workspaceAgents],
|
||||
);
|
||||
const isStale = isLinked && !baseAgent;
|
||||
|
||||
const resolvedModel = isLinked && baseAgent
|
||||
? config.overrides?.model ?? baseAgent.model
|
||||
: config.model;
|
||||
const model = findModel(resolvedModel, availableModels);
|
||||
|
||||
function setConfig(next: AgentNodeConfig) {
|
||||
onNodeConfigChange(node.id, next);
|
||||
}
|
||||
|
||||
function patchConfig(patch: Partial<AgentNodeConfig>) {
|
||||
onNodeConfigChange(node.id, { ...config, ...patch });
|
||||
setConfig({ ...config, ...patch });
|
||||
}
|
||||
|
||||
function patchOverride<K extends keyof WorkflowAgentOverrides>(
|
||||
field: K,
|
||||
value: WorkflowAgentOverrides[K],
|
||||
) {
|
||||
if (!baseAgent) return;
|
||||
const baseValue = baseAgent[field];
|
||||
if (value === baseValue) {
|
||||
// Value matches base — remove the override
|
||||
resetOverride(field);
|
||||
} else {
|
||||
setConfig({
|
||||
...config,
|
||||
overrides: { ...config.overrides, [field]: value },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resetOverride(field: keyof WorkflowAgentOverrides) {
|
||||
if (!config.overrides) return;
|
||||
const { [field]: _, ...rest } = config.overrides;
|
||||
const cleaned = Object.keys(rest).length > 0 ? rest : undefined;
|
||||
setConfig({ ...config, overrides: cleaned });
|
||||
}
|
||||
|
||||
function handleSelectWorkspaceAgent(agentId: string) {
|
||||
const agent = workspaceAgents.find((a) => a.id === agentId);
|
||||
if (!agent) return;
|
||||
setConfig({
|
||||
...config,
|
||||
workspaceAgentId: agentId,
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
instructions: agent.instructions,
|
||||
model: agent.model,
|
||||
reasoningEffort: agent.reasoningEffort,
|
||||
copilot: agent.copilot,
|
||||
overrides: undefined,
|
||||
});
|
||||
onNodeChange(node.id, { label: agent.name });
|
||||
}
|
||||
|
||||
function handleDetach() {
|
||||
if (!baseAgent) return;
|
||||
const overrides = config.overrides ?? {};
|
||||
setConfig({
|
||||
...config,
|
||||
workspaceAgentId: undefined,
|
||||
overrides: undefined,
|
||||
name: overrides.name ?? baseAgent.name,
|
||||
description: overrides.description ?? baseAgent.description,
|
||||
instructions: overrides.instructions ?? baseAgent.instructions,
|
||||
model: overrides.model ?? baseAgent.model,
|
||||
reasoningEffort: overrides.reasoningEffort ?? baseAgent.reasoningEffort,
|
||||
});
|
||||
}
|
||||
|
||||
function handleSwitchToLinked() {
|
||||
if (workspaceAgents.length === 0) return;
|
||||
handleSelectWorkspaceAgent(workspaceAgents[0].id);
|
||||
}
|
||||
|
||||
const displayName = isLinked && baseAgent
|
||||
? config.overrides?.name ?? baseAgent.name
|
||||
: config.name;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<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)]">
|
||||
<Bot className="size-4 text-[var(--color-text-primary)]" />
|
||||
</div>
|
||||
<div className="text-[13px] font-semibold text-[var(--color-text-primary)]">
|
||||
{config.name || 'Unnamed Agent'}
|
||||
{displayName || 'Unnamed Agent'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -119,56 +265,229 @@ function AgentNodeInspector({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Source selector */}
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">Source</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
className={`flex flex-1 items-center justify-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[12px] font-medium transition-all ${
|
||||
!isLinked
|
||||
? 'bg-[var(--color-accent)]/15 text-[var(--color-accent)] ring-1 ring-[var(--color-accent)]/25'
|
||||
: 'text-[var(--color-text-muted)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text-secondary)]'
|
||||
}`}
|
||||
onClick={() => { if (isLinked) handleDetach(); }}
|
||||
type="button"
|
||||
>
|
||||
Inline
|
||||
</button>
|
||||
<button
|
||||
className={`flex flex-1 items-center justify-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[12px] font-medium transition-all ${
|
||||
isLinked
|
||||
? 'bg-[var(--color-accent)]/15 text-[var(--color-accent)] ring-1 ring-[var(--color-accent)]/25'
|
||||
: 'text-[var(--color-text-muted)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text-secondary)]'
|
||||
}`}
|
||||
onClick={() => { if (!isLinked) handleSwitchToLinked(); }}
|
||||
type="button"
|
||||
>
|
||||
<Link2 className="size-3" />
|
||||
Saved Agent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workspace agent picker (linked mode) */}
|
||||
{isLinked && (
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">Agent</span>
|
||||
{workspaceAgents.length > 0 ? (
|
||||
<select
|
||||
className={selectClass}
|
||||
onChange={(e) => handleSelectWorkspaceAgent(e.target.value)}
|
||||
value={config.workspaceAgentId ?? ''}
|
||||
>
|
||||
{isStale && (
|
||||
<option disabled value={config.workspaceAgentId}>
|
||||
(Deleted agent)
|
||||
</option>
|
||||
)}
|
||||
{workspaceAgents.map((agent) => (
|
||||
<option key={agent.id} value={agent.id}>
|
||||
{agent.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<p className="rounded-lg bg-[var(--color-surface-2)] px-3 py-2 text-[12px] text-[var(--color-text-muted)]">
|
||||
No saved agents. Create agents in Settings → Agents.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isStale && (
|
||||
<div className="flex items-start gap-1.5 rounded-lg bg-[var(--color-status-warning)]/10 px-2.5 py-1.5 text-[11px] text-[var(--color-status-warning)]">
|
||||
<AlertCircle className="mt-0.5 size-3 shrink-0" />
|
||||
<span>The linked agent was deleted. Select another or switch to inline.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{baseAgent && (
|
||||
<button
|
||||
className="flex items-center gap-1.5 text-[11px] text-[var(--color-text-muted)] transition hover:text-[var(--color-text-secondary)]"
|
||||
onClick={handleDetach}
|
||||
title="Detach from saved agent and use inline configuration"
|
||||
type="button"
|
||||
>
|
||||
<Unlink className="size-3" />
|
||||
Detach to inline
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Label field (always shown) */}
|
||||
<InputField
|
||||
label="Label"
|
||||
onChange={(v) => onNodeChange(node.id, { label: v })}
|
||||
placeholder="Display label"
|
||||
value={node.label}
|
||||
/>
|
||||
<InputField
|
||||
label="Agent Name"
|
||||
onChange={(v) => patchConfig({ name: v })}
|
||||
placeholder="Agent name"
|
||||
value={config.name}
|
||||
/>
|
||||
<InputField
|
||||
label="Description"
|
||||
onChange={(v) => patchConfig({ description: v })}
|
||||
placeholder="What this agent does"
|
||||
value={config.description}
|
||||
/>
|
||||
<InputField
|
||||
label="Instructions"
|
||||
multiline
|
||||
onChange={(v) => patchConfig({ instructions: v })}
|
||||
placeholder="System instructions for this agent"
|
||||
value={config.instructions}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<ModelSelect
|
||||
models={availableModels}
|
||||
onChange={(value) => {
|
||||
const m = findModel(value, availableModels);
|
||||
patchConfig({
|
||||
model: value,
|
||||
reasoningEffort: resolveReasoningEffort(m, config.reasoningEffort),
|
||||
});
|
||||
}}
|
||||
value={config.model}
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<ReasoningEffortSelect
|
||||
label="Reasoning"
|
||||
onChange={(value) => patchConfig({ reasoningEffort: value })}
|
||||
supportedEfforts={getSupportedReasoningEfforts(model)}
|
||||
value={config.reasoningEffort}
|
||||
{/* Inline mode — direct editing */}
|
||||
{!isLinked && (
|
||||
<>
|
||||
<InputField
|
||||
label="Agent Name"
|
||||
onChange={(v) => patchConfig({ name: v })}
|
||||
placeholder="Agent name"
|
||||
value={config.name}
|
||||
/>
|
||||
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
|
||||
Controls how much the model reasons before responding. Higher values produce more careful, thorough answers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<InputField
|
||||
label="Description"
|
||||
onChange={(v) => patchConfig({ description: v })}
|
||||
placeholder="What this agent does"
|
||||
value={config.description}
|
||||
/>
|
||||
<InputField
|
||||
label="Instructions"
|
||||
multiline
|
||||
onChange={(v) => patchConfig({ instructions: v })}
|
||||
placeholder="System instructions for this agent"
|
||||
value={config.instructions}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<ModelSelect
|
||||
models={availableModels}
|
||||
onChange={(value) => {
|
||||
const m = findModel(value, availableModels);
|
||||
patchConfig({
|
||||
model: value,
|
||||
reasoningEffort: resolveReasoningEffort(m, config.reasoningEffort),
|
||||
});
|
||||
}}
|
||||
value={config.model}
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<ReasoningEffortSelect
|
||||
label="Reasoning"
|
||||
onChange={(value) => patchConfig({ reasoningEffort: value })}
|
||||
supportedEfforts={getSupportedReasoningEfforts(model)}
|
||||
value={config.reasoningEffort}
|
||||
/>
|
||||
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
|
||||
Controls how much the model reasons before responding. Higher values produce more careful, thorough answers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Linked mode — overridable fields */}
|
||||
{isLinked && baseAgent && (
|
||||
<>
|
||||
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)]/50 px-3 py-2 text-[11px] leading-relaxed text-[var(--color-text-muted)]">
|
||||
Configuration inherited from <strong className="text-[var(--color-text-secondary)]">{baseAgent.name}</strong>. Edit fields below to override per-node.
|
||||
</div>
|
||||
|
||||
<OverridableField
|
||||
baseValue={baseAgent.name}
|
||||
label="Agent Name"
|
||||
onChange={(v) => patchOverride('name', v)}
|
||||
onReset={() => resetOverride('name')}
|
||||
overrideValue={config.overrides?.name}
|
||||
placeholder="Agent name"
|
||||
/>
|
||||
<OverridableField
|
||||
baseValue={baseAgent.description}
|
||||
label="Description"
|
||||
onChange={(v) => patchOverride('description', v)}
|
||||
onReset={() => resetOverride('description')}
|
||||
overrideValue={config.overrides?.description}
|
||||
placeholder="What this agent does"
|
||||
/>
|
||||
<OverridableField
|
||||
baseValue={baseAgent.instructions}
|
||||
label="Instructions"
|
||||
multiline
|
||||
onChange={(v) => patchOverride('instructions', v)}
|
||||
onReset={() => resetOverride('instructions')}
|
||||
overrideValue={config.overrides?.instructions}
|
||||
placeholder="System instructions for this agent"
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">Model</span>
|
||||
{config.overrides?.model !== undefined && (
|
||||
<button
|
||||
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-accent)] transition hover:bg-[var(--color-accent)]/10"
|
||||
onClick={() => resetOverride('model')}
|
||||
title="Reset to saved agent value"
|
||||
type="button"
|
||||
>
|
||||
<RotateCcw className="size-2.5" />
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ModelSelect
|
||||
models={availableModels}
|
||||
onChange={(value) => {
|
||||
const m = findModel(value, availableModels);
|
||||
patchOverride('model', value);
|
||||
patchOverride('reasoningEffort', resolveReasoningEffort(m, config.overrides?.reasoningEffort ?? baseAgent.reasoningEffort));
|
||||
}}
|
||||
value={resolvedModel}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">Reasoning</span>
|
||||
{config.overrides?.reasoningEffort !== undefined && (
|
||||
<button
|
||||
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-accent)] transition hover:bg-[var(--color-accent)]/10"
|
||||
onClick={() => resetOverride('reasoningEffort')}
|
||||
title="Reset to saved agent value"
|
||||
type="button"
|
||||
>
|
||||
<RotateCcw className="size-2.5" />
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ReasoningEffortSelect
|
||||
label=""
|
||||
onChange={(value) => patchOverride('reasoningEffort', value)}
|
||||
supportedEfforts={getSupportedReasoningEfforts(model)}
|
||||
value={config.overrides?.reasoningEffort ?? baseAgent.reasoningEffort}
|
||||
/>
|
||||
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
|
||||
Controls how much the model reasons before responding. Higher values produce more careful, thorough answers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -404,6 +723,7 @@ function EdgeInspector({
|
||||
|
||||
export function WorkflowGraphInspector({
|
||||
availableModels,
|
||||
workspaceAgents,
|
||||
workflow,
|
||||
workflows,
|
||||
selectedNodeId,
|
||||
@@ -464,6 +784,7 @@ export function WorkflowGraphInspector({
|
||||
onNodeChange={onNodeChange}
|
||||
onNodeConfigChange={onNodeConfigChange}
|
||||
onNodeRemove={onNodeRemove}
|
||||
workspaceAgents={workspaceAgents}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Bot, FunctionSquare, GitBranch, Play, Radio, Square } from 'lucide-react';
|
||||
import { Bot, FunctionSquare, GitBranch, Link2, Play, Radio, Square } from 'lucide-react';
|
||||
|
||||
import type { WorkflowNodeKind } from '@shared/domain/workflow';
|
||||
import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
|
||||
|
||||
interface WorkflowNodePaletteProps {
|
||||
onAddNode: (kind: WorkflowNodeKind) => void;
|
||||
onAddWorkspaceAgentNode: (agentId: string) => void;
|
||||
workspaceAgents: ReadonlyArray<WorkspaceAgentDefinition>;
|
||||
disabledKinds?: ReadonlySet<WorkflowNodeKind>;
|
||||
}
|
||||
|
||||
@@ -30,7 +33,7 @@ const paletteGroups: PaletteGroup[] = [
|
||||
{
|
||||
label: 'Agents',
|
||||
items: [
|
||||
{ kind: 'agent', label: 'Agent', icon: Bot, color: 'text-[var(--color-accent)]' },
|
||||
{ kind: 'agent', label: 'New Agent', icon: Bot, color: 'text-[var(--color-accent)]' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -48,7 +51,7 @@ const paletteGroups: PaletteGroup[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export function WorkflowNodePalette({ onAddNode, disabledKinds }: WorkflowNodePaletteProps) {
|
||||
export function WorkflowNodePalette({ onAddNode, onAddWorkspaceAgentNode, workspaceAgents, disabledKinds }: WorkflowNodePaletteProps) {
|
||||
return (
|
||||
<div className="space-y-4 p-3">
|
||||
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
|
||||
@@ -81,6 +84,25 @@ export function WorkflowNodePalette({ onAddNode, disabledKinds }: WorkflowNodePa
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Saved workspace agents in the Agents group */}
|
||||
{group.label === 'Agents' && workspaceAgents.length > 0 && (
|
||||
<>
|
||||
<div className="mx-1 my-1.5 border-t border-[var(--color-border)]" />
|
||||
{workspaceAgents.map((agent) => (
|
||||
<button
|
||||
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[12px] text-[var(--color-text-secondary)] transition-all duration-200 hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]"
|
||||
key={agent.id}
|
||||
onClick={() => onAddWorkspaceAgentNode(agent.id)}
|
||||
title={agent.description || agent.name}
|
||||
type="button"
|
||||
>
|
||||
<Link2 className="size-3.5 text-[var(--color-accent)]" />
|
||||
<span className="truncate">{agent.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user