diff --git a/electron/process-detector.ts b/electron/process-detector.ts index 7a0f87c2..e532a832 100644 --- a/electron/process-detector.ts +++ b/electron/process-detector.ts @@ -13,6 +13,9 @@ const CLI_PATTERNS: [RegExp, string][] = [ [/\bkimi\b/, "kimi"], [/\bgemini\b/, "gemini"], [/\bopencode\b/, "opencode"], + [/\baider\b/, "aider"], + [/\bamp\b/, "amp"], + [/\broo\b/, "roocode"], [/\blazygit\b/, "lazygit"], [/\btmux\b/, "tmux"], ]; diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 3af3329d..9ad02433 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -85,7 +85,9 @@ function ShortcutRow({ ); } -const AGENT_TYPES = ["claude", "codex", "kimi", "gemini", "opencode"] as const; +import { AI_AGENTS } from "../terminal/agentRegistry"; + +const AGENT_TYPES = AI_AGENTS.map((a) => a.id) as unknown as readonly TerminalType[]; type ValidateResult = | { ok: true; resolvedPath: string; version: string | null } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index b943f4a4..91f2f602 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -16,16 +16,11 @@ const STATUS_COLOR: Record = { idle: "var(--text-muted)", }; -const TYPE_LABEL: Record = { - shell: "Shell", - claude: "Claude", - codex: "Codex", - kimi: "Kimi", - gemini: "Gemini", - opencode: "OpenCode", - lazygit: "lazygit", - tmux: "Tmux", -}; +import { ALL_TERMINAL_TYPE_IDS, getAgentLabel } from "../terminal/agentRegistry"; + +const TYPE_LABEL: Record = Object.fromEntries( + ALL_TERMINAL_TYPE_IDS.map((id) => [id, getAgentLabel(id)]), +) as Record; const iconBtnClass = "w-5 h-5 flex items-center justify-center rounded hover:bg-[var(--sidebar-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors shrink-0"; diff --git a/src/stores/terminalState.ts b/src/stores/terminalState.ts index a5a7f12e..fd2f3e26 100644 --- a/src/stores/terminalState.ts +++ b/src/stores/terminalState.ts @@ -7,6 +7,9 @@ export const DEFAULT_SPAN: Record kimi: { cols: 1, rows: 1 }, gemini: { cols: 1, rows: 1 }, opencode: { cols: 1, rows: 1 }, + aider: { cols: 1, rows: 1 }, + amp: { cols: 1, rows: 1 }, + roocode: { cols: 1, rows: 1 }, lazygit: { cols: 1, rows: 1 }, tmux: { cols: 1, rows: 1 }, }; diff --git a/src/terminal/TerminalTile.tsx b/src/terminal/TerminalTile.tsx index 8a5b65d8..7e414ea7 100644 --- a/src/terminal/TerminalTile.tsx +++ b/src/terminal/TerminalTile.tsx @@ -41,16 +41,11 @@ interface Props { onSpanChange?: (span: { cols: number; rows: number }) => void; } -const TYPE_CONFIG: Record = { - shell: { color: "#888", label: "Shell" }, - claude: { color: "#f5a623", label: "Claude" }, - codex: { color: "#7928ca", label: "Codex" }, - kimi: { color: "#0070f3", label: "Kimi" }, - gemini: { color: "#4285f4", label: "Gemini" }, - opencode: { color: "#50e3c2", label: "OpenCode" }, - lazygit: { color: "#e84d31", label: "Lazygit" }, - tmux: { color: "#1bb91f", label: "Tmux" }, -}; +import { ALL_TERMINAL_TYPE_IDS, getAgentColor, getAgentLabel } from "./agentRegistry"; + +const TYPE_CONFIG: Record = Object.fromEntries( + ALL_TERMINAL_TYPE_IDS.map((id) => [id, { color: getAgentColor(id), label: getAgentLabel(id) }]), +); async function pollSessionId( ptyId: number, diff --git a/src/terminal/agentRegistry.ts b/src/terminal/agentRegistry.ts new file mode 100644 index 00000000..e9f81646 --- /dev/null +++ b/src/terminal/agentRegistry.ts @@ -0,0 +1,157 @@ +/** + * Central agent registry — the single source of truth for all supported + * AI coding agents and tool types in TermCanvas. + * + * To add a new agent: + * 1. Add an entry to AGENT_REGISTRY below. + * 2. That's it. The rest of the app reads from this registry. + */ + +export interface AgentDefinition { + /** Unique identifier used as the TerminalType discriminator. */ + id: string; + /** Human-readable display name. */ + label: string; + /** Accent color for the terminal tile badge. */ + color: string; + /** CLI command name (used for process detection and launch). */ + command: string; + /** Regex pattern to match the process name in `ps` output. */ + detectPattern: RegExp; + /** + * Whether this agent is an AI coding agent (shown in Settings > Agents). + * Non-agent tools like lazygit/tmux/shell are `false`. + */ + isAgent: boolean; + /** Whether this type supports session resume. */ + supportsResume: boolean; +} + +/** + * Ordered list of all known terminal types. + * The order matters for process detection: first match wins. + * + * NOTE: "shell" is intentionally excluded — it's the default fallback + * and has no CLI detection pattern. + */ +export const AGENT_REGISTRY: readonly AgentDefinition[] = [ + { + id: "claude", + label: "Claude", + color: "#f5a623", + command: "claude", + detectPattern: /\bclaude\b/, + isAgent: true, + supportsResume: true, + }, + { + id: "codex", + label: "Codex", + color: "#7928ca", + command: "codex", + detectPattern: /\bcodex\b/, + isAgent: true, + supportsResume: true, + }, + { + id: "kimi", + label: "Kimi", + color: "#0070f3", + command: "kimi", + detectPattern: /\bkimi\b/, + isAgent: true, + supportsResume: true, + }, + { + id: "gemini", + label: "Gemini", + color: "#4285f4", + command: "gemini", + detectPattern: /\bgemini\b/, + isAgent: true, + supportsResume: true, + }, + { + id: "opencode", + label: "OpenCode", + color: "#50e3c2", + command: "opencode", + detectPattern: /\bopencode\b/, + isAgent: true, + supportsResume: true, + }, + { + id: "aider", + label: "Aider", + color: "#14b8a6", + command: "aider", + detectPattern: /\baider\b/, + isAgent: true, + supportsResume: false, + }, + { + id: "amp", + label: "Amp", + color: "#6366f1", + command: "amp", + detectPattern: /\bamp\b/, + isAgent: true, + supportsResume: false, + }, + { + id: "roocode", + label: "Roo Code", + color: "#ec4899", + command: "roo", + detectPattern: /\broo\b/, + isAgent: true, + supportsResume: false, + }, + // Non-agent tools + { + id: "lazygit", + label: "Lazygit", + color: "#e84d31", + command: "lazygit", + detectPattern: /\blazygit\b/, + isAgent: false, + supportsResume: false, + }, + { + id: "tmux", + label: "Tmux", + color: "#1bb91f", + command: "tmux", + detectPattern: /\btmux\b/, + isAgent: false, + supportsResume: false, + }, +] as const; + +// --- Derived lookups (computed once) --- + +/** Map from agent id to its definition. */ +export const AGENT_BY_ID = new Map( + AGENT_REGISTRY.map((a) => [a.id, a]), +); + +/** Only the AI coding agents (for Settings > Agents tab). */ +export const AI_AGENTS = AGENT_REGISTRY.filter((a) => a.isAgent); + +/** All valid terminal type IDs including "shell". */ +export const ALL_TERMINAL_TYPE_IDS = [ + "shell", + ...AGENT_REGISTRY.map((a) => a.id), +] as const; + +/** Type-safe label lookup including "shell". */ +export function getAgentLabel(id: string): string { + if (id === "shell") return "Shell"; + return AGENT_BY_ID.get(id)?.label ?? id; +} + +/** Type-safe color lookup including "shell". */ +export function getAgentColor(id: string): string { + if (id === "shell") return "#888"; + return AGENT_BY_ID.get(id)?.color ?? "#888"; +} diff --git a/src/terminal/cliConfig.ts b/src/terminal/cliConfig.ts index aa2601bb..723016e2 100644 --- a/src/terminal/cliConfig.ts +++ b/src/terminal/cliConfig.ts @@ -170,6 +170,60 @@ export const TERMINAL_CONFIG: Record = { pasteStrategy: "separate", }, }, + aider: { + type: "aider", + launch: { + shell: "aider", + resumeArgs: () => [], + newArgs: () => [], + }, + composer: { + supportsComposer: true, + allowedStatuses: INTERACTIVE_STATUSES, + inputMode: "bracketed-paste", + supportsImages: false, + pasteKeySequence: () => "", + imageFallback: "error", + pasteDelayMs: 120, + pasteStrategy: "separate", + }, + }, + amp: { + type: "amp", + launch: { + shell: "amp", + resumeArgs: () => [], + newArgs: () => [], + }, + composer: { + supportsComposer: true, + allowedStatuses: INTERACTIVE_STATUSES, + inputMode: "bracketed-paste", + supportsImages: false, + pasteKeySequence: () => "", + imageFallback: "error", + pasteDelayMs: 120, + pasteStrategy: "separate", + }, + }, + roocode: { + type: "roocode", + launch: { + shell: "roo", + resumeArgs: () => [], + newArgs: () => [], + }, + composer: { + supportsComposer: true, + allowedStatuses: INTERACTIVE_STATUSES, + inputMode: "bracketed-paste", + supportsImages: false, + pasteKeySequence: () => "", + imageFallback: "error", + pasteDelayMs: 120, + pasteStrategy: "separate", + }, + }, lazygit: { type: "lazygit", launch: { diff --git a/src/terminal/slashCommands.ts b/src/terminal/slashCommands.ts index bcc348f4..a7fa69ab 100644 --- a/src/terminal/slashCommands.ts +++ b/src/terminal/slashCommands.ts @@ -46,6 +46,9 @@ const COMMANDS_BY_TYPE: Record = { kimi: NO_COMMANDS, gemini: NO_COMMANDS, opencode: NO_COMMANDS, + aider: NO_COMMANDS, + amp: NO_COMMANDS, + roocode: NO_COMMANDS, lazygit: NO_COMMANDS, tmux: NO_COMMANDS, }; diff --git a/src/types/index.ts b/src/types/index.ts index 925b3c24..192feed3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,6 +5,9 @@ export type TerminalType = | "kimi" | "gemini" | "opencode" + | "aider" + | "amp" + | "roocode" | "lazygit" | "tmux";