From 0845deda0a974fd31355a69f8f768444cc595fa0 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 10:40:36 -0700 Subject: [PATCH 01/12] Add Amp Code CLI support --- .amp/settings.json | 31 +++++++++ .gitignore | 4 ++ AGENTS.md | 2 +- README.md | 1 + apps/desktop/docs/EXTERNAL_FILES.md | 1 + .../src/lib/trpc/routers/settings/index.ts | 1 + .../lib/agent-setup/agent-wrappers-amp.ts | 11 +++ .../lib/agent-setup/agent-wrappers-common.ts | 1 + .../lib/agent-setup/agent-wrappers.test.ts | 12 ++++ .../main/lib/agent-setup/agent-wrappers.ts | 1 + .../desktop/src/main/lib/agent-setup/index.ts | 2 + apps/docs/content/docs/agent-integration.mdx | 1 + apps/docs/content/docs/mcp.mdx | 30 +++++++-- apps/docs/content/docs/terminal-presets.mdx | 1 + .../router/mcp-overview/mcp-overview.test.ts | 67 +++++++++++++++++++ .../router/mcp-overview/mcp-overview.ts | 38 +++++++++-- .../devices/start-agent-session/shared.ts | 2 +- packages/shared/src/agent-command.test.ts | 10 +++ packages/shared/src/agent-command.ts | 7 ++ .../ui/src/assets/icons/preset-icons/amp.svg | 12 ++++ .../ui/src/assets/icons/preset-icons/index.ts | 3 + 21 files changed, 224 insertions(+), 14 deletions(-) create mode 100644 .amp/settings.json create mode 100644 apps/desktop/src/main/lib/agent-setup/agent-wrappers-amp.ts create mode 100644 packages/ui/src/assets/icons/preset-icons/amp.svg diff --git a/.amp/settings.json b/.amp/settings.json new file mode 100644 index 00000000000..3a47f5f8030 --- /dev/null +++ b/.amp/settings.json @@ -0,0 +1,31 @@ +{ + "amp.mcpServers": { + "superset": { + "url": "https://api.superset.sh/api/agent/mcp" + }, + "expo-mcp": { + "url": "https://mcp.expo.dev/mcp", + "enabled": false + }, + "maestro": { + "command": "maestro", + "args": ["mcp"] + }, + "neon": { + "url": "https://mcp.neon.tech/mcp" + }, + "linear": { + "url": "https://mcp.linear.app/mcp" + }, + "sentry": { + "url": "https://mcp.sentry.dev/mcp" + }, + "posthog": { + "url": "https://mcp.posthog.com/mcp" + }, + "desktop-automation": { + "command": "bun", + "args": ["run", "packages/desktop-mcp/src/bin.ts"] + } + } +} diff --git a/.gitignore b/.gitignore index 6220f7db587..d5301e6e97b 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,7 @@ superset-dev-data/ !.codex/config.toml !.codex/commands !.codex/prompts + +# Amp workspace config (track shared settings; ignore runtime state) +.amp/* +!.amp/settings.json diff --git a/AGENTS.md b/AGENTS.md index aa82ff4921f..4574f4366f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ bun run clean:workspaces # Clean all workspace node_modules 1. **Type safety** - avoid `any` unless necessary 2. **Prefer `gh` CLI** - when performing git operations (PRs, issues, checkout, etc.), prefer the GitHub CLI (`gh`) over raw `git` commands where possible 3. **Shared command source** - keep command definitions in `.agents/commands/` only. `.claude/commands` and `.cursor/commands` should be symlinks to `../.agents/commands`. (`packages/chat` discovers slash commands from `.claude/commands`.) -4. **Workspace MCP config** - keep shared MCP servers in `.mcp.json`; `.cursor/mcp.json` should link to `../.mcp.json`. Codex uses `.codex/config.toml` (run with `CODEX_HOME=.codex codex ...`). OpenCode uses `opencode.json` and should mirror the same MCP set using OpenCode's `remote`/`local` schema. +4. **Workspace MCP config** - keep shared MCP servers in `.mcp.json`; `.cursor/mcp.json` should link to `../.mcp.json`. Codex uses `.codex/config.toml` (run with `CODEX_HOME=.codex codex ...`). OpenCode uses `opencode.json` and Amp uses `.amp/settings.json`; both should mirror the same MCP set using their native schemas (`mcp` for OpenCode, `amp.mcpServers` for Amp). 5. **Mastracode fork workflow** - for Superset's internal `mastracode` fork bundle and release process, follow `docs/mastracode-fork-workflow.md`. diff --git a/README.md b/README.md index 5bf89fa00c4..8bace469071 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Superset works with any CLI-based coding agent, including: | Agent | Status | |:------|:-------| +| [Amp Code](https://ampcode.com/) | Fully supported | | [Claude Code](https://github.com/anthropics/claude-code) | Fully supported | | [OpenAI Codex CLI](https://github.com/openai/codex) | Fully supported | | [Cursor Agent](https://docs.cursor.com/agent) | Fully supported | diff --git a/apps/desktop/docs/EXTERNAL_FILES.md b/apps/desktop/docs/EXTERNAL_FILES.md index 22980e1e6c3..aa14854b286 100644 --- a/apps/desktop/docs/EXTERNAL_FILES.md +++ b/apps/desktop/docs/EXTERNAL_FILES.md @@ -17,6 +17,7 @@ This separation prevents multiple instances from interfering with each other. | File | Purpose | |------|---------| +| `amp` | Wrapper for Amp CLI that preserves Superset terminal context | | `claude` | Wrapper for Claude Code CLI that injects notification hooks | | `codex` | Wrapper for Codex CLI that injects notification hooks | | `droid` | Wrapper for Factory Droid CLI that preserves Superset hook integration | diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index c59032c3772..ded27268cf9 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -145,6 +145,7 @@ function getResolvedAgentPresets() { const DEFAULT_PRESET_AGENTS = [ "claude", + "amp", "codex", "copilot", "mastracode", diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-amp.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-amp.ts new file mode 100644 index 00000000000..0ab3df0902f --- /dev/null +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-amp.ts @@ -0,0 +1,11 @@ +import { buildWrapperScript, createWrapper } from "./agent-wrappers-common"; + +/** + * Creates the Amp wrapper that preserves Superset's terminal environment. + * Amp does not currently expose stable hook support, so this wrapper is a + * pass-through binary shim only. + */ +export function createAmpWrapper(): void { + const script = buildWrapperScript("amp", `exec "$REAL_BIN" "$@"`); + createWrapper("amp", script); +} diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts index 26e5720a8fc..bba6badcfc1 100644 --- a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts @@ -5,6 +5,7 @@ import { BIN_DIR } from "./paths"; export const WRAPPER_MARKER = "# Superset agent-wrapper v1"; export const SUPERSET_MANAGED_BINARIES = [ "claude", + "amp", "codex", "droid", "opencode", diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts index c1e714a131b..8f48951a626 100644 --- a/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts @@ -56,6 +56,7 @@ mock.module("node:os", () => ({ })); const { + createAmpWrapper, buildCodexWrapperExecLine, buildCopilotWrapperExecLine, buildWrapperScript, @@ -220,6 +221,17 @@ describe("agent-wrappers copilot", () => { expect(wrapper).toContain('exec "$REAL_BIN" "$@"'); }); + it("creates amp wrapper passthrough", () => { + createAmpWrapper(); + + const wrapperPath = path.join(TEST_BIN_DIR, "amp"); + const wrapper = readFileSync(wrapperPath, "utf-8"); + + expect(wrapper).toContain("# Superset wrapper for amp"); + expect(wrapper).toContain('REAL_BIN="$(find_real_binary "amp")"'); + expect(wrapper).toContain('exec "$REAL_BIN" "$@"'); + }); + it("creates droid wrapper passthrough", () => { createDroidWrapper(); diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers.ts index aa66274d808..4162078113c 100644 --- a/apps/desktop/src/main/lib/agent-setup/agent-wrappers.ts +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers.ts @@ -1,3 +1,4 @@ +export { createAmpWrapper } from "./agent-wrappers-amp"; export { buildCodexWrapperExecLine, cleanupGlobalOpenCodePlugin, diff --git a/apps/desktop/src/main/lib/agent-setup/index.ts b/apps/desktop/src/main/lib/agent-setup/index.ts index 8476c7bfcd3..a01212a99ad 100644 --- a/apps/desktop/src/main/lib/agent-setup/index.ts +++ b/apps/desktop/src/main/lib/agent-setup/index.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import { cleanupGlobalOpenCodePlugin, + createAmpWrapper, createClaudeSettingsJson, createClaudeWrapper, createCodexHooksJson, @@ -48,6 +49,7 @@ export function setupAgentHooks(): void { cleanupGlobalOpenCodePlugin(); createNotifyScript(); + createAmpWrapper(); createClaudeSettingsJson(); createClaudeWrapper(); createCodexHooksJson(); diff --git a/apps/docs/content/docs/agent-integration.mdx b/apps/docs/content/docs/agent-integration.mdx index 6461bc89b6e..5b8a9fad693 100644 --- a/apps/docs/content/docs/agent-integration.mdx +++ b/apps/docs/content/docs/agent-integration.mdx @@ -11,6 +11,7 @@ Run AI coding agents in isolated workspaces. Each agent works independently with ## Supported Agents +- **Amp** - Amp Code CLI - **Claude Code** - Anthropic's CLI - **Codex** - OpenAI's assistant - **OpenCode** - Open-source alternative diff --git a/apps/docs/content/docs/mcp.mdx b/apps/docs/content/docs/mcp.mdx index 38730a3536f..afa52a67af8 100644 --- a/apps/docs/content/docs/mcp.mdx +++ b/apps/docs/content/docs/mcp.mdx @@ -15,13 +15,18 @@ Superset provides an [MCP (Model Context Protocol)](https://modelcontextprotocol | **Workspaces** | Create, update, switch, delete, list, navigate workspaces | | **Devices** | List devices, projects, and app context | | **Organization** | List members and task statuses | -| **AI Sessions** | Start autonomous AI agent sessions (Claude, Codex, Gemini, OpenCode, Pi, Copilot, Cursor Agent) and subagents | +| **AI Sessions** | Start autonomous AI agent sessions (Amp, Claude, Codex, Gemini, OpenCode, Pi, Copilot, Cursor Agent) and subagents | ## Setup ### CLI Options - + + +```bash title="terminal" +amp mcp add --workspace superset https://api.superset.sh/api/agent/mcp +``` + ```bash title="terminal" claude mcp add superset --transport http https://api.superset.sh/api/agent/mcp @@ -72,7 +77,20 @@ opencode mcp add Alternatively, you can manually configure the MCP server for each client: - + + +Add to `.amp/settings.json` in your project root: + +```json title=".amp/settings.json" +{ + "amp.mcpServers": { + "superset": { + "url": "https://api.superset.sh/api/agent/mcp" + } + } +} +``` + Add a `.mcp.json` to your project root. Claude Code auto-discovers this file and handles OAuth authentication. @@ -239,12 +257,12 @@ API keys grant full access to your organization. Keep them secret and never comm | Tool | Description | |------|-------------| -| `start_agent_session` | Start an autonomous AI agent session for a task in an existing workspace. Requires `taskId`. Supports Claude, Codex, Gemini, OpenCode, Pi, Copilot, Cursor Agent, and Superset Chat (defaults to Claude). When `paneId` is provided, adds a new pane to the tab containing that pane instead of initializing a new tab. | -| `start_agent_session_with_prompt` | Start an autonomous AI agent session in an existing workspace using a direct `prompt` instead of a task. Supports Claude, Codex, Gemini, OpenCode, Pi, Copilot, Cursor Agent, and Superset Chat (defaults to Claude). When `paneId` is provided, adds a new pane to the tab containing that pane instead of initializing a new tab. | +| `start_agent_session` | Start an autonomous AI agent session for a task in an existing workspace. Requires `taskId`. Supports Amp, Claude, Codex, Gemini, OpenCode, Pi, Copilot, Cursor Agent, and Superset Chat (defaults to Claude). When `paneId` is provided, adds a new pane to the tab containing that pane instead of initializing a new tab. | +| `start_agent_session_with_prompt` | Start an autonomous AI agent session in an existing workspace using a direct `prompt` instead of a task. Supports Amp, Claude, Codex, Gemini, OpenCode, Pi, Copilot, Cursor Agent, and Superset Chat (defaults to Claude). When `paneId` is provided, adds a new pane to the tab containing that pane instead of initializing a new tab. | ## Chat Integration -In the built-in chat panel, use the `/mcp` slash command to see your workspace's configured MCP servers — their names, transport type (local/remote), and current state (enabled/disabled/invalid). This reads from your workspace MCP config (`.mastracode/mcp.json` or `.mcp.json`). +In the built-in chat panel, use the `/mcp` slash command to see your workspace's configured MCP servers — their names, transport type (local/remote), and current state (enabled/disabled/invalid). This reads from your workspace MCP config (`.mastracode/mcp.json`, `.amp/settings.json`, or `.mcp.json`). ## Example Usage diff --git a/apps/docs/content/docs/terminal-presets.mdx b/apps/docs/content/docs/terminal-presets.mdx index 811fcf76256..5baecb51971 100644 --- a/apps/docs/content/docs/terminal-presets.mdx +++ b/apps/docs/content/docs/terminal-presets.mdx @@ -43,6 +43,7 @@ Presets are parallel by default. Pre-configured presets for popular AI agents: +- **amp** - `amp` - **claude** - `claude --dangerously-skip-permissions` - **codex** - Full danger mode with high reasoning effort - **gemini** - `gemini --yolo` diff --git a/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.test.ts b/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.test.ts index 0f294920fd2..0f6621cb29f 100644 --- a/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.test.ts +++ b/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.test.ts @@ -114,6 +114,44 @@ describe("getMcpOverview", () => { ]); }); + it("reads servers from .amp/settings.json when present", () => { + const cwd = createTempDirectory(); + mkdirSync(join(cwd, ".amp"), { recursive: true }); + writeFileSync( + join(cwd, ".amp", "settings.json"), + JSON.stringify({ + "amp.mcpServers": { + ampRemote: { + url: "https://amp.example.com/mcp", + }, + ampLocalDisabled: { + command: "bun", + args: ["run", "mcp.ts"], + enabled: false, + }, + }, + }), + "utf-8", + ); + + const result = getMcpOverview(cwd); + expect(result.sourcePath).toBe(join(cwd, ".amp", "settings.json")); + expect(result.servers).toEqual([ + { + name: "ampLocalDisabled", + state: "disabled", + transport: "local", + target: "bun run mcp.ts", + }, + { + name: "ampRemote", + state: "enabled", + transport: "remote", + target: "https://amp.example.com/mcp", + }, + ]); + }); + it("falls back to .mcp.json when .mastracode/mcp.json is invalid", () => { const cwd = createTempDirectory(); mkdirSync(join(cwd, ".mastracode"), { recursive: true }); @@ -142,4 +180,33 @@ describe("getMcpOverview", () => { }, ]); }); + + it("falls back to .mcp.json when .amp/settings.json is invalid", () => { + const cwd = createTempDirectory(); + mkdirSync(join(cwd, ".amp"), { recursive: true }); + writeFileSync(join(cwd, ".amp", "settings.json"), "{ invalid", "utf-8"); + writeFileSync( + join(cwd, ".mcp.json"), + JSON.stringify({ + mcpServers: { + fallbackRemote: { + type: "http", + url: "https://fallback.example.com/mcp", + }, + }, + }), + "utf-8", + ); + + const result = getMcpOverview(cwd); + expect(result.sourcePath).toBe(join(cwd, ".mcp.json")); + expect(result.servers).toEqual([ + { + name: "fallbackRemote", + state: "enabled", + transport: "remote", + target: "https://fallback.example.com/mcp", + }, + ]); + }); }); diff --git a/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.ts b/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.ts index 7e3f99bfc73..5f9a19e7edb 100644 --- a/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.ts +++ b/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.ts @@ -2,12 +2,38 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { z } from "zod"; -const MCP_SETTINGS_FILES = [".mastracode/mcp.json", ".mcp.json"] as const; - const mcpSettingsSchema = z.object({ mcpServers: z.record(z.string(), z.unknown()), }); +const ampMcpSettingsSchema = z.object({ + "amp.mcpServers": z.record(z.string(), z.unknown()), +}); + +const MCP_SETTINGS_FILES = [ + { + relativePath: ".mastracode/mcp.json", + readServers: (parsed: unknown) => { + const result = mcpSettingsSchema.safeParse(parsed); + return result.success ? result.data.mcpServers : null; + }, + }, + { + relativePath: ".amp/settings.json", + readServers: (parsed: unknown) => { + const result = ampMcpSettingsSchema.safeParse(parsed); + return result.success ? result.data["amp.mcpServers"] : null; + }, + }, + { + relativePath: ".mcp.json", + readServers: (parsed: unknown) => { + const result = mcpSettingsSchema.safeParse(parsed); + return result.success ? result.data.mcpServers : null; + }, + }, +] as const; + export type McpServerState = "enabled" | "disabled" | "invalid"; export type McpServerTransport = "remote" | "local" | "unknown"; @@ -29,7 +55,7 @@ function resolveMcpServers(cwd: string): { } { let firstExistingPath: string | null = null; - for (const relativePath of MCP_SETTINGS_FILES) { + for (const { relativePath, readServers } of MCP_SETTINGS_FILES) { const sourcePath = join(cwd, relativePath); if (!existsSync(sourcePath)) { continue; @@ -46,14 +72,14 @@ function resolveMcpServers(cwd: string): { continue; } - const result = mcpSettingsSchema.safeParse(parsed); - if (!result.success) { + const servers = readServers(parsed); + if (!servers) { continue; } return { sourcePath, - servers: result.data.mcpServers, + servers, }; } diff --git a/packages/mcp/src/tools/devices/start-agent-session/shared.ts b/packages/mcp/src/tools/devices/start-agent-session/shared.ts index 58ab03c0465..c9023c82f26 100644 --- a/packages/mcp/src/tools/devices/start-agent-session/shared.ts +++ b/packages/mcp/src/tools/devices/start-agent-session/shared.ts @@ -44,7 +44,7 @@ export const commonInputSchemaShape = { .enum(STARTABLE_AGENT_TYPES) .optional() .describe( - 'AI agent to use: "claude", "codex", "gemini", "opencode", "pi", "copilot", "cursor-agent", or "superset-chat". Defaults to "claude".', + 'AI agent to use: "claude", "amp", "codex", "gemini", "opencode", "pi", "copilot", "cursor-agent", or "superset-chat". Defaults to "claude".', ), }; diff --git a/packages/shared/src/agent-command.test.ts b/packages/shared/src/agent-command.test.ts index 7619707cef0..080ce6e2fab 100644 --- a/packages/shared/src/agent-command.test.ts +++ b/packages/shared/src/agent-command.test.ts @@ -27,6 +27,16 @@ describe("buildAgentPromptCommand", () => { ); }); + it("uses Amp execute mode for prompt launches", () => { + const command = buildAgentPromptCommand({ + prompt: "hello", + randomId: "amp-1234", + agent: "amp", + }); + + expect(command).toStartWith("amp -x \"$(cat <<'SUPERSET_PROMPT_amp1234'"); + }); + it("uses pi interactive mode for prompt launches", () => { const command = buildAgentPromptCommand({ prompt: "hello", diff --git a/packages/shared/src/agent-command.ts b/packages/shared/src/agent-command.ts index a9feb535400..9d7d8e4183e 100644 --- a/packages/shared/src/agent-command.ts +++ b/packages/shared/src/agent-command.ts @@ -5,6 +5,7 @@ import { export const AGENT_TYPES = [ "claude", + "amp", "codex", "gemini", "mastracode", @@ -18,6 +19,7 @@ export type AgentType = (typeof AGENT_TYPES)[number]; export const AGENT_LABELS: Record = { claude: "Claude", + amp: "Amp", codex: "Codex", gemini: "Gemini", mastracode: "Mastracode", @@ -29,6 +31,7 @@ export const AGENT_LABELS: Record = { export const AGENT_PRESET_COMMANDS: Record = { claude: ["claude --dangerously-skip-permissions"], + amp: ["amp"], codex: [ 'codex -c model_reasoning_effort="high" --dangerously-bypass-approvals-and-sandbox -c model_reasoning_summary="detailed" -c model_supports_reasoning_summaries=true', ], @@ -43,6 +46,7 @@ export const AGENT_PRESET_COMMANDS: Record = { export const AGENT_PRESET_DESCRIPTIONS: Record = { claude: "Anthropic's coding agent for reading code, editing files, and running terminal workflows.", + amp: "Amp's coding agent for terminal-first coding, subagents, and task work.", codex: "OpenAI's coding agent for reading, modifying, and running code across tasks.", gemini: @@ -69,6 +73,9 @@ export const AGENT_PROMPT_COMMANDS: Record< claude: { command: AGENT_PRESET_COMMANDS.claude[0] ?? "claude", }, + amp: { + command: "amp -x", + }, codex: { command: `${AGENT_PRESET_COMMANDS.codex[0] ?? "codex"} --`, }, diff --git a/packages/ui/src/assets/icons/preset-icons/amp.svg b/packages/ui/src/assets/icons/preset-icons/amp.svg new file mode 100644 index 00000000000..a16e28a793e --- /dev/null +++ b/packages/ui/src/assets/icons/preset-icons/amp.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/packages/ui/src/assets/icons/preset-icons/index.ts b/packages/ui/src/assets/icons/preset-icons/index.ts index 900210c6b72..635e5913794 100644 --- a/packages/ui/src/assets/icons/preset-icons/index.ts +++ b/packages/ui/src/assets/icons/preset-icons/index.ts @@ -1,3 +1,4 @@ +import ampIcon from "./amp.svg"; import claudeIcon from "./claude.svg"; import codexIcon from "./codex.svg"; import codexWhiteIcon from "./codex-white.svg"; @@ -19,6 +20,7 @@ export interface PresetIconSet { } export const PRESET_ICONS: Record = { + amp: { light: ampIcon, dark: ampIcon }, claude: { light: claudeIcon, dark: claudeIcon }, codex: { light: codexIcon, dark: codexWhiteIcon }, copilot: { light: copilotIcon, dark: copilotWhiteIcon }, @@ -42,6 +44,7 @@ export function getPresetIcon( } export { + ampIcon, claudeIcon, codexIcon, codexWhiteIcon, From 6875bb870510cef40ecdc1c2a383ce3e19bc2b87 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 12:11:33 -0700 Subject: [PATCH 02/12] Refactor agent registry and add custom agent CRUD groundwork --- .../agent-preset-router.utils.test.ts | 72 +++++++++ .../settings/agent-preset-router.utils.ts | 114 ++++++++++++- .../src/lib/trpc/routers/settings/index.ts | 120 ++++++++++++-- .../lib/agent-setup/agent-wrappers-common.ts | 12 +- .../agent-setup/desktop-agent-capabilities.ts | 100 ++++++++++++ .../lib/agent-setup/desktop-agent-setup.ts | 65 ++++++++ .../desktop/src/main/lib/agent-setup/index.ts | 47 +----- .../main/lib/agent-setup/shell-wrappers.ts | 9 +- .../src/shared/utils/agent-settings.test.ts | 93 +++++++++++ .../src/shared/utils/agent-settings.ts | 101 +++++++++++- .../devices/start-agent-session/shared.ts | 18 ++- packages/shared/src/agent-catalog.ts | 20 +-- packages/shared/src/agent-command.ts | 115 +++---------- .../shared/src/builtin-terminal-agents.ts | 152 ++++++++++++++++++ 14 files changed, 859 insertions(+), 179 deletions(-) create mode 100644 apps/desktop/src/main/lib/agent-setup/desktop-agent-capabilities.ts create mode 100644 apps/desktop/src/main/lib/agent-setup/desktop-agent-setup.ts create mode 100644 packages/shared/src/builtin-terminal-agents.ts diff --git a/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.test.ts b/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.test.ts index abcbf78285b..24e8c04f6e0 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.test.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.test.ts @@ -2,8 +2,12 @@ import { describe, expect, test } from "bun:test"; import { getBuiltinAgentDefinition } from "@superset/shared/agent-catalog"; import { TRPCError } from "@trpc/server"; import { + createCustomAgentInputSchema, normalizeAgentPresetPatch, + normalizeCreateCustomAgentInput, + normalizeCustomAgentPatch, updateAgentPresetInputSchema, + updateCustomAgentInputSchema, } from "./agent-preset-router.utils"; describe("updateAgentPresetInputSchema", () => { @@ -76,3 +80,71 @@ describe("normalizeAgentPresetPatch", () => { ).toThrow(TRPCError); }); }); + +describe("custom agent schemas", () => { + test("rejects empty custom-agent patches", () => { + const result = updateCustomAgentInputSchema.safeParse({ + id: "custom:test", + patch: {}, + }); + + expect(result.success).toBe(false); + }); + + test("accepts custom-agent create payloads", () => { + const result = createCustomAgentInputSchema.safeParse({ + label: " Team Agent ", + command: " team-agent ", + promptCommand: " team-agent --prompt ", + taskPromptTemplate: " Task {{slug}} ", + }); + + expect(result.success).toBe(true); + }); +}); + +describe("custom agent normalization", () => { + test("trims custom-agent create input and clears blank optional strings", () => { + const normalized = normalizeCreateCustomAgentInput({ + label: " Team Agent ", + description: " ", + command: " team-agent ", + promptCommand: " team-agent --prompt ", + promptCommandSuffix: " ", + taskPromptTemplate: " Task {{slug}} ", + enabled: false, + }); + + expect(normalized).toEqual({ + label: "Team Agent", + description: undefined, + command: "team-agent", + promptCommand: "team-agent --prompt", + promptCommandSuffix: undefined, + taskPromptTemplate: "Task {{slug}}", + enabled: false, + }); + }); + + test("normalizes custom-agent patches and clears blank optional strings to null", () => { + const normalized = normalizeCustomAgentPatch({ + description: " ", + promptCommandSuffix: " ", + command: " team-agent ", + }); + + expect(normalized).toEqual({ + description: null, + promptCommandSuffix: null, + command: "team-agent", + }); + }); + + test("rejects custom-agent task templates with unknown variables", () => { + expect(() => + normalizeCustomAgentPatch({ + taskPromptTemplate: "Task {{unknown}}", + }), + ).toThrow(TRPCError); + }); +}); diff --git a/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.ts b/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.ts index 6b6e6b68b5d..5a8864a4501 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.ts @@ -1,6 +1,9 @@ import type { AgentDefinition } from "@superset/shared/agent-catalog"; import { TRPCError } from "@trpc/server"; -import type { AgentPresetPatch } from "shared/utils/agent-settings"; +import type { + AgentPresetPatch, + CustomAgentDefinitionPatch, +} from "shared/utils/agent-settings"; import { validateTaskPromptTemplate } from "shared/utils/agent-settings"; import { z } from "zod"; @@ -22,6 +25,25 @@ export const updateAgentPresetInputSchema = z.object({ }), }); +export const createCustomAgentInputSchema = z.object({ + label: z.string(), + description: z.string().nullable().optional(), + command: z.string(), + promptCommand: z.string(), + promptCommandSuffix: z.string().nullable().optional(), + taskPromptTemplate: z.string(), + enabled: z.boolean().optional(), +}); + +export const updateCustomAgentInputSchema = z.object({ + id: z.string().regex(/^custom:/), + patch: createCustomAgentInputSchema + .partial() + .refine((patch) => Object.keys(patch).length > 0, { + message: "Patch must include at least one field", + }), +}); + function toTrimmedRequiredValue(field: string, value: string): string { const trimmed = value.trim(); if (!trimmed) { @@ -95,3 +117,93 @@ export function normalizeAgentPresetPatch({ return normalized; } + +function normalizeOptionalText( + value: string | null | undefined, +): string | null { + const normalized = value?.trim() ?? ""; + return normalized ? normalized : null; +} + +export function normalizeCreateCustomAgentInput( + input: z.infer, +) { + const taskPromptTemplate = toTrimmedRequiredValue( + "Task prompt template", + input.taskPromptTemplate, + ); + const validation = validateTaskPromptTemplate(taskPromptTemplate); + if (!validation.valid) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Unknown task prompt variables: ${validation.unknownVariables.join(", ")}`, + }); + } + + return { + label: toTrimmedRequiredValue("Label", input.label), + description: normalizeOptionalText(input.description) ?? undefined, + command: toTrimmedRequiredValue("Command", input.command), + promptCommand: toTrimmedRequiredValue( + "Prompt command", + input.promptCommand, + ), + promptCommandSuffix: + normalizeOptionalText(input.promptCommandSuffix) ?? undefined, + taskPromptTemplate, + enabled: input.enabled, + } as const; +} + +export function normalizeCustomAgentPatch( + patch: z.infer["patch"], +): CustomAgentDefinitionPatch { + const normalized: CustomAgentDefinitionPatch = {}; + + if (patch.enabled !== undefined) { + normalized.enabled = patch.enabled; + } + if (patch.label !== undefined) { + normalized.label = toTrimmedRequiredValue("Label", patch.label); + } + if (patch.description !== undefined) { + normalized.description = normalizeOptionalText(patch.description); + } + if (patch.command !== undefined) { + normalized.command = toTrimmedRequiredValue("Command", patch.command); + } + if (patch.promptCommand !== undefined) { + normalized.promptCommand = toTrimmedRequiredValue( + "Prompt command", + patch.promptCommand, + ); + } + if (patch.promptCommandSuffix !== undefined) { + normalized.promptCommandSuffix = normalizeOptionalText( + patch.promptCommandSuffix, + ); + } + if (patch.taskPromptTemplate !== undefined) { + const taskPromptTemplate = toTrimmedRequiredValue( + "Task prompt template", + patch.taskPromptTemplate, + ); + const validation = validateTaskPromptTemplate(taskPromptTemplate); + if (!validation.valid) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Unknown task prompt variables: ${validation.unknownVariables.join(", ")}`, + }); + } + normalized.taskPromptTemplate = taskPromptTemplate; + } + + if (Object.keys(normalized).length === 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Patch must include at least one supported field", + }); + } + + return normalized; +} diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index ded27268cf9..fd4f9a65b5b 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -13,6 +13,7 @@ import { import { AGENT_PRESET_COMMANDS, AGENT_PRESET_DESCRIPTIONS, + DEFAULT_TERMINAL_PRESET_AGENT_TYPES, } from "@superset/shared/agent-command"; import { TRPCError } from "@trpc/server"; import { app } from "electron"; @@ -37,17 +38,26 @@ import { } from "shared/ringtones"; import { type AgentDefinitionId, + applyCustomAgentDefinitionPatch, createOverrideEnvelopeWithPatch, + deleteCustomAgentDefinition, getAgentDefinitionById, + getCustomAgentDefinitionById, readAgentPresetOverrides, + resetAllAgentPresetOverrides, resetAgentPresetOverride, resolveAgentConfigs, + upsertCustomAgentDefinition, } from "shared/utils/agent-settings"; import { z } from "zod"; import { publicProcedure, router } from "../.."; import { getGitAuthorName, getGitHubUsername } from "../workspaces/utils/git"; import { + createCustomAgentInputSchema, normalizeAgentPresetPatch, + normalizeCreateCustomAgentInput, + normalizeCustomAgentPatch, + updateCustomAgentInputSchema, updateAgentPresetInputSchema, } from "./agent-preset-router.utils"; import { @@ -136,6 +146,20 @@ function saveAgentPresetOverrides(overrides: AgentPresetOverrideEnvelope) { .run(); } +function saveAgentCustomDefinitions(definitions: AgentCustomDefinition[]) { + localDb + .insert(settings) + .values({ + id: 1, + agentCustomDefinitions: definitions, + }) + .onConflictDoUpdate({ + target: settings.id, + set: { agentCustomDefinitions: definitions }, + }) + .run(); +} + function getResolvedAgentPresets() { return resolveAgentConfigs({ customDefinitions: readRawAgentCustomDefinitions(), @@ -143,25 +167,13 @@ function getResolvedAgentPresets() { }); } -const DEFAULT_PRESET_AGENTS = [ - "claude", - "amp", - "codex", - "copilot", - "mastracode", - "opencode", - "pi", - "gemini", -] as const; - -const DEFAULT_PRESETS: Omit[] = DEFAULT_PRESET_AGENTS.map( - (name) => ({ +const DEFAULT_PRESETS: Omit[] = + DEFAULT_TERMINAL_PRESET_AGENT_TYPES.map((name) => ({ name, description: AGENT_PRESET_DESCRIPTIONS[name], cwd: "", commands: AGENT_PRESET_COMMANDS[name], - }), -); + })); function initializeDefaultPresets() { const row = getSettings(); @@ -205,6 +217,82 @@ export const createSettingsRouter = () => { return getNormalizedTerminalPresets(); }), getAgentPresets: publicProcedure.query(() => getResolvedAgentPresets()), + createCustomAgent: publicProcedure + .input(createCustomAgentInputSchema) + .mutation(({ input }) => { + const definition = { + id: `custom:${crypto.randomUUID()}` as const, + kind: "terminal" as const, + ...normalizeCreateCustomAgentInput(input), + }; + const nextDefinitions = upsertCustomAgentDefinition({ + currentDefinitions: readRawAgentCustomDefinitions(), + definition, + }); + + saveAgentCustomDefinitions(nextDefinitions); + + return getResolvedAgentPresets().find( + (preset) => preset.id === definition.id, + ); + }), + updateCustomAgent: publicProcedure + .input(updateCustomAgentInputSchema) + .mutation(({ input }) => { + const definition = getCustomAgentDefinitionById({ + customDefinitions: readRawAgentCustomDefinitions(), + id: input.id as `custom:${string}`, + }); + if (!definition) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Custom agent ${input.id} not found`, + }); + } + + const nextDefinitions = upsertCustomAgentDefinition({ + currentDefinitions: readRawAgentCustomDefinitions(), + definition: applyCustomAgentDefinitionPatch({ + definition, + patch: normalizeCustomAgentPatch(input.patch), + }), + }); + + saveAgentCustomDefinitions(nextDefinitions); + + return getResolvedAgentPresets().find( + (preset) => preset.id === input.id, + ); + }), + deleteCustomAgent: publicProcedure + .input(z.object({ id: z.string().regex(/^custom:/) })) + .mutation(({ input }) => { + const existingDefinition = getCustomAgentDefinitionById({ + customDefinitions: readRawAgentCustomDefinitions(), + id: input.id as `custom:${string}`, + }); + if (!existingDefinition) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Custom agent ${input.id} not found`, + }); + } + + saveAgentCustomDefinitions( + deleteCustomAgentDefinition({ + currentDefinitions: readRawAgentCustomDefinitions(), + id: input.id as `custom:${string}`, + }), + ); + saveAgentPresetOverrides( + resetAgentPresetOverride({ + currentOverrides: readRawAgentPresetOverrides(), + id: input.id as AgentDefinitionId, + }), + ); + + return { success: true }; + }), updateAgentPreset: publicProcedure .input(updateAgentPresetInputSchema) .mutation(({ input }) => { @@ -247,7 +335,7 @@ export const createSettingsRouter = () => { return { success: true }; }), resetAllAgentPresets: publicProcedure.mutation(() => { - saveAgentPresetOverrides({ version: 1, presets: [] }); + saveAgentPresetOverrides(resetAllAgentPresetOverrides()); return { success: true }; }), createTerminalPreset: publicProcedure diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts index bba6badcfc1..55a2f14ab8a 100644 --- a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts @@ -1,18 +1,10 @@ import fs from "node:fs"; import path from "node:path"; +import { SUPERSET_MANAGED_BINARIES } from "./desktop-agent-capabilities"; import { BIN_DIR } from "./paths"; export const WRAPPER_MARKER = "# Superset agent-wrapper v1"; -export const SUPERSET_MANAGED_BINARIES = [ - "claude", - "amp", - "codex", - "droid", - "opencode", - "gemini", - "copilot", - "mastracode", -] as const; +export { SUPERSET_MANAGED_BINARIES }; const SUPERSET_MANAGED_HOOK_PATH_PATTERN = /\/\.superset(?:-[^/'"\s\\]+)?\//; diff --git a/apps/desktop/src/main/lib/agent-setup/desktop-agent-capabilities.ts b/apps/desktop/src/main/lib/agent-setup/desktop-agent-capabilities.ts new file mode 100644 index 00000000000..2f0c60a93de --- /dev/null +++ b/apps/desktop/src/main/lib/agent-setup/desktop-agent-capabilities.ts @@ -0,0 +1,100 @@ +import type { AgentType } from "@superset/shared/agent-command"; + +export type SupersetManagedBinary = AgentType | "droid"; + +export const DESKTOP_AGENT_SETUP_ACTIONS = [ + "notify-script", + "cleanup-global-opencode-plugin", + "amp-wrapper", + "claude-settings-json", + "claude-wrapper", + "codex-hooks-json", + "codex-wrapper", + "droid-wrapper", + "droid-settings-json", + "opencode-plugin", + "opencode-wrapper", + "cursor-hook-script", + "cursor-agent-wrapper", + "cursor-hooks-json", + "gemini-hook-script", + "gemini-wrapper", + "gemini-settings-json", + "mastra-wrapper", + "mastra-hooks-json", + "copilot-hook-script", + "copilot-wrapper", +] as const; + +export type DesktopAgentSetupAction = + (typeof DESKTOP_AGENT_SETUP_ACTIONS)[number]; + +interface DesktopAgentSetupTarget { + id: AgentType | "droid"; + setupActions: readonly DesktopAgentSetupAction[]; + managedBinary?: boolean; +} + +export const DESKTOP_AGENT_SETUP_BOOTSTRAP_ACTIONS = [ + "cleanup-global-opencode-plugin", + "notify-script", +] as const satisfies readonly DesktopAgentSetupAction[]; + +export const DESKTOP_AGENT_SETUP_TARGETS = [ + { + id: "amp", + setupActions: ["amp-wrapper"], + managedBinary: true, + }, + { + id: "claude", + setupActions: ["claude-settings-json", "claude-wrapper"], + managedBinary: true, + }, + { + id: "codex", + setupActions: ["codex-hooks-json", "codex-wrapper"], + managedBinary: true, + }, + { + id: "droid", + setupActions: ["droid-wrapper", "droid-settings-json"], + managedBinary: true, + }, + { + id: "opencode", + setupActions: ["opencode-plugin", "opencode-wrapper"], + managedBinary: true, + }, + { + id: "cursor-agent", + setupActions: [ + "cursor-hook-script", + "cursor-agent-wrapper", + "cursor-hooks-json", + ], + }, + { + id: "gemini", + setupActions: [ + "gemini-hook-script", + "gemini-wrapper", + "gemini-settings-json", + ], + managedBinary: true, + }, + { + id: "mastracode", + setupActions: ["mastra-wrapper", "mastra-hooks-json"], + managedBinary: true, + }, + { + id: "copilot", + setupActions: ["copilot-hook-script", "copilot-wrapper"], + managedBinary: true, + }, +] as const satisfies readonly DesktopAgentSetupTarget[]; + +export const SUPERSET_MANAGED_BINARIES = DESKTOP_AGENT_SETUP_TARGETS.filter( + (target) => "managedBinary" in target && target.managedBinary, +).map((target) => target.id) satisfies SupersetManagedBinary[]; diff --git a/apps/desktop/src/main/lib/agent-setup/desktop-agent-setup.ts b/apps/desktop/src/main/lib/agent-setup/desktop-agent-setup.ts new file mode 100644 index 00000000000..e25e4baa66e --- /dev/null +++ b/apps/desktop/src/main/lib/agent-setup/desktop-agent-setup.ts @@ -0,0 +1,65 @@ +import { + cleanupGlobalOpenCodePlugin, + createAmpWrapper, + createClaudeSettingsJson, + createClaudeWrapper, + createCodexHooksJson, + createCodexWrapper, + createCopilotHookScript, + createCopilotWrapper, + createCursorAgentWrapper, + createCursorHookScript, + createCursorHooksJson, + createDroidSettingsJson, + createDroidWrapper, + createGeminiHookScript, + createGeminiSettingsJson, + createGeminiWrapper, + createMastraHooksJson, + createMastraWrapper, + createOpenCodePlugin, + createOpenCodeWrapper, +} from "./agent-wrappers"; +import { + DESKTOP_AGENT_SETUP_BOOTSTRAP_ACTIONS, + DESKTOP_AGENT_SETUP_TARGETS, + type DesktopAgentSetupAction, +} from "./desktop-agent-capabilities"; +import { createNotifyScript } from "./notify-hook"; + +const DESKTOP_AGENT_SETUP_RUNNERS: Record void> = + { + "notify-script": createNotifyScript, + "cleanup-global-opencode-plugin": cleanupGlobalOpenCodePlugin, + "amp-wrapper": createAmpWrapper, + "claude-settings-json": createClaudeSettingsJson, + "claude-wrapper": createClaudeWrapper, + "codex-hooks-json": createCodexHooksJson, + "codex-wrapper": createCodexWrapper, + "droid-wrapper": createDroidWrapper, + "droid-settings-json": createDroidSettingsJson, + "opencode-plugin": createOpenCodePlugin, + "opencode-wrapper": createOpenCodeWrapper, + "cursor-hook-script": createCursorHookScript, + "cursor-agent-wrapper": createCursorAgentWrapper, + "cursor-hooks-json": createCursorHooksJson, + "gemini-hook-script": createGeminiHookScript, + "gemini-wrapper": createGeminiWrapper, + "gemini-settings-json": createGeminiSettingsJson, + "mastra-wrapper": createMastraWrapper, + "mastra-hooks-json": createMastraHooksJson, + "copilot-hook-script": createCopilotHookScript, + "copilot-wrapper": createCopilotWrapper, + }; + +export function setupDesktopAgentCapabilities(): void { + for (const action of DESKTOP_AGENT_SETUP_BOOTSTRAP_ACTIONS) { + DESKTOP_AGENT_SETUP_RUNNERS[action](); + } + + for (const target of DESKTOP_AGENT_SETUP_TARGETS) { + for (const action of target.setupActions) { + DESKTOP_AGENT_SETUP_RUNNERS[action](); + } + } +} diff --git a/apps/desktop/src/main/lib/agent-setup/index.ts b/apps/desktop/src/main/lib/agent-setup/index.ts index a01212a99ad..3b8ddd33324 100644 --- a/apps/desktop/src/main/lib/agent-setup/index.ts +++ b/apps/desktop/src/main/lib/agent-setup/index.ts @@ -1,27 +1,5 @@ import fs from "node:fs"; -import { - cleanupGlobalOpenCodePlugin, - createAmpWrapper, - createClaudeSettingsJson, - createClaudeWrapper, - createCodexHooksJson, - createCodexWrapper, - createCopilotHookScript, - createCopilotWrapper, - createCursorAgentWrapper, - createCursorHookScript, - createCursorHooksJson, - createDroidSettingsJson, - createDroidWrapper, - createGeminiHookScript, - createGeminiSettingsJson, - createGeminiWrapper, - createMastraHooksJson, - createMastraWrapper, - createOpenCodePlugin, - createOpenCodeWrapper, -} from "./agent-wrappers"; -import { createNotifyScript } from "./notify-hook"; +import { setupDesktopAgentCapabilities } from "./desktop-agent-setup"; import { BASH_DIR, BIN_DIR, @@ -46,28 +24,7 @@ export function setupAgentHooks(): void { fs.mkdirSync(BASH_DIR, { recursive: true }); fs.mkdirSync(OPENCODE_PLUGIN_DIR, { recursive: true }); - cleanupGlobalOpenCodePlugin(); - - createNotifyScript(); - createAmpWrapper(); - createClaudeSettingsJson(); - createClaudeWrapper(); - createCodexHooksJson(); - createCodexWrapper(); - createDroidWrapper(); - createDroidSettingsJson(); - createOpenCodePlugin(); - createOpenCodeWrapper(); - createCursorHookScript(); - createCursorAgentWrapper(); - createCursorHooksJson(); - createGeminiHookScript(); - createGeminiWrapper(); - createGeminiSettingsJson(); - createMastraWrapper(); - createMastraHooksJson(); - createCopilotHookScript(); - createCopilotWrapper(); + setupDesktopAgentCapabilities(); createZshWrapper(); createBashWrapper(); diff --git a/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts b/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts index 30a73a4f631..32e489b0ee3 100644 --- a/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts +++ b/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts @@ -1,7 +1,10 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { SUPERSET_MANAGED_BINARIES } from "./agent-wrappers-common"; +import { + type SupersetManagedBinary, + SUPERSET_MANAGED_BINARIES, +} from "./desktop-agent-capabilities"; import { BASH_DIR, BIN_DIR, ZSH_DIR } from "./paths"; export interface ShellWrapperPaths { @@ -85,7 +88,7 @@ function buildManagedCommandPrelude(shellName: string, binDir: string): string { if (shellName === "fish") { const escapedBinDir = escapeFishDoubleQuoted(binDir); return SUPERSET_MANAGED_BINARIES.map( - (name) => + (name: SupersetManagedBinary) => `functions -q ${name}; and functions -e ${name} function ${name} set -l _superset_wrapper "${escapedBinDir}/${name}" @@ -99,7 +102,7 @@ end`, } return SUPERSET_MANAGED_BINARIES.map( - (name) => + (name: SupersetManagedBinary) => `unalias ${name} 2>/dev/null || true ${name}() { _superset_wrapper=${quoteShellLiteral(`${binDir}/${name}`)} diff --git a/apps/desktop/src/shared/utils/agent-settings.test.ts b/apps/desktop/src/shared/utils/agent-settings.test.ts index 5b5c671c8f2..a1202f77b67 100644 --- a/apps/desktop/src/shared/utils/agent-settings.test.ts +++ b/apps/desktop/src/shared/utils/agent-settings.test.ts @@ -1,8 +1,11 @@ import { describe, expect, test } from "bun:test"; import { getBuiltinAgentDefinition } from "@superset/shared/agent-catalog"; import { + applyCustomAgentDefinitionPatch, createOverrideEnvelopeWithPatch, + deleteCustomAgentDefinition, resolveAgentConfigs, + upsertCustomAgentDefinition, } from "./agent-settings"; describe("resolveAgentConfigs", () => { @@ -60,6 +63,34 @@ describe("resolveAgentConfigs", () => { enabled: true, }); }); + + test("includes custom terminal configs from stored definitions", () => { + const custom = resolveAgentConfigs({ + customDefinitions: [ + { + id: "custom:amp-team", + kind: "terminal", + label: "Amp Team", + description: "Team Amp wrapper", + command: "amp --team", + promptCommand: "amp -x --team", + taskPromptTemplate: "Task {{slug}}", + enabled: false, + }, + ], + }).find((preset) => preset.id === "custom:amp-team"); + + expect(custom).toMatchObject({ + id: "custom:amp-team", + source: "user", + kind: "terminal", + label: "Amp Team", + command: "amp --team", + promptCommand: "amp -x --team", + taskPromptTemplate: "Task {{slug}}", + enabled: false, + }); + }); }); describe("createOverrideEnvelopeWithPatch", () => { @@ -122,3 +153,65 @@ describe("createOverrideEnvelopeWithPatch", () => { }); }); }); + +describe("custom agent definition helpers", () => { + test("upserts and patches custom definitions", () => { + const created = upsertCustomAgentDefinition({ + currentDefinitions: [], + definition: { + id: "custom:team-agent", + kind: "terminal", + label: "Team Agent", + command: "team-agent", + promptCommand: "team-agent --prompt", + taskPromptTemplate: "Task {{slug}}", + }, + }); + + const updated = applyCustomAgentDefinitionPatch({ + definition: created[0]!, + patch: { + description: "Shared team wrapper", + promptCommandSuffix: "--yolo", + enabled: false, + }, + }); + + expect(updated).toMatchObject({ + id: "custom:team-agent", + description: "Shared team wrapper", + promptCommandSuffix: "--yolo", + enabled: false, + }); + }); + + test("deletes custom definitions by id", () => { + const definitions = deleteCustomAgentDefinition({ + currentDefinitions: [ + { + id: "custom:keep", + kind: "terminal", + label: "Keep", + command: "keep", + promptCommand: "keep --prompt", + taskPromptTemplate: "Task {{slug}}", + }, + { + id: "custom:remove", + kind: "terminal", + label: "Remove", + command: "remove", + promptCommand: "remove --prompt", + taskPromptTemplate: "Task {{slug}}", + }, + ], + id: "custom:remove", + }); + + expect(definitions).toEqual([ + expect.objectContaining({ + id: "custom:keep", + }), + ]); + }); +}); diff --git a/apps/desktop/src/shared/utils/agent-settings.ts b/apps/desktop/src/shared/utils/agent-settings.ts index ecfaa50cc80..546700218ff 100644 --- a/apps/desktop/src/shared/utils/agent-settings.ts +++ b/apps/desktop/src/shared/utils/agent-settings.ts @@ -86,6 +86,16 @@ export type AgentPresetPatch = Partial<{ model: string | null; }>; +export type CustomAgentDefinitionPatch = Partial<{ + enabled: boolean; + label: string; + description: string | null; + command: string; + promptCommand: string; + promptCommandSuffix: string | null; + taskPromptTemplate: string; +}>; + function toCustomAgentDefinition( customDefinition: AgentCustomDefinition, ): TerminalAgentDefinition { @@ -103,7 +113,7 @@ function toCustomAgentDefinition( }; } -function readCustomDefinitions( +export function readAgentCustomDefinitions( customDefinitions: AgentCustomDefinition[] | null | undefined, ): AgentCustomDefinition[] { return (customDefinitions ?? []).flatMap((definition) => { @@ -126,12 +136,99 @@ export function getAgentDefinitions( ): AgentDefinition[] { return [ ...BUILTIN_AGENT_DEFINITIONS, - ...readCustomDefinitions(customDefinitions).map((definition) => + ...readAgentCustomDefinitions(customDefinitions).map((definition) => toCustomAgentDefinition(definition), ), ]; } +export function getCustomAgentDefinitionById({ + customDefinitions, + id, +}: { + customDefinitions?: AgentCustomDefinition[] | null; + id: `custom:${string}`; +}): AgentCustomDefinition | null { + return ( + readAgentCustomDefinitions(customDefinitions).find( + (definition) => definition.id === id, + ) ?? null + ); +} + +export function upsertCustomAgentDefinition({ + currentDefinitions, + definition, +}: { + currentDefinitions?: AgentCustomDefinition[] | null; + definition: AgentCustomDefinition; +}): AgentCustomDefinition[] { + const definitions = readAgentCustomDefinitions(currentDefinitions); + const nextDefinition = agentCustomDefinitionSchema.parse(definition); + const index = definitions.findIndex( + (candidate) => candidate.id === nextDefinition.id, + ); + if (index === -1) { + return [...definitions, nextDefinition]; + } + + return definitions.map((candidate, candidateIndex) => + candidateIndex === index ? nextDefinition : candidate, + ); +} + +export function applyCustomAgentDefinitionPatch({ + definition, + patch, +}: { + definition: AgentCustomDefinition; + patch: CustomAgentDefinitionPatch; +}): AgentCustomDefinition { + const nextDefinition: AgentCustomDefinition = { ...definition }; + + if (Object.hasOwn(patch, "enabled")) { + nextDefinition.enabled = patch.enabled; + } + if (Object.hasOwn(patch, "label") && patch.label !== undefined) { + nextDefinition.label = patch.label; + } + if (Object.hasOwn(patch, "description")) { + nextDefinition.description = patch.description ?? undefined; + } + if (Object.hasOwn(patch, "command") && patch.command !== undefined) { + nextDefinition.command = patch.command; + } + if ( + Object.hasOwn(patch, "promptCommand") && + patch.promptCommand !== undefined + ) { + nextDefinition.promptCommand = patch.promptCommand; + } + if (Object.hasOwn(patch, "promptCommandSuffix")) { + nextDefinition.promptCommandSuffix = patch.promptCommandSuffix ?? undefined; + } + if ( + Object.hasOwn(patch, "taskPromptTemplate") && + patch.taskPromptTemplate !== undefined + ) { + nextDefinition.taskPromptTemplate = patch.taskPromptTemplate; + } + + return agentCustomDefinitionSchema.parse(nextDefinition); +} + +export function deleteCustomAgentDefinition({ + currentDefinitions, + id, +}: { + currentDefinitions?: AgentCustomDefinition[] | null; + id: `custom:${string}`; +}): AgentCustomDefinition[] { + return readAgentCustomDefinitions(currentDefinitions).filter( + (definition) => definition.id !== id, + ); +} + function getOverriddenFields( override: AgentPresetOverride | undefined, definition: AgentDefinition, diff --git a/packages/mcp/src/tools/devices/start-agent-session/shared.ts b/packages/mcp/src/tools/devices/start-agent-session/shared.ts index c9023c82f26..a706eb885cf 100644 --- a/packages/mcp/src/tools/devices/start-agent-session/shared.ts +++ b/packages/mcp/src/tools/devices/start-agent-session/shared.ts @@ -30,6 +30,20 @@ export type StartAgentSessionToolName = export const nonEmptyString = z.string().trim().min(1); +function describeSupportedAgents(): string { + const quotedAgents = STARTABLE_AGENT_TYPES.map((agent) => `"${agent}"`); + const lastAgent = quotedAgents.at(-1); + if (!lastAgent) { + return 'AI agent to use. Defaults to "claude".'; + } + + if (quotedAgents.length === 1) { + return `AI agent to use: ${lastAgent}. Defaults to "claude".`; + } + + return `AI agent to use: ${quotedAgents.slice(0, -1).join(", ")}, or ${lastAgent}. Defaults to "claude".`; +} + export const commonInputSchemaShape = { deviceId: nonEmptyString.describe("Target device ID"), workspaceId: nonEmptyString.describe( @@ -43,9 +57,7 @@ export const commonInputSchemaShape = { agent: z .enum(STARTABLE_AGENT_TYPES) .optional() - .describe( - 'AI agent to use: "claude", "amp", "codex", "gemini", "opencode", "pi", "copilot", "cursor-agent", or "superset-chat". Defaults to "claude".', - ), + .describe(describeSupportedAgents()), }; export const taskInputSchemaShape = { diff --git a/packages/shared/src/agent-catalog.ts b/packages/shared/src/agent-catalog.ts index fb59087be77..d69779f4b26 100644 --- a/packages/shared/src/agent-catalog.ts +++ b/packages/shared/src/agent-catalog.ts @@ -1,15 +1,13 @@ import { AGENT_LABELS, - AGENT_PRESET_COMMANDS, - AGENT_PRESET_DESCRIPTIONS, AGENT_PROMPT_COMMANDS, AGENT_TYPES, - type AgentType, } from "./agent-command"; import { DEFAULT_CHAT_TASK_PROMPT_TEMPLATE, DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, } from "./agent-prompt-template"; +import { BUILTIN_TERMINAL_AGENTS } from "./builtin-terminal-agents"; export const BUILTIN_AGENT_IDS = [...AGENT_TYPES, "superset-chat"] as const; @@ -49,17 +47,17 @@ export const BUILTIN_AGENT_LABELS: Record = { }; function createBuiltinTerminalAgentDefinition( - id: AgentType, + agent: (typeof BUILTIN_TERMINAL_AGENTS)[number], ): TerminalAgentDefinition { - const promptCommand = AGENT_PROMPT_COMMANDS[id]; + const promptCommand = AGENT_PROMPT_COMMANDS[agent.id]; return { - id, + id: agent.id, source: "builtin", kind: "terminal", - defaultLabel: AGENT_LABELS[id], - defaultDescription: AGENT_PRESET_DESCRIPTIONS[id], - defaultCommand: AGENT_PRESET_COMMANDS[id][0] ?? "", + defaultLabel: agent.label, + defaultDescription: agent.description, + defaultCommand: agent.command, defaultPromptCommand: promptCommand.command, defaultPromptCommandSuffix: promptCommand.suffix, defaultTaskPromptTemplate: DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, @@ -68,7 +66,9 @@ function createBuiltinTerminalAgentDefinition( } export const BUILTIN_AGENT_DEFINITIONS: AgentDefinition[] = [ - ...AGENT_TYPES.map((id) => createBuiltinTerminalAgentDefinition(id)), + ...BUILTIN_TERMINAL_AGENTS.map((agent) => + createBuiltinTerminalAgentDefinition(agent), + ), { id: "superset-chat", source: "builtin", diff --git a/packages/shared/src/agent-command.ts b/packages/shared/src/agent-command.ts index 9d7d8e4183e..461b34747da 100644 --- a/packages/shared/src/agent-command.ts +++ b/packages/shared/src/agent-command.ts @@ -2,64 +2,32 @@ import { DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, renderTaskPromptTemplate, } from "./agent-prompt-template"; +import { + BUILTIN_TERMINAL_AGENT_COMMANDS, + BUILTIN_TERMINAL_AGENT_DESCRIPTIONS, + BUILTIN_TERMINAL_AGENT_LABELS, + BUILTIN_TERMINAL_AGENT_PROMPT_COMMANDS, + BUILTIN_TERMINAL_AGENT_TYPES, + type BuiltinTerminalAgentType, +} from "./builtin-terminal-agents"; + +export { + BUILTIN_TERMINAL_AGENTS, + DEFAULT_TERMINAL_PRESET_AGENT_TYPES, +} from "./builtin-terminal-agents"; + +export const AGENT_TYPES = BUILTIN_TERMINAL_AGENT_TYPES; + +export type AgentType = BuiltinTerminalAgentType; + +export const AGENT_LABELS: Record = + BUILTIN_TERMINAL_AGENT_LABELS; + +export const AGENT_PRESET_COMMANDS: Record = + BUILTIN_TERMINAL_AGENT_COMMANDS; -export const AGENT_TYPES = [ - "claude", - "amp", - "codex", - "gemini", - "mastracode", - "opencode", - "pi", - "copilot", - "cursor-agent", -] as const; - -export type AgentType = (typeof AGENT_TYPES)[number]; - -export const AGENT_LABELS: Record = { - claude: "Claude", - amp: "Amp", - codex: "Codex", - gemini: "Gemini", - mastracode: "Mastracode", - opencode: "OpenCode", - pi: "Pi", - copilot: "Copilot", - "cursor-agent": "Cursor Agent", -}; - -export const AGENT_PRESET_COMMANDS: Record = { - claude: ["claude --dangerously-skip-permissions"], - amp: ["amp"], - codex: [ - 'codex -c model_reasoning_effort="high" --dangerously-bypass-approvals-and-sandbox -c model_reasoning_summary="detailed" -c model_supports_reasoning_summaries=true', - ], - gemini: ["gemini --yolo"], - mastracode: ["mastracode"], - opencode: ["opencode"], - pi: ["pi"], - copilot: ["copilot --allow-all"], - "cursor-agent": ["cursor-agent"], -}; - -export const AGENT_PRESET_DESCRIPTIONS: Record = { - claude: - "Anthropic's coding agent for reading code, editing files, and running terminal workflows.", - amp: "Amp's coding agent for terminal-first coding, subagents, and task work.", - codex: - "OpenAI's coding agent for reading, modifying, and running code across tasks.", - gemini: - "Google's open-source terminal agent for coding, problem-solving, and task work.", - mastracode: - "Mastra's coding agent for building, debugging, and shipping code from the terminal.", - opencode: "Open-source coding agent for the terminal, IDE, and desktop.", - pi: "Minimal terminal coding harness for flexible coding workflows.", - copilot: - "GitHub's coding agent for planning, editing, and building in your repo.", - "cursor-agent": - "Cursor's coding agent for editing, running, and debugging code in parallel.", -}; +export const AGENT_PRESET_DESCRIPTIONS: Record = + BUILTIN_TERMINAL_AGENT_DESCRIPTIONS; export interface AgentPromptCommandDefaults { command: string; @@ -69,38 +37,7 @@ export interface AgentPromptCommandDefaults { export const AGENT_PROMPT_COMMANDS: Record< AgentType, AgentPromptCommandDefaults -> = { - claude: { - command: AGENT_PRESET_COMMANDS.claude[0] ?? "claude", - }, - amp: { - command: "amp -x", - }, - codex: { - command: `${AGENT_PRESET_COMMANDS.codex[0] ?? "codex"} --`, - }, - gemini: { - command: "gemini", - suffix: "--yolo", - }, - mastracode: { - command: AGENT_PRESET_COMMANDS.mastracode[0] ?? "mastracode", - }, - opencode: { - command: "opencode --prompt", - }, - pi: { - command: AGENT_PRESET_COMMANDS.pi[0] ?? "pi", - }, - copilot: { - command: "copilot -i --allow-all", - suffix: "--yolo", - }, - "cursor-agent": { - command: AGENT_PRESET_COMMANDS["cursor-agent"][0] ?? "cursor-agent", - suffix: "--yolo", - }, -}; +> = BUILTIN_TERMINAL_AGENT_PROMPT_COMMANDS; export interface TaskInput { id: string; diff --git a/packages/shared/src/builtin-terminal-agents.ts b/packages/shared/src/builtin-terminal-agents.ts new file mode 100644 index 00000000000..d03e65aea2f --- /dev/null +++ b/packages/shared/src/builtin-terminal-agents.ts @@ -0,0 +1,152 @@ +export interface BuiltinTerminalAgentManifest { + id: string; + label: string; + description: string; + command: string; + promptCommand?: string; + promptCommandSuffix?: string; + includeInDefaultTerminalPresets?: boolean; +} + +type AgentIdTuple = { + [K in keyof T]: T[K] extends { id: infer TId } ? TId : never; +}; + +function mapAgentIds( + agents: T, +): AgentIdTuple { + return agents.map((agent) => agent.id) as AgentIdTuple; +} + +function createAgentRecord< + const T extends readonly BuiltinTerminalAgentManifest[], + TValue, +>( + agents: T, + getValue: (agent: T[number]) => TValue, +): Record { + return Object.fromEntries( + agents.map((agent) => [agent.id, getValue(agent)]), + ) as Record; +} + +export const BUILTIN_TERMINAL_AGENTS = [ + { + id: "claude", + label: "Claude", + description: + "Anthropic's coding agent for reading code, editing files, and running terminal workflows.", + command: "claude --dangerously-skip-permissions", + includeInDefaultTerminalPresets: true, + }, + { + id: "amp", + label: "Amp", + description: + "Amp's coding agent for terminal-first coding, subagents, and task work.", + command: "amp", + promptCommand: "amp -x", + includeInDefaultTerminalPresets: true, + }, + { + id: "codex", + label: "Codex", + description: + "OpenAI's coding agent for reading, modifying, and running code across tasks.", + command: + 'codex -c model_reasoning_effort="high" --dangerously-bypass-approvals-and-sandbox -c model_reasoning_summary="detailed" -c model_supports_reasoning_summaries=true', + promptCommand: + 'codex -c model_reasoning_effort="high" --dangerously-bypass-approvals-and-sandbox -c model_reasoning_summary="detailed" -c model_supports_reasoning_summaries=true --', + includeInDefaultTerminalPresets: true, + }, + { + id: "gemini", + label: "Gemini", + description: + "Google's open-source terminal agent for coding, problem-solving, and task work.", + command: "gemini --yolo", + promptCommand: "gemini", + promptCommandSuffix: "--yolo", + includeInDefaultTerminalPresets: true, + }, + { + id: "mastracode", + label: "Mastracode", + description: + "Mastra's coding agent for building, debugging, and shipping code from the terminal.", + command: "mastracode", + includeInDefaultTerminalPresets: true, + }, + { + id: "opencode", + label: "OpenCode", + description: "Open-source coding agent for the terminal, IDE, and desktop.", + command: "opencode", + promptCommand: "opencode --prompt", + includeInDefaultTerminalPresets: true, + }, + { + id: "pi", + label: "Pi", + description: + "Minimal terminal coding harness for flexible coding workflows.", + command: "pi", + includeInDefaultTerminalPresets: true, + }, + { + id: "copilot", + label: "Copilot", + description: + "GitHub's coding agent for planning, editing, and building in your repo.", + command: "copilot --allow-all", + promptCommand: "copilot -i --allow-all", + promptCommandSuffix: "--yolo", + includeInDefaultTerminalPresets: true, + }, + { + id: "cursor-agent", + label: "Cursor Agent", + description: + "Cursor's coding agent for editing, running, and debugging code in parallel.", + command: "cursor-agent", + promptCommandSuffix: "--yolo", + }, +] as const satisfies readonly BuiltinTerminalAgentManifest[]; + +export type BuiltinTerminalAgentType = + (typeof BUILTIN_TERMINAL_AGENTS)[number]["id"]; + +export const BUILTIN_TERMINAL_AGENT_TYPES = mapAgentIds( + BUILTIN_TERMINAL_AGENTS, +); + +export const BUILTIN_TERMINAL_AGENT_LABELS = createAgentRecord( + BUILTIN_TERMINAL_AGENTS, + (agent) => agent.label, +); + +export const BUILTIN_TERMINAL_AGENT_DESCRIPTIONS = createAgentRecord( + BUILTIN_TERMINAL_AGENTS, + (agent) => agent.description, +); + +export const BUILTIN_TERMINAL_AGENT_COMMANDS = createAgentRecord( + BUILTIN_TERMINAL_AGENTS, + (agent) => [agent.command], +); + +export const BUILTIN_TERMINAL_AGENT_PROMPT_COMMANDS = createAgentRecord( + BUILTIN_TERMINAL_AGENTS, + (agent) => ({ + command: "promptCommand" in agent ? agent.promptCommand : agent.command, + suffix: + "promptCommandSuffix" in agent ? agent.promptCommandSuffix : undefined, + }), +); + +export const DEFAULT_TERMINAL_PRESET_AGENT_TYPES = + BUILTIN_TERMINAL_AGENTS.filter( + (agent) => + "includeInDefaultTerminalPresets" in agent && + agent.includeInDefaultTerminalPresets, + ).map((agent) => agent.id) satisfies BuiltinTerminalAgentType[]; From 45c79b7db8e1740c93d3c0e289cb7ffd9a57e276 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 21:54:30 -0700 Subject: [PATCH 03/12] Use official Amp press kit logos --- .../ui/src/assets/icons/preset-icons/amp-white.svg | 3 +++ packages/ui/src/assets/icons/preset-icons/amp.svg | 13 ++----------- packages/ui/src/assets/icons/preset-icons/index.ts | 4 +++- 3 files changed, 8 insertions(+), 12 deletions(-) create mode 100644 packages/ui/src/assets/icons/preset-icons/amp-white.svg diff --git a/packages/ui/src/assets/icons/preset-icons/amp-white.svg b/packages/ui/src/assets/icons/preset-icons/amp-white.svg new file mode 100644 index 00000000000..81a6cb6ccdb --- /dev/null +++ b/packages/ui/src/assets/icons/preset-icons/amp-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/preset-icons/amp.svg b/packages/ui/src/assets/icons/preset-icons/amp.svg index a16e28a793e..615bf50067b 100644 --- a/packages/ui/src/assets/icons/preset-icons/amp.svg +++ b/packages/ui/src/assets/icons/preset-icons/amp.svg @@ -1,12 +1,3 @@ - - - - - - - - + + diff --git a/packages/ui/src/assets/icons/preset-icons/index.ts b/packages/ui/src/assets/icons/preset-icons/index.ts index 635e5913794..66118986016 100644 --- a/packages/ui/src/assets/icons/preset-icons/index.ts +++ b/packages/ui/src/assets/icons/preset-icons/index.ts @@ -1,4 +1,5 @@ import ampIcon from "./amp.svg"; +import ampWhiteIcon from "./amp-white.svg"; import claudeIcon from "./claude.svg"; import codexIcon from "./codex.svg"; import codexWhiteIcon from "./codex-white.svg"; @@ -20,7 +21,7 @@ export interface PresetIconSet { } export const PRESET_ICONS: Record = { - amp: { light: ampIcon, dark: ampIcon }, + amp: { light: ampIcon, dark: ampWhiteIcon }, claude: { light: claudeIcon, dark: claudeIcon }, codex: { light: codexIcon, dark: codexWhiteIcon }, copilot: { light: copilotIcon, dark: copilotWhiteIcon }, @@ -45,6 +46,7 @@ export function getPresetIcon( export { ampIcon, + ampWhiteIcon, claudeIcon, codexIcon, codexWhiteIcon, From 09565a5b5311b9d9eac2919a4c8eb90fe0ca3226 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 23:16:24 -0700 Subject: [PATCH 04/12] Use Amp square mark icon --- packages/ui/src/assets/icons/preset-icons/amp-white.svg | 3 --- packages/ui/src/assets/icons/preset-icons/amp.svg | 6 ++++-- packages/ui/src/assets/icons/preset-icons/index.ts | 4 +--- 3 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 packages/ui/src/assets/icons/preset-icons/amp-white.svg diff --git a/packages/ui/src/assets/icons/preset-icons/amp-white.svg b/packages/ui/src/assets/icons/preset-icons/amp-white.svg deleted file mode 100644 index 81a6cb6ccdb..00000000000 --- a/packages/ui/src/assets/icons/preset-icons/amp-white.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/assets/icons/preset-icons/amp.svg b/packages/ui/src/assets/icons/preset-icons/amp.svg index 615bf50067b..dde8755df7e 100644 --- a/packages/ui/src/assets/icons/preset-icons/amp.svg +++ b/packages/ui/src/assets/icons/preset-icons/amp.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/packages/ui/src/assets/icons/preset-icons/index.ts b/packages/ui/src/assets/icons/preset-icons/index.ts index 66118986016..635e5913794 100644 --- a/packages/ui/src/assets/icons/preset-icons/index.ts +++ b/packages/ui/src/assets/icons/preset-icons/index.ts @@ -1,5 +1,4 @@ import ampIcon from "./amp.svg"; -import ampWhiteIcon from "./amp-white.svg"; import claudeIcon from "./claude.svg"; import codexIcon from "./codex.svg"; import codexWhiteIcon from "./codex-white.svg"; @@ -21,7 +20,7 @@ export interface PresetIconSet { } export const PRESET_ICONS: Record = { - amp: { light: ampIcon, dark: ampWhiteIcon }, + amp: { light: ampIcon, dark: ampIcon }, claude: { light: claudeIcon, dark: claudeIcon }, codex: { light: codexIcon, dark: codexWhiteIcon }, copilot: { light: copilotIcon, dark: copilotWhiteIcon }, @@ -46,7 +45,6 @@ export function getPresetIcon( export { ampIcon, - ampWhiteIcon, claudeIcon, codexIcon, codexWhiteIcon, From 1786ad8458fa23111cbfedd78a89ace4d7b73bcb Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 23:23:00 -0700 Subject: [PATCH 05/12] Fix lint warning in agent settings test --- apps/desktop/src/shared/utils/agent-settings.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/shared/utils/agent-settings.test.ts b/apps/desktop/src/shared/utils/agent-settings.test.ts index a1202f77b67..576d6a51432 100644 --- a/apps/desktop/src/shared/utils/agent-settings.test.ts +++ b/apps/desktop/src/shared/utils/agent-settings.test.ts @@ -167,9 +167,14 @@ describe("custom agent definition helpers", () => { taskPromptTemplate: "Task {{slug}}", }, }); + const createdDefinition = created[0]; + + if (!createdDefinition) { + throw new Error("Expected custom agent definition to be created"); + } const updated = applyCustomAgentDefinitionPatch({ - definition: created[0]!, + definition: createdDefinition, patch: { description: "Shared team wrapper", promptCommandSuffix: "--yolo", From 6f73769a7c9c01f6d8f0eb0379a836b2f0ad7538 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 23:25:10 -0700 Subject: [PATCH 06/12] Stop tracking repo Amp workspace settings --- .amp/settings.json | 31 ------------------------------- .gitignore | 1 - AGENTS.md | 2 +- 3 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 .amp/settings.json diff --git a/.amp/settings.json b/.amp/settings.json deleted file mode 100644 index 3a47f5f8030..00000000000 --- a/.amp/settings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "amp.mcpServers": { - "superset": { - "url": "https://api.superset.sh/api/agent/mcp" - }, - "expo-mcp": { - "url": "https://mcp.expo.dev/mcp", - "enabled": false - }, - "maestro": { - "command": "maestro", - "args": ["mcp"] - }, - "neon": { - "url": "https://mcp.neon.tech/mcp" - }, - "linear": { - "url": "https://mcp.linear.app/mcp" - }, - "sentry": { - "url": "https://mcp.sentry.dev/mcp" - }, - "posthog": { - "url": "https://mcp.posthog.com/mcp" - }, - "desktop-automation": { - "command": "bun", - "args": ["run", "packages/desktop-mcp/src/bin.ts"] - } - } -} diff --git a/.gitignore b/.gitignore index 3029a160c75..983e65a898d 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,3 @@ superset-dev-data/ # Amp workspace config (track shared settings; ignore runtime state) .amp/* -!.amp/settings.json diff --git a/AGENTS.md b/AGENTS.md index ee4c552128b..65f03e6db61 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ bun run clean:workspaces # Clean all workspace node_modules 1. **Type safety** - avoid `any` unless necessary 2. **Prefer `gh` CLI** - when performing git operations (PRs, issues, checkout, etc.), prefer the GitHub CLI (`gh`) over raw `git` commands where possible 3. **Shared command source** - keep command definitions in `.agents/commands/` only. `.claude/commands` and `.cursor/commands` should be symlinks to `../.agents/commands`. (`packages/chat` discovers slash commands from `.claude/commands`.) -4. **Workspace MCP config** - keep shared MCP servers in `.mcp.json`; `.cursor/mcp.json` should link to `../.mcp.json`. Codex uses `.codex/config.toml` (run with `CODEX_HOME=.codex codex ...`). OpenCode uses `opencode.json` and Amp uses `.amp/settings.json`; both should mirror the same MCP set using their native schemas (`mcp` for OpenCode, `amp.mcpServers` for Amp). +4. **Workspace MCP config** - keep shared MCP servers in `.mcp.json`; `.cursor/mcp.json` should link to `../.mcp.json`. Codex uses `.codex/config.toml` (run with `CODEX_HOME=.codex codex ...`). OpenCode uses `opencode.json` and should mirror the same MCP set using OpenCode's native `mcp` schema. 5. **Mastra dependencies** - use the published upstream `mastracode` and `@mastra/*` packages. Do not add fork tarball overrides or custom patch steps unless explicitly requested. From 0e61e33e02058d432daa5d23a7fac319c33bdf54 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 23:26:14 -0700 Subject: [PATCH 07/12] Lint --- apps/desktop/src/lib/trpc/routers/settings/index.ts | 4 ++-- apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index fd4f9a65b5b..09a6ea3e48b 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -44,8 +44,8 @@ import { getAgentDefinitionById, getCustomAgentDefinitionById, readAgentPresetOverrides, - resetAllAgentPresetOverrides, resetAgentPresetOverride, + resetAllAgentPresetOverrides, resolveAgentConfigs, upsertCustomAgentDefinition, } from "shared/utils/agent-settings"; @@ -57,8 +57,8 @@ import { normalizeAgentPresetPatch, normalizeCreateCustomAgentInput, normalizeCustomAgentPatch, - updateCustomAgentInputSchema, updateAgentPresetInputSchema, + updateCustomAgentInputSchema, } from "./agent-preset-router.utils"; import { setFontSettingsSchema, diff --git a/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts b/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts index 32e489b0ee3..a38d404c3eb 100644 --- a/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts +++ b/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts @@ -2,8 +2,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { - type SupersetManagedBinary, SUPERSET_MANAGED_BINARIES, + type SupersetManagedBinary, } from "./desktop-agent-capabilities"; import { BASH_DIR, BIN_DIR, ZSH_DIR } from "./paths"; From d845acc0e21367b98e88d3c6f9857f9c51986110 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 23:40:24 -0700 Subject: [PATCH 08/12] Use stdin for interactive Amp prompts --- .../shared/utils/agent-launch-request.test.ts | 50 +++++++++++++++++ .../src/shared/utils/agent-settings.test.ts | 12 +++++ .../src/shared/utils/agent-settings.ts | 31 ++++++++++- packages/shared/src/agent-command.test.ts | 19 +++++-- packages/shared/src/agent-command.ts | 54 +++++++++++++++---- .../shared/src/builtin-terminal-agents.ts | 2 +- 6 files changed, 151 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/shared/utils/agent-launch-request.test.ts b/apps/desktop/src/shared/utils/agent-launch-request.test.ts index 0b880eeab2f..de4bc0b4d63 100644 --- a/apps/desktop/src/shared/utils/agent-launch-request.test.ts +++ b/apps/desktop/src/shared/utils/agent-launch-request.test.ts @@ -85,6 +85,28 @@ describe("buildPromptAgentLaunchRequest", () => { }, }); }); + + test("builds Amp prompt launches in interactive stdin mode", () => { + const configsById = indexResolvedAgentConfigs(resolveAgentConfigs({})); + const request = buildPromptAgentLaunchRequest({ + workspaceId: "workspace-1", + source: "new-workspace", + selectedAgent: "amp", + prompt: "wasssup", + configsById, + }); + + expect(request).toMatchObject({ + kind: "terminal", + agentType: "amp", + }); + expect(request?.kind).toBe("terminal"); + if (request?.kind !== "terminal") { + throw new Error("Expected terminal launch request"); + } + expect(request.terminal.command).toStartWith("amp <<'SUPERSET_PROMPT_"); + expect(request.terminal.command).not.toContain("amp -x"); + }); }); describe("buildTaskAgentLaunchRequest", () => { @@ -167,6 +189,34 @@ describe("buildTaskAgentLaunchRequest", () => { }); }); + test("builds Amp task launches in interactive stdin mode", () => { + const configsById = indexResolvedAgentConfigs(resolveAgentConfigs({})); + const request = buildTaskAgentLaunchRequest({ + workspaceId: "workspace-1", + source: "open-in-workspace", + selectedAgent: "amp", + task: TASK, + autoRun: false, + configsById, + }); + + expect(request).toMatchObject({ + kind: "terminal", + agentType: "amp", + terminal: { + taskPromptFileName: "task-demo-task.md", + autoExecute: false, + }, + }); + expect(request?.kind).toBe("terminal"); + if (request?.kind !== "terminal") { + throw new Error("Expected terminal launch request"); + } + expect(request.terminal.command).toBe( + "amp < '.superset/task-demo-task.md'", + ); + }); + test("rejects disabled agents", () => { const configsById = indexResolvedAgentConfigs( resolveAgentConfigs({ diff --git a/apps/desktop/src/shared/utils/agent-settings.test.ts b/apps/desktop/src/shared/utils/agent-settings.test.ts index 576d6a51432..31b24bb7dbe 100644 --- a/apps/desktop/src/shared/utils/agent-settings.test.ts +++ b/apps/desktop/src/shared/utils/agent-settings.test.ts @@ -64,6 +64,18 @@ describe("resolveAgentConfigs", () => { }); }); + test("uses amp as the built-in prompt command for Amp", () => { + const amp = resolveAgentConfigs({}).find((preset) => preset.id === "amp"); + + expect(amp).toMatchObject({ + id: "amp", + kind: "terminal", + command: "amp", + promptCommand: "amp", + enabled: true, + }); + }); + test("includes custom terminal configs from stored definitions", () => { const custom = resolveAgentConfigs({ customDefinitions: [ diff --git a/apps/desktop/src/shared/utils/agent-settings.ts b/apps/desktop/src/shared/utils/agent-settings.ts index 546700218ff..12e9b3951ca 100644 --- a/apps/desktop/src/shared/utils/agent-settings.ts +++ b/apps/desktop/src/shared/utils/agent-settings.ts @@ -386,6 +386,16 @@ function buildHeredoc( ].join("\n"); } +function buildStdinHeredoc( + prompt: string, + delimiter: string, + command: string, + suffix?: string, +): string { + const fullCommand = suffix ? `${command} ${suffix}` : command; + return [`${fullCommand} <<'${delimiter}'`, prompt, delimiter].join("\n"); +} + function buildFileCommand( filePath: string, command: string, @@ -395,6 +405,19 @@ function buildFileCommand( return `${command} "$(cat '${escapedPath}')"${suffix ? ` ${suffix}` : ""}`; } +function buildStdinFileCommand( + filePath: string, + command: string, + suffix?: string, +): string { + const escapedPath = filePath.replaceAll("'", "'\\''"); + return `${command}${suffix ? ` ${suffix}` : ""} < '${escapedPath}'`; +} + +function shouldUseStdinPrompt(config: TerminalResolvedAgentConfig): boolean { + return config.id === "amp"; +} + export function getCommandFromAgentConfig( config: TerminalResolvedAgentConfig, ): string | null { @@ -420,7 +443,9 @@ export function buildPromptCommandFromAgentConfig({ } const suffix = config.promptCommandSuffix?.trim() || undefined; - return buildHeredoc(prompt, delimiter, promptCommand, suffix); + return shouldUseStdinPrompt(config) + ? buildStdinHeredoc(prompt, delimiter, promptCommand, suffix) + : buildHeredoc(prompt, delimiter, promptCommand, suffix); } export function buildFileCommandFromAgentConfig({ @@ -434,7 +459,9 @@ export function buildFileCommandFromAgentConfig({ if (!promptCommand) return null; const suffix = config.promptCommandSuffix?.trim() || undefined; - return buildFileCommand(filePath, promptCommand, suffix); + return shouldUseStdinPrompt(config) + ? buildStdinFileCommand(filePath, promptCommand, suffix) + : buildFileCommand(filePath, promptCommand, suffix); } export function buildDefaultTerminalTaskPrompt(task: TaskInput): string { diff --git a/packages/shared/src/agent-command.test.ts b/packages/shared/src/agent-command.test.ts index 080ce6e2fab..301f2ff368e 100644 --- a/packages/shared/src/agent-command.test.ts +++ b/packages/shared/src/agent-command.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "bun:test"; -import { buildAgentPromptCommand } from "./agent-command"; +import { + buildAgentFileCommand, + buildAgentPromptCommand, +} from "./agent-command"; describe("buildAgentPromptCommand", () => { it("adds `--` before codex prompt payload", () => { @@ -27,14 +30,24 @@ describe("buildAgentPromptCommand", () => { ); }); - it("uses Amp execute mode for prompt launches", () => { + it("uses Amp interactive stdin mode for prompt launches", () => { const command = buildAgentPromptCommand({ prompt: "hello", randomId: "amp-1234", agent: "amp", }); - expect(command).toStartWith("amp -x \"$(cat <<'SUPERSET_PROMPT_amp1234'"); + expect(command).toStartWith("amp <<'SUPERSET_PROMPT_amp1234'"); + expect(command).not.toContain("amp -x"); + }); + + it("uses Amp interactive stdin mode for file launches", () => { + const command = buildAgentFileCommand({ + filePath: ".superset/task-demo.md", + agent: "amp", + }); + + expect(command).toBe("amp < '.superset/task-demo.md'"); }); it("uses pi interactive mode for prompt launches", () => { diff --git a/packages/shared/src/agent-command.ts b/packages/shared/src/agent-command.ts index 461b34747da..94489e1704b 100644 --- a/packages/shared/src/agent-command.ts +++ b/packages/shared/src/agent-command.ts @@ -68,6 +68,16 @@ function buildHeredoc( ].join("\n"); } +function buildStdinHeredoc( + prompt: string, + delimiter: string, + command: string, + suffix?: string, +): string { + const fullCommand = suffix ? `${command} ${suffix}` : command; + return [`${fullCommand} <<'${delimiter}'`, prompt, delimiter].join("\n"); +} + function buildFileCommand( filePath: string, command: string, @@ -77,6 +87,19 @@ function buildFileCommand( return `${command} "$(cat '${escapedPath}')"${suffix ? ` ${suffix}` : ""}`; } +function buildStdinFileCommand( + filePath: string, + command: string, + suffix?: string, +): string { + const escapedPath = filePath.replaceAll("'", "'\\''"); + return `${command}${suffix ? ` ${suffix}` : ""} < '${escapedPath}'`; +} + +function shouldUseStdinPrompt(agent: AgentType): boolean { + return agent === "amp"; +} + export function buildAgentFileCommand({ filePath, agent = "claude", @@ -85,11 +108,13 @@ export function buildAgentFileCommand({ agent?: AgentType; }): string { const promptCommand = AGENT_PROMPT_COMMANDS[agent]; - return buildFileCommand( - filePath, - promptCommand.command, - promptCommand.suffix, - ); + return shouldUseStdinPrompt(agent) + ? buildStdinFileCommand( + filePath, + promptCommand.command, + promptCommand.suffix, + ) + : buildFileCommand(filePath, promptCommand.command, promptCommand.suffix); } export function buildAgentPromptCommand({ @@ -106,12 +131,19 @@ export function buildAgentPromptCommand({ delimiter = `${delimiter}_X`; } const promptCommand = AGENT_PROMPT_COMMANDS[agent]; - return buildHeredoc( - prompt, - delimiter, - promptCommand.command, - promptCommand.suffix, - ); + return shouldUseStdinPrompt(agent) + ? buildStdinHeredoc( + prompt, + delimiter, + promptCommand.command, + promptCommand.suffix, + ) + : buildHeredoc( + prompt, + delimiter, + promptCommand.command, + promptCommand.suffix, + ); } export function buildAgentCommand({ diff --git a/packages/shared/src/builtin-terminal-agents.ts b/packages/shared/src/builtin-terminal-agents.ts index d03e65aea2f..c7c2cdf3163 100644 --- a/packages/shared/src/builtin-terminal-agents.ts +++ b/packages/shared/src/builtin-terminal-agents.ts @@ -45,7 +45,7 @@ export const BUILTIN_TERMINAL_AGENTS = [ description: "Amp's coding agent for terminal-first coding, subagents, and task work.", command: "amp", - promptCommand: "amp -x", + promptCommand: "amp", includeInDefaultTerminalPresets: true, }, { From dd029441ac9677439c392ffee83dff28998c2ca2 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 23:46:40 -0700 Subject: [PATCH 09/12] Trim repo-only Amp cleanup changes --- .gitignore | 2 -- AGENTS.md | 2 +- .../src/shared/utils/agent-settings.test.ts | 20 +++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 983e65a898d..c1e234a273d 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,4 @@ superset-dev-data/ !.codex/config.toml !.codex/commands !.codex/prompts - -# Amp workspace config (track shared settings; ignore runtime state) .amp/* diff --git a/AGENTS.md b/AGENTS.md index 65f03e6db61..fd0e6de5459 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ bun run clean:workspaces # Clean all workspace node_modules 1. **Type safety** - avoid `any` unless necessary 2. **Prefer `gh` CLI** - when performing git operations (PRs, issues, checkout, etc.), prefer the GitHub CLI (`gh`) over raw `git` commands where possible 3. **Shared command source** - keep command definitions in `.agents/commands/` only. `.claude/commands` and `.cursor/commands` should be symlinks to `../.agents/commands`. (`packages/chat` discovers slash commands from `.claude/commands`.) -4. **Workspace MCP config** - keep shared MCP servers in `.mcp.json`; `.cursor/mcp.json` should link to `../.mcp.json`. Codex uses `.codex/config.toml` (run with `CODEX_HOME=.codex codex ...`). OpenCode uses `opencode.json` and should mirror the same MCP set using OpenCode's native `mcp` schema. +4. **Workspace MCP config** - keep shared MCP servers in `.mcp.json`; `.cursor/mcp.json` should link to `../.mcp.json`. Codex uses `.codex/config.toml` (run with `CODEX_HOME=.codex codex ...`). OpenCode uses `opencode.json` and should mirror the same MCP set using OpenCode's `remote`/`local` schema. 5. **Mastra dependencies** - use the published upstream `mastracode` and `@mastra/*` packages. Do not add fork tarball overrides or custom patch steps unless explicitly requested. diff --git a/apps/desktop/src/shared/utils/agent-settings.test.ts b/apps/desktop/src/shared/utils/agent-settings.test.ts index 31b24bb7dbe..0779921f6fc 100644 --- a/apps/desktop/src/shared/utils/agent-settings.test.ts +++ b/apps/desktop/src/shared/utils/agent-settings.test.ts @@ -80,25 +80,25 @@ describe("resolveAgentConfigs", () => { const custom = resolveAgentConfigs({ customDefinitions: [ { - id: "custom:amp-team", + id: "custom:team-agent", kind: "terminal", - label: "Amp Team", - description: "Team Amp wrapper", - command: "amp --team", - promptCommand: "amp -x --team", + label: "Team Agent", + description: "Team wrapper", + command: "team-agent", + promptCommand: "team-agent --prompt", taskPromptTemplate: "Task {{slug}}", enabled: false, }, ], - }).find((preset) => preset.id === "custom:amp-team"); + }).find((preset) => preset.id === "custom:team-agent"); expect(custom).toMatchObject({ - id: "custom:amp-team", + id: "custom:team-agent", source: "user", kind: "terminal", - label: "Amp Team", - command: "amp --team", - promptCommand: "amp -x --team", + label: "Team Agent", + command: "team-agent", + promptCommand: "team-agent --prompt", taskPromptTemplate: "Task {{slug}}", enabled: false, }); From 0934bd747672e206a039116f9e5261d0d0eb611e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 23:58:48 -0700 Subject: [PATCH 10/12] Refactor agent prompt transport rendering --- .../src/shared/utils/agent-settings.ts | 83 +++++------------ packages/shared/package.json | 4 + packages/shared/src/agent-catalog.ts | 3 + packages/shared/src/agent-command.ts | 90 ++++--------------- packages/shared/src/agent-prompt-launch.ts | 66 ++++++++++++++ .../shared/src/builtin-terminal-agents.ts | 19 +++- 6 files changed, 133 insertions(+), 132 deletions(-) create mode 100644 packages/shared/src/agent-prompt-launch.ts diff --git a/apps/desktop/src/shared/utils/agent-settings.ts b/apps/desktop/src/shared/utils/agent-settings.ts index 12e9b3951ca..cc35a2bf2e6 100644 --- a/apps/desktop/src/shared/utils/agent-settings.ts +++ b/apps/desktop/src/shared/utils/agent-settings.ts @@ -14,6 +14,11 @@ import { type TerminalAgentDefinition, } from "@superset/shared/agent-catalog"; import type { TaskInput } from "@superset/shared/agent-command"; +import { + buildPromptCommandString, + buildPromptFileCommandString, + type PromptTransport, +} from "@superset/shared/agent-prompt-launch"; import { DEFAULT_CHAT_TASK_PROMPT_TEMPLATE, DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, @@ -55,6 +60,7 @@ export type TerminalResolvedAgentConfig = { command: string; promptCommand: string; promptCommandSuffix?: string; + promptTransport: PromptTransport; taskPromptTemplate: string; overriddenFields: AgentPresetField[]; }; @@ -108,6 +114,9 @@ function toCustomAgentDefinition( defaultCommand: customDefinition.command, defaultPromptCommand: customDefinition.promptCommand, defaultPromptCommandSuffix: customDefinition.promptCommandSuffix, + // Custom agents stay on the default argv transport until we intentionally + // expose prompt transport as part of user-configurable CRUD. + defaultPromptTransport: "argv", defaultTaskPromptTemplate: customDefinition.taskPromptTemplate, defaultEnabled: customDefinition.enabled ?? true, }; @@ -294,6 +303,7 @@ function resolveAgentConfig( definition.defaultPromptCommandSuffix, override, ), + promptTransport: definition.defaultPromptTransport, taskPromptTemplate: override?.taskPromptTemplate ?? definition.defaultTaskPromptTemplate, overriddenFields: getOverriddenFields(override, definition), @@ -371,53 +381,6 @@ export function getFallbackAgentId( return preferredClaude?.id ?? enabledConfigs[0]?.id ?? null; } -function buildHeredoc( - prompt: string, - delimiter: string, - command: string, - suffix?: string, -): string { - const closing = suffix ? `)" ${suffix}` : ')"'; - return [ - `${command} "$(cat <<'${delimiter}'`, - prompt, - delimiter, - closing, - ].join("\n"); -} - -function buildStdinHeredoc( - prompt: string, - delimiter: string, - command: string, - suffix?: string, -): string { - const fullCommand = suffix ? `${command} ${suffix}` : command; - return [`${fullCommand} <<'${delimiter}'`, prompt, delimiter].join("\n"); -} - -function buildFileCommand( - filePath: string, - command: string, - suffix?: string, -): string { - const escapedPath = filePath.replaceAll("'", "'\\''"); - return `${command} "$(cat '${escapedPath}')"${suffix ? ` ${suffix}` : ""}`; -} - -function buildStdinFileCommand( - filePath: string, - command: string, - suffix?: string, -): string { - const escapedPath = filePath.replaceAll("'", "'\\''"); - return `${command}${suffix ? ` ${suffix}` : ""} < '${escapedPath}'`; -} - -function shouldUseStdinPrompt(config: TerminalResolvedAgentConfig): boolean { - return config.id === "amp"; -} - export function getCommandFromAgentConfig( config: TerminalResolvedAgentConfig, ): string | null { @@ -437,15 +400,13 @@ export function buildPromptCommandFromAgentConfig({ const promptCommand = config.promptCommand.trim() || config.command.trim(); if (!promptCommand) return null; - let delimiter = `SUPERSET_PROMPT_${randomId.replaceAll("-", "")}`; - while (prompt.includes(delimiter)) { - delimiter = `${delimiter}_X`; - } - - const suffix = config.promptCommandSuffix?.trim() || undefined; - return shouldUseStdinPrompt(config) - ? buildStdinHeredoc(prompt, delimiter, promptCommand, suffix) - : buildHeredoc(prompt, delimiter, promptCommand, suffix); + return buildPromptCommandString({ + prompt, + randomId, + command: promptCommand, + suffix: config.promptCommandSuffix?.trim() || undefined, + transport: config.promptTransport, + }); } export function buildFileCommandFromAgentConfig({ @@ -458,10 +419,12 @@ export function buildFileCommandFromAgentConfig({ const promptCommand = config.promptCommand.trim() || config.command.trim(); if (!promptCommand) return null; - const suffix = config.promptCommandSuffix?.trim() || undefined; - return shouldUseStdinPrompt(config) - ? buildStdinFileCommand(filePath, promptCommand, suffix) - : buildFileCommand(filePath, promptCommand, suffix); + return buildPromptFileCommandString({ + filePath, + command: promptCommand, + suffix: config.promptCommandSuffix?.trim() || undefined, + transport: config.promptTransport, + }); } export function buildDefaultTerminalTaskPrompt(task: TaskInput): string { diff --git a/packages/shared/package.json b/packages/shared/package.json index 86f5836a6b8..2e0ea3f0e5d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -24,6 +24,10 @@ "types": "./src/agent-command.ts", "default": "./src/agent-command.ts" }, + "./agent-prompt-launch": { + "types": "./src/agent-prompt-launch.ts", + "default": "./src/agent-prompt-launch.ts" + }, "./agent-catalog": { "types": "./src/agent-catalog.ts", "default": "./src/agent-catalog.ts" diff --git a/packages/shared/src/agent-catalog.ts b/packages/shared/src/agent-catalog.ts index d69779f4b26..2ebade3f476 100644 --- a/packages/shared/src/agent-catalog.ts +++ b/packages/shared/src/agent-catalog.ts @@ -3,6 +3,7 @@ import { AGENT_PROMPT_COMMANDS, AGENT_TYPES, } from "./agent-command"; +import type { PromptTransport } from "./agent-prompt-launch"; import { DEFAULT_CHAT_TASK_PROMPT_TEMPLATE, DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, @@ -30,6 +31,7 @@ export interface TerminalAgentDefinition extends BaseAgentDefinition { defaultCommand: string; defaultPromptCommand: string; defaultPromptCommandSuffix?: string; + defaultPromptTransport: PromptTransport; defaultTaskPromptTemplate: string; } @@ -60,6 +62,7 @@ function createBuiltinTerminalAgentDefinition( defaultCommand: agent.command, defaultPromptCommand: promptCommand.command, defaultPromptCommandSuffix: promptCommand.suffix, + defaultPromptTransport: promptCommand.transport, defaultTaskPromptTemplate: DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, defaultEnabled: true, }; diff --git a/packages/shared/src/agent-command.ts b/packages/shared/src/agent-command.ts index 94489e1704b..eb66cf19ac3 100644 --- a/packages/shared/src/agent-command.ts +++ b/packages/shared/src/agent-command.ts @@ -1,3 +1,8 @@ +import { + buildPromptCommandString, + buildPromptFileCommandString, + type PromptTransport, +} from "./agent-prompt-launch"; import { DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, renderTaskPromptTemplate, @@ -32,6 +37,7 @@ export const AGENT_PRESET_DESCRIPTIONS: Record = export interface AgentPromptCommandDefaults { command: string; suffix?: string; + transport: PromptTransport; } export const AGENT_PROMPT_COMMANDS: Record< @@ -53,53 +59,6 @@ export function buildAgentTaskPrompt(task: TaskInput): string { return renderTaskPromptTemplate(DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, task); } -function buildHeredoc( - prompt: string, - delimiter: string, - command: string, - suffix?: string, -): string { - const closing = suffix ? `)" ${suffix}` : ')"'; - return [ - `${command} "$(cat <<'${delimiter}'`, - prompt, - delimiter, - closing, - ].join("\n"); -} - -function buildStdinHeredoc( - prompt: string, - delimiter: string, - command: string, - suffix?: string, -): string { - const fullCommand = suffix ? `${command} ${suffix}` : command; - return [`${fullCommand} <<'${delimiter}'`, prompt, delimiter].join("\n"); -} - -function buildFileCommand( - filePath: string, - command: string, - suffix?: string, -): string { - const escapedPath = filePath.replaceAll("'", "'\\''"); - return `${command} "$(cat '${escapedPath}')"${suffix ? ` ${suffix}` : ""}`; -} - -function buildStdinFileCommand( - filePath: string, - command: string, - suffix?: string, -): string { - const escapedPath = filePath.replaceAll("'", "'\\''"); - return `${command}${suffix ? ` ${suffix}` : ""} < '${escapedPath}'`; -} - -function shouldUseStdinPrompt(agent: AgentType): boolean { - return agent === "amp"; -} - export function buildAgentFileCommand({ filePath, agent = "claude", @@ -108,13 +67,12 @@ export function buildAgentFileCommand({ agent?: AgentType; }): string { const promptCommand = AGENT_PROMPT_COMMANDS[agent]; - return shouldUseStdinPrompt(agent) - ? buildStdinFileCommand( - filePath, - promptCommand.command, - promptCommand.suffix, - ) - : buildFileCommand(filePath, promptCommand.command, promptCommand.suffix); + return buildPromptFileCommandString({ + filePath, + command: promptCommand.command, + suffix: promptCommand.suffix, + transport: promptCommand.transport, + }); } export function buildAgentPromptCommand({ @@ -126,24 +84,14 @@ export function buildAgentPromptCommand({ randomId: string; agent?: AgentType; }): string { - let delimiter = `SUPERSET_PROMPT_${randomId.replaceAll("-", "")}`; - while (prompt.includes(delimiter)) { - delimiter = `${delimiter}_X`; - } const promptCommand = AGENT_PROMPT_COMMANDS[agent]; - return shouldUseStdinPrompt(agent) - ? buildStdinHeredoc( - prompt, - delimiter, - promptCommand.command, - promptCommand.suffix, - ) - : buildHeredoc( - prompt, - delimiter, - promptCommand.command, - promptCommand.suffix, - ); + return buildPromptCommandString({ + prompt, + randomId, + command: promptCommand.command, + suffix: promptCommand.suffix, + transport: promptCommand.transport, + }); } export function buildAgentCommand({ diff --git a/packages/shared/src/agent-prompt-launch.ts b/packages/shared/src/agent-prompt-launch.ts new file mode 100644 index 00000000000..f1a33fd95a3 --- /dev/null +++ b/packages/shared/src/agent-prompt-launch.ts @@ -0,0 +1,66 @@ +/** + * Prompt transports define the small set of ways a CLI can receive prompt + * payloads. Keep this enum intentionally small and add a new transport only + * when a real agent requires it. Avoid arbitrary per-agent shell templates. + */ +export type PromptTransport = "argv" | "stdin"; + +function resolveDelimiter(prompt: string, randomId: string): string { + let delimiter = `SUPERSET_PROMPT_${randomId.replaceAll("-", "")}`; + while (prompt.includes(delimiter)) { + delimiter = `${delimiter}_X`; + } + return delimiter; +} + +function quoteSingleShell(value: string): string { + return value.replaceAll("'", "'\\''"); +} + +function joinCommand(command: string, suffix?: string): string { + return suffix ? `${command} ${suffix}` : command; +} + +export function buildPromptCommandString({ + command, + suffix, + transport, + prompt, + randomId, +}: { + command: string; + suffix?: string; + transport: PromptTransport; + prompt: string; + randomId: string; +}): string { + const delimiter = resolveDelimiter(prompt, randomId); + const fullCommand = joinCommand(command, suffix); + + if (transport === "stdin") { + return `${fullCommand} <<'${delimiter}'\n${prompt}\n${delimiter}`; + } + + return `${command} "$(cat <<'${delimiter}'\n${prompt}\n${delimiter}\n)"${suffix ? ` ${suffix}` : ""}`; +} + +export function buildPromptFileCommandString({ + command, + suffix, + transport, + filePath, +}: { + command: string; + suffix?: string; + transport: PromptTransport; + filePath: string; +}): string { + const escapedPath = quoteSingleShell(filePath); + const fullCommand = joinCommand(command, suffix); + + if (transport === "stdin") { + return `${fullCommand} < '${escapedPath}'`; + } + + return `${command} "$(cat '${escapedPath}')"${suffix ? ` ${suffix}` : ""}`; +} diff --git a/packages/shared/src/builtin-terminal-agents.ts b/packages/shared/src/builtin-terminal-agents.ts index c7c2cdf3163..0b13444bed8 100644 --- a/packages/shared/src/builtin-terminal-agents.ts +++ b/packages/shared/src/builtin-terminal-agents.ts @@ -1,3 +1,5 @@ +import type { PromptTransport } from "./agent-prompt-launch"; + export interface BuiltinTerminalAgentManifest { id: string; label: string; @@ -5,6 +7,12 @@ export interface BuiltinTerminalAgentManifest { command: string; promptCommand?: string; promptCommandSuffix?: string; + /** + * Built-ins can opt into a non-default prompt transport when the CLI only + * supports interactive stdin flows. Keep this declarative; shell rendering + * lives in the shared prompt-launch helper. + */ + promptTransport?: PromptTransport; includeInDefaultTerminalPresets?: boolean; } @@ -46,6 +54,7 @@ export const BUILTIN_TERMINAL_AGENTS = [ "Amp's coding agent for terminal-first coding, subagents, and task work.", command: "amp", promptCommand: "amp", + promptTransport: "stdin", includeInDefaultTerminalPresets: true, }, { @@ -137,10 +146,18 @@ export const BUILTIN_TERMINAL_AGENT_COMMANDS = createAgentRecord( export const BUILTIN_TERMINAL_AGENT_PROMPT_COMMANDS = createAgentRecord( BUILTIN_TERMINAL_AGENTS, - (agent) => ({ + ( + agent, + ): { + command: string; + suffix?: string; + transport: PromptTransport; + } => ({ command: "promptCommand" in agent ? agent.promptCommand : agent.command, suffix: "promptCommandSuffix" in agent ? agent.promptCommandSuffix : undefined, + transport: + "promptTransport" in agent ? (agent.promptTransport ?? "argv") : "argv", }), ); From 6f68173260b0343198336c880bfad1d4f5deb30e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Mon, 30 Mar 2026 11:19:36 -0700 Subject: [PATCH 11/12] Unify builtin and custom agent models --- .../agent-preset-router.utils.test.ts | 11 +- .../settings/agent-preset-router.utils.ts | 42 +++-- .../src/lib/trpc/routers/settings/index.ts | 6 + .../components/AgentCard/AgentCard.tsx | 44 +++-- .../src/shared/utils/agent-settings.test.ts | 12 +- .../src/shared/utils/agent-settings.ts | 150 +++++++++--------- packages/local-db/src/schema/zod.ts | 7 +- packages/shared/package.json | 4 + packages/shared/src/agent-catalog.ts | 110 +++++-------- packages/shared/src/agent-command.ts | 14 +- packages/shared/src/agent-definition.ts | 57 +++++++ .../shared/src/builtin-terminal-agents.ts | 105 ++++++------ 12 files changed, 337 insertions(+), 225 deletions(-) create mode 100644 packages/shared/src/agent-definition.ts diff --git a/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.test.ts b/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.test.ts index 24e8c04f6e0..5a107afb4b3 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.test.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.test.ts @@ -95,7 +95,6 @@ describe("custom agent schemas", () => { const result = createCustomAgentInputSchema.safeParse({ label: " Team Agent ", command: " team-agent ", - promptCommand: " team-agent --prompt ", taskPromptTemplate: " Task {{slug}} ", }); @@ -109,8 +108,9 @@ describe("custom agent normalization", () => { label: " Team Agent ", description: " ", command: " team-agent ", - promptCommand: " team-agent --prompt ", + promptCommand: " team-agent ", promptCommandSuffix: " ", + promptTransport: "argv", taskPromptTemplate: " Task {{slug}} ", enabled: false, }); @@ -119,8 +119,9 @@ describe("custom agent normalization", () => { label: "Team Agent", description: undefined, command: "team-agent", - promptCommand: "team-agent --prompt", + promptCommand: undefined, promptCommandSuffix: undefined, + promptTransport: undefined, taskPromptTemplate: "Task {{slug}}", enabled: false, }); @@ -128,14 +129,18 @@ describe("custom agent normalization", () => { test("normalizes custom-agent patches and clears blank optional strings to null", () => { const normalized = normalizeCustomAgentPatch({ + promptCommand: " ", description: " ", promptCommandSuffix: " ", + promptTransport: "argv", command: " team-agent ", }); expect(normalized).toEqual({ + promptCommand: null, description: null, promptCommandSuffix: null, + promptTransport: null, command: "team-agent", }); }); diff --git a/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.ts b/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.ts index 5a8864a4501..2693ad7e22a 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/agent-preset-router.utils.ts @@ -1,3 +1,4 @@ +import { PROMPT_TRANSPORTS } from "@superset/local-db"; import type { AgentDefinition } from "@superset/shared/agent-catalog"; import { TRPCError } from "@trpc/server"; import type { @@ -29,16 +30,26 @@ export const createCustomAgentInputSchema = z.object({ label: z.string(), description: z.string().nullable().optional(), command: z.string(), - promptCommand: z.string(), + promptCommand: z.string().optional(), promptCommandSuffix: z.string().nullable().optional(), + promptTransport: z.enum(PROMPT_TRANSPORTS).optional(), taskPromptTemplate: z.string(), enabled: z.boolean().optional(), }); export const updateCustomAgentInputSchema = z.object({ id: z.string().regex(/^custom:/), - patch: createCustomAgentInputSchema - .partial() + patch: z + .object({ + label: z.string().optional(), + description: z.string().nullable().optional(), + command: z.string().optional(), + promptCommand: z.string().nullable().optional(), + promptCommandSuffix: z.string().nullable().optional(), + promptTransport: z.enum(PROMPT_TRANSPORTS).nullable().optional(), + taskPromptTemplate: z.string().optional(), + enabled: z.boolean().optional(), + }) .refine((patch) => Object.keys(patch).length > 0, { message: "Patch must include at least one field", }), @@ -128,6 +139,7 @@ function normalizeOptionalText( export function normalizeCreateCustomAgentInput( input: z.infer, ) { + const command = toTrimmedRequiredValue("Command", input.command); const taskPromptTemplate = toTrimmedRequiredValue( "Task prompt template", input.taskPromptTemplate, @@ -140,16 +152,19 @@ export function normalizeCreateCustomAgentInput( }); } + const promptCommand = normalizeOptionalText(input.promptCommand) ?? undefined; + return { label: toTrimmedRequiredValue("Label", input.label), description: normalizeOptionalText(input.description) ?? undefined, - command: toTrimmedRequiredValue("Command", input.command), - promptCommand: toTrimmedRequiredValue( - "Prompt command", - input.promptCommand, - ), + command, + promptCommand: promptCommand === command ? undefined : promptCommand, promptCommandSuffix: normalizeOptionalText(input.promptCommandSuffix) ?? undefined, + promptTransport: + input.promptTransport && input.promptTransport !== "argv" + ? input.promptTransport + : undefined, taskPromptTemplate, enabled: input.enabled, } as const; @@ -173,16 +188,19 @@ export function normalizeCustomAgentPatch( normalized.command = toTrimmedRequiredValue("Command", patch.command); } if (patch.promptCommand !== undefined) { - normalized.promptCommand = toTrimmedRequiredValue( - "Prompt command", - patch.promptCommand, - ); + normalized.promptCommand = normalizeOptionalText(patch.promptCommand); } if (patch.promptCommandSuffix !== undefined) { normalized.promptCommandSuffix = normalizeOptionalText( patch.promptCommandSuffix, ); } + if (patch.promptTransport !== undefined) { + normalized.promptTransport = + patch.promptTransport && patch.promptTransport !== "argv" + ? patch.promptTransport + : null; + } if (patch.taskPromptTemplate !== undefined) { const taskPromptTemplate = toTrimmedRequiredValue( "Task prompt template", diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index 09a6ea3e48b..9fcd6ed7964 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -306,6 +306,12 @@ export const createSettingsRouter = () => { message: `Agent preset ${input.id} not found`, }); } + if (definition.source === "user") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Custom agent ${input.id} must be edited through custom-agent settings`, + }); + } const normalizedPatch = normalizeAgentPresetPatch({ definition, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/AgentCard.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/AgentCard.tsx index c659b912290..fdbda0b4b5b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/AgentCard.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/AgentCard.tsx @@ -27,11 +27,20 @@ export function AgentCard({ showTaskPrompts, }: AgentCardProps) { const utils = electronTrpc.useUtils(); + const isCustomTerminalAgent = + preset.source === "user" && preset.kind === "terminal"; const updatePreset = electronTrpc.settings.updateAgentPreset.useMutation({ onSuccess: async () => { await utils.settings.getAgentPresets.invalidate(); }, }); + const updateCustomAgent = electronTrpc.settings.updateCustomAgent.useMutation( + { + onSuccess: async () => { + await utils.settings.getAgentPresets.invalidate(); + }, + }, + ); const resetPreset = electronTrpc.settings.resetAgentPreset.useMutation({ onSuccess: async () => { await utils.settings.getAgentPresets.invalidate(); @@ -65,6 +74,11 @@ export function AgentCard({ setInputVersion((current) => current + 1); }; + const isMutating = + updatePreset.isPending || + updateCustomAgent.isPending || + resetPreset.isPending; + const mergePresetPatch = ( currentPreset: ResolvedAgentConfig, patch: AgentPresetPatch, @@ -116,10 +130,23 @@ export function AgentCard({ ); try { - const updatedPreset = await updatePreset.mutateAsync({ - id: preset.id, - patch, - }); + const updatedPreset = isCustomTerminalAgent + ? await updateCustomAgent.mutateAsync({ + id: preset.id, + patch: { + enabled: patch.enabled, + label: patch.label, + description: patch.description, + command: patch.command, + promptCommand: patch.promptCommand, + promptCommandSuffix: patch.promptCommandSuffix, + taskPromptTemplate: patch.taskPromptTemplate, + }, + }) + : await updatePreset.mutateAsync({ + id: preset.id, + patch, + }); if (updatedPreset) { utils.settings.getAgentPresets.setData(undefined, (currentPresets) => currentPresets?.map((candidate) => @@ -191,7 +218,7 @@ export function AgentCard({ isOpen={isOpen} showEnabled={showEnabled} enabled={preset.enabled} - isUpdatingEnabled={updatePreset.isPending || resetPreset.isPending} + isUpdatingEnabled={isMutating} onEnabledChange={handleEnabledChange} onToggle={() => handleOpenChange(!isOpen)} /> @@ -214,10 +241,9 @@ export function AgentCard({ onToggle={() => setShowPreview((current) => !current)} /> - + {preset.source === "builtin" && ( + + )} diff --git a/apps/desktop/src/shared/utils/agent-settings.test.ts b/apps/desktop/src/shared/utils/agent-settings.test.ts index 0779921f6fc..d77b67c7270 100644 --- a/apps/desktop/src/shared/utils/agent-settings.test.ts +++ b/apps/desktop/src/shared/utils/agent-settings.test.ts @@ -85,7 +85,7 @@ describe("resolveAgentConfigs", () => { label: "Team Agent", description: "Team wrapper", command: "team-agent", - promptCommand: "team-agent --prompt", + promptTransport: "stdin", taskPromptTemplate: "Task {{slug}}", enabled: false, }, @@ -98,7 +98,8 @@ describe("resolveAgentConfigs", () => { kind: "terminal", label: "Team Agent", command: "team-agent", - promptCommand: "team-agent --prompt", + promptCommand: "team-agent", + promptTransport: "stdin", taskPromptTemplate: "Task {{slug}}", enabled: false, }); @@ -116,7 +117,7 @@ describe("createOverrideEnvelopeWithPatch", () => { }, id: "claude", patch: { - label: definition.defaultLabel, + label: definition.label, description: null, }, }); @@ -175,7 +176,6 @@ describe("custom agent definition helpers", () => { kind: "terminal", label: "Team Agent", command: "team-agent", - promptCommand: "team-agent --prompt", taskPromptTemplate: "Task {{slug}}", }, }); @@ -190,6 +190,7 @@ describe("custom agent definition helpers", () => { patch: { description: "Shared team wrapper", promptCommandSuffix: "--yolo", + promptTransport: "stdin", enabled: false, }, }); @@ -198,6 +199,7 @@ describe("custom agent definition helpers", () => { id: "custom:team-agent", description: "Shared team wrapper", promptCommandSuffix: "--yolo", + promptTransport: "stdin", enabled: false, }); }); @@ -210,7 +212,6 @@ describe("custom agent definition helpers", () => { kind: "terminal", label: "Keep", command: "keep", - promptCommand: "keep --prompt", taskPromptTemplate: "Task {{slug}}", }, { @@ -218,7 +219,6 @@ describe("custom agent definition helpers", () => { kind: "terminal", label: "Remove", command: "remove", - promptCommand: "remove --prompt", taskPromptTemplate: "Task {{slug}}", }, ], diff --git a/apps/desktop/src/shared/utils/agent-settings.ts b/apps/desktop/src/shared/utils/agent-settings.ts index cc35a2bf2e6..1672080066e 100644 --- a/apps/desktop/src/shared/utils/agent-settings.ts +++ b/apps/desktop/src/shared/utils/agent-settings.ts @@ -10,10 +10,12 @@ import { type AgentDefinition, type AgentDefinitionId, BUILTIN_AGENT_DEFINITIONS, + type ChatAgentDefinition, isTerminalAgentDefinition, type TerminalAgentDefinition, } from "@superset/shared/agent-catalog"; import type { TaskInput } from "@superset/shared/agent-command"; +import { createTerminalAgentDefinition } from "@superset/shared/agent-definition"; import { buildPromptCommandString, buildPromptFileCommandString, @@ -50,30 +52,16 @@ const EMPTY_AGENT_PRESET_OVERRIDE_ENVELOPE: AgentPresetOverrideEnvelope = { presets: [], }; -export type TerminalResolvedAgentConfig = { +export type TerminalResolvedAgentConfig = Omit< + TerminalAgentDefinition, + "id" +> & { id: AgentDefinitionId; - source: "builtin" | "user"; - kind: "terminal"; - label: string; - description?: string; - enabled: boolean; - command: string; - promptCommand: string; - promptCommandSuffix?: string; - promptTransport: PromptTransport; - taskPromptTemplate: string; overriddenFields: AgentPresetField[]; }; -export type ChatResolvedAgentConfig = { +export type ChatResolvedAgentConfig = Omit & { id: AgentDefinitionId; - source: "builtin" | "user"; - kind: "chat"; - label: string; - description?: string; - enabled: boolean; - taskPromptTemplate: string; - model?: string; overriddenFields: AgentPresetField[]; }; @@ -97,29 +85,43 @@ export type CustomAgentDefinitionPatch = Partial<{ label: string; description: string | null; command: string; - promptCommand: string; + promptCommand: string | null; promptCommandSuffix: string | null; + promptTransport: PromptTransport | null; taskPromptTemplate: string; }>; -function toCustomAgentDefinition( +function toUserTerminalAgentDefinition( customDefinition: AgentCustomDefinition, ): TerminalAgentDefinition { - return { + return createTerminalAgentDefinition({ id: customDefinition.id as `custom:${string}`, source: "user", kind: "terminal", - defaultLabel: customDefinition.label, - defaultDescription: customDefinition.description, - defaultCommand: customDefinition.command, - defaultPromptCommand: customDefinition.promptCommand, - defaultPromptCommandSuffix: customDefinition.promptCommandSuffix, - // Custom agents stay on the default argv transport until we intentionally - // expose prompt transport as part of user-configurable CRUD. - defaultPromptTransport: "argv", - defaultTaskPromptTemplate: customDefinition.taskPromptTemplate, - defaultEnabled: customDefinition.enabled ?? true, - }; + label: customDefinition.label, + description: customDefinition.description, + command: customDefinition.command, + promptCommand: customDefinition.promptCommand, + promptCommandSuffix: customDefinition.promptCommandSuffix, + promptTransport: customDefinition.promptTransport, + taskPromptTemplate: customDefinition.taskPromptTemplate, + enabled: customDefinition.enabled ?? true, + }); +} + +function canonicalizeCustomAgentDefinition( + definition: AgentCustomDefinition, +): AgentCustomDefinition { + const nextDefinition: AgentCustomDefinition = { ...definition }; + + if (nextDefinition.promptCommand === nextDefinition.command) { + nextDefinition.promptCommand = undefined; + } + if (nextDefinition.promptTransport === "argv") { + nextDefinition.promptTransport = undefined; + } + + return agentCustomDefinitionSchema.parse(nextDefinition); } export function readAgentCustomDefinitions( @@ -127,7 +129,9 @@ export function readAgentCustomDefinitions( ): AgentCustomDefinition[] { return (customDefinitions ?? []).flatMap((definition) => { const parsed = agentCustomDefinitionSchema.safeParse(definition); - return parsed.success ? [parsed.data] : []; + return parsed.success + ? [canonicalizeCustomAgentDefinition(parsed.data)] + : []; }); } @@ -146,7 +150,7 @@ export function getAgentDefinitions( return [ ...BUILTIN_AGENT_DEFINITIONS, ...readAgentCustomDefinitions(customDefinitions).map((definition) => - toCustomAgentDefinition(definition), + toUserTerminalAgentDefinition(definition), ), ]; } @@ -173,7 +177,9 @@ export function upsertCustomAgentDefinition({ definition: AgentCustomDefinition; }): AgentCustomDefinition[] { const definitions = readAgentCustomDefinitions(currentDefinitions); - const nextDefinition = agentCustomDefinitionSchema.parse(definition); + const nextDefinition = canonicalizeCustomAgentDefinition( + agentCustomDefinitionSchema.parse(definition), + ); const index = definitions.findIndex( (candidate) => candidate.id === nextDefinition.id, ); @@ -211,11 +217,14 @@ export function applyCustomAgentDefinitionPatch({ Object.hasOwn(patch, "promptCommand") && patch.promptCommand !== undefined ) { - nextDefinition.promptCommand = patch.promptCommand; + nextDefinition.promptCommand = patch.promptCommand ?? undefined; } if (Object.hasOwn(patch, "promptCommandSuffix")) { nextDefinition.promptCommandSuffix = patch.promptCommandSuffix ?? undefined; } + if (Object.hasOwn(patch, "promptTransport")) { + nextDefinition.promptTransport = patch.promptTransport ?? undefined; + } if ( Object.hasOwn(patch, "taskPromptTemplate") && patch.taskPromptTemplate !== undefined @@ -253,11 +262,11 @@ function getOverriddenFields( } function resolveDescription( - defaultDescription: string | undefined, + description: string | undefined, override: AgentPresetOverride | undefined, ): string | undefined { if (!override || !Object.hasOwn(override, "description")) { - return defaultDescription; + return description; } return override.description ?? undefined; @@ -275,11 +284,11 @@ function resolvePromptCommandSuffix( } function resolveModel( - defaultModel: string | undefined, + model: string | undefined, override: AgentPresetOverride | undefined, ): string | undefined { if (!override || !Object.hasOwn(override, "model")) { - return defaultModel; + return model; } return override.model?.trim() || undefined; @@ -291,35 +300,32 @@ function resolveAgentConfig( ): ResolvedAgentConfig { if (isTerminalAgentDefinition(definition)) { return { - id: definition.id, - source: definition.source, - kind: "terminal", - label: override?.label ?? definition.defaultLabel, - description: resolveDescription(definition.defaultDescription, override), - enabled: override?.enabled ?? definition.defaultEnabled, - command: override?.command ?? definition.defaultCommand, - promptCommand: override?.promptCommand ?? definition.defaultPromptCommand, + ...definition, + id: definition.id as AgentDefinitionId, + label: override?.label ?? definition.label, + description: resolveDescription(definition.description, override), + enabled: override?.enabled ?? definition.enabled, + command: override?.command ?? definition.command, + promptCommand: override?.promptCommand ?? definition.promptCommand, promptCommandSuffix: resolvePromptCommandSuffix( - definition.defaultPromptCommandSuffix, + definition.promptCommandSuffix, override, ), - promptTransport: definition.defaultPromptTransport, taskPromptTemplate: - override?.taskPromptTemplate ?? definition.defaultTaskPromptTemplate, + override?.taskPromptTemplate ?? definition.taskPromptTemplate, overriddenFields: getOverriddenFields(override, definition), }; } return { - id: definition.id, - source: definition.source, - kind: "chat", - label: override?.label ?? definition.defaultLabel, - description: resolveDescription(definition.defaultDescription, override), - enabled: override?.enabled ?? definition.defaultEnabled, + ...definition, + id: definition.id as AgentDefinitionId, + label: override?.label ?? definition.label, + description: resolveDescription(definition.description, override), + enabled: override?.enabled ?? definition.enabled, taskPromptTemplate: - override?.taskPromptTemplate ?? definition.defaultTaskPromptTemplate, - model: resolveModel(definition.defaultModel, override), + override?.taskPromptTemplate ?? definition.taskPromptTemplate, + model: resolveModel(definition.model, override), overriddenFields: getOverriddenFields(override, definition), }; } @@ -477,17 +483,13 @@ export function createOverrideEnvelopeWithPatch({ Object.hasOwn(patch, field); if (hasField("enabled")) { - setOrDelete( - "enabled", - patch.enabled, - patch.enabled !== definition.defaultEnabled, - ); + setOrDelete("enabled", patch.enabled, patch.enabled !== definition.enabled); } if (hasField("label")) { - setOrDelete("label", patch.label, patch.label !== definition.defaultLabel); + setOrDelete("label", patch.label, patch.label !== definition.label); } if (hasField("description")) { - const defaultDescription = definition.defaultDescription; + const defaultDescription = definition.description; const shouldPersist = patch.description === null ? defaultDescription !== undefined @@ -498,7 +500,7 @@ export function createOverrideEnvelopeWithPatch({ setOrDelete( "taskPromptTemplate", patch.taskPromptTemplate, - patch.taskPromptTemplate !== definition.defaultTaskPromptTemplate, + patch.taskPromptTemplate !== definition.taskPromptTemplate, ); } @@ -507,21 +509,21 @@ export function createOverrideEnvelopeWithPatch({ setOrDelete( "command", patch.command, - patch.command !== definition.defaultCommand, + patch.command !== definition.command, ); } if (hasField("promptCommand")) { setOrDelete( "promptCommand", patch.promptCommand, - patch.promptCommand !== definition.defaultPromptCommand, + patch.promptCommand !== definition.promptCommand, ); } if (hasField("promptCommandSuffix")) { const shouldPersist = patch.promptCommandSuffix === null - ? definition.defaultPromptCommandSuffix !== undefined - : patch.promptCommandSuffix !== definition.defaultPromptCommandSuffix; + ? definition.promptCommandSuffix !== undefined + : patch.promptCommandSuffix !== definition.promptCommandSuffix; setOrDelete( "promptCommandSuffix", patch.promptCommandSuffix, @@ -531,8 +533,8 @@ export function createOverrideEnvelopeWithPatch({ } else if (hasField("model")) { const shouldPersist = patch.model === null - ? definition.defaultModel !== undefined - : patch.model !== definition.defaultModel; + ? definition.model !== undefined + : patch.model !== definition.model; setOrDelete("model", patch.model ?? undefined, shouldPersist); } diff --git a/packages/local-db/src/schema/zod.ts b/packages/local-db/src/schema/zod.ts index a85ca4459aa..83b6f2ded48 100644 --- a/packages/local-db/src/schema/zod.ts +++ b/packages/local-db/src/schema/zod.ts @@ -129,6 +129,10 @@ export const AGENT_PRESET_FIELDS = [ export type AgentPresetField = (typeof AGENT_PRESET_FIELDS)[number]; +export const PROMPT_TRANSPORTS = ["argv", "stdin"] as const; + +export type PromptTransport = (typeof PROMPT_TRANSPORTS)[number]; + export const agentPresetOverrideSchema = z.object({ id: z.string(), enabled: z.boolean().optional(), @@ -158,8 +162,9 @@ export const agentCustomDefinitionSchema = z.object({ label: z.string(), description: z.string().optional(), command: z.string(), - promptCommand: z.string(), + promptCommand: z.string().optional(), promptCommandSuffix: z.string().optional(), + promptTransport: z.enum(PROMPT_TRANSPORTS).optional(), taskPromptTemplate: z.string(), enabled: z.boolean().optional(), }); diff --git a/packages/shared/package.json b/packages/shared/package.json index 2e0ea3f0e5d..3f39690ad79 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -28,6 +28,10 @@ "types": "./src/agent-prompt-launch.ts", "default": "./src/agent-prompt-launch.ts" }, + "./agent-definition": { + "types": "./src/agent-definition.ts", + "default": "./src/agent-definition.ts" + }, "./agent-catalog": { "types": "./src/agent-catalog.ts", "default": "./src/agent-catalog.ts" diff --git a/packages/shared/src/agent-catalog.ts b/packages/shared/src/agent-catalog.ts index 2ebade3f476..685f7614337 100644 --- a/packages/shared/src/agent-catalog.ts +++ b/packages/shared/src/agent-catalog.ts @@ -1,87 +1,53 @@ +import type { + AgentDefinition, + AgentDefinitionSource, + AgentKind, + ChatAgentDefinition, + TerminalAgentDefinition, +} from "./agent-definition"; +import { DEFAULT_CHAT_TASK_PROMPT_TEMPLATE } from "./agent-prompt-template"; import { - AGENT_LABELS, - AGENT_PROMPT_COMMANDS, - AGENT_TYPES, -} from "./agent-command"; -import type { PromptTransport } from "./agent-prompt-launch"; -import { - DEFAULT_CHAT_TASK_PROMPT_TEMPLATE, - DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, -} from "./agent-prompt-template"; -import { BUILTIN_TERMINAL_AGENTS } from "./builtin-terminal-agents"; + BUILTIN_TERMINAL_AGENT_TYPES, + BUILTIN_TERMINAL_AGENTS, +} from "./builtin-terminal-agents"; -export const BUILTIN_AGENT_IDS = [...AGENT_TYPES, "superset-chat"] as const; +export const BUILTIN_AGENT_IDS = [ + ...BUILTIN_TERMINAL_AGENT_TYPES, + "superset-chat", +] as const; export type BuiltinAgentId = (typeof BUILTIN_AGENT_IDS)[number]; export type AgentDefinitionId = BuiltinAgentId | `custom:${string}`; -export type AgentDefinitionSource = "builtin" | "user"; -export type AgentKind = "terminal" | "chat"; - -interface BaseAgentDefinition { - id: AgentDefinitionId; - source: AgentDefinitionSource; - kind: AgentKind; - defaultLabel: string; - defaultDescription?: string; - defaultEnabled: boolean; -} - -export interface TerminalAgentDefinition extends BaseAgentDefinition { - kind: "terminal"; - defaultCommand: string; - defaultPromptCommand: string; - defaultPromptCommandSuffix?: string; - defaultPromptTransport: PromptTransport; - defaultTaskPromptTemplate: string; -} - -export interface ChatAgentDefinition extends BaseAgentDefinition { - kind: "chat"; - defaultTaskPromptTemplate: string; - defaultModel?: string; -} -export type AgentDefinition = TerminalAgentDefinition | ChatAgentDefinition; +export type { + AgentDefinition, + AgentDefinitionSource, + AgentKind, + ChatAgentDefinition, + TerminalAgentDefinition, +}; export const BUILTIN_AGENT_LABELS: Record = { - ...AGENT_LABELS, + ...Object.fromEntries( + BUILTIN_TERMINAL_AGENTS.map((agent) => [agent.id, agent.label]), + ), "superset-chat": "Superset Chat", -}; - -function createBuiltinTerminalAgentDefinition( - agent: (typeof BUILTIN_TERMINAL_AGENTS)[number], -): TerminalAgentDefinition { - const promptCommand = AGENT_PROMPT_COMMANDS[agent.id]; +} as Record; - return { - id: agent.id, - source: "builtin", - kind: "terminal", - defaultLabel: agent.label, - defaultDescription: agent.description, - defaultCommand: agent.command, - defaultPromptCommand: promptCommand.command, - defaultPromptCommandSuffix: promptCommand.suffix, - defaultPromptTransport: promptCommand.transport, - defaultTaskPromptTemplate: DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, - defaultEnabled: true, - }; -} +const BUILTIN_CHAT_AGENT: ChatAgentDefinition = { + id: "superset-chat", + source: "builtin", + kind: "chat", + label: "Superset Chat", + description: + "Superset's built-in workspace chat for project-aware help and task launches.", + enabled: true, + taskPromptTemplate: DEFAULT_CHAT_TASK_PROMPT_TEMPLATE, +}; export const BUILTIN_AGENT_DEFINITIONS: AgentDefinition[] = [ - ...BUILTIN_TERMINAL_AGENTS.map((agent) => - createBuiltinTerminalAgentDefinition(agent), - ), - { - id: "superset-chat", - source: "builtin", - kind: "chat", - defaultLabel: BUILTIN_AGENT_LABELS["superset-chat"], - defaultDescription: - "Superset's built-in workspace chat for project-aware help and task launches.", - defaultTaskPromptTemplate: DEFAULT_CHAT_TASK_PROMPT_TEMPLATE, - defaultEnabled: true, - }, + ...BUILTIN_TERMINAL_AGENTS, + BUILTIN_CHAT_AGENT, ]; export function getBuiltinAgentDefinition(id: BuiltinAgentId): AgentDefinition { diff --git a/packages/shared/src/agent-command.ts b/packages/shared/src/agent-command.ts index eb66cf19ac3..58704d0d99e 100644 --- a/packages/shared/src/agent-command.ts +++ b/packages/shared/src/agent-command.ts @@ -59,6 +59,16 @@ export function buildAgentTaskPrompt(task: TaskInput): string { return renderTaskPromptTemplate(DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, task); } +function getAgentPromptCommandDefaults( + agent: AgentType, +): AgentPromptCommandDefaults { + const promptCommand = AGENT_PROMPT_COMMANDS[agent]; + if (!promptCommand) { + throw new Error(`Unknown agent prompt command defaults: ${agent}`); + } + return promptCommand; +} + export function buildAgentFileCommand({ filePath, agent = "claude", @@ -66,7 +76,7 @@ export function buildAgentFileCommand({ filePath: string; agent?: AgentType; }): string { - const promptCommand = AGENT_PROMPT_COMMANDS[agent]; + const promptCommand = getAgentPromptCommandDefaults(agent); return buildPromptFileCommandString({ filePath, command: promptCommand.command, @@ -84,7 +94,7 @@ export function buildAgentPromptCommand({ randomId: string; agent?: AgentType; }): string { - const promptCommand = AGENT_PROMPT_COMMANDS[agent]; + const promptCommand = getAgentPromptCommandDefaults(agent); return buildPromptCommandString({ prompt, randomId, diff --git a/packages/shared/src/agent-definition.ts b/packages/shared/src/agent-definition.ts new file mode 100644 index 00000000000..9743194f544 --- /dev/null +++ b/packages/shared/src/agent-definition.ts @@ -0,0 +1,57 @@ +import type { PromptTransport } from "./agent-prompt-launch"; + +export type AgentDefinitionSource = "builtin" | "user"; +export type AgentKind = "terminal" | "chat"; + +interface BaseAgentDefinition { + id: string; + source: AgentDefinitionSource; + kind: AgentKind; + label: string; + description?: string; + enabled: boolean; + taskPromptTemplate: string; +} + +export interface TerminalAgentDefinition extends BaseAgentDefinition { + kind: "terminal"; + command: string; + promptCommand: string; + promptCommandSuffix?: string; + promptTransport: PromptTransport; +} + +export interface TerminalAgentDefinitionInput + extends Omit { + promptCommand?: string; + promptTransport?: PromptTransport; +} + +export interface ChatAgentDefinition extends BaseAgentDefinition { + kind: "chat"; + model?: string; +} + +export type AgentDefinition = TerminalAgentDefinition | ChatAgentDefinition; + +export function createTerminalAgentDefinition( + input: TerminalAgentDefinitionInput, +): TerminalAgentDefinition { + return { + ...input, + promptCommand: input.promptCommand ?? input.command, + promptTransport: input.promptTransport ?? "argv", + }; +} + +export function isTerminalAgentDefinition( + definition: AgentDefinition, +): definition is TerminalAgentDefinition { + return definition.kind === "terminal"; +} + +export function isChatAgentDefinition( + definition: AgentDefinition, +): definition is ChatAgentDefinition { + return definition.kind === "chat"; +} diff --git a/packages/shared/src/builtin-terminal-agents.ts b/packages/shared/src/builtin-terminal-agents.ts index 0b13444bed8..0f3932df330 100644 --- a/packages/shared/src/builtin-terminal-agents.ts +++ b/packages/shared/src/builtin-terminal-agents.ts @@ -1,35 +1,37 @@ +import { + createTerminalAgentDefinition, + type TerminalAgentDefinition, + type TerminalAgentDefinitionInput, +} from "./agent-definition"; import type { PromptTransport } from "./agent-prompt-launch"; +import { DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE } from "./agent-prompt-template"; -export interface BuiltinTerminalAgentManifest { - id: string; - label: string; +interface BuiltinTerminalAgentManifest + extends Omit< + TerminalAgentDefinitionInput, + "source" | "kind" | "enabled" | "taskPromptTemplate" + > { description: string; - command: string; - promptCommand?: string; - promptCommandSuffix?: string; - /** - * Built-ins can opt into a non-default prompt transport when the CLI only - * supports interactive stdin flows. Keep this declarative; shell rendering - * lives in the shared prompt-launch helper. - */ - promptTransport?: PromptTransport; includeInDefaultTerminalPresets?: boolean; } -type AgentIdTuple = { +export interface BuiltinTerminalAgentDefinition + extends TerminalAgentDefinition { + description: string; + includeInDefaultTerminalPresets?: boolean; +} + +type AgentIdTuple = { [K in keyof T]: T[K] extends { id: infer TId } ? TId : never; }; -function mapAgentIds( +function mapAgentIds( agents: T, ): AgentIdTuple { return agents.map((agent) => agent.id) as AgentIdTuple; } -function createAgentRecord< - const T extends readonly BuiltinTerminalAgentManifest[], - TValue, ->( +function createAgentRecord( agents: T, getValue: (agent: T[number]) => TValue, ): Record { @@ -38,26 +40,41 @@ function createAgentRecord< ) as Record; } +function createBuiltinTerminalAgent< + const T extends BuiltinTerminalAgentManifest, +>(manifest: T): BuiltinTerminalAgentDefinition & { id: T["id"] } { + return { + ...createTerminalAgentDefinition({ + ...manifest, + source: "builtin", + kind: "terminal", + enabled: true, + taskPromptTemplate: DEFAULT_TERMINAL_TASK_PROMPT_TEMPLATE, + }), + description: manifest.description, + includeInDefaultTerminalPresets: manifest.includeInDefaultTerminalPresets, + }; +} + export const BUILTIN_TERMINAL_AGENTS = [ - { + createBuiltinTerminalAgent({ id: "claude", label: "Claude", description: "Anthropic's coding agent for reading code, editing files, and running terminal workflows.", command: "claude --dangerously-skip-permissions", includeInDefaultTerminalPresets: true, - }, - { + }), + createBuiltinTerminalAgent({ id: "amp", label: "Amp", description: "Amp's coding agent for terminal-first coding, subagents, and task work.", command: "amp", - promptCommand: "amp", promptTransport: "stdin", includeInDefaultTerminalPresets: true, - }, - { + }), + createBuiltinTerminalAgent({ id: "codex", label: "Codex", description: @@ -67,8 +84,8 @@ export const BUILTIN_TERMINAL_AGENTS = [ promptCommand: 'codex -c model_reasoning_effort="high" --dangerously-bypass-approvals-and-sandbox -c model_reasoning_summary="detailed" -c model_supports_reasoning_summaries=true --', includeInDefaultTerminalPresets: true, - }, - { + }), + createBuiltinTerminalAgent({ id: "gemini", label: "Gemini", description: @@ -77,32 +94,32 @@ export const BUILTIN_TERMINAL_AGENTS = [ promptCommand: "gemini", promptCommandSuffix: "--yolo", includeInDefaultTerminalPresets: true, - }, - { + }), + createBuiltinTerminalAgent({ id: "mastracode", label: "Mastracode", description: "Mastra's coding agent for building, debugging, and shipping code from the terminal.", command: "mastracode", includeInDefaultTerminalPresets: true, - }, - { + }), + createBuiltinTerminalAgent({ id: "opencode", label: "OpenCode", description: "Open-source coding agent for the terminal, IDE, and desktop.", command: "opencode", promptCommand: "opencode --prompt", includeInDefaultTerminalPresets: true, - }, - { + }), + createBuiltinTerminalAgent({ id: "pi", label: "Pi", description: "Minimal terminal coding harness for flexible coding workflows.", command: "pi", includeInDefaultTerminalPresets: true, - }, - { + }), + createBuiltinTerminalAgent({ id: "copilot", label: "Copilot", description: @@ -111,16 +128,16 @@ export const BUILTIN_TERMINAL_AGENTS = [ promptCommand: "copilot -i --allow-all", promptCommandSuffix: "--yolo", includeInDefaultTerminalPresets: true, - }, - { + }), + createBuiltinTerminalAgent({ id: "cursor-agent", label: "Cursor Agent", description: "Cursor's coding agent for editing, running, and debugging code in parallel.", command: "cursor-agent", promptCommandSuffix: "--yolo", - }, -] as const satisfies readonly BuiltinTerminalAgentManifest[]; + }), +] as const; export type BuiltinTerminalAgentType = (typeof BUILTIN_TERMINAL_AGENTS)[number]["id"]; @@ -153,17 +170,13 @@ export const BUILTIN_TERMINAL_AGENT_PROMPT_COMMANDS = createAgentRecord( suffix?: string; transport: PromptTransport; } => ({ - command: "promptCommand" in agent ? agent.promptCommand : agent.command, - suffix: - "promptCommandSuffix" in agent ? agent.promptCommandSuffix : undefined, - transport: - "promptTransport" in agent ? (agent.promptTransport ?? "argv") : "argv", + command: agent.promptCommand, + suffix: agent.promptCommandSuffix, + transport: agent.promptTransport, }), ); export const DEFAULT_TERMINAL_PRESET_AGENT_TYPES = BUILTIN_TERMINAL_AGENTS.filter( - (agent) => - "includeInDefaultTerminalPresets" in agent && - agent.includeInDefaultTerminalPresets, + (agent) => agent.includeInDefaultTerminalPresets, ).map((agent) => agent.id) satisfies BuiltinTerminalAgentType[]; From 3c13e0530277ed40c679cd84672c2a89a53af1d5 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Mon, 30 Mar 2026 13:18:23 -0700 Subject: [PATCH 12/12] Fix custom agent regressions and MCP precedence --- .../src/lib/trpc/routers/settings/index.ts | 11 +++ .../AgentCard/agent-card.utils.test.ts | 57 ++++++++++++++++ .../components/AgentCard/agent-card.utils.ts | 4 +- .../src/shared/utils/agent-settings.test.ts | 36 ++++++++++ .../src/shared/utils/agent-settings.ts | 7 +- apps/desktop/test-setup.ts | 4 +- .../router/mcp-overview/mcp-overview.test.ts | 67 +++++++++++++++++++ .../router/mcp-overview/mcp-overview.ts | 12 ++-- 8 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.test.ts diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index 9fcd6ed7964..7e86917e93b 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -160,6 +160,15 @@ function saveAgentCustomDefinitions(definitions: AgentCustomDefinition[]) { .run(); } +function clearCustomAgentPresetOverride(id: `custom:${string}`) { + saveAgentPresetOverrides( + resetAgentPresetOverride({ + currentOverrides: readRawAgentPresetOverrides(), + id, + }), + ); +} + function getResolvedAgentPresets() { return resolveAgentConfigs({ customDefinitions: readRawAgentCustomDefinitions(), @@ -231,6 +240,7 @@ export const createSettingsRouter = () => { }); saveAgentCustomDefinitions(nextDefinitions); + clearCustomAgentPresetOverride(definition.id); return getResolvedAgentPresets().find( (preset) => preset.id === definition.id, @@ -259,6 +269,7 @@ export const createSettingsRouter = () => { }); saveAgentCustomDefinitions(nextDefinitions); + clearCustomAgentPresetOverride(input.id as `custom:${string}`); return getResolvedAgentPresets().find( (preset) => preset.id === input.id, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.test.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.test.ts new file mode 100644 index 00000000000..bcf42e46f64 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from "bun:test"; +import type { ResolvedAgentConfig } from "shared/utils/agent-settings"; +import { buildAgentFieldPatch } from "./agent-card.utils"; + +const BUILTIN_TERMINAL_PRESET: ResolvedAgentConfig = { + id: "claude", + source: "builtin", + kind: "terminal", + label: "Claude Code", + command: "claude", + promptCommand: "claude --print", + promptTransport: "argv", + taskPromptTemplate: "Task {{slug}}", + enabled: true, + overriddenFields: [], +}; + +const CUSTOM_TERMINAL_PRESET: ResolvedAgentConfig = { + id: "custom:team-agent", + source: "user", + kind: "terminal", + label: "Team Agent", + command: "team-agent", + promptCommand: "team-agent --prompt", + promptTransport: "argv", + taskPromptTemplate: "Task {{slug}}", + enabled: true, + overriddenFields: [], +}; + +describe("buildAgentFieldPatch", () => { + test("allows clearing the prompt command for custom terminal agents", () => { + expect( + buildAgentFieldPatch({ + preset: CUSTOM_TERMINAL_PRESET, + field: "promptCommand", + value: " ", + }), + ).toEqual({ + patch: { + promptCommand: "", + }, + }); + }); + + test("keeps prompt command required for builtin terminal agents", () => { + expect( + buildAgentFieldPatch({ + preset: BUILTIN_TERMINAL_PRESET, + field: "promptCommand", + value: " ", + }), + ).toEqual({ + error: "Prompt command is required for terminal agents.", + }); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.ts index ee5dbf5c328..985738d668d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/AgentsSettings/components/AgentCard/agent-card.utils.ts @@ -100,7 +100,9 @@ export function buildAgentFieldPatch({ }; } if (!value.trim()) { - return { error: "Prompt command is required for terminal agents." }; + return preset.source === "user" + ? { patch: { promptCommand: "" } } + : { error: "Prompt command is required for terminal agents." }; } return { patch: { promptCommand: value } }; case "promptCommandSuffix": diff --git a/apps/desktop/src/shared/utils/agent-settings.test.ts b/apps/desktop/src/shared/utils/agent-settings.test.ts index d77b67c7270..47128fe828f 100644 --- a/apps/desktop/src/shared/utils/agent-settings.test.ts +++ b/apps/desktop/src/shared/utils/agent-settings.test.ts @@ -104,6 +104,42 @@ describe("resolveAgentConfigs", () => { enabled: false, }); }); + + test("ignores legacy overrides for custom terminal configs", () => { + const custom = resolveAgentConfigs({ + customDefinitions: [ + { + id: "custom:team-agent", + kind: "terminal", + label: "Team Agent", + command: "team-agent", + taskPromptTemplate: "Task {{slug}}", + }, + ], + overrideEnvelope: { + version: 1, + presets: [ + { + id: "custom:team-agent", + label: "Stale Override", + command: "stale-command", + promptCommand: "stale-command --prompt", + enabled: false, + }, + ], + }, + }).find((preset) => preset.id === "custom:team-agent"); + + expect(custom).toMatchObject({ + id: "custom:team-agent", + source: "user", + label: "Team Agent", + command: "team-agent", + promptCommand: "team-agent", + enabled: true, + overriddenFields: [], + }); + }); }); describe("createOverrideEnvelopeWithPatch", () => { diff --git a/apps/desktop/src/shared/utils/agent-settings.ts b/apps/desktop/src/shared/utils/agent-settings.ts index 1672080066e..c13e88ae28d 100644 --- a/apps/desktop/src/shared/utils/agent-settings.ts +++ b/apps/desktop/src/shared/utils/agent-settings.ts @@ -345,7 +345,12 @@ export function resolveAgentConfigs({ ); return getAgentDefinitions(customDefinitions).map((definition) => - resolveAgentConfig(definition, overridesById.get(definition.id)), + resolveAgentConfig( + definition, + definition.source === "builtin" + ? overridesById.get(definition.id) + : undefined, + ), ); } diff --git a/apps/desktop/test-setup.ts b/apps/desktop/test-setup.ts index f9c6f088c21..62fed566507 100644 --- a/apps/desktop/test-setup.ts +++ b/apps/desktop/test-setup.ts @@ -175,8 +175,9 @@ const agentCustomDefinitionSchema = z.object({ label: z.string(), description: z.string().optional(), command: z.string(), - promptCommand: z.string(), + promptCommand: z.string().optional(), promptCommandSuffix: z.string().optional(), + promptTransport: z.enum(["argv", "stdin"]).optional(), taskPromptTemplate: z.string(), enabled: z.boolean().optional(), }); @@ -194,6 +195,7 @@ const localDbMock = () => ({ agentPresetOverrideSchema, agentPresetOverrideEnvelopeSchema, agentCustomDefinitionSchema, + PROMPT_TRANSPORTS: ["argv", "stdin"], EXTERNAL_APPS: [], EXECUTION_MODES: ["sequential", "parallel"], BRANCH_PREFIX_MODES: ["none", "github", "author", "custom"], diff --git a/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.test.ts b/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.test.ts index 0f6621cb29f..9b308fe28f3 100644 --- a/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.test.ts +++ b/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.test.ts @@ -152,6 +152,45 @@ describe("getMcpOverview", () => { ]); }); + it("prefers .mcp.json over .amp/settings.json", () => { + const cwd = createTempDirectory(); + mkdirSync(join(cwd, ".amp"), { recursive: true }); + writeFileSync( + join(cwd, ".mcp.json"), + JSON.stringify({ + mcpServers: { + sharedRemote: { + type: "http", + url: "https://shared.example.com/mcp", + }, + }, + }), + "utf-8", + ); + writeFileSync( + join(cwd, ".amp", "settings.json"), + JSON.stringify({ + "amp.mcpServers": { + personalRemote: { + url: "https://personal.example.com/mcp", + }, + }, + }), + "utf-8", + ); + + const result = getMcpOverview(cwd); + expect(result.sourcePath).toBe(join(cwd, ".mcp.json")); + expect(result.servers).toEqual([ + { + name: "sharedRemote", + state: "enabled", + transport: "remote", + target: "https://shared.example.com/mcp", + }, + ]); + }); + it("falls back to .mcp.json when .mastracode/mcp.json is invalid", () => { const cwd = createTempDirectory(); mkdirSync(join(cwd, ".mastracode"), { recursive: true }); @@ -209,4 +248,32 @@ describe("getMcpOverview", () => { }, ]); }); + + it("falls back to .amp/settings.json when .mcp.json is invalid", () => { + const cwd = createTempDirectory(); + mkdirSync(join(cwd, ".amp"), { recursive: true }); + writeFileSync(join(cwd, ".mcp.json"), "{ invalid", "utf-8"); + writeFileSync( + join(cwd, ".amp", "settings.json"), + JSON.stringify({ + "amp.mcpServers": { + ampFallback: { + url: "https://amp.example.com/mcp", + }, + }, + }), + "utf-8", + ); + + const result = getMcpOverview(cwd); + expect(result.sourcePath).toBe(join(cwd, ".amp", "settings.json")); + expect(result.servers).toEqual([ + { + name: "ampFallback", + state: "enabled", + transport: "remote", + target: "https://amp.example.com/mcp", + }, + ]); + }); }); diff --git a/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.ts b/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.ts index 5f9a19e7edb..c626f52083d 100644 --- a/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.ts +++ b/packages/chat/src/server/desktop/router/mcp-overview/mcp-overview.ts @@ -19,17 +19,17 @@ const MCP_SETTINGS_FILES = [ }, }, { - relativePath: ".amp/settings.json", + relativePath: ".mcp.json", readServers: (parsed: unknown) => { - const result = ampMcpSettingsSchema.safeParse(parsed); - return result.success ? result.data["amp.mcpServers"] : null; + const result = mcpSettingsSchema.safeParse(parsed); + return result.success ? result.data.mcpServers : null; }, }, { - relativePath: ".mcp.json", + relativePath: ".amp/settings.json", readServers: (parsed: unknown) => { - const result = mcpSettingsSchema.safeParse(parsed); - return result.success ? result.data.mcpServers : null; + const result = ampMcpSettingsSchema.safeParse(parsed); + return result.success ? result.data["amp.mcpServers"] : null; }, }, ] as const;