* fix(agents): preserve Claude thinking selections Model switches now resolve thinking against the target model's saved preference instead of resetting to the catalog default. Claude owns its configured-model defaults and legacy Fable alias handling, while persisted permission modes remain unchanged. * chore(claude): tag Fable compatibility shim * fix(app): resolve persisted model aliases * fix(models): preserve aliases for older clients * fix(models): hide compatibility aliases from listings * fix(settings): hide compatibility model aliases * test(claude): cover legacy catalog entry * test(app): expect mock thinking controls in schedules * fix(app): preserve queued thinking preferences * fix(app): reconcile schedule provider snapshots * fix(app): preserve schedule model selections * fix(claude): preserve exact configured model ids * fix(app): preserve committed thinking preferences * fix(app): preserve aliased schedule thinking
325 lines
9.2 KiB
TypeScript
325 lines
9.2 KiB
TypeScript
import { z } from "zod";
|
|
import type { AgentMode } from "./agent-types.js";
|
|
|
|
export type AgentModeColorTier = "safe" | "moderate" | "dangerous" | "planning" | `#${string}`;
|
|
// Open string by design: the client looks icons up in a registry and falls back
|
|
// to a default for unknown values. Daemon downgrades unknown icons for clients
|
|
// that pre-date the open-string contract (see CLIENT_CAPS.customModeIcons).
|
|
export type AgentModeIcon = string;
|
|
|
|
export interface AgentModeVisuals {
|
|
icon: AgentModeIcon;
|
|
colorTier: AgentModeColorTier;
|
|
}
|
|
|
|
export type AgentProviderModeDefinition = Omit<AgentMode, "icon" | "colorTier"> &
|
|
AgentModeVisuals & {
|
|
// Marks the provider's most-permissioned no-prompt mode. Selecting it means tools run without approval; the runtime mechanism is provider-specific.
|
|
isUnattended?: boolean;
|
|
};
|
|
|
|
// TODO: `modes` should not be static. Providers (especially ACP) report their
|
|
// own modes at runtime via session/new. We should fetch modes from the provider
|
|
// as source of truth and enrich with UI metadata (icons, colorTier) on top.
|
|
export interface AgentProviderDefinition {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
enabledByDefault?: boolean;
|
|
defaultModeId: string | null;
|
|
modes: AgentProviderModeDefinition[];
|
|
voice?: {
|
|
enabled: boolean;
|
|
defaultModeId: string;
|
|
defaultModel?: string;
|
|
};
|
|
}
|
|
|
|
const CLAUDE_MODES: AgentProviderModeDefinition[] = [
|
|
{
|
|
id: "plan",
|
|
label: "Plan Mode",
|
|
description: "Analyze the codebase without executing tools or edits",
|
|
icon: "ShieldEllipsis",
|
|
colorTier: "planning",
|
|
},
|
|
{
|
|
id: "default",
|
|
label: "Always Ask",
|
|
description: "Prompts for permission the first time a tool is used",
|
|
icon: "Shield",
|
|
colorTier: "safe",
|
|
},
|
|
{
|
|
id: "acceptEdits",
|
|
label: "Accept File Edits",
|
|
description: "Automatically approves edit-focused tools without prompting",
|
|
icon: "ShieldPlus",
|
|
colorTier: "moderate",
|
|
},
|
|
{
|
|
id: "auto",
|
|
label: "Auto mode",
|
|
description: "Uses a model classifier to review permission prompts automatically",
|
|
icon: "ShieldCheck",
|
|
colorTier: "moderate",
|
|
},
|
|
{
|
|
id: "bypassPermissions",
|
|
label: "Bypass",
|
|
description: "Skip all permission prompts (use with caution)",
|
|
icon: "ShieldOff",
|
|
colorTier: "dangerous",
|
|
isUnattended: true,
|
|
},
|
|
];
|
|
|
|
const CODEX_MODES: AgentProviderModeDefinition[] = [
|
|
{
|
|
id: "auto",
|
|
label: "Default Permissions",
|
|
description: "Edit files and run commands with Codex's default approval flow.",
|
|
icon: "Shield",
|
|
colorTier: "moderate",
|
|
},
|
|
{
|
|
id: "auto-review",
|
|
label: "Auto-review",
|
|
description:
|
|
"Same workspace-write permissions as Default, but eligible `on-request` approvals are routed through the auto-reviewer subagent.",
|
|
icon: "ShieldCheck",
|
|
colorTier: "moderate",
|
|
},
|
|
{
|
|
id: "full-access",
|
|
label: "Full Access",
|
|
description: "Edit files, run commands, and access the network without additional prompts.",
|
|
icon: "ShieldOff",
|
|
colorTier: "dangerous",
|
|
isUnattended: true,
|
|
},
|
|
];
|
|
|
|
const COPILOT_MODES: AgentProviderModeDefinition[] = [
|
|
{
|
|
id: "https://agentclientprotocol.com/protocol/session-modes#agent",
|
|
label: "Agent",
|
|
description: "Default agent mode for conversational interactions",
|
|
icon: "Shield",
|
|
colorTier: "moderate",
|
|
},
|
|
{
|
|
id: "https://agentclientprotocol.com/protocol/session-modes#plan",
|
|
label: "Plan",
|
|
description: "Plan mode for creating and executing multi-step plans",
|
|
icon: "ShieldEllipsis",
|
|
colorTier: "planning",
|
|
},
|
|
{
|
|
id: "allow-all",
|
|
label: "Allow All",
|
|
description: "Automatically approves all Copilot tool, path, and URL requests.",
|
|
icon: "ShieldOff",
|
|
colorTier: "dangerous",
|
|
isUnattended: true,
|
|
},
|
|
];
|
|
|
|
const OPENCODE_MODES: AgentProviderModeDefinition[] = [
|
|
{
|
|
id: "build",
|
|
label: "Build",
|
|
description: "Allows edits and tool execution for implementation work",
|
|
icon: "Shield",
|
|
colorTier: "moderate",
|
|
},
|
|
{
|
|
id: "plan",
|
|
label: "Plan",
|
|
description: "Read-only planning mode that avoids file edits",
|
|
icon: "ShieldEllipsis",
|
|
colorTier: "planning",
|
|
},
|
|
];
|
|
|
|
export const OMP_MODES: AgentProviderModeDefinition[] = [
|
|
{
|
|
id: "full",
|
|
label: "Full Access",
|
|
description: "Launches OMP with yolo approval mode so tools run without prompts.",
|
|
icon: "ShieldOff",
|
|
colorTier: "dangerous",
|
|
isUnattended: true,
|
|
},
|
|
{
|
|
id: "write",
|
|
label: "Write Approval",
|
|
description: "Launches OMP with write approval mode — reads are free, writes require approval.",
|
|
icon: "ShieldAlert",
|
|
colorTier: "moderate",
|
|
},
|
|
{
|
|
id: "ask",
|
|
label: "Always Ask",
|
|
description: "Launches OMP with always-ask approval mode for write and exec tools.",
|
|
icon: "ShieldCheck",
|
|
colorTier: "safe",
|
|
},
|
|
];
|
|
|
|
const MOCK_LOAD_TEST_MODES: AgentProviderModeDefinition[] = [
|
|
{
|
|
id: "load-test",
|
|
label: "Load Test",
|
|
description: "Streams repeated markdown, reasoning, and tool calls for app stress testing",
|
|
icon: "ShieldOff",
|
|
colorTier: "dangerous",
|
|
},
|
|
{
|
|
id: "approval-test",
|
|
label: "Approval Test",
|
|
description: "Alternate development-only permission mode for preference tests",
|
|
icon: "ShieldCheck",
|
|
colorTier: "safe",
|
|
},
|
|
];
|
|
|
|
const MOCK_SLOW_MODES: AgentProviderModeDefinition[] = [
|
|
{
|
|
id: "default",
|
|
label: "Default",
|
|
description: "Dev-only mode for the mock slow provider",
|
|
icon: "ShieldOff",
|
|
colorTier: "dangerous",
|
|
},
|
|
];
|
|
|
|
export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
|
{
|
|
id: "claude",
|
|
label: "Claude",
|
|
description: "Anthropic's multi-tool assistant with MCP support, streaming, and deep reasoning",
|
|
defaultModeId: "auto",
|
|
modes: CLAUDE_MODES,
|
|
voice: {
|
|
enabled: true,
|
|
defaultModeId: "default",
|
|
defaultModel: "haiku",
|
|
},
|
|
},
|
|
{
|
|
id: "codex",
|
|
label: "Codex",
|
|
description: "OpenAI's Codex workspace agent with sandbox controls and optional network access",
|
|
defaultModeId: "auto-review",
|
|
modes: CODEX_MODES,
|
|
voice: {
|
|
enabled: true,
|
|
defaultModeId: "auto",
|
|
defaultModel: "gpt-5.4-mini",
|
|
},
|
|
},
|
|
{
|
|
id: "copilot",
|
|
label: "Copilot",
|
|
description: "GitHub Copilot via Agent Client Protocol with dynamic modes and session support",
|
|
defaultModeId: "https://agentclientprotocol.com/protocol/session-modes#agent",
|
|
modes: COPILOT_MODES,
|
|
},
|
|
{
|
|
id: "opencode",
|
|
label: "OpenCode",
|
|
description: "Open-source coding assistant with multi-provider model support",
|
|
// No static default: OpenCode users can rename or delete any agent,
|
|
// including "build". Leaving this unset means the daemon and OpenCode
|
|
// itself decide (see normalizeOpenCodeModeId in opencode-agent.ts).
|
|
defaultModeId: null,
|
|
modes: OPENCODE_MODES,
|
|
voice: {
|
|
enabled: true,
|
|
defaultModeId: "build",
|
|
},
|
|
},
|
|
{
|
|
id: "pi",
|
|
label: "Pi",
|
|
description: "Minimal terminal-based coding agent with multi-provider LLM support",
|
|
defaultModeId: null,
|
|
modes: [],
|
|
},
|
|
{
|
|
id: "omp",
|
|
label: "Oh My Pi",
|
|
description: "Multi-provider coding agent with native approvals, host tools, and subagents",
|
|
enabledByDefault: false,
|
|
defaultModeId: "full",
|
|
modes: OMP_MODES,
|
|
},
|
|
];
|
|
|
|
export const DEV_AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
|
{
|
|
id: "mock",
|
|
label: "Mock Load Test",
|
|
description:
|
|
"Development-only provider that emits synthetic agent traffic for performance tests",
|
|
defaultModeId: "load-test",
|
|
modes: MOCK_LOAD_TEST_MODES,
|
|
},
|
|
{
|
|
id: "mock-slow",
|
|
label: "Mock Slow Provider",
|
|
description: "Dev-only: hangs during model discovery to test loading and timeout UI",
|
|
defaultModeId: "default",
|
|
modes: MOCK_SLOW_MODES,
|
|
},
|
|
];
|
|
|
|
export function getAgentProviderDefinition(
|
|
provider: string,
|
|
definitions: AgentProviderDefinition[] = [
|
|
...AGENT_PROVIDER_DEFINITIONS,
|
|
...DEV_AGENT_PROVIDER_DEFINITIONS,
|
|
],
|
|
): AgentProviderDefinition {
|
|
const definition = definitions.find((entry) => entry.id === provider);
|
|
if (!definition) {
|
|
throw new Error(`Unknown agent provider: ${provider}`);
|
|
}
|
|
return definition;
|
|
}
|
|
|
|
export const BUILTIN_PROVIDER_IDS = AGENT_PROVIDER_DEFINITIONS.map((d) => d.id);
|
|
export const AGENT_PROVIDER_IDS = BUILTIN_PROVIDER_IDS;
|
|
|
|
export const AgentProviderSchema = z.string();
|
|
|
|
export function isValidAgentProvider(
|
|
value: string,
|
|
validIds: Iterable<string> = BUILTIN_PROVIDER_IDS,
|
|
): boolean {
|
|
return Array.isArray(validIds) ? validIds.includes(value) : new Set(validIds).has(value);
|
|
}
|
|
|
|
export function getUnattendedModeId(
|
|
provider: string,
|
|
definitions: AgentProviderDefinition[] = [
|
|
...AGENT_PROVIDER_DEFINITIONS,
|
|
...DEV_AGENT_PROVIDER_DEFINITIONS,
|
|
],
|
|
): string | undefined {
|
|
const definition = definitions.find((entry) => entry.id === provider);
|
|
return definition?.modes.find((mode) => mode.isUnattended)?.id;
|
|
}
|
|
|
|
export function getModeVisuals(
|
|
provider: string,
|
|
modeId: string,
|
|
definitions: AgentProviderDefinition[],
|
|
): AgentModeVisuals | undefined {
|
|
const definition = definitions.find((entry) => entry.id === provider);
|
|
const mode = definition?.modes.find((m) => m.id === modeId);
|
|
if (!mode) return undefined;
|
|
return { icon: mode.icon, colorTier: mode.colorTier };
|
|
}
|