diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-antigravity.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-antigravity.ts new file mode 100644 index 00000000000..b3e398b0fb1 --- /dev/null +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-antigravity.ts @@ -0,0 +1,178 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { env } from "shared/env.shared"; +import { + buildWrapperScript, + createWrapper, + writeFileIfChanged, +} from "./agent-wrappers-common"; +import { HOOKS_DIR } from "./paths"; + +export const ANTIGRAVITY_HOOK_SCRIPT_NAME = "antigravity-hook.sh"; + +const ANTIGRAVITY_HOOK_SIGNATURE = "# Superset antigravity hook"; +const ANTIGRAVITY_HOOK_VERSION = "v1"; +export const ANTIGRAVITY_HOOK_MARKER = `${ANTIGRAVITY_HOOK_SIGNATURE} ${ANTIGRAVITY_HOOK_VERSION}`; + +/** + * Top-level key we own in hooks.json. Antigravity namespaces hooks by name and + * merges every named block for a given event, so scoping our handlers under a + * single key leaves user-defined hooks untouched. + */ +export const ANTIGRAVITY_HOOK_NAME = "superset"; + +const ANTIGRAVITY_HOOK_TEMPLATE_PATH = path.join( + __dirname, + "templates", + "antigravity-hook.template.sh", +); + +interface AntigravityHookHandler { + type?: string; + command: string; + timeout?: number; + [key: string]: unknown; +} + +interface AntigravityHooksJson { + [hookName: string]: unknown; +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function getAntigravityHookScriptPath(): string { + return path.join(HOOKS_DIR, ANTIGRAVITY_HOOK_SCRIPT_NAME); +} + +/** + * Antigravity's global customization root is `~/.gemini/config/` (shared with + * Antigravity 2.0 and the IDE), not `~/.gemini/` itself -- that is where the + * deprecated Gemini CLI kept settings.json. + */ +export function getAntigravityHooksJsonPath(): string { + return path.join(os.homedir(), ".gemini", "config", "hooks.json"); +} + +export function getAntigravityHookScriptContent(): string { + const template = fs.readFileSync(ANTIGRAVITY_HOOK_TEMPLATE_PATH, "utf-8"); + return template + .replace("{{MARKER}}", ANTIGRAVITY_HOOK_MARKER) + .replaceAll("{{DEFAULT_PORT}}", String(env.DESKTOP_NOTIFICATIONS_PORT)); +} + +/** + * Antigravity hook payloads carry no event-name field, so the Superset event is + * passed as argv instead of being derived from stdin. + * + * Event choice is deliberate: + * - `PreInvocation` fires before each model call -> Start. + * - `PostInvocation` fires after each loop iteration -> Start, keeping the + * terminal marked busy across long tool loops. + * - `Stop` fires when the execution loop terminates -> Stop. + * + * `PostToolUse` is deliberately NOT used as the busy keepalive even though it + * looks like the natural fit. Antigravity dispatches a trailing `PostToolUse` + * for the final step ~1s AFTER `Stop` on any turn that ends without a tool + * call, so registering it makes `Start` the last event Superset sees and pins + * the terminal to "working" forever (status is last-write-wins and the Start + * branch has no staleness gate). `PostInvocation` always precedes `Stop`. + * Note also that `matcher: "*"` matches every step type, not just tool calls -- + * those trailing steps carry `toolCall: null`. + * + * `PreToolUse` is intentionally not registered: its contract requires a + * `decision` field ("allow" | "deny" | "ask" | "force_ask"), and emitting an + * empty object there risks gating the user's tool calls. + */ +function buildSupersetHookBlock( + hookScriptPath: string, +): Record { + const startCommand: AntigravityHookHandler = { + type: "command", + command: `"${hookScriptPath}" Start`, + }; + const stopCommand: AntigravityHookHandler = { + type: "command", + command: `"${hookScriptPath}" Stop`, + }; + + // Every event we register takes a flat handler list; only the tool-matched + // events (PreToolUse/PostToolUse) would need a `matcher` group wrapper. + return { + PreInvocation: [startCommand], + PostInvocation: [startCommand], + Stop: [stopCommand], + }; +} + +/** + * Reads existing ~/.gemini/config/hooks.json, replaces our own named block, and + * preserves every other hook the user (or a plugin) defined. + * + * Returns `null` when the existing file cannot be parsed as a JSON object, so + * the caller skips the write rather than clobbering hooks it failed to read. + * This root is shared by the CLI, Antigravity 2.0 and the IDE, so a blind + * rewrite would take out every surface's hooks at once. + */ +export function getAntigravityHooksJsonContent( + hookScriptPath: string, +): string | null { + const globalPath = getAntigravityHooksJsonPath(); + + let existing: AntigravityHooksJson = {}; + if (fs.existsSync(globalPath)) { + let parsed: unknown; + try { + parsed = JSON.parse(fs.readFileSync(globalPath, "utf-8")); + } catch (error) { + console.warn( + "[agent-setup] Could not parse existing ~/.gemini/config/hooks.json; skipping Antigravity hook merge:", + error, + ); + return null; + } + if (!isPlainObject(parsed)) { + console.warn( + "[agent-setup] Expected ~/.gemini/config/hooks.json to contain a JSON object; skipping Antigravity hook merge", + ); + return null; + } + existing = parsed; + } + + existing[ANTIGRAVITY_HOOK_NAME] = buildSupersetHookBlock(hookScriptPath); + + return JSON.stringify(existing, null, 2); +} + +export function createAntigravityHookScript(): void { + const scriptPath = getAntigravityHookScriptPath(); + const content = getAntigravityHookScriptContent(); + const changed = writeFileIfChanged(scriptPath, content, 0o755); + console.log( + `[agent-setup] ${changed ? "Updated" : "Verified"} Antigravity hook script`, + ); +} + +export function createAntigravityWrapper(): void { + const script = buildWrapperScript("agy", `exec "$REAL_BIN" "$@"`, { + agentId: "agy", + }); + createWrapper("agy", script); +} + +export function createAntigravityHooksJson(): void { + const hookScriptPath = getAntigravityHookScriptPath(); + const globalPath = getAntigravityHooksJsonPath(); + const content = getAntigravityHooksJsonContent(hookScriptPath); + if (content === null) return; + + const dir = path.dirname(globalPath); + fs.mkdirSync(dir, { recursive: true }); + const changed = writeFileIfChanged(globalPath, content, 0o644); + console.log( + `[agent-setup] ${changed ? "Updated" : "Verified"} Antigravity hooks.json`, + ); +} 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 dcb53fba6b1..a7fdef435b6 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 @@ -58,8 +58,13 @@ mock.module("node:os", () => ({ const { AMP_PLUGIN_MARKER, + ANTIGRAVITY_HOOK_NAME, createAmpPlugin, createAmpWrapper, + createAntigravityHooksJson, + createAntigravityWrapper, + getAntigravityHooksJsonContent, + getAntigravityHooksJsonPath, buildCodexWrapperExecLine, buildCopilotWrapperExecLine, buildWrapperScript, @@ -1742,3 +1747,124 @@ describe("agent-wrappers pi", () => { expect(installed).toContain("export default function"); }); }); + +describe("antigravity wrapper", () => { + beforeEach(() => { + mockedHomeDir = path.join(TEST_ROOT, "home"); + mkdirSync(TEST_BIN_DIR, { recursive: true }); + mkdirSync(TEST_HOOKS_DIR, { recursive: true }); + }); + + afterEach(() => { + rmSync(TEST_ROOT, { recursive: true, force: true }); + }); + + it("wraps the agy binary and stamps the agent id", () => { + createAntigravityWrapper(); + const script = readFileSync(path.join(TEST_BIN_DIR, "agy"), "utf-8"); + expect(script).toContain('export SUPERSET_AGENT_ID="agy"'); + expect(script).toContain('find_real_binary "agy"'); + expect(script).toContain('exec "$REAL_BIN" "$@"'); + }); +}); + +describe("antigravity hooks.json", () => { + const hookPath = "/tmp/.superset/hooks/antigravity-hook.sh"; + + beforeEach(() => { + mockedHomeDir = path.join(TEST_ROOT, "home"); + mkdirSync(TEST_HOOKS_DIR, { recursive: true }); + }); + + afterEach(() => { + rmSync(TEST_ROOT, { recursive: true, force: true }); + }); + + it("targets the ~/.gemini/config customization root", () => { + expect(getAntigravityHooksJsonPath()).toBe( + path.join(mockedHomeDir, ".gemini", "config", "hooks.json"), + ); + }); + + it("passes the Superset event as argv, since the payload omits it", () => { + const parsed = JSON.parse( + getAntigravityHooksJsonContent(hookPath) ?? "", + ) as Record>; + const block = parsed[ANTIGRAVITY_HOOK_NAME]; + + // Every registered event takes a flat handler list. + expect(block.PreInvocation).toEqual([ + { type: "command", command: `"${hookPath}" Start` }, + ]); + expect(block.PostInvocation).toEqual([ + { type: "command", command: `"${hookPath}" Start` }, + ]); + expect(block.Stop).toEqual([ + { type: "command", command: `"${hookPath}" Stop` }, + ]); + }); + + it("never registers PreToolUse, whose contract requires a decision field", () => { + const block = JSON.parse(getAntigravityHooksJsonContent(hookPath) ?? "")[ + ANTIGRAVITY_HOOK_NAME + ] as Record; + expect(block.PreToolUse).toBeUndefined(); + }); + + // Antigravity dispatches a trailing PostToolUse ~1s AFTER Stop on turns that + // end without a tool call. Registering it would make Start the last event + // Superset sees and pin the terminal to "working" forever. + it("never registers PostToolUse, which can fire after Stop", () => { + const block = JSON.parse(getAntigravityHooksJsonContent(hookPath) ?? "")[ + ANTIGRAVITY_HOOK_NAME + ] as Record; + expect(block.PostToolUse).toBeUndefined(); + }); + + it("preserves foreign hook blocks and is idempotent", () => { + const hooksPath = getAntigravityHooksJsonPath(); + mkdirSync(path.dirname(hooksPath), { recursive: true }); + writeFileSync( + hooksPath, + JSON.stringify({ + "lint-checker": { + PostToolUse: [ + { + matcher: "run_command", + hooks: [{ type: "command", command: "./scripts/lint.sh" }], + }, + ], + }, + }), + ); + + const once = getAntigravityHooksJsonContent(hookPath); + writeFileSync(hooksPath, once ?? ""); + const twice = getAntigravityHooksJsonContent(hookPath); + + expect(twice).toBe(once); + const parsed = JSON.parse(twice ?? "") as Record; + expect(parsed["lint-checker"]).toBeDefined(); + expect(parsed[ANTIGRAVITY_HOOK_NAME]).toBeDefined(); + }); + + // ~/.gemini/config/ is shared by the CLI, Antigravity 2.0 and the IDE, so + // rewriting a file we could not read would take out every surface's hooks. + for (const [label, contents] of [ + ["unparseable", "{ not json"], + ["a top-level array", '[{"nope": true}]'], + ["JSON null", "null"], + ] as const) { + it(`leaves hooks.json untouched when it contains ${label}`, () => { + const hooksPath = getAntigravityHooksJsonPath(); + mkdirSync(path.dirname(hooksPath), { recursive: true }); + writeFileSync(hooksPath, contents); + + expect(getAntigravityHooksJsonContent(hookPath)).toBeNull(); + + // The writer must skip entirely rather than clobber the file. + createAntigravityHooksJson(); + expect(readFileSync(hooksPath, "utf-8")).toBe(contents); + }); + } +}); 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 628cb08e52c..f59096bdaa1 100644 --- a/apps/desktop/src/main/lib/agent-setup/agent-wrappers.ts +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers.ts @@ -6,6 +6,18 @@ export { getAmpGlobalPluginPath, getAmpPluginContent, } from "./agent-wrappers-amp"; +export { + ANTIGRAVITY_HOOK_MARKER, + ANTIGRAVITY_HOOK_NAME, + ANTIGRAVITY_HOOK_SCRIPT_NAME, + createAntigravityHookScript, + createAntigravityHooksJson, + createAntigravityWrapper, + getAntigravityHookScriptContent, + getAntigravityHookScriptPath, + getAntigravityHooksJsonContent, + getAntigravityHooksJsonPath, +} from "./agent-wrappers-antigravity"; export { buildCodexWrapperExecLine, cleanupGlobalOpenCodePlugin, 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 index feb7fc51bf1..e4c669b80dd 100644 --- a/apps/desktop/src/main/lib/agent-setup/desktop-agent-capabilities.ts +++ b/apps/desktop/src/main/lib/agent-setup/desktop-agent-capabilities.ts @@ -22,6 +22,9 @@ export const DESKTOP_AGENT_SETUP_ACTIONS = [ "gemini-hook-script", "gemini-wrapper", "gemini-settings-json", + "antigravity-hook-script", + "antigravity-wrapper", + "antigravity-hooks-json", "mastra-wrapper", "mastra-hooks-json", "copilot-hook-script", @@ -91,6 +94,15 @@ export const DESKTOP_AGENT_SETUP_TARGETS = [ ], managedBinary: true, }, + { + id: "agy", + setupActions: [ + "antigravity-hook-script", + "antigravity-wrapper", + "antigravity-hooks-json", + ], + managedBinary: true, + }, { id: "mastracode", setupActions: ["mastra-wrapper", "mastra-hooks-json"], 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 index 13dcfb29635..c3ab3bb6241 100644 --- a/apps/desktop/src/main/lib/agent-setup/desktop-agent-setup.ts +++ b/apps/desktop/src/main/lib/agent-setup/desktop-agent-setup.ts @@ -2,6 +2,9 @@ import { cleanupGlobalOpenCodePlugin, createAmpPlugin, createAmpWrapper, + createAntigravityHookScript, + createAntigravityHooksJson, + createAntigravityWrapper, createClaudeSettingsJson, createClaudeWrapper, createCodexHooksJson, @@ -52,6 +55,9 @@ const DESKTOP_AGENT_SETUP_RUNNERS: Record void> = "gemini-hook-script": createGeminiHookScript, "gemini-wrapper": createGeminiWrapper, "gemini-settings-json": createGeminiSettingsJson, + "antigravity-hook-script": createAntigravityHookScript, + "antigravity-wrapper": createAntigravityWrapper, + "antigravity-hooks-json": createAntigravityHooksJson, "mastra-wrapper": createMastraWrapper, "mastra-hooks-json": createMastraHooksJson, "copilot-hook-script": createCopilotHookScript, diff --git a/apps/desktop/src/main/lib/agent-setup/notify-hook.test.ts b/apps/desktop/src/main/lib/agent-setup/notify-hook.test.ts index b4dc0e9496b..70a56b61aa4 100644 --- a/apps/desktop/src/main/lib/agent-setup/notify-hook.test.ts +++ b/apps/desktop/src/main/lib/agent-setup/notify-hook.test.ts @@ -63,6 +63,7 @@ describe("per-agent hook scripts dispatch to v2", () => { "cursor-hook.template.sh", "copilot-hook.template.sh", "gemini-hook.template.sh", + "antigravity-hook.template.sh", ]) { it(`${template} posts v2 first and falls back to v1`, () => { const script = readFileSync( diff --git a/apps/desktop/src/main/lib/agent-setup/templates/antigravity-hook.template.sh b/apps/desktop/src/main/lib/agent-setup/templates/antigravity-hook.template.sh new file mode 100644 index 00000000000..43c479be2b5 --- /dev/null +++ b/apps/desktop/src/main/lib/agent-setup/templates/antigravity-hook.template.sh @@ -0,0 +1,72 @@ +#!/bin/bash +{{MARKER}} +# Antigravity CLI (`agy`) lifecycle hook. JSON in via stdin; MUST print valid +# JSON to stdout before exit or the agent loop stalls -- Antigravity runs hooks +# synchronously and blocks on them. +# +# Unlike other agents, Antigravity's hook payload carries no event-name field +# (common fields are conversationId/workspacePaths/transcriptPath/modelName +# only), so hooks.json passes the Superset event as argv $1. + +EVENT_TYPE="$1" + +case "$EVENT_TYPE" in + Start|Stop) ;; + *) + printf '{}\n' + exit 0 + ;; +esac + +INPUT=$(cat) + +# Payload keys are camelCase (protojson). conversationId is Antigravity's +# session identifier. +HOOK_SESSION_ID=$(printf '%s' "$INPUT" | grep -oE '"conversationId"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"') + +# Emit before dispatching. An empty object is the no-op result for every event +# we register: PreInvocation/PostInvocation treat injectSteps as optional, +# PostToolUse expects exactly {}, and Stop only blocks termination when +# decision is "continue". +printf '{}\n' + +# Antigravity is already invoked with Superset's own event names, so no +# remapping is needed -- the alias keeps the v1 payload shape identical to the +# other agent hook templates. +V1_EVENT_TYPE="$EVENT_TYPE" + +json_escape() { + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' +} + +if [ -n "$SUPERSET_HOST_AGENT_HOOK_URL" ] && [ -n "$SUPERSET_TERMINAL_ID" ]; then + PAYLOAD="{\"json\":{\"terminalId\":\"$(json_escape "$SUPERSET_TERMINAL_ID")\",\"eventType\":\"$(json_escape "$EVENT_TYPE")\",\"agent\":{\"agentId\":\"$(json_escape "$SUPERSET_AGENT_ID")\",\"sessionId\":\"$(json_escape "$HOOK_SESSION_ID")\"}}}" + + STATUS_CODE=$(curl -sX POST "$SUPERSET_HOST_AGENT_HOOK_URL" \ + --connect-timeout 2 --max-time 5 \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" \ + -o /dev/null -w "%{http_code}" 2>/dev/null) + + case "$STATUS_CODE" in + 2*) exit 0 ;; + *) echo "[antigravity-hook] host-service dispatch failed status=$STATUS_CODE; falling back to v1" >&2 ;; + esac +fi + +[ -z "$SUPERSET_TAB_ID" ] && [ -z "$SUPERSET_TERMINAL_ID" ] && exit 0 + +curl -sG "http://127.0.0.1:${SUPERSET_PORT:-{{DEFAULT_PORT}}}/hook/complete" \ + --connect-timeout 1 --max-time 2 \ + --data-urlencode "paneId=$SUPERSET_PANE_ID" \ + --data-urlencode "tabId=$SUPERSET_TAB_ID" \ + --data-urlencode "workspaceId=$SUPERSET_WORKSPACE_ID" \ + --data-urlencode "terminalId=$SUPERSET_TERMINAL_ID" \ + --data-urlencode "sessionId=$HOOK_SESSION_ID" \ + --data-urlencode "hookSessionId=$HOOK_SESSION_ID" \ + --data-urlencode "eventType=$V1_EVENT_TYPE" \ + --data-urlencode "env=$SUPERSET_ENV" \ + --data-urlencode "version=$SUPERSET_HOOK_VERSION" \ + > /dev/null 2>&1 + +exit 0 diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentIconPicker/agent-icon-options.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentIconPicker/agent-icon-options.ts index 29094642d7a..9a6a8b7c502 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentIconPicker/agent-icon-options.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentIconPicker/agent-icon-options.ts @@ -13,6 +13,7 @@ export const AGENT_ICON_OPTIONS: readonly AgentIconOption[] = [ { id: "codex", label: "Codex" }, { id: "cursor-agent", label: "Cursor" }, { id: "gemini", label: "Gemini" }, + { id: "agy", label: "Antigravity" }, { id: "copilot", label: "Copilot" }, { id: "amp", label: "Amp" }, { id: "opencode", label: "OpenCode" }, diff --git a/apps/docs/content/docs/terminal-presets.mdx b/apps/docs/content/docs/terminal-presets.mdx index 04e024b4914..b2862862238 100644 --- a/apps/docs/content/docs/terminal-presets.mdx +++ b/apps/docs/content/docs/terminal-presets.mdx @@ -46,7 +46,8 @@ Pre-configured presets for popular AI agents. Defaults mirror the bundled Supers - **amp** - `amp` (built-in permission rules auto-deny destructive ops) - **claude** - `claude --dangerously-skip-permissions` - **codex** - `codex --dangerously-bypass-approvals-and-sandbox` (most permissive) -- **gemini** - `gemini --approval-mode=auto_edit` +- **gemini** - `gemini --approval-mode=auto_edit` (deprecated upstream; Google is retiring the Gemini CLI in favour of Antigravity) +- **agy** - `agy --mode accept-edits` (Google's Antigravity CLI, the Gemini CLI's replacement) - **copilot** - `copilot --allow-tool=write` - **cursor-agent** - `cursor-agent` (prompts for every action) - **mastracode** - Mastra's coding agent (opt-in: auto-approves all actions by default at the CLI; no startup flag to restrict) diff --git a/packages/shared/src/builtin-terminal-agents.ts b/packages/shared/src/builtin-terminal-agents.ts index e96d8de8d24..1e2e27cd281 100644 --- a/packages/shared/src/builtin-terminal-agents.ts +++ b/packages/shared/src/builtin-terminal-agents.ts @@ -96,6 +96,19 @@ export const BUILTIN_TERMINAL_AGENTS = [ nonInteractiveCommand: "gemini --skip-trust -p", includeInDefaultTerminalPresets: true, }), + createBuiltinTerminalAgent({ + // Google's replacement for the deprecated Gemini CLI. The id matches the + // binary name (`agy`) because SUPERSET_MANAGED_BINARIES derives the shell + // shim name from the agent id. + id: "agy", + label: "Antigravity", + description: + "Google's terminal agent harness for reasoning, editing, and running code from the terminal.", + command: "agy --mode accept-edits", + promptCommand: "agy --mode accept-edits", + nonInteractiveCommand: "agy -p", + includeInDefaultTerminalPresets: true, + }), createBuiltinTerminalAgent({ id: "mastracode", label: "Mastracode", diff --git a/packages/ui/src/assets/icons/preset-icons/antigravity.svg b/packages/ui/src/assets/icons/preset-icons/antigravity.svg new file mode 100644 index 00000000000..e7e8233f3e4 --- /dev/null +++ b/packages/ui/src/assets/icons/preset-icons/antigravity.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 16b98dfe414..f8a435ed529 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 antigravityIcon from "./antigravity.svg"; import claudeIcon from "./claude.svg"; import codexIcon from "./codex.svg"; import codexWhiteIcon from "./codex-white.svg"; @@ -26,6 +27,8 @@ export interface PresetIconSet { export const PRESET_ICONS: Record = { amp: { light: ampIcon, dark: ampIcon }, + // Keyed by agent id: the Antigravity CLI binary is `agy`. + agy: { light: antigravityIcon, dark: antigravityIcon }, claude: { light: claudeIcon, dark: claudeIcon }, codex: { light: codexIcon, dark: codexWhiteIcon }, copilot: { light: copilotIcon, dark: copilotWhiteIcon }, @@ -61,6 +64,7 @@ export function getPresetIcon( export { ampIcon, + antigravityIcon, claudeIcon, codexIcon, codexWhiteIcon,