diff --git a/docs/site/content/docs/agents/antigravity.mdx b/docs/site/content/docs/agents/antigravity.mdx new file mode 100644 index 00000000000..9dd985508b2 --- /dev/null +++ b/docs/site/content/docs/agents/antigravity.mdx @@ -0,0 +1,101 @@ +--- +title: Antigravity CLI setup +description: Install, find, and launch Google's agy CLI, including custom install paths, WSL, and remote hosts. +--- + +Orca launches Google's **Antigravity CLI**, whose command is `agy`. Installing the Antigravity desktop application alone does not put that command on your machine, and the separate `gemini` command is a different agent in Orca. To save and switch between several `agy` logins, see [Antigravity accounts](/docs/agents/antigravity-accounts). + +## Check the CLI first + +Install the CLI with [Google's current instructions](https://antigravity.google/docs/cli-overview), then open a new terminal so it picks up any updated `PATH`. + +On Windows (PowerShell): + +```powershell +Get-Command agy -All | Select-Object CommandType, Source, Definition +agy --version +``` + +On macOS, Linux, or inside a WSL distribution: + +```sh +command -v agy +agy --version +``` + +A PowerShell alias or function is not an executable Orca can launch; use the path of the actual CLI executable. + +Run `agy` in a project directory, complete Google's sign-in, and send a small prompt. Finding the binary or listing models with `agy models` does not prove the account can generate a reply. + +## Find it in Orca + +Open **Settings → Agents** and click **Refresh**. This searches again for installed agents; it does not install the CLI or sign you in. + +If `agy` is installed somewhere Orca does not search, expand the **Antigravity** row and enter the executable's full path in **Command**: + +```text +/opt/agent-tools/agy +``` + +On Windows, a path without spaces needs no quotes (`C:\Tools\agy.exe`). Quote a path that contains spaces. PowerShell, Orca's default Windows terminal shell, also needs its `&` call operator in front of a quoted path; Command Prompt does not: + +```text +& "C:\Agent Tools\agy.exe" +``` + +Keep launch flags out of **Command**. The field saves when you press Enter or leave it, and **Reset** restores `agy`. Once the executable exists at that path on the host, Orca lists Antigravity as installed: it appears in the New Tab menu and the new-workspace agent picker, and can be your default agent. A path Orca cannot find leaves Antigravity listed as not installed. + +## WSL and remote hosts + +Install and sign in to `agy` on the machine that runs the workspace: the WSL distribution, the [SSH host](/docs/ssh), or the [Remote Orca Server](/docs/remote-servers). A working CLI on your laptop does not make it available there. + +On Windows, the **Command** value is shared by Windows and WSL workspaces. A Windows path does not name a Linux executable, so a WSL workspace does not find `agy` through it, and a WSL workspace that finds `agy` on its own `PATH` still launches the Windows path. If you run Antigravity in both, add the Windows install to `PATH` instead of setting **Command**. For an SSH host or Remote Orca Server, choose that host in the Settings host menu before editing **Command**; the value is stored and checked on that host. + +## API-key authentication in WSL + +Google sign-in is the normal way to authenticate `agy`; an API key is not required for it. If you intentionally use a Gemini API key instead, `agy` needs two things on the machine that runs it: + +1. The `GEMINI_API_KEY` environment variable. +1. The API-key provider selected in `~/.gemini/antigravity-cli/settings.json`. Merge this into the existing JSON and keep your other settings: + + ```json + { + "modelProvider": "gemini" + } + ``` + + Without this setting, `agy` may still ask for Google sign-in even when the key is present. + +In WSL, both belong to the **Linux user inside the distribution**. A Windows environment variable is not automatically a Linux one, and changing a Windows variable does not update an Orca instance or terminal that is already running. + +The simplest setup is to export the key in the distribution's shell startup file (for example `~/.bashrc`), then open a new Orca terminal in that workspace. To forward a key you keep as a Windows environment variable instead, run this in PowerShell; use `'Machine'` instead of `'User'` for a system variable. It preserves your other `WSLENV` entries: + +```powershell +$env:GEMINI_API_KEY = [Environment]::GetEnvironmentVariable('GEMINI_API_KEY', 'User') +$entries = @($env:WSLENV -split ':' | Where-Object { $_ -and ($_ -split '/')[0] -ne 'GEMINI_API_KEY' }) +$env:WSLENV = ($entries + 'GEMINI_API_KEY/u') -join ':' +``` + +These assignments affect only this PowerShell process and programs it starts, so quit Orca and start it from this PowerShell window for the key to reach WSL. Never put the key in the **Command** or **Arguments** fields. + +Inside the distribution, check that the key arrived without printing it, then request a short reply: + +```sh +if [ -n "${GEMINI_API_KEY:-}" ]; then echo 'API key present'; else echo 'API key missing'; fi +agy --print 'Reply exactly AGY_AUTH_OK. Do not use tools.' --output-format json --print-timeout 30s +``` + +A present key proves only that it was forwarded; a successful reply proves `agy` can authenticate and generate. + +## Troubleshooting + +| Symptom | Next check | +| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| The Antigravity desktop app opens, but Orca does not list the agent | Confirm `agy --version` works; Orca needs the CLI executable. | +| `agy` works in a new terminal but Orca does not list it | Click **Refresh** in **Settings → Agents**, or enter the executable's full path in **Command**. | +| A custom path with spaces is not found | Quote the path (with `&` in front for PowerShell), point at the executable rather than a shell alias, and keep flags out of **Command**. | +| Works in Windows workspaces, missing in WSL | Install `agy` inside the distribution, and see the note above about a Windows path in **Command**. | +| A request returns 401 or `UNAUTHENTICATED` | Run `agy` on the same host outside Orca and complete Google's sign-in, or set up API-key authentication above. | +| A model name is rejected | Run `agy models` on the host and pick a model it lists. | + +When reporting a setup problem, include the Orca version, the host that runs the workspace, `agy --version`, and the error text. Remove account identifiers, keys, and private paths first. diff --git a/docs/site/content/docs/agents/meta.json b/docs/site/content/docs/agents/meta.json index 8a2015580f7..5b1b9d61aca 100644 --- a/docs/site/content/docs/agents/meta.json +++ b/docs/site/content/docs/agents/meta.json @@ -8,6 +8,7 @@ "codex", "cursor-cli", "codex-hot-swap", + "antigravity", "antigravity-accounts", "native-chat", "session-history", diff --git a/docs/site/content/docs/agents/supported.mdx b/docs/site/content/docs/agents/supported.mdx index 0f5051e3c2b..98313aa6d1a 100644 --- a/docs/site/content/docs/agents/supported.mdx +++ b/docs/site/content/docs/agents/supported.mdx @@ -22,6 +22,8 @@ Use **Settings → Agents → Agent Permissions** when you want to switch all un To restore prompts for one agent only, edit that agent's default arguments or environment in Settings. Orca treats a non-empty custom value as an explicit override and opts that agent out of future permission-mode migrations. +If an agent's CLI is installed outside `PATH`, expand its row in **Settings → Agents** and enter the executable's full path in **Command**. Orca lists the agent as installed once that path exists on the host that runs the workspace. + | Agent | Notes | Docs | | ------------------ | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Claude Code | Deep integration: usage, hot-swap, hooks | [Anthropic](https://docs.anthropic.com/claude/docs/claude-code) | @@ -34,7 +36,7 @@ To restore prompts for one agent only, edit that agent's default arguments or en | OMP | Auto-setup, hooks, status | [OMP](https://omp.sh) | | Prime Agent | Auto-setup, hooks, status, session history | [Prime Intellect](https://github.com/PrimeIntellect-ai/prime-agent) | | Gemini | Auto-setup | [Google](https://github.com/google-gemini/gemini-cli) | -| Antigravity | Auto-setup, hooks, status, usage, [account switching](/docs/agents/antigravity-accounts) | [Google](https://antigravity.google/docs/cli-overview) | +| Antigravity | [Setup](/docs/agents/antigravity), hooks, status, usage, [account switching](/docs/agents/antigravity-accounts) | [Google](https://antigravity.google/docs/cli-overview) | | Ante | Auto-setup, status | [Ante](https://github.com/AntigmaLabs/ante-preview) | | Aider | Auto-setup | [Aider](https://aider.chat/docs/) | | Goose | Auto-setup | [Block](https://goose-docs.ai/docs/quickstart/) | diff --git a/src/main/ipc/preflight.ts b/src/main/ipc/preflight.ts index 44a2b6a3188..5df5385a9c9 100644 --- a/src/main/ipc/preflight.ts +++ b/src/main/ipc/preflight.ts @@ -20,6 +20,7 @@ import { readZCodeInteractiveCapability } from '../zcode/interactive-capability' import { getPreflightWslTarget } from './preflight-runtime-target' import { runAsWslExecUserAct } from '../wsl/discovery/wsl-background-exec-gate' import { deferRefusedPreflight } from './preflight-wsl-refusal-retry' +import { agentCommandOverrideDetectionOptions } from '../preflight/installed-agent-detection' type AgentDetectionRequest = PreflightRuntimeContext & { enabledOnly?: boolean } /** Set by a user act (window focus, a click, opening a picker): it may start its WSL distro. */ @@ -38,12 +39,17 @@ function runPreflightAct( export function registerPreflightHandlers(store?: Pick): void { const detectionOptions = (args?: AgentDetectionRequest) => { if (!args?.enabledOnly) { - return undefined + // Overrides still apply: a configured CLI outside PATH must be listed in every inventory. + return agentCommandOverrideDetectionOptions(store?.getSettings().agentCmdOverrides) } if (!store) { throw new Error('Agent detection settings unavailable') } - return { disabledTuiAgents: store.getSettings().disabledTuiAgents } + const settings = store.getSettings() + return { + disabledTuiAgents: settings.disabledTuiAgents, + agentCmdOverrides: settings.agentCmdOverrides + } } ipcMain.handle( 'preflight:check', diff --git a/src/main/preflight/agent-command-detection.ts b/src/main/preflight/agent-command-detection.ts index 187004b1464..826f96d7182 100644 --- a/src/main/preflight/agent-command-detection.ts +++ b/src/main/preflight/agent-command-detection.ts @@ -8,6 +8,7 @@ import { import { findCommandsOnLocalPath } from '../ipc/command-path/command-path-resolver' import { buildLocalPreflightEnv } from '../ipc/preflight-local-env' import { readGuestWindowsDriveMounts } from '../ipc/wsl-guest-windows-drive-path' +import { hasPathSeparatorToken } from '../../shared/managed-agent-command-token' export async function detectAgentCommandsOnHost( commands: readonly string[], @@ -25,7 +26,10 @@ export async function detectAgentCommandsOnHost( const onPathCommands = await findCommandsOnLocalPath(commands, { env: buildLocalPreflightEnv() }) - const missedCommands = commands.filter((cmd) => !onPathCommands.has(cmd)) + // A path was already checked directly; the install-dir fallback would count any absolute path as found. + const missedCommands = commands.filter( + (cmd) => !onPathCommands.has(cmd) && !hasPathSeparatorToken(cmd) + ) // Resolve cold-GUI install dirs once; use the same WSL-filtered PATH while retaining process PATH elsewhere. const guestMounts = readGuestWindowsDriveMounts() // The batch includes the fallback's PATH normalization and Windows executable names. diff --git a/src/main/preflight/installed-agent-detection.test.ts b/src/main/preflight/installed-agent-detection.test.ts new file mode 100644 index 00000000000..a5904d4d674 --- /dev/null +++ b/src/main/preflight/installed-agent-detection.test.ts @@ -0,0 +1,35 @@ +import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('../ipc/command-path/agent-detection-shell-path', () => ({ + hydrateShellPathForAgentDetection: vi.fn() +})) + +import { detectInstalledAgents } from './installed-agent-detection' + +describe('detectInstalledAgents command overrides', () => { + let dir: string + beforeEach(async () => { + dir = await mkdtemp(path.join(tmpdir(), 'orca agent override ')) + }) + afterEach(async () => { + await rm(dir, { recursive: true, force: true }) + }) + + it('lists an agent outside PATH only when its override executable exists', async () => { + const executable = path.join(dir, process.platform === 'win32' ? 'ante.exe' : 'ante') + await writeFile(executable, '#!/bin/sh\n') + await chmod(executable, 0o755) + + await expect( + detectInstalledAgents(undefined, { agentCmdOverrides: { ante: `"${executable}"` } }) + ).resolves.toContain('ante') + await expect( + detectInstalledAgents(undefined, { + agentCmdOverrides: { ante: `"${path.join(dir, 'missing', 'ante')}"` } + }) + ).resolves.not.toContain('ante') + }) +}) diff --git a/src/main/preflight/installed-agent-detection.ts b/src/main/preflight/installed-agent-detection.ts index f54721093d0..8683c79cccc 100644 --- a/src/main/preflight/installed-agent-detection.ts +++ b/src/main/preflight/installed-agent-detection.ts @@ -1,6 +1,9 @@ // orca skills loads detection outside Electron; exclude integration-client dependencies. +import { homedir } from 'node:os' +import path from 'node:path' import { getTuiAgentDetectionProbeCommands, + isDetectionUnsupportedInRuntime, KNOWN_TUI_AGENT_DETECTION_COMMANDS, resolveDetectedTuiAgentIds, type TuiAgentDetectionCommand @@ -13,10 +16,14 @@ import { import { detectAgentCommandsOnHost } from './agent-command-detection' import { normalizeDisabledTuiAgents } from '../../shared/agent-launch/catalog/tui-agent-selection' import type { TuiAgent } from '../../shared/agent-launch/catalog/tui-agent' +import type { TuiAgentDetectionRuntime } from '../../shared/agent-launch/catalog/tui-agent-config-types' +import { extractExecutableToken } from '../../shared/managed-agent-command-token' export type InstalledAgentDetectionOptions = { /** Agents disabled in Settings → Agents. Background callers skip probing them entirely. */ disabledTuiAgents?: readonly TuiAgent[] + /** Settings → Agents command overrides on this host; a resolvable override counts as installed. */ + agentCmdOverrides?: Partial> | null } function activeDetectionCommands( @@ -30,18 +37,88 @@ function activeDetectionCommands( return KNOWN_TUI_AGENT_DETECTION_COMMANDS.filter((command) => !disabled.has(command.id)) } +/** Options carrying only configured overrides, so hosts without any keep the plain PATH probe. */ +export function agentCommandOverrideDetectionOptions( + overrides: InstalledAgentDetectionOptions['agentCmdOverrides'] +): InstalledAgentDetectionOptions | undefined { + return overrides && Object.values(overrides).some((command) => command?.trim()) + ? { agentCmdOverrides: overrides } + : undefined +} + +type OverrideProbe = { command: string; requiredCommands: string[] } + +function overrideExecutableToken( + override: string | undefined, + platform: NodeJS.Platform +): string | null { + // PowerShell needs `& "C:\path with spaces\agy.exe"` to run a quoted path; the call operator is not the executable. + const input = platform === 'win32' ? override?.trim().replace(/^&\s+/, '') : override + const token = extractExecutableToken(input, { platform }) + // Probing only stats the path (WSL quotes it), so reject just what cannot name a file. + return token && !/[\0\r\n]/.test(token) ? token : null +} + +/** Each enabled agent's override executable, probed on the same host as the default commands. */ +function overrideProbeCommands( + commands: readonly TuiAgentDetectionCommand[], + overrides: InstalledAgentDetectionOptions['agentCmdOverrides'], + runtime: TuiAgentDetectionRuntime +): Map { + const probes = new Map() + if (!overrides) { + return probes + } + const platform = runtime === 'wsl' ? 'linux' : process.platform + const supported = commands.filter((command) => !isDetectionUnsupportedInRuntime(command, runtime)) + for (const id of new Set(supported.map((command) => command.id))) { + const token = overrideExecutableToken(overrides[id], platform) + if (!token) { + continue + } + // The terminal shell expands `~`; the WSL probe runs unexpanded, so only expand on this machine. + const command = + runtime !== 'wsl' && + (token.startsWith('~/') || (platform === 'win32' && token.startsWith('~\\'))) + ? path.join(homedir(), token.slice(2)) + : token + const own = supported.filter((entry) => entry.id === id) + const ownCommands = new Set(own.map((entry) => entry.cmd)) + // The override replaces the agent's own binary, not a separate CLI it depends on. + const requiredCommands = own + .flatMap((entry) => entry.requiredCommands ?? []) + .filter((required) => !ownCommands.has(required)) + probes.set(id, { command, requiredCommands }) + } + return probes +} + export async function detectInstalledAgents( context?: PreflightRuntimeContext, options?: InstalledAgentDetectionOptions ): Promise { const runtime = getPreflightWslTarget(context) ? 'wsl' : process.platform const commands = activeDetectionCommands(options) - const probeCommands = getTuiAgentDetectionProbeCommands(commands, runtime) - return resolveDetectedTuiAgentIds( - commands, - await detectAgentCommandsOnHost(probeCommands, { context }), - runtime - ) + // Why: a CLI outside PATH (e.g. agy in a custom install dir) is reachable only through its override. + const overrides = overrideProbeCommands(commands, options?.agentCmdOverrides, runtime) + const probeCommands = [ + ...new Set([ + ...getTuiAgentDetectionProbeCommands(commands, runtime), + ...[...overrides.values()].map((probe) => probe.command) + ]) + ] + const found = await detectAgentCommandsOnHost(probeCommands, { context }) + const detected = resolveDetectedTuiAgentIds(commands, found, runtime) + for (const [id, probe] of overrides) { + if ( + !detected.includes(id) && + found.has(probe.command) && + probe.requiredCommands.every((required) => found.has(required)) + ) { + detected.push(id) + } + } + return detected } export async function detectInstalledAgentsWithShellPathHydration( diff --git a/src/main/preflight/workspace-agent-detection.ts b/src/main/preflight/workspace-agent-detection.ts index ed10916d19a..06f076abde7 100644 --- a/src/main/preflight/workspace-agent-detection.ts +++ b/src/main/preflight/workspace-agent-detection.ts @@ -32,7 +32,10 @@ import { refreshShellPathAndDetectAgents, type RefreshAgentsResult } from './agent-detection' -import type { InstalledAgentDetectionOptions } from './installed-agent-detection' +import { + agentCommandOverrideDetectionOptions, + type InstalledAgentDetectionOptions +} from './installed-agent-detection' import { assertWslDistroRunningForExec } from '../wsl/discovery/wsl-background-exec-gate' export type AgentDetectionHost = @@ -193,7 +196,8 @@ export async function detectProjectLaunchContext( throw new Error('worktree_execution_host_unresolved') } const agents = await detectAgentsOnHost( - ssh ? { kind: 'ssh', connectionId: ssh } : { kind: 'local', context: { projectRuntime } } + ssh ? { kind: 'ssh', connectionId: ssh } : { kind: 'local', context: { projectRuntime } }, + agentCommandOverrideDetectionOptions(store.getSettings?.().agentCmdOverrides) ) return { agents: agents.filter(isTuiAgent), diff --git a/src/main/runtime/rpc/methods/preflight/preflight-selected-target.test.ts b/src/main/runtime/rpc/methods/preflight/preflight-selected-target.test.ts index e80a9c6ce57..c3b68bb146f 100644 --- a/src/main/runtime/rpc/methods/preflight/preflight-selected-target.test.ts +++ b/src/main/runtime/rpc/methods/preflight/preflight-selected-target.test.ts @@ -267,15 +267,18 @@ describe('qualified discovery at the runtime RPC boundary', () => { params: { worktreeId: 'copy::C:\\same', worktreeHostId: 'local' } }) ).toMatchObject({ ok: true }) - expect(refresh).toHaveBeenCalledWith({ - kind: 'local', - context: { - projectRuntime: expect.objectContaining({ - status: 'resolved', - runtime: expect.objectContaining({ kind: 'wsl', distro: 'ProjectDistro' }) - }) - } - }) + expect(refresh).toHaveBeenCalledWith( + { + kind: 'local', + context: { + projectRuntime: expect.objectContaining({ + status: 'resolved', + runtime: expect.objectContaining({ kind: 'wsl', distro: 'ProjectDistro' }) + }) + } + }, + undefined + ) }) it.each([false, true])( 'uses the actual cold and warm folder-project instance in either repo order (reverse=%s)', @@ -315,7 +318,10 @@ describe('qualified discovery at the runtime RPC boundary', () => { await call('preflight.refreshAgents', { worktreeId, worktreeHostId: 'local' }) ).toMatchObject({ ok: true, result: { agents: ['codex'] } }) } - expect(refresh).toHaveBeenLastCalledWith({ kind: 'local', context: expect.anything() }) + expect(refresh).toHaveBeenLastCalledWith( + { kind: 'local', context: expect.anything() }, + undefined + ) const probes = detectInstalledAgentsWithShellPathHydrationMock.mock.calls.length for (const params of [ { worktreeId: 'copy::/missing', worktreeHostId: 'local' }, diff --git a/src/main/runtime/rpc/methods/preflight/preflight.ts b/src/main/runtime/rpc/methods/preflight/preflight.ts index 2ef3d66fcb5..c0562a581ad 100644 --- a/src/main/runtime/rpc/methods/preflight/preflight.ts +++ b/src/main/runtime/rpc/methods/preflight/preflight.ts @@ -1,4 +1,9 @@ import { defineMethod } from '../../core' +import type { OrcaRuntimeService } from '../../../orca-runtime' +import { + agentCommandOverrideDetectionOptions, + type InstalledAgentDetectionOptions +} from '../../../../preflight/installed-agent-detection' import { resolveQualifiedAgentDetectionHost } from './preflight-workspace-target' import { runPreflightCheck } from '../../../../preflight/agent-detection' import { detectRemoteWindowsTerminalCapabilities } from '../../../../ipc/preflight-remote-windows-terminal-capabilities' @@ -16,6 +21,20 @@ import { PreflightDetectRemoteWindowsTerminalCapabilities } from '../../../../../shared/rpc-contract/preflight-params' +/** This host's command overrides, so a configured CLI outside PATH is listed like a detected one. */ +function hostDetectionOptions( + runtime: OrcaRuntimeService +): InstalledAgentDetectionOptions | undefined { + try { + return agentCommandOverrideDetectionOptions( + runtime.requireStore().getSettings().agentCmdOverrides + ) + } catch { + // A runtime without its store still lists PATH-detected agents. + return undefined + } +} + export const PREFLIGHT_METHODS = [ defineMethod({ name: 'preflight.check', @@ -36,7 +55,7 @@ export const PREFLIGHT_METHODS = [ const host = await resolveQualifiedAgentDetectionHost(runtime, params) // A client's inventory read is background work: it must not start a stopped WSL distro (E9). await assertAgentDetectionHostRunning(host) - return detectAgentsOnHost(host) + return detectAgentsOnHost(host, hostDetectionOptions(runtime)) } }), defineMethod({ @@ -59,7 +78,7 @@ export const PREFLIGHT_METHODS = [ handler: async (params, { runtime }) => { const host = await resolveQualifiedAgentDetectionHost(runtime, params) await assertAgentDetectionHostRunning(host) - return refreshAgentsOnHost(host) + return refreshAgentsOnHost(host, hostDetectionOptions(runtime)) } }) ] diff --git a/src/main/runtime/workspaces/startup/runtime-worktree-agent-startup.ts b/src/main/runtime/workspaces/startup/runtime-worktree-agent-startup.ts index 287130c9802..7ae6a162a4a 100644 --- a/src/main/runtime/workspaces/startup/runtime-worktree-agent-startup.ts +++ b/src/main/runtime/workspaces/startup/runtime-worktree-agent-startup.ts @@ -24,6 +24,7 @@ import { nameLocalTypedLineShell } from '../../terminal/launch/agent-launch-typed-line-shell' import { detectAgentsOnHost } from '../../../preflight/workspace-agent-detection' +import { agentCommandOverrideDetectionOptions } from '../../../preflight/installed-agent-detection' import type { RuntimeStore } from '../../runtime-store-contract' import type { RuntimeManagedWorktreeCreateArgs } from '../creation/runtime-managed-worktree-create-types' @@ -72,7 +73,10 @@ export async function resolveWorktreeStartupDraftAgent( // Disabled agents cannot be picked here, so background detection never probes them (#34635). detected = await detectAgentsOnHost( sshConnectionId ? { kind: 'ssh', connectionId: sshConnectionId } : { kind: 'local' }, - { disabledTuiAgents: settings.disabledTuiAgents } + { + disabledTuiAgents: settings.disabledTuiAgents, + ...agentCommandOverrideDetectionOptions(settings.agentCmdOverrides) + } ) } catch { detected = [] diff --git a/src/main/workspaces/execution-host/local-project-runtime-resolution.ts b/src/main/workspaces/execution-host/local-project-runtime-resolution.ts index 2f356d4d841..9973a2be3a2 100644 --- a/src/main/workspaces/execution-host/local-project-runtime-resolution.ts +++ b/src/main/workspaces/execution-host/local-project-runtime-resolution.ts @@ -36,7 +36,10 @@ export type ProjectRuntimeResolutionStore = { getProjects?: Store['getProjects'] getRepo?: Store['getRepo'] getSettings?: () => Partial< - Pick + Pick< + GlobalSettings, + 'localWindowsRuntimeDefault' | 'experimentalWslHosts' | 'agentCmdOverrides' + > > } diff --git a/src/renderer/src/components/settings/agents/launch/AgentCatalogRow.tsx b/src/renderer/src/components/settings/agents/launch/AgentCatalogRow.tsx index 4afe31f4ca3..5f797e4a536 100644 --- a/src/renderer/src/components/settings/agents/launch/AgentCatalogRow.tsx +++ b/src/renderer/src/components/settings/agents/launch/AgentCatalogRow.tsx @@ -158,13 +158,6 @@ export function AgentCatalogRow({ )?.focus() }) } - const expandable = - isDetected || - permission !== undefined || - permissionChoice !== undefined || - chatRuntime !== undefined || - launchChoiceEditor !== undefined - return (
@@ -262,33 +255,28 @@ export function AgentCatalogRow({
- {expandable && ( - - )} +
- {expandable && cmdOpen && ( + {cmdOpen && (
{chatRuntime} {!launchChoiceEditor && permissionChoice && ( @@ -304,6 +292,23 @@ export function AgentCatalogRow({ /> )} {!isDetected && launchChoiceEditor?.(permissionRef)} + {!isDetected && ( +
+ +

+ {translate( + 'settings.agents.undetectedCommandHint', + 'If the {{agent}} CLI is installed outside PATH, enter the full path to its executable on this host.', + { agent: label } + )} +

+
+ )} {isDetected && (
save({ type: 'availability', agent: agent.id, enabled }), - onSaveOverride: (value) => mutate({ type: 'command', agent: agent.id, value }), + onSaveOverride: async (value) => { + const changed = value !== (settings.agentCmdOverrides[agent.id] ?? '') + await mutate({ type: 'command', agent: agent.id, value }) + // Detection probes the override; local inventories reset on the settings change, a host's need a refresh. + if (changed && !local) { + void refresh() + } + }, onSaveArgs: (value) => typed ? saveUserArgs(agent.id, agent.label, value) diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 5b85445b5ab..db9fb5b8757 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -1601,7 +1601,6 @@ "fullAccessAsRoot": "{{agent}} won't use Full access while it runs as root. Choose another permission for {{agent}} in Settings → Agents for the computer running this chat, then start a new chat.", "historyInOtherAccount": "This chat's history is in another Claude account. Switch back to that account to continue it.", "historyTooLarge": "This conversation's history is too large to restore here.", - "pluginInputRequired": "{{agent}} stopped because the \"{{plugin}}\" plugin is waiting for input in a terminal pane. Disable the plugin for chat sessions, or run the task in a {{agent}} terminal.", "hostFault": "Orca ran into a problem, so this didn't go through.", "hostFaultTryAgain": "Orca ran into a problem, so this didn't go through. Try again.", "hostRestarted": "Orca restarted before this message was sent.", @@ -1611,13 +1610,14 @@ "launchPreparationFailed": "Orca couldn't prepare {{agent}} to start on the computer running this chat.", "managedAccountEnvOverride": "This Claude launch sets its own Anthropic sign-in variables. Remove them to use a managed Claude account.", "managedAccountUnsupported": "While a Claude account is added in WSL, Claude chats need a Windows Claude account.", + "modelNotSelected": "{{agent}} has no model selected. Check your model selection and {{agent}} sign-in, then send again.", "modelUnavailable": "{{agent}} couldn't use {{detail}}. Pick another model and send again.", "modelUnavailableUpdate": "{{agent}} couldn't use {{detail}}. Update {{agent}} to {{minimumVersion}} or later to keep it after a restart, or pick another model and send again.", - "modelNotSelected": "{{agent}} has no model selected. Check your model selection and {{agent}} sign-in, then send again.", "noTurnToStop": "{{agent}} had no turn running to stop.", "notDelivered": "This message was not delivered.", "notDeliveredSendAgain": "This message was not delivered. Send it again to continue.", "notSignedIn": "{{agent}} is not signed in.", + "pluginInputRequired": "{{agent}} stopped because the \"{{plugin}}\" plugin is waiting for input in a terminal pane. Disable the plugin for chat sessions, or run the task in a {{agent}} terminal.", "previousExitUnverifiable": "Couldn't stop {{agent}} from before.", "providerExitedAnswer": "{{agent}} has stopped, so your answer was not sent. Send a message to continue.", "providerExitedRejection": "{{agent}} stopped before this message was sent.", @@ -1684,6 +1684,10 @@ "plan": "Plan", "stoppedBeforeStart": "Stopped manually" }, + "openedPullRequests": { + "many_one": "Opened {{count}} pull request", + "many_other": "Opened {{count}} pull requests" + }, "providerFrame": { "byteCount_one": "{{count}} byte", "byteCount_other": "{{count}} bytes" @@ -1910,29 +1914,6 @@ "waitForStart": "Wait for the agent to finish starting.", "waitForTurn": "Wait for the agent to finish responding, or stop it.", "waitMoment": "Wait a moment." - }, - "usage": { - "description": "Show usage for this chat’s Codex account", - "loading": "Loading Codex usage…", - "failed": "Codex usage couldn’t be loaded. Try again.", - "codexVersion": "This Codex version doesn’t report usage. Update Codex.", - "apiKeyAuth": "Codex usage isn’t available with an API key. Sign in to Codex with a ChatGPT account to view usage.", - "title": "Codex usage", - "account": "Account used by this chat", - "retry": "Retry", - "resetFailed": "Codex limits couldn’t be reset. Try again.", - "confirmReset": "This uses one Codex rate-limit reset credit for {{account}} and resets any eligible usage windows immediately.", - "sharedAccount": "Current shared account", - "outcome": { - "reset": "Codex limits were reset.", - "nothingToReset": "Nothing to reset right now.", - "noCredit": "No Codex reset credit is left on this account.", - "alreadyRedeemed": "That reset credit was already used." - } - }, - "openedPullRequests": { - "many_one": "Opened {{count}} pull request", - "many_other": "Opened {{count}} pull requests" } }, "onboarding": { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index e4cb0f3d7c3..4cb8b19ebe0 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -1017,7 +1017,8 @@ "defaultSurfaceSetAllChat": "Set all to Chat", "defaultSurfaceSetAllTerminal": "Set all to Terminal", "sameAsChoice": "Same as {{agent}} · {{choice}}", - "chatUnavailableOnWindows": "{{value0}} Chat isn't available on Windows yet, so {{value0}} opens in a terminal on this host. Chats on a macOS or Linux host still work." + "chatUnavailableOnWindows": "{{value0}} Chat isn't available on Windows yet, so {{value0}} opens in a terminal on this host. Chats on a macOS or Linux host still work.", + "undetectedCommandHint": "If the {{agent}} CLI is installed outside PATH, enter the full path to its executable on this host." }, "followSymlinkedDirectoriesKeywords": { "symlink": "symlink", diff --git a/src/renderer/src/store/slices/local-detected-agent-state.ts b/src/renderer/src/store/slices/local-detected-agent-state.ts index d3e52be61cb..9c8d7dddb09 100644 --- a/src/renderer/src/store/slices/local-detected-agent-state.ts +++ b/src/renderer/src/store/slices/local-detected-agent-state.ts @@ -20,6 +20,17 @@ import { } from './local-detected-agent-store-state' import { readLocalAgentInventoryOutcome } from './local-agent-inventory-outcome' +function haveSameCommandOverrides( + a: Partial> | undefined, + b: Partial> | undefined +): boolean { + if (a === b) { + return true + } + const keys = new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]) + return [...keys].every((key) => (a?.[key]?.trim() ?? '') === (b?.[key]?.trim() ?? '')) +} + type LocalDetectedAgentStateCreator = StateCreator export const createLocalDetectedAgentState: LocalDetectedAgentStateCreator = (set, get, store) => { @@ -256,10 +267,22 @@ export const createLocalDetectedAgentState: LocalDetectedAgentStateCreator = (se } } store.subscribe((state, previous) => { - if (state.settings?.disabledTuiAgents === previous.settings?.disabledTuiAgents) { + // Settings hydration is not an edit; detection already ran with the persisted overrides. + const overridesChanged = + previous.settings != null && + !haveSameCommandOverrides( + state.settings?.agentCmdOverrides, + previous.settings?.agentCmdOverrides + ) + if ( + !overridesChanged && + state.settings?.disabledTuiAgents === previous.settings?.disabledTuiAgents + ) { return } + // Detection probes command overrides, so a changed override invalidates every local inventory. if ( + overridesChanged || !haveSameDisabledTuiAgents( state.settings?.disabledTuiAgents, previous.settings?.disabledTuiAgents