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 KayaandCopilot committed 2026-04-02 17:09:54 +02:00
1 parent 10316a2871
commit e39fffaf3b
17 files changed
+808 -17

No files matched your search

+2
View File
@@ -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',
+7
View File
@@ -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>;
+5
View File
@@ -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 {
+4
View File
@@ -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 } : {}),
+104
View File
@@ -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(),
};
}