Add Antigravity agent support (#2389)

This commit is contained in:
Neil 2026-05-19 20:24:06 -07:00 committed by GitHub
parent 3299635013
commit 8afdbea256
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 939 additions and 8 deletions

View File

@ -14,7 +14,7 @@
<p align="center">
<strong>The AI Orchestrator for 100x builders.</strong><br/>
Run Claude Code, Codex, Grok, or OpenCode side-by-side across repos — each in its own worktree, tracked in one place.<br/>
Run Claude Code, Codex, Grok, Antigravity, or OpenCode side-by-side across repos — each in its own worktree, tracked in one place.<br/>
Available for <strong>macOS, Windows, and Linux</strong>.
</p>
@ -33,6 +33,7 @@ Orca supports any CLI agent (*not just this list*).
<a href="https://github.com/openai/codex"><kbd><img src="https://www.google.com/s2/favicons?domain=openai.com&sz=64" width="16" valign="middle" /> Codex</kbd></a> &nbsp;
<a href="https://x.ai/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=x.ai&sz=64" width="16" valign="middle" /> Grok</kbd></a> &nbsp;
<a href="https://github.com/google-gemini/gemini-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=gemini.google.com&sz=64" width="16" valign="middle" /> Gemini</kbd></a> &nbsp;
<a href="https://antigravity.google/docs/cli-overview"><kbd><img src="https://www.google.com/s2/favicons?domain=antigravity.google&sz=64" width="16" valign="middle" /> Antigravity</kbd></a> &nbsp;
<a href="https://pi.dev"><kbd><img src="https://pi.dev/favicon.svg" width="16" valign="middle" /> Pi</kbd></a> &nbsp;
<a href="https://hermes-agent.nousresearch.com/docs/"><kbd><img src="https://www.google.com/s2/favicons?domain=nousresearch.com&sz=64" width="16" valign="middle" /> Hermes Agent</kbd></a> &nbsp;
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" width="16" valign="middle" /> OpenCode</kbd></a> &nbsp;
@ -59,7 +60,7 @@ Orca supports any CLI agent (*not just this list*).
## Features
- **No login required** — Bring your own Claude Code, Codex, or Grok subscription.
- **No login required** — Bring your own Claude Code, Codex, Grok, or Antigravity subscription.
- **Worktree-native** — Every feature gets its own worktree. No stashing, no branch juggling. Spin up and switch instantly.
- **Multi-agent terminals** — Run multiple AI agents side-by-side in tabs and panes. See which ones are active at a glance.
- **Built-in source control** — Review AI-generated diffs, make quick edits, and commit without leaving Orca.

View File

@ -1,3 +1,4 @@
/* eslint-disable max-lines -- Why: this fixture verifies the shared remote hook installer fake across every managed agent so SSH regressions are caught together. */
import { describe, expect, it, vi } from 'vitest'
import type { SFTPWrapper } from 'ssh2'
@ -10,6 +11,7 @@ vi.mock('electron', () => ({
import { CodexHookService } from '../codex/hook-service'
import { CursorHookService } from '../cursor/hook-service'
import { GeminiHookService } from '../gemini/hook-service'
import { AntigravityHookService } from '../antigravity/hook-service'
import { ClaudeHookService } from '../claude/hook-service'
import { GrokHookService } from '../grok/hook-service'
import { CopilotHookService } from '../copilot/hook-service'
@ -124,6 +126,11 @@ describe('remote hook service installers', () => {
path: '/home/dev/.orca/agent-hooks/gemini-hook.sh',
install: (sftp: SFTPWrapper) => new GeminiHookService().installRemote(sftp, '/home/dev')
},
{
path: '/home/dev/.orca/agent-hooks/antigravity-hook.sh',
install: (sftp: SFTPWrapper) =>
new AntigravityHookService().installRemote(sftp, '/home/dev')
},
{
path: '/home/dev/.orca/agent-hooks/cursor-hook.sh',
install: (sftp: SFTPWrapper) => new CursorHookService().installRemote(sftp, '/home/dev')
@ -196,12 +203,14 @@ describe('remote hook service installers', () => {
expect(fs.files.get('/home/dev/.orca/agent-hooks/codex-hook.sh')).toContain('#!/bin/sh')
})
it('installs remote Gemini, Cursor, and Grok configs using their CLI-specific schemas', async () => {
it('installs remote Gemini, Antigravity, Cursor, and Grok configs using their CLI-specific schemas', async () => {
const gemini = createFakeSftp()
const antigravity = createFakeSftp()
const cursor = createFakeSftp()
const grok = createFakeSftp()
await new GeminiHookService().installRemote(gemini.sftp, '/home/dev')
await new AntigravityHookService().installRemote(antigravity.sftp, '/home/dev')
await new CursorHookService().installRemote(cursor.sftp, '/home/dev')
await new GrokHookService().installRemote(grok.sftp, '/home/dev')
@ -214,6 +223,27 @@ describe('remote hook service installers', () => {
expect(command).toMatch(/^if \[ -x /)
}
const antigravityConfig = JSON.parse(
antigravity.fs.files.get('/home/dev/.gemini/config/hooks.json')!
) as {
'orca-status': Record<
string,
{ matcher?: string; command?: string; hooks?: { command: string }[] }[]
>
}
for (const eventName of ['PreInvocation', 'PostInvocation', 'Stop']) {
const command = antigravityConfig['orca-status'][eventName]?.[0]?.command
expect(command).toContain('/home/dev/.orca/agent-hooks/antigravity-hook.sh')
expect(command).toContain(`ORCA_ANTIGRAVITY_EVENT='${eventName}'`)
}
for (const eventName of ['PreToolUse', 'PostToolUse']) {
const definition = antigravityConfig['orca-status'][eventName]?.[0]
const command = definition?.hooks?.[0]?.command
expect(definition?.matcher).toBe('*')
expect(command).toContain('/home/dev/.orca/agent-hooks/antigravity-hook.sh')
expect(command).toContain(`ORCA_ANTIGRAVITY_EVENT='${eventName}'`)
}
const cursorConfig = JSON.parse(cursor.fs.files.get('/home/dev/.cursor/hooks.json')!) as {
version: number
hooks: Record<string, { command?: string; hooks?: unknown[] }[]>

View File

@ -3,6 +3,7 @@ import type { AgentHookInstallStatus } from '../../shared/agent-hook-types'
import { claudeHookService } from '../claude/hook-service'
import { codexHookService } from '../codex/hook-service'
import { geminiHookService } from '../gemini/hook-service'
import { antigravityHookService } from '../antigravity/hook-service'
import { cursorHookService } from '../cursor/hook-service'
import { grokHookService } from '../grok/hook-service'
import { hermesHookService } from '../hermes/hook-service'
@ -16,6 +17,7 @@ const REMOTE_MANAGED_HOOK_INSTALLERS: readonly RemoteManagedHookInstaller[] = [
['claude', (sftp, remoteHome) => claudeHookService.installRemote(sftp, remoteHome)],
['codex', (sftp, remoteHome) => codexHookService.installRemote(sftp, remoteHome)],
['gemini', (sftp, remoteHome) => geminiHookService.installRemote(sftp, remoteHome)],
['antigravity', (sftp, remoteHome) => antigravityHookService.installRemote(sftp, remoteHome)],
['cursor', (sftp, remoteHome) => cursorHookService.installRemote(sftp, remoteHome)],
['grok', (sftp, remoteHome) => grokHookService.installRemote(sftp, remoteHome)],
['hermes', (sftp, remoteHome) => hermesHookService.installRemote(sftp, remoteHome)]

View File

@ -0,0 +1,139 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { dirname, join } from 'path'
const { homedirMock } = vi.hoisted(() => ({
homedirMock: vi.fn<() => string>()
}))
vi.mock('os', async () => {
const actual = (await vi.importActual('os')) as Record<string, unknown>
return {
...actual,
homedir: homedirMock
}
})
import { AntigravityHookService } from './hook-service'
describe('AntigravityHookService', () => {
let homeDir: string
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), 'orca-antigravity-home-'))
homedirMock.mockReturnValue(homeDir)
})
afterEach(() => {
vi.clearAllMocks()
rmSync(homeDir, { recursive: true, force: true })
})
it('installs Antigravity global hooks.json bundle and managed script', () => {
const status = new AntigravityHookService().install()
expect(status.state).toBe('installed')
expect(status.configPath).toBe(join(homeDir, '.gemini', 'config', 'hooks.json'))
expect(status.managedHooksPresent).toBe(true)
const config = JSON.parse(
readFileSync(join(homeDir, '.gemini', 'config', 'hooks.json'), 'utf8')
) as {
'orca-status': Record<
string,
{ matcher?: string; command?: string; hooks?: { command: string }[] }[]
>
}
expect(Object.keys(config['orca-status']).sort()).toEqual(
['PostInvocation', 'PostToolUse', 'PreInvocation', 'PreToolUse', 'Stop'].sort()
)
expect(config['orca-status'].PreToolUse[0].matcher).toBe('*')
expect(config['orca-status'].PostToolUse[0].matcher).toBe('*')
expect(config['orca-status'].PreInvocation[0].command).toContain('antigravity-hook')
expect(config['orca-status'].PreInvocation[0].command).toContain(
"ORCA_ANTIGRAVITY_EVENT='PreInvocation'"
)
expect(config['orca-status'].Stop[0].command).toContain("ORCA_ANTIGRAVITY_EVENT='Stop'")
const script = readFileSync(
join(homeDir, '.orca', 'agent-hooks', 'antigravity-hook.sh'),
'utf8'
)
expect(script).toContain('/hook/antigravity')
expect(script).toContain('hook_event_name=${ORCA_ANTIGRAVITY_EVENT}')
expect(script).toContain('payload=$(cat)')
expect(script).toContain('{"decision":""}')
})
it('preserves user-authored hook bundles and entries in Orca bundle', () => {
const configPath = join(homeDir, '.gemini', 'config', 'hooks.json')
mkdirSync(dirname(configPath), { recursive: true })
writeFileSync(
configPath,
`${JSON.stringify(
{
'user-hook': {
PreInvocation: [{ type: 'command', command: '/usr/local/bin/user-hook' }]
},
'orca-status': {
PreInvocation: [{ type: 'command', command: '/usr/local/bin/orca-extra' }]
}
},
null,
2
)}\n`
)
new AntigravityHookService().install()
const config = JSON.parse(readFileSync(configPath, 'utf8')) as {
'user-hook': { PreInvocation: { command: string }[] }
'orca-status': { PreInvocation: { command: string }[] }
}
expect(config['user-hook'].PreInvocation[0].command).toBe('/usr/local/bin/user-hook')
const commands = config['orca-status'].PreInvocation.map((entry) => entry.command)
expect(commands).toContain('/usr/local/bin/orca-extra')
expect(commands.some((command) => command.includes('antigravity-hook.sh'))).toBe(true)
})
it('removes stale managed Antigravity hook entries from retired events', () => {
const configPath = join(homeDir, '.gemini', 'config', 'hooks.json')
mkdirSync(dirname(configPath), { recursive: true })
writeFileSync(
configPath,
`${JSON.stringify(
{
'orca-status': {
OldEvent: [
{
type: 'command',
command: '/tmp/old/agent-hooks/antigravity-hook.sh'
}
],
PreToolUse: [
{
matcher: '*',
hooks: [{ type: 'command', command: '/tmp/old/agent-hooks/antigravity-hook.sh' }]
}
]
}
},
null,
2
)}\n`
)
new AntigravityHookService().install()
const config = JSON.parse(readFileSync(configPath, 'utf8')) as {
'orca-status': Record<string, { command?: string; hooks?: { command: string }[] }[]>
}
expect(config['orca-status'].OldEvent).toBeUndefined()
const commands = config['orca-status'].PreToolUse.flatMap((definition) =>
(definition.hooks ?? []).map((hook) => hook.command)
)
expect(commands).toHaveLength(1)
expect(commands[0]).toContain(join(homeDir, '.orca', 'agent-hooks', 'antigravity-hook.sh'))
})
})

View File

@ -0,0 +1,326 @@
import { homedir } from 'os'
import { join } from 'path'
import type { SFTPWrapper } from 'ssh2'
import type { AgentHookInstallState, AgentHookInstallStatus } from '../../shared/agent-hook-types'
import {
createManagedCommandMatcher,
getSharedManagedScriptPath,
readHooksJson,
removeManagedCommands,
wrapPosixHookCommand,
writeHooksJson,
writeManagedScript,
type HookDefinition,
type HooksConfig
} from '../agent-hooks/installer-utils'
import {
readHooksJsonRemote,
writeHooksJsonRemote,
writeManagedScriptRemote
} from '../agent-hooks/installer-utils-remote'
const ANTIGRAVITY_HOOK_BUNDLE_NAME = 'orca-status'
const ANTIGRAVITY_EVENTS = [
{ eventName: 'PreInvocation', schema: 'direct' },
{ eventName: 'PostInvocation', schema: 'direct' },
{ eventName: 'Stop', schema: 'direct' },
{ eventName: 'PreToolUse', schema: 'tool' },
{ eventName: 'PostToolUse', schema: 'tool' }
] as const
type AntigravityEvent = (typeof ANTIGRAVITY_EVENTS)[number]
function getConfigPath(): string {
// Why: Antigravity's hook docs define global hooks in ~/.gemini/config/hooks.json,
// not in the CLI settings file used by Gemini CLI.
return join(homedir(), '.gemini', 'config', 'hooks.json')
}
function getManagedScriptFileName(): string {
return process.platform === 'win32' ? 'antigravity-hook.cmd' : 'antigravity-hook.sh'
}
function getManagedScriptPath(): string {
return getSharedManagedScriptPath(getManagedScriptFileName())
}
function getManagedCommand(scriptPath: string, eventName: string): string {
if (process.platform === 'win32') {
return `cmd /d /s /c "set "ORCA_ANTIGRAVITY_EVENT=${eventName}" && call "${scriptPath}""`
}
return wrapPosixHookCommand(scriptPath, { ORCA_ANTIGRAVITY_EVENT: eventName })
}
function getManagedScript(target: 'local' | 'posix' = 'local'): string {
if (target === 'local' && process.platform === 'win32') {
return [
'@echo off',
'setlocal',
'if /I "%ORCA_ANTIGRAVITY_EVENT%"=="Stop" (',
' echo {"decision":""}',
') else (',
' echo {}',
')',
'if defined ORCA_AGENT_HOOK_ENDPOINT if exist "%ORCA_AGENT_HOOK_ENDPOINT%" call "%ORCA_AGENT_HOOK_ENDPOINT%" 2>nul',
'if "%ORCA_AGENT_HOOK_PORT%"=="" exit /b 0',
'if "%ORCA_AGENT_HOOK_TOKEN%"=="" exit /b 0',
'if "%ORCA_PANE_KEY%"=="" exit /b 0',
buildWindowsAntigravityHookPostCommand(),
'exit /b 0',
''
].join('\r\n')
}
return [
'#!/bin/sh',
'case "$ORCA_ANTIGRAVITY_EVENT" in',
' Stop)',
' printf \'{"decision":""}\\n\'',
' ;;',
' *)',
// Why: Antigravity accepts an empty JSON object for passive status hooks;
// returning allow/ask/deny from PreToolUse would change the user's tool
// permission policy.
' printf "{}\\n"',
' ;;',
'esac',
'if [ -n "$ORCA_AGENT_HOOK_ENDPOINT" ] && [ -r "$ORCA_AGENT_HOOK_ENDPOINT" ]; then',
' . "$ORCA_AGENT_HOOK_ENDPOINT" 2>/dev/null || :',
'fi',
'if [ -z "$ORCA_AGENT_HOOK_PORT" ] || [ -z "$ORCA_AGENT_HOOK_TOKEN" ] || [ -z "$ORCA_PANE_KEY" ]; then',
' exit 0',
'fi',
'payload=$(cat)',
'if [ -z "$payload" ]; then',
' exit 0',
'fi',
'curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/antigravity" \\',
' -H "Content-Type: application/x-www-form-urlencoded" \\',
' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\',
' --data-urlencode "paneKey=${ORCA_PANE_KEY}" \\',
' --data-urlencode "tabId=${ORCA_TAB_ID}" \\',
' --data-urlencode "worktreeId=${ORCA_WORKTREE_ID}" \\',
' --data-urlencode "env=${ORCA_AGENT_HOOK_ENV}" \\',
' --data-urlencode "version=${ORCA_AGENT_HOOK_VERSION}" \\',
' --data-urlencode "hook_event_name=${ORCA_ANTIGRAVITY_EVENT}" \\',
' --data-urlencode "payload=${payload}" >/dev/null 2>&1 || true',
'exit 0',
''
].join('\n')
}
function buildWindowsAntigravityHookPostCommand(): string {
return `powershell -NoProfile -ExecutionPolicy Bypass -Command "$utf8=[System.Text.UTF8Encoding]::new($false); [Console]::InputEncoding=$utf8; [Console]::OutputEncoding=$utf8; $inputData=[Console]::In.ReadToEnd(); if ([string]::IsNullOrWhiteSpace($inputData)) { exit 0 }; try { $body=@{ paneKey=$env:ORCA_PANE_KEY; tabId=$env:ORCA_TAB_ID; worktreeId=$env:ORCA_WORKTREE_ID; env=$env:ORCA_AGENT_HOOK_ENV; version=$env:ORCA_AGENT_HOOK_VERSION; hook_event_name=$env:ORCA_ANTIGRAVITY_EVENT; payload=($inputData | ConvertFrom-Json) } | ConvertTo-Json -Depth 100 -Compress; $bodyBytes=$utf8.GetBytes($body); Invoke-WebRequest -UseBasicParsing -Method Post -Uri ('http://127.0.0.1:' + $env:ORCA_AGENT_HOOK_PORT + '/hook/antigravity') -ContentType 'application/json; charset=utf-8' -Headers @{ 'X-Orca-Agent-Hook-Token'=$env:ORCA_AGENT_HOOK_TOKEN } -Body $bodyBytes | Out-Null } catch {}"`
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function getBundle(config: HooksConfig): Record<string, unknown> {
const existing = config[ANTIGRAVITY_HOOK_BUNDLE_NAME]
return isRecord(existing) ? { ...existing } : {}
}
function hasManagedCommand(definitions: HookDefinition[], command: string): boolean {
return definitions.some(
(definition) =>
definition.command === command ||
(Array.isArray(definition.hooks) && definition.hooks.some((hook) => hook.command === command))
)
}
function buildEventDefinition(event: AntigravityEvent, command: string): HookDefinition {
if (event.schema === 'tool') {
return {
matcher: '*',
hooks: [{ type: 'command', command }]
}
}
return { type: 'command', command }
}
function removeManagedCommandsFromBundle(
bundle: Record<string, unknown>,
isManagedCommand: (command: string | undefined) => boolean
): Record<string, unknown> {
const next = { ...bundle }
for (const [eventName, definitions] of Object.entries(next)) {
if (!Array.isArray(definitions)) {
continue
}
const cleaned = removeManagedCommands(definitions as HookDefinition[], isManagedCommand)
if (cleaned.length === 0) {
delete next[eventName]
} else {
next[eventName] = cleaned
}
}
return next
}
function buildInstalledConfig(
config: HooksConfig,
commandForEvent: (eventName: string) => string,
scriptFileName: string
): void {
const isManagedCommand = createManagedCommandMatcher(scriptFileName)
const bundle = removeManagedCommandsFromBundle(getBundle(config), isManagedCommand)
for (const event of ANTIGRAVITY_EVENTS) {
const current = Array.isArray(bundle[event.eventName])
? (bundle[event.eventName] as HookDefinition[])
: []
const cleaned = removeManagedCommands(current, isManagedCommand)
bundle[event.eventName] = [
...cleaned,
buildEventDefinition(event, commandForEvent(event.eventName))
]
}
config[ANTIGRAVITY_HOOK_BUNDLE_NAME] = bundle
}
function removeInstalledConfig(config: HooksConfig, scriptFileName: string): void {
const isManagedCommand = createManagedCommandMatcher(scriptFileName)
const bundle = removeManagedCommandsFromBundle(getBundle(config), isManagedCommand)
if (Object.keys(bundle).length === 0) {
delete config[ANTIGRAVITY_HOOK_BUNDLE_NAME]
return
}
config[ANTIGRAVITY_HOOK_BUNDLE_NAME] = bundle
}
export class AntigravityHookService {
getStatus(): AgentHookInstallStatus {
const configPath = getConfigPath()
const scriptPath = getManagedScriptPath()
const config = readHooksJson(configPath)
if (!config) {
return {
agent: 'antigravity',
state: 'error',
configPath,
managedHooksPresent: false,
detail: 'Could not parse Antigravity hooks.json'
}
}
const bundle = getBundle(config)
const missing: string[] = []
let presentCount = 0
for (const event of ANTIGRAVITY_EVENTS) {
const definitions = Array.isArray(bundle[event.eventName])
? (bundle[event.eventName] as HookDefinition[])
: []
if (hasManagedCommand(definitions, getManagedCommand(scriptPath, event.eventName))) {
presentCount += 1
} else {
missing.push(event.eventName)
}
}
const managedHooksPresent = presentCount > 0
let state: AgentHookInstallState
let detail: string | null
if (missing.length === 0) {
state = 'installed'
detail = null
} else if (presentCount === 0) {
state = 'not_installed'
detail = null
} else {
state = 'partial'
detail = `Managed hook missing for events: ${missing.join(', ')}`
}
return { agent: 'antigravity', state, configPath, managedHooksPresent, detail }
}
install(): AgentHookInstallStatus {
const configPath = getConfigPath()
const scriptPath = getManagedScriptPath()
const config = readHooksJson(configPath)
if (!config) {
return {
agent: 'antigravity',
state: 'error',
configPath,
managedHooksPresent: false,
detail: 'Could not parse Antigravity hooks.json'
}
}
buildInstalledConfig(
config,
(eventName) => getManagedCommand(scriptPath, eventName),
getManagedScriptFileName()
)
writeManagedScript(scriptPath, getManagedScript())
writeHooksJson(configPath, config)
return this.getStatus()
}
async installRemote(sftp: SFTPWrapper, remoteHome: string): Promise<AgentHookInstallStatus> {
const home = remoteHome.replace(/\/$/, '')
const remoteConfigPath = `${home}/.gemini/config/hooks.json`
const remoteScriptPath = `${home}/.orca/agent-hooks/antigravity-hook.sh`
try {
const config = await readHooksJsonRemote(sftp, remoteConfigPath)
if (!config) {
return {
agent: 'antigravity',
state: 'error',
configPath: remoteConfigPath,
managedHooksPresent: false,
detail: 'Could not parse remote Antigravity hooks.json'
}
}
buildInstalledConfig(
config,
(eventName) =>
wrapPosixHookCommand(remoteScriptPath, { ORCA_ANTIGRAVITY_EVENT: eventName }),
'antigravity-hook.sh'
)
await writeManagedScriptRemote(sftp, remoteScriptPath, getManagedScript('posix'))
await writeHooksJsonRemote(sftp, remoteConfigPath, config)
return {
agent: 'antigravity',
state: 'installed',
configPath: remoteConfigPath,
managedHooksPresent: true,
detail: null
}
} catch (err) {
return {
agent: 'antigravity',
state: 'error',
configPath: remoteConfigPath,
managedHooksPresent: false,
detail: err instanceof Error ? err.message : String(err)
}
}
}
remove(): AgentHookInstallStatus {
const configPath = getConfigPath()
const config = readHooksJson(configPath)
if (!config) {
return {
agent: 'antigravity',
state: 'error',
configPath,
managedHooksPresent: false,
detail: 'Could not parse Antigravity hooks.json'
}
}
removeInstalledConfig(config, getManagedScriptFileName())
writeHooksJson(configPath, config)
return this.getStatus()
}
}
export const antigravityHookService = new AntigravityHookService()

View File

@ -57,6 +57,7 @@ import { setMigrationUnsupportedPtyListener } from './agent-hooks/migration-unsu
import { claudeHookService } from './claude/hook-service'
import { codexHookService } from './codex/hook-service'
import { geminiHookService } from './gemini/hook-service'
import { antigravityHookService } from './antigravity/hook-service'
import { cursorHookService } from './cursor/hook-service'
import { droidHookService } from './droid/hook-service'
import { grokHookService } from './grok/hook-service'
@ -899,6 +900,7 @@ app.whenReady().then(async () => {
['claude', () => claudeHookService.install()],
['codex', () => codexHookService.install()],
['gemini', () => geminiHookService.install()],
['antigravity', () => antigravityHookService.install()],
['cursor', () => cursorHookService.install()],
['droid', () => droidHookService.install()],
['grok', () => grokHookService.install()],

View File

@ -52,6 +52,9 @@ vi.mock('../codex/hook-service', () => ({
vi.mock('../gemini/hook-service', () => ({
geminiHookService: { getStatus: vi.fn(() => ({ agent: 'gemini', state: 'absent' })) }
}))
vi.mock('../antigravity/hook-service', () => ({
antigravityHookService: { getStatus: vi.fn(() => ({ agent: 'antigravity', state: 'absent' })) }
}))
vi.mock('../cursor/hook-service', () => ({
cursorHookService: { getStatus: vi.fn(() => ({ agent: 'cursor', state: 'absent' })) }
}))
@ -101,6 +104,17 @@ describe('agentStatus:getSnapshot IPC', () => {
})
})
describe('agentHooks:antigravityStatus IPC', () => {
it('returns Antigravity hook installation status', async () => {
const { registerAgentHookHandlers } = await import('./agent-hooks')
registerAgentHookHandlers()
const handler = handleHandlers.get('agentHooks:antigravityStatus')
expect(handler).toBeDefined()
expect(handler!({})).toEqual({ agent: 'antigravity', state: 'absent' })
})
})
describe('agentStatus:inferInterrupt IPC', () => {
it('forwards valid inference requests to the hook server', async () => {
inferInterrupt.mockReturnValue(true)

View File

@ -13,6 +13,7 @@ import {
import { claudeHookService } from '../claude/hook-service'
import { codexHookService } from '../codex/hook-service'
import { geminiHookService } from '../gemini/hook-service'
import { antigravityHookService } from '../antigravity/hook-service'
import { cursorHookService } from '../cursor/hook-service'
import { droidHookService } from '../droid/hook-service'
import { grokHookService } from '../grok/hook-service'
@ -33,6 +34,7 @@ export function registerAgentHookHandlers(): void {
ipcMain.removeHandler('agentHooks:claudeStatus')
ipcMain.removeHandler('agentHooks:codexStatus')
ipcMain.removeHandler('agentHooks:geminiStatus')
ipcMain.removeHandler('agentHooks:antigravityStatus')
ipcMain.removeHandler('agentHooks:cursorStatus')
ipcMain.removeHandler('agentHooks:droidStatus')
ipcMain.removeHandler('agentHooks:grokStatus')
@ -121,6 +123,19 @@ export function registerAgentHookHandlers(): void {
}
}
})
ipcMain.handle('agentHooks:antigravityStatus', (): AgentHookInstallStatus => {
try {
return antigravityHookService.getStatus()
} catch (err) {
return {
agent: 'antigravity',
state: 'error',
configPath: '',
managedHooksPresent: false,
detail: err instanceof Error ? err.message : String(err)
}
}
})
ipcMain.handle('agentHooks:cursorStatus', (): AgentHookInstallStatus => {
try {
return cursorHookService.getStatus()

View File

@ -8,6 +8,7 @@ const AGENT_TYPE_LABELS: Readonly<Record<string, string>> = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
antigravity: 'Antigravity',
opencode: 'OpenCode',
cursor: 'Cursor',
aider: 'Aider',

View File

@ -1214,6 +1214,7 @@ export type PreloadApi = {
claudeStatus: () => Promise<AgentHookInstallStatus>
codexStatus: () => Promise<AgentHookInstallStatus>
geminiStatus: () => Promise<AgentHookInstallStatus>
antigravityStatus: () => Promise<AgentHookInstallStatus>
cursorStatus: () => Promise<AgentHookInstallStatus>
droidStatus: () => Promise<AgentHookInstallStatus>
grokStatus: () => Promise<AgentHookInstallStatus>

View File

@ -1255,6 +1255,8 @@ const api = {
ipcRenderer.invoke('agentHooks:codexStatus'),
geminiStatus: (): Promise<AgentHookInstallStatus> =>
ipcRenderer.invoke('agentHooks:geminiStatus'),
antigravityStatus: (): Promise<AgentHookInstallStatus> =>
ipcRenderer.invoke('agentHooks:antigravityStatus'),
cursorStatus: (): Promise<AgentHookInstallStatus> =>
ipcRenderer.invoke('agentHooks:cursorStatus'),
droidStatus: (): Promise<AgentHookInstallStatus> =>

View File

@ -5,7 +5,7 @@ import {
} from '../../../../shared/agent-detection'
const TITLE_AGENT_TOKEN_RE =
/(?<![\w./\\-])(claude|codex|gemini|opencode|openclaw|aider|copilot|cursor-agent|cursor|droid|hermes|grok|pi)(?![\w./\\-])/i
/(?<![\w./\\-])(claude|codex|gemini|antigravity|agy|opencode|openclaw|aider|copilot|cursor-agent|cursor|droid|hermes|grok|pi)(?![\w./\\-])/i
export function titleHasExplicitAgentIdentity(title: string): boolean {
if (!title) {

View File

@ -61,6 +61,13 @@ export const AGENT_CATALOG: AgentCatalogEntry[] = [
faviconDomain: 'gemini.google.com',
homepageUrl: 'https://github.com/google-gemini/gemini-cli'
},
{
id: 'antigravity',
label: 'Antigravity',
cmd: 'agy',
faviconDomain: 'antigravity.google',
homepageUrl: 'https://antigravity.google/docs/cli-overview'
},
{
id: 'aider',
label: 'Aider',

View File

@ -389,6 +389,8 @@ describe('getAgentLabel', () => {
expect(getAgentLabel('✦ Gemini CLI')).toBe('Gemini CLI')
expect(getAgentLabel('⠂ Claude Code')).toBe('Claude Code')
expect(getAgentLabel('⠋ Codex is thinking')).toBe('Codex')
expect(getAgentLabel('Antigravity running')).toBe('Antigravity')
expect(getAgentLabel('agy working')).toBe('Antigravity')
expect(getAgentLabel('Grok running')).toBe('Grok')
expect(getAgentLabel('⠋ Droid')).toBe('Droid')
expect(getAgentLabel('Droid ready')).toBe('Droid')
@ -703,6 +705,10 @@ describe('formatAgentTypeLabel', () => {
expect(formatAgentTypeLabel('gemini')).toBe('Gemini')
})
it("maps 'antigravity' to 'Antigravity'", () => {
expect(formatAgentTypeLabel('antigravity')).toBe('Antigravity')
})
it("maps 'cursor' to 'Cursor'", () => {
expect(formatAgentTypeLabel('cursor')).toBe('Cursor')
})
@ -731,6 +737,7 @@ describe('agentTypeToIconAgent', () => {
it("round-trips iconable agent types like 'claude'", () => {
expect(agentTypeToIconAgent('claude')).toBe('claude')
expect(agentTypeToIconAgent('antigravity')).toBe('antigravity')
})
it('returns null for arbitrary non-iconable strings', () => {

View File

@ -113,6 +113,7 @@ const WELL_KNOWN_LABELS: Record<string, string> = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
antigravity: 'Antigravity',
copilot: 'GitHub Copilot',
opencode: 'OpenCode',
cursor: 'Cursor',
@ -151,6 +152,7 @@ const ICONABLE_AGENT_TYPES: Record<TuiAgent, true> = {
opencode: true,
pi: true,
gemini: true,
antigravity: true,
aider: true,
goose: true,
amp: true,

View File

@ -38,6 +38,22 @@ describe('buildAgentStartupPlan', () => {
})
})
it('uses Antigravity interactive prompt mode with the agy binary', () => {
expect(
buildAgentStartupPlan({
agent: 'antigravity',
prompt: 'Investigate this regression',
cmdOverrides: {},
platform: 'linux'
})
).toEqual({
agent: 'antigravity',
launchCommand: "agy --prompt-interactive 'Investigate this regression'",
expectedProcess: 'agy',
followupPrompt: null
})
})
it('launches aider first and injects the draft prompt after startup', () => {
expect(
buildAgentStartupPlan({

View File

@ -78,6 +78,7 @@ const SHELL_PROCESS_NAMES = new Set([
const AGENT_PROCESS_NAMES = new Set([
'aider',
'amp',
'agy',
'claude',
'claude-code',
'codex',

View File

@ -957,7 +957,16 @@ function createCliApi(): NonNullable<Partial<PreloadApi>['cli']> {
function createAgentHooksApi(): NonNullable<Partial<PreloadApi>['agentHooks']> {
const status = (
agent: 'claude' | 'codex' | 'gemini' | 'cursor' | 'droid' | 'grok' | 'copilot' | 'hermes'
agent:
| 'claude'
| 'codex'
| 'gemini'
| 'antigravity'
| 'cursor'
| 'droid'
| 'grok'
| 'copilot'
| 'hermes'
) =>
Promise.resolve({
agent,
@ -970,6 +979,7 @@ function createAgentHooksApi(): NonNullable<Partial<PreloadApi>['agentHooks']> {
claudeStatus: () => status('claude'),
codexStatus: () => status('codex'),
geminiStatus: () => status('gemini'),
antigravityStatus: () => status('antigravity'),
cursorStatus: () => status('cursor'),
droidStatus: () => status('droid'),
grokStatus: () => status('grok'),

View File

@ -27,6 +27,7 @@ export const AGENT_NAMES = [
'copilot',
'cursor',
'gemini',
'antigravity',
'opencode',
'openclaw',
'aider',
@ -41,6 +42,7 @@ const DROID_AGENT_NAME_RE = /(?<![\w./\\-])droid(?![\w./\\-])/i
// substring list because cwd/path titles like `~/hermes/working` would
// otherwise count as agent activity.
const HERMES_AGENT_NAME_RE = /(?<![\w./\\-])hermes(?![\w./\\-])/i
const AGY_AGENT_NAME_RE = /(?<![\w./\\-])agy(?![\w./\\-])/i
// Why: idle keywords used inside `detectAgentStatusFromTitle` to map titles
// like "Codex done", "OpenCode ready", "Aider idle" to AgentStatus 'idle'.
@ -174,6 +176,7 @@ function containsLegacyAgentName(title: string): boolean {
function containsAgentName(title: string): boolean {
return (
containsLegacyAgentName(title) ||
AGY_AGENT_NAME_RE.test(title) ||
DROID_AGENT_NAME_RE.test(title) ||
HERMES_AGENT_NAME_RE.test(title)
)
@ -367,6 +370,9 @@ export function getAgentLabel(title: string): string | null {
if (lower.includes('grok')) {
return 'Grok'
}
if (lower.includes('antigravity') || AGY_AGENT_NAME_RE.test(title)) {
return 'Antigravity'
}
if (lower.includes('opencode')) {
return 'OpenCode'
}
@ -446,8 +452,9 @@ export function detectAgentStatusFromTitle(title: string): AgentStatus | null {
const hasDroidAgentName = DROID_AGENT_NAME_RE.test(title)
const hasHermesAgentName = HERMES_AGENT_NAME_RE.test(title)
const hasAgyAgentName = AGY_AGENT_NAME_RE.test(title)
const hasLegacyAgentName = containsLegacyAgentName(title)
if (hasLegacyAgentName || hasDroidAgentName || hasHermesAgentName) {
if (hasLegacyAgentName || hasDroidAgentName || hasHermesAgentName || hasAgyAgentName) {
if (containsAny(title, ['action required', 'permission', 'waiting'])) {
return 'permission'
}

View File

@ -39,6 +39,7 @@ describe('shared agent-hook-listener', () => {
it('routes pathnames to a known source or null', () => {
expect(resolveHookSource('/hook/claude')).toBe('claude')
expect(resolveHookSource('/hook/cursor')).toBe('cursor')
expect(resolveHookSource('/hook/antigravity')).toBe('antigravity')
expect(resolveHookSource('/hook/grok')).toBe('grok')
expect(resolveHookSource('/hook/hermes')).toBe('hermes')
expect(resolveHookSource('/hook/unknown')).toBeNull()
@ -131,6 +132,203 @@ describe('shared agent-hook-listener', () => {
expect(event!.payload.prompt).toBe('')
})
it('normalizes Antigravity invocation and tool hooks', () => {
const started = normalizeHookPayload(
state,
'antigravity',
{
paneKey: PANE_KEY,
tabId: 'tab-1',
worktreeId: 'wt',
hook_event_name: 'PreInvocation',
payload: { prompt: 'run tests' }
},
'production'
)
expect(started?.payload).toMatchObject({
state: 'working',
prompt: 'run tests',
agentType: 'antigravity'
})
const tool = normalizeHookPayload(
state,
'antigravity',
{
paneKey: PANE_KEY,
tabId: 'tab-1',
hook_event_name: 'PreToolUse',
payload: {
toolCall: {
name: 'run_command',
args: { CommandLine: 'pnpm test' }
}
}
},
'production'
)
expect(tool?.payload).toMatchObject({
state: 'working',
prompt: 'run tests',
agentType: 'antigravity',
toolName: 'run_command',
toolInput: 'pnpm test'
})
})
it('maps Antigravity feedback tools to waiting state', () => {
const question = normalizeHookPayload(
state,
'antigravity',
{
paneKey: PANE_KEY,
hook_event_name: 'PreToolUse',
payload: {
toolCall: {
name: 'ask_question',
args: { Prompt: 'Which path should I use?' }
}
}
},
'production'
)
expect(question?.payload).toMatchObject({
state: 'waiting',
agentType: 'antigravity',
toolName: 'ask_question',
toolInput: 'Which path should I use?'
})
const permission = normalizeHookPayload(
state,
'antigravity',
{
paneKey: PANE_KEY,
hook_event_name: 'PreToolUse',
payload: {
toolCall: {
name: 'ask_permission',
args: { Action: 'run command', Target: 'pnpm lint' }
}
}
},
'production'
)
expect(permission?.payload).toMatchObject({
state: 'waiting',
agentType: 'antigravity',
toolName: 'ask_permission',
toolInput: 'run command'
})
})
it('resets Antigravity tool state on a new invocation', () => {
normalizeHookPayload(
state,
'antigravity',
{
paneKey: PANE_KEY,
hook_event_name: 'PreToolUse',
payload: {
toolCall: { name: 'run_command', args: { CommandLine: 'pnpm test' } }
}
},
'production'
)
const nextTurn = normalizeHookPayload(
state,
'antigravity',
{
paneKey: PANE_KEY,
hook_event_name: 'PreInvocation',
payload: { prompt: 'new task' }
},
'production'
)
expect(nextTurn?.payload).toMatchObject({
state: 'working',
prompt: 'new task',
agentType: 'antigravity'
})
expect(nextTurn?.payload.toolName).toBeUndefined()
expect(nextTurn?.payload.toolInput).toBeUndefined()
})
it('normalizes Antigravity Stop hooks and reads final text from the transcript', () => {
const tmpDir = mkdtempSync(join(tmpdir(), 'orca-antigravity-transcript-'))
const transcriptPath = join(tmpDir, 'transcript.jsonl')
try {
writeFileSync(
transcriptPath,
`${[
JSON.stringify({ source: 'USER', type: 'REQUEST', content: 'hi' }),
JSON.stringify({
source: 'MODEL',
type: 'PLANNER_RESPONSE',
content: 'Antigravity is wired up.'
})
].join('\n')}\n`
)
const done = normalizeHookPayload(
state,
'antigravity',
{
paneKey: PANE_KEY,
hook_event_name: 'Stop',
payload: { fullyIdle: true, transcriptPath }
},
'production'
)
expect(done?.payload).toMatchObject({
state: 'done',
agentType: 'antigravity',
lastAssistantMessage: 'Antigravity is wired up.'
})
} finally {
rmSync(tmpDir, { recursive: true, force: true })
}
})
it('keeps Antigravity working when Stop reports the agent is not fully idle', () => {
const event = normalizeHookPayload(
state,
'antigravity',
{
paneKey: PANE_KEY,
hook_event_name: 'Stop',
payload: { fullyIdle: false }
},
'production'
)
expect(event?.payload).toMatchObject({
state: 'working',
agentType: 'antigravity'
})
})
it('treats Antigravity Stop transcripts as pending result text', () => {
expect(
hasPendingAgentResultText('antigravity', {
hook_event_name: 'Stop',
payload: { transcriptPath: '/tmp/antigravity-transcript.jsonl' }
})
).toBe(true)
expect(
hasPendingAgentResultText('antigravity', {
hook_event_name: 'Stop',
payload: {
transcriptPath: '/tmp/antigravity-transcript.jsonl',
last_assistant_message: 'done'
}
})
).toBe(false)
})
it('normalizes Grok hookEventName payloads and keeps prompt across tool events', () => {
const prompt = normalizeHookPayload(
state,

View File

@ -323,6 +323,7 @@ const TOOL_INPUT_KEYS_BY_TOOL: Record<string, readonly string[]> = {
edit_file: ['file_path', 'path'],
replace: ['file_path', 'path'],
run_shell_command: ['command'],
run_command: ['CommandLine', 'command', 'cmd'],
glob: ['pattern'],
search_file_content: ['pattern'],
web_fetch: ['url'],
@ -353,7 +354,20 @@ const TOOL_INPUT_KEYS_BY_TOOL: Record<string, readonly string[]> = {
browser_type: ['text', 'target', 'selector'],
session_search: ['query'],
skill_manage: ['action', 'name', 'file_path'],
delegate_task: ['task', 'prompt', 'description']
delegate_task: ['task', 'prompt', 'description'],
view_file: ['AbsolutePath', 'path', 'file_path'],
write_to_file: ['TargetFile', 'path', 'file_path'],
replace_file_content: ['TargetFile', 'path', 'file_path'],
multi_replace_file_content: ['TargetFile', 'path', 'file_path'],
list_dir: ['DirectoryPath', 'path'],
find_by_name: ['SearchDirectory', 'Pattern', 'query'],
grep_search: ['SearchPath', 'Query', 'query', 'pattern'],
search_web: ['query'],
read_url_content: ['Url', 'url'],
manage_task: ['TaskId', 'Action'],
schedule: ['Prompt', 'DurationSeconds', 'CronExpression'],
ask_question: ['question', 'questions'],
ask_permission: ['Action', 'Target', 'Reason']
}
const FALLBACK_TOOL_INPUT_KEYS = [
@ -371,7 +385,15 @@ const FALLBACK_TOOL_INPUT_KEYS = [
'text',
'action',
'name',
'description'
'description',
'CommandLine',
'AbsolutePath',
'TargetFile',
'DirectoryPath',
'SearchPath',
'Query',
'Url',
'Prompt'
] as const
function deriveToolInputPreview(
@ -501,6 +523,14 @@ function extractAssistantTextFromLine(line: string): string | undefined {
}
}
}
if (
record.source === 'MODEL' &&
record.type === 'PLANNER_RESPONSE' &&
typeof record.content === 'string' &&
record.content.trim().length > 0
) {
return record.content
}
const nestedMessage = record.message as Record<string, unknown> | undefined
const role =
record.role ?? nestedMessage?.role ?? (record.type === 'assistant' ? 'assistant' : undefined)
@ -603,6 +633,8 @@ function readLastAssistantFromGrokChatHistory(
}
export function hasPendingAgentResultText(source: AgentHookSource, body: unknown): boolean {
const envelope =
typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : null
const record = parseHookBodyPayloadRecord(body)
if (!record) {
return false
@ -616,6 +648,15 @@ export function hasPendingAgentResultText(source: AgentHookSource, body: unknown
const transcriptPath = record.transcript_path ?? record.transcriptPath
return typeof transcriptPath === 'string' && transcriptPath.trim().length > 0
}
const eventName =
envelope?.hook_event_name ??
envelope?.hookEventName ??
record.hook_event_name ??
record.hookEventName
if (source === 'antigravity' && eventName === 'Stop') {
const transcriptPath = record.transcriptPath ?? record.transcript_path
return typeof transcriptPath === 'string' && transcriptPath.trim().length > 0
}
if (
source === 'grok' &&
isGrokEvent(record.hookEventName ?? record.hook_event_name, 'stop', 'session_end')
@ -781,6 +822,44 @@ function extractGeminiToolFields(
return {}
}
function readAntigravityToolCall(hookPayload: Record<string, unknown>): {
toolName?: string
toolInputSource?: unknown
} {
const toolCall = hookPayload.toolCall
if (typeof toolCall !== 'object' || toolCall === null) {
return {}
}
const record = toolCall as Record<string, unknown>
return {
toolName: readFirstString(record, ['name', 'toolName', 'tool_name']),
toolInputSource: record.args
}
}
function extractAntigravityToolFields(
eventName: unknown,
hookPayload: Record<string, unknown>
): ToolSnapshot {
if (eventName === 'PreToolUse' || eventName === 'PostToolUse') {
const toolCall = readAntigravityToolCall(hookPayload)
const toolName = toolCall.toolName
const toolInput =
deriveToolInputPreview(toolName, toolCall.toolInputSource) ??
deriveFallbackToolInputPreview(toolCall.toolInputSource)
return { toolName, toolInput }
}
if (eventName === 'Stop') {
const message =
readString(hookPayload, 'last_assistant_message') ??
readLastAssistantFromTranscript(hookPayload.transcriptPath ?? hookPayload.transcript_path)
if (message) {
return { lastAssistantMessage: message }
}
}
return {}
}
function extractOpenCodeToolFields(
eventName: unknown,
hookPayload: Record<string, unknown>
@ -1286,6 +1365,8 @@ function isNewTurnEvent(source: AgentHookSource, eventName: unknown): boolean {
return eventName === 'SessionStart' || eventName === 'UserPromptSubmit'
case 'gemini':
return eventName === 'BeforeAgent'
case 'antigravity':
return eventName === 'PreInvocation'
case 'opencode':
return false
case 'cursor':
@ -1324,6 +1405,8 @@ function extractToolFields(
return extractCodexToolFields(eventName, hookPayload)
case 'gemini':
return extractGeminiToolFields(eventName, hookPayload)
case 'antigravity':
return extractAntigravityToolFields(eventName, hookPayload)
case 'opencode':
return extractOpenCodeToolFields(eventName, hookPayload)
case 'cursor':
@ -1436,6 +1519,57 @@ function normalizeGeminiEvent(
)
}
function isAntigravityFeedbackTool(toolName: string | undefined): boolean {
return toolName === 'ask_question' || toolName === 'ask_permission'
}
function normalizeAntigravityEvent(
state: HookListenerState,
eventName: unknown,
promptText: string,
paneKey: string,
hookPayload: Record<string, unknown>
): ParsedAgentStatusPayload | null {
const toolName = readAntigravityToolCall(hookPayload).toolName
const stateName =
eventName === 'PreToolUse' && isAntigravityFeedbackTool(toolName)
? 'waiting'
: eventName === 'Stop'
? hookPayload.fullyIdle === false
? 'working'
: 'done'
: eventName === 'PreInvocation' ||
eventName === 'PostInvocation' ||
eventName === 'PreToolUse' ||
eventName === 'PostToolUse'
? 'working'
: null
if (!stateName) {
return null
}
const snapshot = resolveToolState(
state,
paneKey,
extractToolFields('antigravity', eventName, hookPayload),
{ resetOnNewTurn: isNewTurnEvent('antigravity', eventName) }
)
return parseAgentStatusPayload(
JSON.stringify({
state: stateName,
prompt: resolvePrompt(state, paneKey, promptText, {
resetOnNewTurn: isNewTurnEvent('antigravity', eventName)
}),
agentType: 'antigravity',
toolName: snapshot.toolName,
toolInput: snapshot.toolInput,
lastAssistantMessage: snapshot.lastAssistantMessage
})
)
}
function normalizeCodexEvent(
state: HookListenerState,
eventName: unknown,
@ -1952,6 +2086,9 @@ export function normalizeHookPayload(
case 'gemini':
payload = normalizeGeminiEvent(state, eventName, promptText, paneKey, hookPayloadRecord)
break
case 'antigravity':
payload = normalizeAntigravityEvent(state, eventName, promptText, paneKey, hookPayloadRecord)
break
case 'opencode':
payload = normalizeOpenCodeEvent(state, eventName, promptText, paneKey, hookPayloadRecord)
break
@ -2002,6 +2139,7 @@ export const HOOK_SOURCE_BY_PATHNAME: Readonly<Record<string, AgentHookSource>>
'/hook/claude': 'claude',
'/hook/codex': 'codex',
'/hook/gemini': 'gemini',
'/hook/antigravity': 'antigravity',
'/hook/opencode': 'opencode',
'/hook/cursor': 'cursor',
'/hook/pi': 'pi',

View File

@ -34,6 +34,7 @@ export type AgentHookSource =
| 'claude'
| 'codex'
| 'gemini'
| 'antigravity'
| 'opencode'
| 'cursor'
| 'pi'

View File

@ -7,6 +7,7 @@ export const AGENT_HOOK_TARGETS = [
'claude',
'codex',
'gemini',
'antigravity',
'cursor',
'droid',
'grok',

View File

@ -20,6 +20,7 @@ const TUI_AGENT_KIND_BY_AGENT = {
opencode: 'opencode',
pi: 'pi',
gemini: 'gemini',
antigravity: 'antigravity',
aider: 'aider',
goose: 'goose',
amp: 'amp',

View File

@ -15,6 +15,7 @@ export type WellKnownAgentType =
| 'claude'
| 'codex'
| 'gemini'
| 'antigravity'
| 'opencode'
| 'cursor'
| 'copilot'

View File

@ -41,6 +41,7 @@ export const AGENT_KIND_VALUES = [
'opencode',
'pi',
'gemini',
'antigravity',
'aider',
'goose',
'amp',

View File

@ -112,6 +112,12 @@ export const TUI_AGENT_CONFIG: Record<TuiAgent, TuiAgentConfig> = {
expectedProcess: 'gemini',
promptInjectionMode: 'flag-prompt-interactive'
},
antigravity: {
detectCmd: 'agy',
launchCmd: 'agy',
expectedProcess: 'agy',
promptInjectionMode: 'flag-prompt-interactive'
},
aider: {
detectCmd: 'aider',
launchCmd: 'aider',

View File

@ -1406,6 +1406,7 @@ export type TuiAgent =
| 'opencode' // OpenCode
| 'pi' // Pi (pi.dev)
| 'gemini' // Gemini CLI
| 'antigravity' // Google Antigravity CLI
| 'aider' // Aider
| 'goose' // Goose
| 'amp' // Amp