mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 13:38:43 +02:00
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:
@@ -36,6 +36,11 @@ import {
|
||||
type ReasoningEffort,
|
||||
validatePatternDefinition,
|
||||
} from '@shared/domain/pattern';
|
||||
import {
|
||||
normalizeWorkspaceAgentDefinition,
|
||||
resolvePatternAgents,
|
||||
type WorkspaceAgentDefinition,
|
||||
} from '@shared/domain/workspaceAgent';
|
||||
import {
|
||||
applyDiscoveredMcpServerStatus,
|
||||
listAcceptedDiscoveredMcpServers,
|
||||
@@ -813,6 +818,39 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async saveWorkspaceAgent(agent: WorkspaceAgentDefinition): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const agents = workspace.settings.agents ?? [];
|
||||
const existingIndex = agents.findIndex((current) => current.id === agent.id);
|
||||
const timestamp = nowIso();
|
||||
const candidate = normalizeWorkspaceAgentDefinition({
|
||||
...agent,
|
||||
createdAt: existingIndex >= 0 ? agents[existingIndex].createdAt : timestamp,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
|
||||
if (!candidate.name) {
|
||||
throw new Error('Workspace agent name is required.');
|
||||
}
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
agents[existingIndex] = candidate;
|
||||
} else {
|
||||
agents.push(candidate);
|
||||
}
|
||||
|
||||
workspace.settings.agents = agents;
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async deleteWorkspaceAgent(agentId: string): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
workspace.settings.agents = (workspace.settings.agents ?? []).filter(
|
||||
(agent) => agent.id !== agentId,
|
||||
);
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async createSession(projectId: string, patternId: string): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const project = this.requireProject(workspace, projectId);
|
||||
@@ -955,7 +993,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
const project = this.requireProject(workspace, session.projectId);
|
||||
const pattern = this.requirePattern(workspace, session.patternId);
|
||||
const effectivePattern = this.applyProjectCustomizationToPattern(
|
||||
await this.buildEffectivePattern(pattern, session),
|
||||
await this.buildEffectivePattern(pattern, session, workspace.settings.agents ?? []),
|
||||
project,
|
||||
);
|
||||
const projectInstructions = resolveProjectInstructionsContent(project.customization);
|
||||
@@ -1014,7 +1052,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
const project = this.requireProject(workspace, session.projectId);
|
||||
const pattern = this.requirePattern(workspace, session.patternId);
|
||||
const effectivePattern = this.applyProjectCustomizationToPattern(
|
||||
await this.buildEffectivePattern(pattern, session),
|
||||
await this.buildEffectivePattern(pattern, session, workspace.settings.agents ?? []),
|
||||
project,
|
||||
);
|
||||
const projectInstructions = resolveProjectInstructionsContent(project.customization);
|
||||
@@ -1068,7 +1106,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
const project = this.requireProject(workspace, session.projectId);
|
||||
const pattern = this.requirePattern(workspace, session.patternId);
|
||||
const effectivePattern = this.applyProjectCustomizationToPattern(
|
||||
await this.buildEffectivePattern(pattern, session),
|
||||
await this.buildEffectivePattern(pattern, session, workspace.settings.agents ?? []),
|
||||
project,
|
||||
);
|
||||
const projectInstructions = resolveProjectInstructionsContent(project.customization);
|
||||
@@ -2919,10 +2957,12 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
private async buildEffectivePattern(
|
||||
pattern: PatternDefinition,
|
||||
session: SessionRecord,
|
||||
workspaceAgents: ReadonlyArray<WorkspaceAgentDefinition>,
|
||||
): Promise<PatternDefinition> {
|
||||
const resolvedPattern = resolvePatternAgents(pattern, workspaceAgents);
|
||||
const patternWithSessionConfig = session.sessionModelConfig
|
||||
? applySessionModelConfig(pattern, session)
|
||||
: pattern;
|
||||
? applySessionModelConfig(resolvedPattern, session)
|
||||
: resolvedPattern;
|
||||
const patternWithApprovalSettings = applySessionApprovalSettings(patternWithSessionConfig, session);
|
||||
|
||||
const modelCatalog = await this.loadAvailableModelCatalog();
|
||||
|
||||
@@ -34,6 +34,7 @@ import type {
|
||||
SaveLspProfileInput,
|
||||
SaveMcpServerInput,
|
||||
SavePatternInput,
|
||||
SaveWorkspaceAgentInput,
|
||||
SendSessionMessageInput,
|
||||
SetPatternFavoriteInput,
|
||||
SetProjectAgentProfileEnabledInput,
|
||||
@@ -148,6 +149,12 @@ export function registerIpcHandlers(
|
||||
ipcMain.handle(ipcChannels.deleteLspProfile, (_event, profileId: string) =>
|
||||
service.deleteLspProfile(profileId),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.saveWorkspaceAgent, (_event, input: SaveWorkspaceAgentInput) =>
|
||||
service.saveWorkspaceAgent(input.agent),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.deleteWorkspaceAgent, (_event, agentId: string) =>
|
||||
service.deleteWorkspaceAgent(agentId),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.describeTerminal, () => service.describeTerminal());
|
||||
ipcMain.handle(ipcChannels.createTerminal, () => service.createTerminal());
|
||||
ipcMain.handle(ipcChannels.restartTerminal, () => service.restartTerminal());
|
||||
|
||||
@@ -37,6 +37,8 @@ const api: ElectronApi = {
|
||||
deleteMcpServer: (serverId) => ipcRenderer.invoke(ipcChannels.deleteMcpServer, serverId),
|
||||
saveLspProfile: (input) => ipcRenderer.invoke(ipcChannels.saveLspProfile, input),
|
||||
deleteLspProfile: (profileId) => ipcRenderer.invoke(ipcChannels.deleteLspProfile, profileId),
|
||||
saveWorkspaceAgent: (input) => ipcRenderer.invoke(ipcChannels.saveWorkspaceAgent, input),
|
||||
deleteWorkspaceAgent: (agentId) => ipcRenderer.invoke(ipcChannels.deleteWorkspaceAgent, agentId),
|
||||
describeTerminal: () => ipcRenderer.invoke(ipcChannels.describeTerminal),
|
||||
createTerminal: () => ipcRenderer.invoke(ipcChannels.createTerminal),
|
||||
restartTerminal: () => ipcRenderer.invoke(ipcChannels.restartTerminal),
|
||||
|
||||
@@ -49,6 +49,7 @@ import { isScratchpadProject, SCRATCHPAD_PROJECT_ID } from '@shared/domain/proje
|
||||
import type { ProjectGitFileReference } from '@shared/domain/project';
|
||||
import { applySessionModelConfig } from '@shared/domain/session';
|
||||
import type { AppearanceTheme, LspProfileDefinition, McpServerDefinition } from '@shared/domain/tooling';
|
||||
import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
import type { UpdateStatus } from '@shared/contracts/ipc';
|
||||
import { createId, nowIso } from '@shared/utils/ids';
|
||||
@@ -106,6 +107,20 @@ function createDraftLspProfile(): LspProfileDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
function createDraftWorkspaceAgent(defaultModelId: string): WorkspaceAgentDefinition {
|
||||
const timestamp = nowIso();
|
||||
return {
|
||||
id: createId('agent'),
|
||||
name: '',
|
||||
description: '',
|
||||
instructions: '',
|
||||
model: defaultModelId,
|
||||
reasoningEffort: 'high',
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const api = getElectronApi();
|
||||
const [workspace, setWorkspace] = useState<WorkspaceState>();
|
||||
@@ -728,6 +743,17 @@ export default function App() {
|
||||
onSavePattern={async (pattern) => {
|
||||
await api.savePattern({ pattern });
|
||||
}}
|
||||
onSaveWorkspaceAgent={async (agent) => {
|
||||
await api.saveWorkspaceAgent({ agent });
|
||||
}}
|
||||
onDeleteWorkspaceAgent={async (id) => {
|
||||
await api.deleteWorkspaceAgent(id);
|
||||
}}
|
||||
onNewWorkspaceAgent={() => {
|
||||
const defaultModel = availableModels[0] ?? findModel('gpt-5.4', availableModels) ?? findModel('gpt-5.4');
|
||||
return createDraftWorkspaceAgent(defaultModel?.id ?? 'gpt-5.4');
|
||||
}}
|
||||
workspaceAgents={workspace.settings.agents ?? []}
|
||||
onSetTheme={(theme) => void api.setTheme(theme)}
|
||||
notificationsEnabled={workspace.settings.notificationsEnabled !== false}
|
||||
onSetNotificationsEnabled={(enabled) => void api.setNotificationsEnabled(enabled)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,8 @@ export interface GraphNodeData extends Record<string, unknown> {
|
||||
provider?: ModelProvider;
|
||||
/** Short display name for the agent's model (agent nodes only). */
|
||||
modelLabel?: string;
|
||||
/** True when the agent references a workspace agent definition. */
|
||||
isLinked?: boolean;
|
||||
}
|
||||
|
||||
/* ── View-model projection ─────────────────────────────────── */
|
||||
@@ -81,6 +83,10 @@ export function toCanvasNodes(
|
||||
}
|
||||
}
|
||||
|
||||
const isLinked = node.kind === 'agent' && node.agentId
|
||||
? Boolean(agents.find((a) => a.id === node.agentId)?.workspaceAgentId)
|
||||
: false;
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
type: resolveNodeType(node.kind),
|
||||
@@ -93,6 +99,7 @@ export function toCanvasNodes(
|
||||
readOnly: isSystemNode(node.kind),
|
||||
provider,
|
||||
modelLabel,
|
||||
isLinked,
|
||||
},
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
|
||||
@@ -26,6 +26,8 @@ export const ipcChannels = {
|
||||
deleteMcpServer: 'tooling:mcp:delete',
|
||||
saveLspProfile: 'tooling:lsp:save',
|
||||
deleteLspProfile: 'tooling:lsp:delete',
|
||||
saveWorkspaceAgent: 'workspace-agents:save',
|
||||
deleteWorkspaceAgent: 'workspace-agents:delete',
|
||||
describeTerminal: 'terminal:describe',
|
||||
createTerminal: 'terminal:create',
|
||||
restartTerminal: 'terminal:restart',
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
import type { ChatMessageAttachment } from '@shared/domain/attachment';
|
||||
import type { ProjectPromptInvocation } from '@shared/domain/projectCustomization';
|
||||
import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
|
||||
|
||||
export interface CreateSessionInput {
|
||||
projectId: string;
|
||||
@@ -118,6 +119,10 @@ export interface SaveLspProfileInput {
|
||||
profile: LspProfileDefinition;
|
||||
}
|
||||
|
||||
export interface SaveWorkspaceAgentInput {
|
||||
agent: WorkspaceAgentDefinition;
|
||||
}
|
||||
|
||||
export type DiscoveredToolingResolution = 'accept' | 'dismiss';
|
||||
|
||||
export interface RescanProjectConfigsInput {
|
||||
@@ -273,6 +278,8 @@ export interface ElectronApi {
|
||||
deleteMcpServer(serverId: string): Promise<WorkspaceState>;
|
||||
saveLspProfile(input: SaveLspProfileInput): Promise<WorkspaceState>;
|
||||
deleteLspProfile(profileId: string): Promise<WorkspaceState>;
|
||||
saveWorkspaceAgent(input: SaveWorkspaceAgentInput): Promise<WorkspaceState>;
|
||||
deleteWorkspaceAgent(agentId: string): Promise<WorkspaceState>;
|
||||
updateSessionTooling(input: UpdateSessionToolingInput): Promise<WorkspaceState>;
|
||||
updateSessionApprovalSettings(input: UpdateSessionApprovalSettingsInput): Promise<WorkspaceState>;
|
||||
createSession(input: CreateSessionInput): Promise<WorkspaceState>;
|
||||
|
||||
@@ -33,6 +33,7 @@ export const reasoningEffortOptions: ReadonlyArray<{ value: ReasoningEffort; lab
|
||||
];
|
||||
|
||||
import type { PatternAgentCopilotConfig } from '@shared/contracts/sidecar';
|
||||
import type { PatternAgentOverrides } from '@shared/domain/workspaceAgent';
|
||||
|
||||
export interface PatternAgentDefinition {
|
||||
id: string;
|
||||
@@ -42,6 +43,10 @@ export interface PatternAgentDefinition {
|
||||
model: string;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
copilot?: PatternAgentCopilotConfig;
|
||||
/** When set, this agent references a workspace agent instead of being fully inline. */
|
||||
workspaceAgentId?: string;
|
||||
/** Per-pattern overrides applied on top of the workspace agent base. */
|
||||
overrides?: PatternAgentOverrides;
|
||||
}
|
||||
|
||||
export interface PatternGraphPosition {
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
type ProjectDiscoveredTooling,
|
||||
} from '@shared/domain/discoveredTooling';
|
||||
import { nowIso } from '@shared/utils/ids';
|
||||
import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
|
||||
import { normalizeWorkspaceAgentDefinition } from '@shared/domain/workspaceAgent';
|
||||
|
||||
export type McpServerTransport = 'local' | 'http' | 'sse';
|
||||
|
||||
@@ -63,6 +65,7 @@ export interface WorkspaceSettings {
|
||||
theme: AppearanceTheme;
|
||||
tooling: WorkspaceToolingSettings;
|
||||
discoveredUserTooling: DiscoveredToolingState;
|
||||
agents?: WorkspaceAgentDefinition[];
|
||||
terminalHeight?: number;
|
||||
notificationsEnabled?: boolean;
|
||||
minimizeToTray?: boolean;
|
||||
@@ -207,6 +210,7 @@ export function normalizeWorkspaceSettings(settings?: Partial<WorkspaceSettings>
|
||||
lspProfiles: (settings?.tooling?.lspProfiles ?? []).map(normalizeLspProfileDefinition),
|
||||
},
|
||||
discoveredUserTooling: normalizeDiscoveredToolingState(settings?.discoveredUserTooling),
|
||||
agents: (settings?.agents ?? []).map(normalizeWorkspaceAgentDefinition),
|
||||
...(terminalHeight !== undefined ? { terminalHeight } : {}),
|
||||
...(settings?.notificationsEnabled !== undefined ? { notificationsEnabled: settings.notificationsEnabled } : {}),
|
||||
...(settings?.minimizeToTray !== undefined ? { minimizeToTray: settings.minimizeToTray } : {}),
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { PatternAgentCopilotConfig } from '@shared/contracts/sidecar';
|
||||
import type { PatternAgentDefinition, PatternDefinition, ReasoningEffort } from '@shared/domain/pattern';
|
||||
|
||||
export interface WorkspaceAgentDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
model: string;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
copilot?: PatternAgentCopilotConfig;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PatternAgentOverrides {
|
||||
name?: string;
|
||||
description?: string;
|
||||
instructions?: string;
|
||||
model?: string;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
}
|
||||
|
||||
export interface WorkspaceAgentUsage {
|
||||
patternId: string;
|
||||
patternName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a single pattern agent by merging its workspace agent base with
|
||||
* per-pattern overrides. Returns the agent unchanged if it is inline.
|
||||
*/
|
||||
export function resolvePatternAgent(
|
||||
agent: PatternAgentDefinition,
|
||||
workspaceAgents: ReadonlyArray<WorkspaceAgentDefinition>,
|
||||
): PatternAgentDefinition {
|
||||
if (!agent.workspaceAgentId) {
|
||||
return agent;
|
||||
}
|
||||
|
||||
const base = workspaceAgents.find((wa) => wa.id === agent.workspaceAgentId);
|
||||
if (!base) {
|
||||
return agent;
|
||||
}
|
||||
|
||||
const overrides = agent.overrides ?? {};
|
||||
return {
|
||||
id: agent.id,
|
||||
name: overrides.name ?? base.name,
|
||||
description: overrides.description ?? base.description,
|
||||
instructions: overrides.instructions ?? base.instructions,
|
||||
model: overrides.model ?? base.model,
|
||||
reasoningEffort: overrides.reasoningEffort ?? base.reasoningEffort,
|
||||
copilot: base.copilot,
|
||||
workspaceAgentId: agent.workspaceAgentId,
|
||||
overrides: agent.overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all agents in a pattern, producing a new pattern whose agents
|
||||
* have workspace-agent references merged with their base definitions.
|
||||
*/
|
||||
export function resolvePatternAgents(
|
||||
pattern: PatternDefinition,
|
||||
workspaceAgents: ReadonlyArray<WorkspaceAgentDefinition>,
|
||||
): PatternDefinition {
|
||||
return {
|
||||
...pattern,
|
||||
agents: pattern.agents.map((agent) => resolvePatternAgent(agent, workspaceAgents)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns every pattern that references the given workspace agent.
|
||||
*/
|
||||
export function findWorkspaceAgentUsages(
|
||||
agentId: string,
|
||||
patterns: ReadonlyArray<PatternDefinition>,
|
||||
): WorkspaceAgentUsage[] {
|
||||
const usages: WorkspaceAgentUsage[] = [];
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.agents.some((a) => a.workspaceAgentId === agentId)) {
|
||||
usages.push({ patternId: pattern.id, patternName: pattern.name });
|
||||
}
|
||||
}
|
||||
return usages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a workspace agent definition, trimming string fields and
|
||||
* ensuring consistent shape.
|
||||
*/
|
||||
export function normalizeWorkspaceAgentDefinition(
|
||||
agent: WorkspaceAgentDefinition,
|
||||
): WorkspaceAgentDefinition {
|
||||
return {
|
||||
...agent,
|
||||
name: agent.name.trim(),
|
||||
description: agent.description.trim(),
|
||||
instructions: agent.instructions.trim(),
|
||||
model: agent.model.trim(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user