mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
fix(agents): list agents found through a custom command path (#39178)
This commit is contained in:
@@ -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.
|
||||
@@ -8,6 +8,7 @@
|
||||
"codex",
|
||||
"cursor-cli",
|
||||
"codex-hot-swap",
|
||||
"antigravity",
|
||||
"antigravity-accounts",
|
||||
"native-chat",
|
||||
"session-history",
|
||||
|
||||
@@ -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/) |
|
||||
|
||||
@@ -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<T>(
|
||||
export function registerPreflightHandlers(store?: Pick<Store, 'getSettings'>): 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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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<Record<TuiAgent, string>> | 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<TuiAgent, OverrideProbe> {
|
||||
const probes = new Map<TuiAgent, OverrideProbe>()
|
||||
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<string[]> {
|
||||
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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -36,7 +36,10 @@ export type ProjectRuntimeResolutionStore = {
|
||||
getProjects?: Store['getProjects']
|
||||
getRepo?: Store['getRepo']
|
||||
getSettings?: () => Partial<
|
||||
Pick<GlobalSettings, 'localWindowsRuntimeDefault' | 'experimentalWslHosts'>
|
||||
Pick<
|
||||
GlobalSettings,
|
||||
'localWindowsRuntimeDefault' | 'experimentalWslHosts' | 'agentCmdOverrides'
|
||||
>
|
||||
>
|
||||
}
|
||||
|
||||
|
||||
@@ -158,13 +158,6 @@ export function AgentCatalogRow({
|
||||
)?.focus()
|
||||
})
|
||||
}
|
||||
const expandable =
|
||||
isDetected ||
|
||||
permission !== undefined ||
|
||||
permissionChoice !== undefined ||
|
||||
chatRuntime !== undefined ||
|
||||
launchChoiceEditor !== undefined
|
||||
|
||||
return (
|
||||
<div className={cn('py-3', !isDetected && !opensAs?.cliMissing && 'opacity-70')}>
|
||||
<div className="flex flex-wrap items-start gap-3">
|
||||
@@ -262,33 +255,28 @@ export function AgentCatalogRow({
|
||||
<ExternalLink className="size-3.5" />
|
||||
</a>
|
||||
<div className="flex size-7 items-center justify-center">
|
||||
{expandable && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setCmdOpen((previous) => !previous)}
|
||||
aria-label={
|
||||
cmdOpen
|
||||
? translate(
|
||||
'settings.agents.collapseAgentSettings',
|
||||
'Collapse agent settings'
|
||||
)
|
||||
: translate('settings.agents.expandAgentSettings', 'Expand agent settings')
|
||||
}
|
||||
className="size-7 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn('size-3.5 transition-transform', cmdOpen && 'rotate-180')}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setCmdOpen((previous) => !previous)}
|
||||
aria-label={
|
||||
cmdOpen
|
||||
? translate('settings.agents.collapseAgentSettings', 'Collapse agent settings')
|
||||
: translate('settings.agents.expandAgentSettings', 'Expand agent settings')
|
||||
}
|
||||
className="size-7 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn('size-3.5 transition-transform', cmdOpen && 'rotate-180')}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expandable && cmdOpen && (
|
||||
{cmdOpen && (
|
||||
<div className="mt-3 space-y-3 pl-10">
|
||||
{chatRuntime}
|
||||
{!launchChoiceEditor && permissionChoice && (
|
||||
@@ -304,6 +292,23 @@ export function AgentCatalogRow({
|
||||
/>
|
||||
)}
|
||||
{!isDetected && launchChoiceEditor?.(permissionRef)}
|
||||
{!isDetected && (
|
||||
<div>
|
||||
<AgentCommandOverrideInput
|
||||
key={cmdOverride ?? defaultCmd}
|
||||
defaultCmd={defaultCmd}
|
||||
cmdOverride={cmdOverride}
|
||||
onSaveOverride={onSaveOverride}
|
||||
/>
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
'settings.agents.undetectedCommandHint',
|
||||
'If the {{agent}} CLI is installed outside PATH, enter the full path to its executable on this host.',
|
||||
{ agent: label }
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isDetected && (
|
||||
<div>
|
||||
<AgentCommandOverrideInput
|
||||
|
||||
@@ -229,7 +229,7 @@ export function AgentLaunchSettingsCatalog({
|
||||
isDetected,
|
||||
isEnabled: isTuiAgentEnabled(agent.id, settings.disabledTuiAgents),
|
||||
isDefault: isDetected && settings.defaultTuiAgent === agent.id,
|
||||
cmdOverride: isDetected ? settings.agentCmdOverrides[agent.id] : undefined,
|
||||
cmdOverride: settings.agentCmdOverrides[agent.id],
|
||||
argsOverride:
|
||||
choicesSupported && settings.agentChoices
|
||||
? (settings.agentChoices.userArgs[agent.id] ?? '')
|
||||
@@ -267,7 +267,14 @@ export function AgentLaunchSettingsCatalog({
|
||||
agent: settings.defaultTuiAgent === agent.id ? 'blank' : agent.id
|
||||
}),
|
||||
onSetEnabled: (enabled) => 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)
|
||||
|
||||
+6
-25
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -20,6 +20,17 @@ import {
|
||||
} from './local-detected-agent-store-state'
|
||||
import { readLocalAgentInventoryOutcome } from './local-agent-inventory-outcome'
|
||||
|
||||
function haveSameCommandOverrides(
|
||||
a: Partial<Record<string, string>> | undefined,
|
||||
b: Partial<Record<string, string>> | 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<AppState, [], [], LocalDetectedAgentState>
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user