Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions src/renderer/assets/agent-icons/acp/agents.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,32 @@
}
},
{
"id": "gemini",
"name": "Gemini CLI",
"version": "0.45.1",
"description": "Google's official CLI for Gemini",
"id": "antigravity",
"name": "Antigravity CLI",
"version": "1.0.12",
"description": "Google's official CLI for Antigravity",
"distribution": {
"npx": {
"package": "@google/gemini-cli@0.45.1",
"args": ["--acp"]
"binary": {
"darwin-aarch64": {
"cmd": "agy",
"args": ["--acp"]
},
"darwin-x86_64": {
"cmd": "agy",
"args": ["--acp"]
},
"linux-aarch64": {
"cmd": "agy",
"args": ["--acp"]
},
"linux-x86_64": {
"cmd": "agy",
"args": ["--acp"]
},
"windows-x86_64": {
"cmd": "agy.exe",
"args": ["--acp"]
}
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/assets/agent-icons/acp/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@
"file": "fast-agent.svg"
},
{
"id": "gemini",
"name": "Gemini CLI",
"id": "antigravity",
"name": "Antigravity CLI",
Comment on lines +88 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

manifest=$(fd -p manifest.json src/renderer/assets/agent-icons/acp)
agents=$(fd -p agents.json src/renderer/assets/agent-icons/acp)

echo "== Antigravity manifest entry =="
rg -n -C2 '"id": "antigravity"|"name": "Antigravity CLI"|"file":' "$manifest"

echo
echo "== Remaining Gemini/Antigravity references in ACP icon metadata =="
rg -n 'gemini|antigravity' "$manifest" "$agents"

echo
echo "== ACP icon files =="
fd . src/renderer/assets/agent-icons/acp

Repository: gnoviawan/termul

Length of output: 6016


Update the Antigravity manifest entry to use the correct icon
src/renderer/assets/agent-icons/acp/manifest.json still maps "id": "antigravity" / "name": "Antigravity CLI" to "file": "gemini.svg", so this entry renders the Gemini asset instead of an Antigravity-specific one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/assets/agent-icons/acp/manifest.json` around lines 88 - 89, The
Antigravity manifest entry is still pointing to the Gemini asset, so update the
manifest mapping for the antigravity entry to use the correct Antigravity icon
file instead of gemini.svg. Locate the entry by its "id": "antigravity" and
"name": "Antigravity CLI" in the manifest and change only the referenced icon
asset so this agent renders the right graphic.

"file": "gemini.svg"
},
{
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/agents/AgentLauncher.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ describe('AgentLauncher ACP new thread', () => {
expect(agentPicker).not.toHaveTextContent('Codex CLI')
fireEvent.click(agentPicker)
expect(await screen.findByText('Claude Agent')).toBeInTheDocument()
expect(screen.getByText('Gemini CLI')).toBeInTheDocument()
expect(screen.getByText('Antigravity CLI')).toBeInTheDocument()
expect(screen.getByText('Cursor')).toBeInTheDocument()
expect(screen.getByText('OpenCode')).toBeInTheDocument()
expect(screen.getByText('pi ACP')).toBeInTheDocument()
Expand Down
36 changes: 25 additions & 11 deletions src/renderer/components/agents/AgentLauncher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,34 +175,48 @@ export function AgentLauncher({ paneId, className }: AgentLauncherProps): React.
}
}, [persistSelection, selectedConfigId, supportedAgents])

const selectedConfigRef = useRef(selectedConfig)
selectedConfigRef.current = selectedConfig

const acpConfigsRef = useRef(acpConfigs)
acpConfigsRef.current = acpConfigs

const configSpawnKey = useMemo(() => {
if (!selectedConfig) return ''
return `${selectedConfig.command} ${selectedConfig.args.join(' ')} ${JSON.stringify(selectedConfig.env)}`
}, [selectedConfig])
Comment on lines +184 to +187

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make configSpawnKey unambiguous.

Line 186 flattens command, args, and env into a space-delimited string. Distinct spawn configs can collide (['--foo bar'] vs ['--foo', 'bar']), which means this effect can miss a real config change and skip the re-prepare it is trying to force.

Suggested change
  const configSpawnKey = useMemo(() => {
    if (!selectedConfig) return ''
-   return `${selectedConfig.command} ${selectedConfig.args.join(' ')} ${JSON.stringify(selectedConfig.env)}`
+   return JSON.stringify({
+     command: selectedConfig.command,
+     args: selectedConfig.args,
+     env: Object.entries(selectedConfig.env).sort(([a], [b]) => a.localeCompare(b))
+   })
  }, [selectedConfig])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const configSpawnKey = useMemo(() => {
if (!selectedConfig) return ''
return `${selectedConfig.command} ${selectedConfig.args.join(' ')} ${JSON.stringify(selectedConfig.env)}`
}, [selectedConfig])
const configSpawnKey = useMemo(() => {
if (!selectedConfig) return ''
return JSON.stringify({
command: selectedConfig.command,
args: selectedConfig.args,
env: Object.entries(selectedConfig.env).sort(([a], [b]) => a.localeCompare(b))
})
}, [selectedConfig])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/agents/AgentLauncher.tsx` around lines 184 - 187,
`configSpawnKey` in AgentLauncher is currently built from a space-delimited
string, which can collide for different spawn configs and miss real changes.
Update the `useMemo` that computes `configSpawnKey` to use an unambiguous
representation of `selectedConfig` by separating `command`, `args`, and `env`
with a structured serialization or delimiter-safe encoding, so the effect
reliably re-runs when any spawn config field changes.


useEffect(() => {
if (!activeConfigId || !projectRoot || selectedEntry?.status !== 'ready' || !selectedConfig)
if (
!activeConfigId ||
!projectRoot ||
selectedEntry?.status !== 'ready' ||
!selectedConfigRef.current
)
return
let cancelled = false
void (async () => {
try {
if (!acpConfigs.some((config) => config.id === selectedConfig.id)) {
await saveAgentConfig(selectedConfig)
const currentConfig = selectedConfigRef.current
if (!currentConfig) return
const currentConfigs = acpConfigsRef.current
if (!currentConfigs.some((config) => config.id === currentConfig.id)) {
await saveAgentConfig(currentConfig)
if (cancelled) return
}
useAcpStore.getState().prepareChat(activeConfigId, projectRoot)
} catch (err) {
console.warn('[acp] failed to prepare supported agent', activeConfigId, err)
}
})()
// Re-prepare when command/args/env change
void configSpawnKey
const key = prepareChatKey(activeConfigId, projectRoot, undefined)
return () => {
cancelled = true
useAcpStore.getState().cancelPreparedChat(key)
}
}, [
activeConfigId,
acpConfigs,
projectRoot,
saveAgentConfig,
selectedConfig,
selectedEntry?.status
])
}, [activeConfigId, projectRoot, saveAgentConfig, selectedEntry?.status, configSpawnKey])

const handleSelectAgent = useCallback(
(entry: SupportedAcpAgentEntry) => {
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/chat/agent-templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AGENT_TEMPLATES, templateById, templateIcon } from './agent-templates'
describe('agent-templates', () => {
it('includes the well-known ACP registry agents', () => {
const ids = AGENT_TEMPLATES.map((t) => t.id)
for (const id of ['gemini', 'claude-acp', 'codex-acp', 'github-copilot-cli', 'custom']) {
for (const id of ['antigravity', 'claude-acp', 'codex-acp', 'github-copilot-cli', 'custom']) {
expect(ids).toContain(id)
}
})
Expand All @@ -28,12 +28,12 @@ describe('agent-templates', () => {
})

it('templateById resolves a known template and returns undefined otherwise', () => {
expect(templateById('gemini')?.label).toBe('Gemini CLI')
expect(templateById('antigravity')?.label).toBe('Antigravity CLI')
expect(templateById('does-not-exist')).toBeUndefined()
})

it('templateIcon returns a component for agents with icons and undefined otherwise', () => {
expect(templateIcon('gemini')).toBeTypeOf('function')
expect(templateIcon('antigravity')).toBeTypeOf('function')
expect(templateIcon('codex-acp')).toBeTypeOf('function')
// custom has no icon
expect(templateIcon('custom')).toBeUndefined()
Expand Down
12 changes: 6 additions & 6 deletions src/renderer/components/chat/agent-templates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ export interface AgentTemplate {

export const AGENT_TEMPLATES: AgentTemplate[] = [
{
id: 'gemini',
label: 'Gemini CLI',
notes: "Google's official CLI for Gemini. Runs via npx.",
id: 'antigravity',
label: 'Antigravity CLI',
notes: "Google's official CLI for Antigravity. Runs natively via agy.",
icon: GeminiIcon,
config: {
name: 'Gemini CLI',
command: 'npx',
args: ['-y', '@google/gemini-cli', '--acp'],
name: 'Antigravity CLI',
command: 'agy',
args: ['--acp'],
env: {},
allowTerminal: false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('AcpAgentsSettings', () => {

expect(screen.getByText('Codex CLI')).toBeInTheDocument()
expect(screen.getByText('Claude Agent')).toBeInTheDocument()
expect(screen.getByText('Gemini CLI')).toBeInTheDocument()
expect(screen.getByText('Antigravity CLI')).toBeInTheDocument()
expect(screen.getByText('Cursor')).toBeInTheDocument()
expect(screen.getByText('OpenCode')).toBeInTheDocument()
expect(screen.getByText('pi ACP')).toBeInTheDocument()
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/hooks/use-acp-agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,18 @@ describe('useAcpAgents', () => {
mockLoadAgentConfigs.mockImplementation(async () => {
stateRef.current.agentConfigs = [
config('acp-registry:claude-acp'),
config('acp-registry:gemini')
config('acp-registry:antigravity')
]
})
mockPersistRead.mockResolvedValue({
success: true,
data: { agentId: 'acp-registry:gemini', mode: 'acp' }
data: { agentId: 'acp-registry:antigravity', mode: 'acp' }
})

renderHook(() => useAcpAgents())

await waitFor(() => {
expect(mockPrewarmAgent).toHaveBeenCalledWith('acp-registry:gemini', '/work/proj-1')
expect(mockPrewarmAgent).toHaveBeenCalledWith('acp-registry:antigravity', '/work/proj-1')
})
expect(mockPrewarmAgent).toHaveBeenCalledTimes(1)
})
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/lib/agents/acp-registry-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const catalog: AcpRegistryCatalog = {
fetchedAt: '2026-06-01T00:00:00Z',
entries: [
{ id: 'claude-acp', name: 'Claude Code', icon: 'https://cdn/claude.svg' },
{ id: 'gemini', name: 'Gemini', icon: 'https://cdn/gemini.svg' }
{ id: 'antigravity', name: 'Antigravity', icon: 'https://cdn/antigravity.svg' }
]
}

Expand Down
56 changes: 47 additions & 9 deletions src/renderer/lib/agents/acp-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ describe('currentPlatformArch', () => {
describe('deriveAgentConfig', () => {
it('derives an npx distribution with -y prefix', () => {
const res = deriveAgentConfig(
agent({ npx: { package: '@google/gemini-cli@0.45.0', args: ['--acp'] } }),
agent({ npx: { package: 'some-acp-agent@1.0.0', args: ['--acp'] } }),
'windows-x86_64'
)
expect(res).toEqual({
kind: 'runnable',
config: {
name: 'Agent X',
command: 'npx',
args: ['-y', '@google/gemini-cli@0.45.0', '--acp'],
args: ['-y', 'some-acp-agent@1.0.0', '--acp'],
env: {},
allowTerminal: false
}
Expand Down Expand Up @@ -113,25 +113,55 @@ describe('deriveAgentConfig', () => {
}
})

it('returns needs-install for a binary present on the current platform-arch', () => {
it('returns needs-install for a binary present on the current platform-arch with archive', () => {
const res = deriveAgentConfig(
agent({ binary: { 'windows-x86_64': { cmd: './stakpak.exe', args: ['acp'] } } }),
agent({
binary: {
'windows-x86_64': {
cmd: './stakpak.exe',
archive: 'https://example.com/stakpak.zip',
args: ['acp']
}
}
}),
'windows-x86_64'
)
expect(res).toEqual({
kind: 'needs-install',
cmd: './stakpak.exe',
args: ['acp'],
env: {},
archiveUrl: undefined
archiveUrl: 'https://example.com/stakpak.zip'
})
})

it('returns runnable for a binary present on the current platform-arch without archive', () => {
const res = deriveAgentConfig(
agent({ binary: { 'windows-x86_64': { cmd: './stakpak.exe', args: ['acp'] } } }),
'windows-x86_64'
)
expect(res).toEqual({
kind: 'runnable',
config: {
name: 'Agent X',
command: './stakpak.exe',
args: ['acp'],
env: {},
allowTerminal: false
}
})
})

it('carries per-platform binary env into needs-install', () => {
const res = deriveAgentConfig(
agent({
binary: {
'windows-x86_64': { cmd: 'vtcode.exe', args: ['acp'], env: { VT_ACP_ENABLED: '1' } }
'windows-x86_64': {
cmd: 'vtcode.exe',
archive: 'https://example.com/vt.zip',
args: ['acp'],
env: { VT_ACP_ENABLED: '1' }
}
}
}),
'windows-x86_64'
Expand All @@ -141,13 +171,21 @@ describe('deriveAgentConfig', () => {
cmd: 'vtcode.exe',
args: ['acp'],
env: { VT_ACP_ENABLED: '1' },
archiveUrl: undefined
archiveUrl: 'https://example.com/vt.zip'
})
})

it('rejects a flag-like package and falls through to binary/unavailable', () => {
const res = deriveAgentConfig(
agent({ npx: { package: '--evil-flag' }, binary: { 'windows-x86_64': { cmd: './x.exe' } } }),
agent({
npx: { package: '--evil-flag' },
binary: {
'windows-x86_64': {
cmd: './x.exe',
archive: 'https://example.com/x.zip'
}
}
}),
'windows-x86_64'
)
// npx is skipped (unsafe package) -> falls through to the binary path.
Expand All @@ -156,7 +194,7 @@ describe('deriveAgentConfig', () => {
cmd: './x.exe',
args: [],
env: {},
archiveUrl: undefined
archiveUrl: 'https://example.com/x.zip'
})
})

Expand Down
25 changes: 19 additions & 6 deletions src/renderer/lib/agents/acp-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,25 @@ export function deriveAgentConfig(agent: RegistryAgent, platformArch: string): D
const target = dist.binary?.[platformArch]
if (target) {
const archiveUrl = supportedArchiveUrl(target.archive)
return {
kind: 'needs-install',
cmd: target.cmd,
args: [...(target.args ?? [])],
env: env(target.env),
archiveUrl
if (target.archive !== undefined) {
return {
kind: 'needs-install',
cmd: target.cmd,
args: [...(target.args ?? [])],
env: env(target.env),
archiveUrl
}
} else {
return {
kind: 'runnable',
config: {
name: agent.name,
command: target.cmd,
args: [...(target.args ?? [])],
env: env(target.env),
allowTerminal: false
}
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/renderer/lib/agents/agent-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('built-in agent definitions', () => {
expect(byId.codex).toMatchObject({ command: 'codex', promptMode: 'positional' })
expect(byId.cursor).toMatchObject({ command: 'cursor-agent', promptMode: 'positional' })
expect(byId['gemini-cli']).toMatchObject({
command: 'gemini',
command: 'agy',
promptMode: 'flag',
promptFlag: '-i'
})
Expand Down Expand Up @@ -149,7 +149,7 @@ describe('built-in agent definitions', () => {
args: ['P']
})
expect(buildAgentArgv(getBuiltInAgent('gemini-cli')!, 'P')).toEqual({
program: 'gemini',
program: 'agy',
args: ['-i', 'P']
})
expect(buildAgentArgv(getBuiltInAgent('opencode')!, 'P')).toEqual({
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/lib/agents/agent-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ export const BUILT_IN_AGENTS: readonly TerminalAgentDefinition[] = [
},
{
id: 'gemini-cli',
name: 'Gemini CLI',
command: 'gemini',
name: 'Antigravity CLI',
command: 'agy',
baseArgs: [],
promptMode: 'flag',
promptFlag: '-i',
registryId: 'gemini',
registryId: 'antigravity',
icon: geminiIcon as string,
isBuiltIn: true
},
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/lib/agents/supported-acp-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
export const SUPPORTED_ACP_AGENT_IDS = [
'codex-acp',
'claude-acp',
'gemini',
'antigravity',
'cursor',
Comment on lines 9 to 13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve legacy acp-registry:gemini data during this rename.

After Line 12 swaps the supported ID, buildSupportedAcpAgents() only looks up registryConfigId(id), and useAcpAgents() restores lastSelectedAgent by exact configId. Existing acp-registry:gemini installs/selections will stop matching after upgrade, so users silently fall back to another agent unless the old ID is aliased or migrated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/lib/agents/supported-acp-agents.ts` around lines 9 - 13, The ACP
agent ID rename in SUPPORTED_ACP_AGENT_IDS breaks existing `acp-registry:gemini`
selections because buildSupportedAcpAgents() and useAcpAgents() only match the
new config ID. Update the supported-agent resolution path in
supported-acp-agents.ts so the old `acp-registry:gemini` value is treated as an
alias or migrated to the new ID, and ensure registryConfigId()/lastSelectedAgent
restore logic can still match legacy installs without falling back to a
different agent.

'opencode',
'pi-acp'
Expand Down
Loading