From 0176b662ecda3faa08e0c806d6a8622cf4f7d77c Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:19:09 -0700 Subject: [PATCH 1/6] feat(agent-launch): hiding an agent only hides it from pickers The Settings switch that turns an agent off now only decides whether it shows in menus and pickers. An explicit launch of a hidden agent works from every path: - Host: removes the agent.launch planner, OpenCode session, resume, phone session terminal, worktree-create startup and orchestration launcher refusals, and the bare-command matcher's hidden skip. A linked draft starts the requested agent as is; only the default and detected picks skip hidden agents. - Typed commands match Settings command overrides before built-in commands, so a collision resolves to the agent the user named. - Status hooks follow only the status-hooks switch. Hiding an agent no longer removes its hooks or re-runs hook installation; hooks still go only to agents whose CLI is found on the machine. - Desktop window: dashboard spawn, saved AI-button / fix-checks agents, automation save, session continuation, the Source Control launch check, task-page direct launch and composer submit no longer refuse or drop an explicitly chosen hidden agent. The phone client's own pre-checks move to a follow-up PR. --- config/scripts/ci-shard-timings.json | 2 +- config/scripts/locale-ja-value-overrides.mjs | 1 - .../managed-agent-hook-controls.test.ts | 69 ++------- .../managed-agent-hook-controls.ts | 55 ++----- .../agent-hooks/managed-agent-hook-startup.ts | 3 +- .../managed-hook-detection-commands.test.ts | 8 +- .../managed-hook-detection-commands.ts | 32 ++-- .../agent-hooks/opencode-plugin-settings.ts | 17 +-- .../wsl-hook-relay-guest-install.ts | 5 +- .../wsl-opencode-install-race.test.ts | 29 ++-- .../wsl-opencode-settings-lifetime.test.ts | 27 ++-- .../agent-hooks/wsl-opencode-settings.test.ts | 47 +++--- .../startup-agent-worktree-create.test.ts | 4 +- .../claude-launch-routing.test.ts | 3 +- .../claude-profile-router.test.ts | 3 +- .../claude-accounts/claude-profile-router.ts | 5 +- .../claude-profile-wsl-router.test.ts | 3 +- ...-structured-launch-account-routing.test.ts | 6 +- src/main/ipc/pty-ipc-spawn-drivers.ts | 1 - .../ipc/pty-spawn-env-terminal-basics.test.ts | 62 +------- src/main/ipc/pty/host-env/assembly.ts | 13 +- src/main/ipc/pty/host-env/opencode-config.ts | 6 +- .../opencode-hook-installation.test.ts | 142 ++++++------------ src/main/ipc/pty/host-env/types.ts | 2 - src/main/ipc/pty/ipc/spawn-env-codex.ts | 1 - src/main/ipc/pty/provider/local-configure.ts | 1 - src/main/ipc/pty/runtime/spawn-preflight.ts | 1 - src/main/ipc/settings.test.ts | 53 +------ src/main/ipc/settings.ts | 11 +- ...code-status-plugin-startup-refresh.test.ts | 6 - .../opencode-status-plugin-startup-refresh.ts | 6 +- src/main/orcad/orcad-managed-agent-hooks.ts | 7 +- .../managed-worktree-create-telemetry.test.ts | 7 +- .../orca-runtime-create-agent-session.ts | 4 - .../orca-runtime-create-managed-worktree.ts | 19 +-- ...e-get-agent-session-execution-namespace.ts | 4 - ...a-runtime-get-terminal-interactive-wait.ts | 13 +- ...form-mobile-session-pty-records-refresh.ts | 4 +- ...resolve-mobile-session-terminal-command.ts | 4 - .../orca-runtime-stop-requested-pty-ids.ts | 5 +- ...rca-runtime-test-scenario-builders.spec.ts | 7 +- .../mobile-session-tabs-part-08.spec.ts | 11 +- .../mobile-session-tabs-part-12.spec.ts | 26 ---- .../paired-settings.spec.ts | 20 ++- ...nal-creation-and-readiness-part-02.spec.ts | 24 --- ...nal-creation-and-readiness-part-03.spec.ts | 21 ++- ...worktree-setup-and-startup-part-03.spec.ts | 31 ---- ...worktree-setup-and-startup-part-04.spec.ts | 3 +- .../runtime/rpc/methods/agent-hooks.test.ts | 15 +- src/main/runtime/rpc/methods/agent-hooks.ts | 4 +- .../worker/worker-start-validation.ts | 2 +- .../runtime-agent-launch-resolution.test.ts | 15 ++ .../runtime-agent-launch-resolution.ts | 42 +++--- .../runtime/runtime-agent-terminal-startup.ts | 4 - src/main/runtime/runtime-client-settings.ts | 17 +-- .../runtime-worktree-agent-startup.test.ts | 17 +-- .../runtime/runtime-worktree-agent-startup.ts | 11 +- .../startup/codex-hook-startup-wiring.test.ts | 6 +- ...st.ts => codex-launch-hook-switch.test.ts} | 26 +--- src/main/startup/codex-launch-preparation.ts | 5 +- .../codex-session-resume-daemon-guard.test.ts | 2 +- .../startup/codex-session-resume-launch.ts | 5 +- src/main/startup/main-process-pty-startup.ts | 7 +- .../startup/main-process-ready-runtime.ts | 4 +- .../automations/automation-save-action.ts | 17 +-- .../dashboard/launch-dashboard-agent.test.ts | 3 +- .../dashboard/launch-dashboard-agent.ts | 3 +- .../buildSourceControlAgentDeliveryPlan.ts | 1 - .../source-control/ai/recovery-launch.ts | 7 +- .../composer-name-ownership.test.ts | 1 - .../composer-submit-orchestration.ts | 2 - .../folder-submit-orchestration.ts | 9 +- .../quick-submit-source-preparation.ts | 9 +- src/renderer/src/i18n/locales/en.json | 3 - src/renderer/src/i18n/locales/es.json | 3 - src/renderer/src/i18n/locales/fr.json | 3 - src/renderer/src/i18n/locales/ja.json | 3 - src/renderer/src/i18n/locales/ko.json | 3 - src/renderer/src/i18n/locales/zh.json | 3 - .../src/lib/fix-checks-agent-launch.ts | 12 +- .../lib/launch-agent-session-continuation.ts | 13 -- .../launch-work-item-direct-agent-routing.ts | 6 +- .../src/lib/launch-work-item-direct.test.ts | 34 ----- ...-control-agent-action-launch-check.test.ts | 10 -- ...ource-control-agent-action-launch-check.ts | 11 -- src/shared/agent-status-hooks-setting.ts | 17 --- ...orchestration-dispatch-refusal-contract.ts | 2 +- .../source-control-launch-agent-selection.ts | 5 +- src/shared/tui-agent-selection.test.ts | 14 +- src/shared/tui-agent-selection.ts | 6 - 90 files changed, 295 insertions(+), 921 deletions(-) create mode 100644 src/main/runtime/runtime-agent-launch-resolution.test.ts rename src/main/startup/{codex-launch-per-agent-hook-opt-out.test.ts => codex-launch-hook-switch.test.ts} (89%) diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index a40748fb10c1..4bdb37379971 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -5534,7 +5534,7 @@ "src/main/startup/cli-launch-redirect.test.ts": 114, "src/main/startup/codex-account-restart-resume.test.ts": 292, "src/main/startup/codex-hook-startup-wiring.test.ts": 6575, - "src/main/startup/codex-launch-per-agent-hook-opt-out.test.ts": 187, + "src/main/startup/codex-launch-hook-switch.test.ts": 187, "src/main/startup/codex-session-resume-daemon-guard.test.ts": 143, "src/main/startup/configure-process-dev-parent-shutdown.test.ts": 113, "src/main/startup/configure-process-profile-state.test.ts": 143, diff --git a/config/scripts/locale-ja-value-overrides.mjs b/config/scripts/locale-ja-value-overrides.mjs index 1d220ba7c0f9..ee98d8a44f6d 100644 --- a/config/scripts/locale-ja-value-overrides.mjs +++ b/config/scripts/locale-ja-value-overrides.mjs @@ -444,7 +444,6 @@ const JA_AUDIT_OVERRIDES = { 'Agent スキルのセットアップを実行する前に、Orca CLI をインストールしてください。', 'Run grok in a terminal on the computer running Orca and wait for it to start. If prompted, complete sign-in, then retry usage. You do not need to send a chat message.': 'Orca を実行しているコンピュータのターミナルで grok を実行し、起動するまで待ってください。サインインを求められた場合は完了してから、使用状況を再取得してください。チャットメッセージを送る必要はありません。', - 'Choose an enabled agent before saving.': '保存する前に、有効な Agent を選択してください。', 'Selected agent is disabled. Choose an enabled agent before creating.': '選択した Agent は無効です。作成する前に、有効な Agent を選択してください。', 'Choose an agent before starting.': '開始する前に、 Agent を選択してください。', diff --git a/src/main/agent-hooks/managed-agent-hook-controls.test.ts b/src/main/agent-hooks/managed-agent-hook-controls.test.ts index bd9dbc2c7e3b..8a7214308a90 100644 --- a/src/main/agent-hooks/managed-agent-hook-controls.test.ts +++ b/src/main/agent-hooks/managed-agent-hook-controls.test.ts @@ -207,38 +207,21 @@ describe('managed agent hook controls', () => { ]) }) - it('removes disabled agents without probing or reinstalling them', async () => { - mocks.detect.mockResolvedValue({ codex: { state: 'found' } }) + it('installs hooks for an agent hidden from pickers', async () => { + mocks.detect.mockResolvedValue({ claude: { state: 'found' }, codex: { state: 'found' } }) + // Why a variable: the hidden list is no longer part of the hook settings type, yet must be ignored. + const settings = { agentCmdOverrides: {}, disabledTuiAgents: ['claude'] } - const results = await applyAgentStatusHooksEnabled(true, { - agentCmdOverrides: {}, - disabledTuiAgents: ['claude'] - }) + const results = await applyAgentStatusHooksEnabled(true, settings) - expect(mocks.removeClaude).toHaveBeenCalledTimes(1) - expect(mocks.installClaude).not.toHaveBeenCalled() - expect(mocks.installCodex).toHaveBeenCalledTimes(1) + expect(mocks.removeClaude).not.toHaveBeenCalled() + expect(mocks.installClaude).toHaveBeenCalledTimes(1) expect(results).toEqual([ - expect.objectContaining({ agent: 'claude', state: 'not_installed' }), + expect.objectContaining({ agent: 'claude', state: 'installed' }), expect.objectContaining({ agent: 'codex', state: 'installed' }) ]) }) - it('does not install an agent disabled while detection was running', async () => { - mocks.detect.mockResolvedValue({ - claude: { state: 'found' }, - codex: { state: 'found' } - }) - - await installManagedAgentHooks( - { agentCmdOverrides: {} }, - { shouldContinue: (agent) => agent !== 'claude' } - ) - - expect(mocks.installClaude).not.toHaveBeenCalled() - expect(mocks.installCodex).toHaveBeenCalledTimes(1) - }) - it('does not finish a startup install after shutdown begins', async () => { let releaseDetection: ((value: Record) => void) | undefined mocks.detect.mockImplementation( @@ -247,11 +230,11 @@ describe('managed agent hook controls', () => { releaseDetection = resolve }) ) - const settings = { agentStatusHooksEnabled: true, disabledTuiAgents: [] } + const settings = { agentStatusHooksEnabled: true } let isQuitting = false const install = installManagedAgentHooks(settings, { - shouldContinue: (agent) => shouldContinueManagedHookStartup(isQuitting, settings, agent) + shouldContinue: () => shouldContinueManagedHookStartup(isQuitting, settings) }) await vi.waitFor(() => expect(mocks.detect).toHaveBeenCalledTimes(1)) isQuitting = true @@ -262,21 +245,6 @@ describe('managed agent hook controls', () => { expect(mocks.installCodex).not.toHaveBeenCalled() }) - it('does not remove an agent enabled by a newer settings update', async () => { - mocks.detect.mockResolvedValue({ codex: { state: 'found' } }) - - await applyAgentStatusHooksEnabled( - true, - { - agentCmdOverrides: {}, - disabledTuiAgents: ['claude'] - }, - { shouldContinue: () => true } - ) - - expect(mocks.removeClaude).not.toHaveBeenCalled() - }) - it('removes every managed hook when the global setting is off', async () => { await applyAgentStatusHooksEnabled(false) @@ -310,21 +278,4 @@ describe('startup managed hook reconciliation (STA-5679)', () => { expect(resolveStartupManagedHookAction({})).toBe('install') expect(resolveStartupManagedHookAction(null)).toBe('install') }) - - it('does not remove disabled agents during startup install reconciliation', async () => { - const settings = { - agentStatusHooksEnabled: true, - agentCmdOverrides: {}, - disabledTuiAgents: ['claude' as const] - } - mocks.detect.mockResolvedValue({ codex: { state: 'found' } }) - - await installManagedAgentHooks(settings, { - shouldContinue: (agent) => shouldContinueManagedHookStartup(false, settings, agent) - }) - - expect(mocks.removeClaude).not.toHaveBeenCalled() - expect(mocks.installClaude).not.toHaveBeenCalled() - expect(mocks.installCodex).toHaveBeenCalledTimes(1) - }) }) diff --git a/src/main/agent-hooks/managed-agent-hook-controls.ts b/src/main/agent-hooks/managed-agent-hook-controls.ts index 70a2b9bb24d5..d0cc0c4fae63 100644 --- a/src/main/agent-hooks/managed-agent-hook-controls.ts +++ b/src/main/agent-hooks/managed-agent-hook-controls.ts @@ -1,14 +1,7 @@ import type { AgentHookInstallStatus, AgentHookTarget } from '../../shared/agent-hook-types' -import { - getManagedAgentHookTarget, - isManagedAgentHookTarget -} from '../../shared/managed-agent-hook-targets' -import { normalizeDisabledTuiAgents } from '../../shared/tui-agent-selection' +import { getManagedAgentHookTarget } from '../../shared/managed-agent-hook-targets' import type { GlobalSettings } from '../../shared/global-settings-types' -import { - isAgentStatusHooksEnabled, - isAgentStatusHooksEnabledForAgent -} from '../../shared/agent-status-hooks-setting' +import { isAgentStatusHooksEnabled } from '../../shared/agent-status-hooks-setting' import { probeClaudeCliVersion } from '../claude/claude-hook-event-versions' import { detectLocalManagedAgentCliPresence } from './local-agent-cli-presence' import { @@ -22,13 +15,10 @@ import { } from './managed-agent-hook-registry' export { MANAGED_AGENT_HOOK_INSTALLERS } from './managed-agent-hook-registry' -export { - isAgentStatusHooksEnabled, - isAgentStatusHooksEnabledForAgent -} from '../../shared/agent-status-hooks-setting' +export { isAgentStatusHooksEnabled } from '../../shared/agent-status-hooks-setting' type ManagedHookSettings = Partial< - Pick + Pick > | null type InstallOptions = { @@ -59,10 +49,9 @@ export function resolveStartupManagedHookAction( export function shouldContinueManagedHookStartup( isQuitting: boolean, - settings: ManagedHookSettings, - agent: AgentHookTarget + settings: ManagedHookSettings ): boolean { - return !isQuitting && isAgentStatusHooksEnabledForAgent(settings, agent) + return !isQuitting && isAgentStatusHooksEnabled(settings) } function errorStatus(agent: AgentHookTarget, error: unknown): AgentHookInstallStatus { @@ -141,9 +130,7 @@ export async function installManagedAgentHooks( ): Promise { await refreshExistingManagedScripts(options) const installers = selectedInstallers(options) - const disabled = new Set(normalizeDisabledTuiAgents(settings?.disabledTuiAgents)) - const enabledInstallers = installers.filter(([agent]) => !disabled.has(agent)) - const targets = enabledInstallers.flatMap(([agent]) => { + const targets = installers.flatMap(([agent]) => { const target = getManagedAgentHookTarget(agent) return target ? [target] : [] }) @@ -154,20 +141,12 @@ export async function installManagedAgentHooks( }) } catch (error) { const detail = error instanceof Error ? error.message : String(error) - return installers.map(([agent]) => - disabled.has(agent) - ? skippedStatus(agent, 'agent_disabled', 'Agent is disabled in Settings.') - : skippedStatus(agent, 'cli_presence_unknown', detail) - ) + return installers.map(([agent]) => skippedStatus(agent, 'cli_presence_unknown', detail)) } const results: AgentHookInstallStatus[] = [] for (const entry of installers) { const [agent] = entry - if (disabled.has(agent)) { - results.push(skippedStatus(agent, 'agent_disabled', 'Agent is disabled in Settings.')) - continue - } if (options.shouldContinue && !options.shouldContinue(agent)) { results.push( skippedStatus( @@ -256,21 +235,5 @@ export async function applyAgentStatusHooksEnabled( if (!enabled) { return await removeManagedAgentHooks() } - const disabled = normalizeDisabledTuiAgents(settings?.disabledTuiAgents).filter( - isManagedAgentHookTarget - ) - const installed = await installManagedAgentHooks(settings, options) - const disabledToRemove = options.shouldContinue - ? disabled.filter((agent) => !options.shouldContinue?.(agent)) - : disabled - if (disabledToRemove.length === 0) { - return installed - } - const removed = new Map( - (await removeManagedAgentHooks({ agents: disabledToRemove })).map((status) => [ - status.agent, - status - ]) - ) - return installed.map((status) => removed.get(status.agent) ?? status) + return await installManagedAgentHooks(settings, options) } diff --git a/src/main/agent-hooks/managed-agent-hook-startup.ts b/src/main/agent-hooks/managed-agent-hook-startup.ts index c845bf4bd363..7cfa997dc9d5 100644 --- a/src/main/agent-hooks/managed-agent-hook-startup.ts +++ b/src/main/agent-hooks/managed-agent-hook-startup.ts @@ -22,8 +22,7 @@ export function reconcileStartupManagedAgentHooks(host: { void installManagedAgentHooks(host.getSettings(), { shouldHydrateShellPath: host.shouldHydrateShellPath, onInstallError: recordManagedHookInstallFailure, - shouldContinue: (agent) => - shouldContinueManagedHookStartup(host.isQuitting(), host.getSettings(), agent) + shouldContinue: () => shouldContinueManagedHookStartup(host.isQuitting(), host.getSettings()) }).catch((error: unknown) => console.warn('[agent-hooks] failed to reconcile managed hooks on startup:', error) ) diff --git a/src/main/agent-hooks/managed-hook-detection-commands.test.ts b/src/main/agent-hooks/managed-hook-detection-commands.test.ts index 15f4c041860f..2c10c00a38bc 100644 --- a/src/main/agent-hooks/managed-hook-detection-commands.test.ts +++ b/src/main/agent-hooks/managed-hook-detection-commands.test.ts @@ -5,16 +5,12 @@ import { } from './managed-hook-detection-commands' describe('managed hook detection commands', () => { - it('omits disabled agents and includes safe command overrides', () => { + it('includes safe command overrides', () => { const commands = buildManagedHookDetectionCommands( - { - disabledTuiAgents: ['claude'], - agentCmdOverrides: { codex: '/opt/codex custom' } - }, + { agentCmdOverrides: { codex: '/opt/codex custom' } }, 'linux' ) - expect(commands.some((command) => command.id === 'claude')).toBe(false) expect(commands).toContainEqual({ id: 'codex', cmd: '/opt/codex' }) }) diff --git a/src/main/agent-hooks/managed-hook-detection-commands.ts b/src/main/agent-hooks/managed-hook-detection-commands.ts index 0d3ee8280d59..a7dc48825e4f 100644 --- a/src/main/agent-hooks/managed-hook-detection-commands.ts +++ b/src/main/agent-hooks/managed-hook-detection-commands.ts @@ -4,36 +4,32 @@ import { isSafeOverrideExecutableToken } from '../../shared/managed-agent-command-token' import { MANAGED_AGENT_HOOK_TARGETS } from '../../shared/managed-agent-hook-targets' -import { normalizeDisabledTuiAgents } from '../../shared/tui-agent-selection' import type { GlobalSettings } from '../../shared/global-settings-types' import type { TuiAgentDetectionCommand } from '../ipc/tui-agent-detection-commands' import { parseClaudeCliVersion } from '../claude/claude-hook-event-versions' export type ManagedHookDetectionSettings = Partial< - Pick + Pick > | null export function buildManagedHookDetectionCommands( settings: ManagedHookDetectionSettings, platform: NodeJS.Platform ): TuiAgentDetectionCommand[] { - const disabled = new Set(normalizeDisabledTuiAgents(settings?.disabledTuiAgents)) - return MANAGED_AGENT_HOOK_TARGETS.filter((target) => !disabled.has(target.tuiAgent)).flatMap( - (target) => { - const commands = new Set(target.executableCandidates) - const override = extractExecutableToken(settings?.agentCmdOverrides?.[target.tuiAgent], { - platform - }) - if (override && isSafeOverrideExecutableToken(override)) { - commands.add(override) - } - return [...commands].map((cmd) => ({ - id: target.tuiAgent, - cmd, - ...(target.agent === 'claude' ? { reportVersion: true as const } : {}) - })) + return MANAGED_AGENT_HOOK_TARGETS.flatMap((target) => { + const commands = new Set(target.executableCandidates) + const override = extractExecutableToken(settings?.agentCmdOverrides?.[target.tuiAgent], { + platform + }) + if (override && isSafeOverrideExecutableToken(override)) { + commands.add(override) } - ) + return [...commands].map((cmd) => ({ + id: target.tuiAgent, + cmd, + ...(target.agent === 'claude' ? { reportVersion: true as const } : {}) + })) + }) } export function detectedManagedHookAgents(values: unknown): AgentHookTarget[] { diff --git a/src/main/agent-hooks/opencode-plugin-settings.ts b/src/main/agent-hooks/opencode-plugin-settings.ts index bb22a481fa29..af055de50bc4 100644 --- a/src/main/agent-hooks/opencode-plugin-settings.ts +++ b/src/main/agent-hooks/opencode-plugin-settings.ts @@ -1,23 +1,18 @@ import type { PluginSources } from '../../wsl-guest/plugin-overlay' -import { isAgentStatusHooksEnabledForAgent } from '../../shared/agent-status-hooks-setting' +import { isAgentStatusHooksEnabled } from '../../shared/agent-status-hooks-setting' import type { ManagedHookDetectionSettings } from './managed-hook-detection-commands' -function enabled(settings: ManagedHookDetectionSettings, agent: 'opencode' | 'opencode2'): boolean { - return isAgentStatusHooksEnabledForAgent(settings, agent) -} - export function openCodePluginSettingsKey(settings: ManagedHookDetectionSettings): string { - return `${enabled(settings, 'opencode')}:${enabled(settings, 'opencode2')}` + return String(isAgentStatusHooksEnabled(settings)) } export function selectOpenCodePluginSources( sources: PluginSources, settings: ManagedHookDetectionSettings ): PluginSources { - // Older relays retain omitted sources; an empty string replaces the cache and cannot write a plugin. - return { - ...sources, - opencodePluginSource: enabled(settings, 'opencode') ? sources.opencodePluginSource : '', - opencode2PluginSource: enabled(settings, 'opencode2') ? sources.opencode2PluginSource : '' + if (isAgentStatusHooksEnabled(settings)) { + return sources } + // Older relays retain omitted sources; an empty string replaces the cache and cannot write a plugin. + return { ...sources, opencodePluginSource: '', opencode2PluginSource: '' } } diff --git a/src/main/agent-hooks/wsl-hook-relay-guest-install.ts b/src/main/agent-hooks/wsl-hook-relay-guest-install.ts index d891fb0427ec..e2ed66df418d 100644 --- a/src/main/agent-hooks/wsl-hook-relay-guest-install.ts +++ b/src/main/agent-hooks/wsl-hook-relay-guest-install.ts @@ -104,10 +104,7 @@ async function installGuestHooksAndPlugins( return } const currentSettings = deps.managedHookSettings() - const settings = currentSettings && { - ...currentSettings, - disabledTuiAgents: currentSettings.disabledTuiAgents?.slice() - } + const settings = currentSettings && { ...currentSettings } const settingsKey = openCodePluginSettingsKey(settings) state.lastAttemptOpenCodeSettings = settingsKey const overlay = await requestGuestOpenCodeOverlayDir( diff --git a/src/main/agent-hooks/wsl-opencode-install-race.test.ts b/src/main/agent-hooks/wsl-opencode-install-race.test.ts index 240feeac6c60..291d63c1896f 100644 --- a/src/main/agent-hooks/wsl-opencode-install-race.test.ts +++ b/src/main/agent-hooks/wsl-opencode-install-race.test.ts @@ -2,7 +2,6 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { TuiAgent } from '../../shared/tui-agent' import { RelayDispatcher } from '../../wsl-guest/dispatcher' import { PluginOverlayManager } from '../../wsl-guest/plugin-overlay' import { createInstallPluginsHandler } from '../../wsl-guest/wsl-install-plugins-handler' @@ -30,7 +29,7 @@ function heldRequest() { let root: string let state: WslRelayDistroState -let settings: { disabledTuiAgents: TuiAgent[] } +let settings: { agentStatusHooksEnabled: boolean } let guests: { mux: SshChannelMultiplexer; dispatcher: RelayDispatcher }[] const sources = { opencodePluginSource: '// v1', opencode2PluginSource: '// v2' } const deps = { @@ -78,7 +77,7 @@ function connect(guest: ReturnType) { beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'orca-wsl-install-race-')) guests = [] - settings = { disabledTuiAgents: [] } + settings = { agentStatusHooksEnabled: true } deps.installHooks.mockReset().mockResolvedValue([]) deps.warn.mockReset() state = { @@ -103,10 +102,10 @@ it.each([true, false])('coalesces waiters through a held %s enablement toggle', const second = heldRequest() const guest = createGuest('guest', (index) => (index === 1 ? first.wait() : second.wait())) connect(guest) - settings.disabledTuiAgents = enabled ? [] : ['opencode', 'opencode2'] + settings.agentStatusHooksEnabled = enabled const initial = runWslRelayGuestInstall(deps, state, guest.mux, guest.home) await first.reached - settings.disabledTuiAgents = enabled ? ['opencode', 'opencode2'] : [] + settings.agentStatusHooksEnabled = !enabled let settled = 0 const waiters = Array.from({ length: 4 }, () => maybeRerunWslRelayGuestInstall(deps, state).then(() => { @@ -144,7 +143,7 @@ it('drains reentrant toggles during successive requests without duplicate passes const waiters: Promise[] = [] const guest = createGuest('guest', async (index) => { if (index <= 2) { - settings.disabledTuiAgents = index === 1 ? ['opencode'] : ['opencode', 'opencode2'] + settings.agentStatusHooksEnabled = index !== 1 waiters.push(maybeRerunWslRelayGuestInstall(deps, state)) } }) @@ -155,11 +154,11 @@ it('drains reentrant toggles during successive requests without duplicate passes guest.requests.map((params) => [params.opencodePluginSource, params.opencode2PluginSource]) ).toEqual([ ['// v1', '// v2'], - ['', '// v2'], - ['', ''] + ['', ''], + ['// v1', '// v2'] ]) - expect(state.opencodeOverlayDir).toBeUndefined() - expect(state.opencode2OverlayDir).toBeUndefined() + expect(state.opencodeOverlayDir).toBeTruthy() + expect(state.opencode2OverlayDir).toBeTruthy() }) it('does not reinstall for rapid toggles that return to the in-flight settings', async () => { @@ -168,9 +167,9 @@ it('does not reinstall for rapid toggles that return to the in-flight settings', connect(guest) const initial = runWslRelayGuestInstall(deps, state, guest.mux, guest.home) await held.reached - settings.disabledTuiAgents = ['opencode', 'opencode2'] + settings.agentStatusHooksEnabled = false const disabled = maybeRerunWslRelayGuestInstall(deps, state) - settings.disabledTuiAgents = [] + settings.agentStatusHooksEnabled = true const enabled = maybeRerunWslRelayGuestInstall(deps, state) held.release() await Promise.all([initial, disabled, enabled]) @@ -231,7 +230,7 @@ it.each([true, false])( ) await reached.promise if (changed) { - settings.disabledTuiAgents = ['opencode', 'opencode2'] + settings.agentStatusHooksEnabled = false } const waiter = maybeRerunWslRelayGuestInstall(deps, state) rejected.reject(new Error('fixture installation failed')) @@ -256,7 +255,7 @@ it('does not publish a held response or reinstall after ownership is removed', a connect(guest) const initial = runWslRelayGuestInstall(deps, state, guest.mux, guest.home) await held.reached - settings.disabledTuiAgents = ['opencode', 'opencode2'] + settings.agentStatusHooksEnabled = false const waiter = maybeRerunWslRelayGuestInstall(deps, state) state.mux = undefined held.release() @@ -316,7 +315,7 @@ it('does not dispatch a recheck on a disposed mux', async () => { connect(guest) const initial = runWslRelayGuestInstall(deps, state, guest.mux, guest.home) await held.reached - settings.disabledTuiAgents = ['opencode', 'opencode2'] + settings.agentStatusHooksEnabled = false const waiter = maybeRerunWslRelayGuestInstall(deps, state) guest.mux.dispose() held.release() diff --git a/src/main/agent-hooks/wsl-opencode-settings-lifetime.test.ts b/src/main/agent-hooks/wsl-opencode-settings-lifetime.test.ts index 2c04fd5b153a..45465d6c6e5b 100644 --- a/src/main/agent-hooks/wsl-opencode-settings-lifetime.test.ts +++ b/src/main/agent-hooks/wsl-opencode-settings-lifetime.test.ts @@ -10,7 +10,6 @@ import { createInstallPluginsHandler } from '../../wsl-guest/wsl-install-plugins import { WslHookRelayManager } from './wsl-hook-relay-manager' import { REINSTALL_MIN_INTERVAL_MS } from './wsl-hook-relay-deps' import type { MultiplexerTransport } from '../ssh/ssh-channel-multiplexer' -import type { TuiAgent } from '../../shared/tui-agent' import { AGENT_HOOK_INSTALL_PLUGINS_METHOD, AGENT_HOOK_REQUEST_REPLAY_METHOD @@ -47,7 +46,7 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet let manager: WslHookRelayManager let guests: ReturnType[] let activeGuest: ReturnType - let settings: { disabledTuiAgents: TuiAgent[] } + let settings: { agentStatusHooksEnabled: boolean } let requests: Record[] let now: number let detectionCount: number @@ -116,7 +115,7 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'orca-wsl-manager-boundary-')) - settings = { disabledTuiAgents: [] } + settings = { agentStatusHooksEnabled: true } requests = [] now = Date.now() vi.spyOn(Date, 'now').mockImplementation(() => now) @@ -167,10 +166,9 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet rmSync(root, { recursive: true, force: true }) }) + // Why initially enabled only: hooks off stops the relay, so a live relay always starts enabled. const cases = (['detect', 'hooks', 'plugin'] as const).flatMap((stage) => - [true, false].flatMap((initialEnabled) => - [true, false].map((toggleBack) => ({ stage, initialEnabled, toggleBack })) - ) + [true, false].map((toggleBack) => ({ stage, initialEnabled: true, toggleBack })) ) it.each(cases)( @@ -178,7 +176,7 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet async ({ stage, initialEnabled, toggleBack }) => { pause = stage const setEnabled = (enabled: boolean) => { - settings = { disabledTuiAgents: enabled ? [] : ['opencode', 'opencode2'] } + settings = { agentStatusHooksEnabled: enabled } } setEnabled(initialEnabled) await manager.ensureForDistro('fixture') @@ -248,12 +246,12 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet now += REINSTALL_MIN_INTERVAL_MS + 1 const refresh = manager.ensureForDistro('fixture') await beforeSources.reached - settings = { disabledTuiAgents: ['opencode', 'opencode2'] } + settings = { agentStatusHooksEnabled: false } const disabled = manager.ensureForDistro('fixture') beforeSources.release() await pendingRequest.reached rejectRequest = true - settings = { disabledTuiAgents: [] } + settings = { agentStatusHooksEnabled: true } const restored = manager.ensureForDistro('fixture') pendingRequest.release() await Promise.all([refresh, disabled, restored]) @@ -278,7 +276,7 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet } const refresh = manager.ensureForDistro('fixture', undefined, 'omp') await pendingRequest.reached - settings.disabledTuiAgents = ['opencode', 'opencode2'] + settings.agentStatusHooksEnabled = false const disabled = manager.ensureForDistro('fixture') pendingRequest.release() await secondKind.reached @@ -287,7 +285,7 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet opencodePluginSource: '', opencode2PluginSource: '' }) - settings.disabledTuiAgents = [] + settings.agentStatusHooksEnabled = true const restored = manager.ensureForDistro('fixture') secondKind.release() await Promise.all([refresh, disabled, restored]) @@ -313,7 +311,7 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet now += REINSTALL_MIN_INTERVAL_MS + 1 const refresh = manager.ensureForDistro('fixture') await beforeSources.reached - settings.disabledTuiAgents = ['opencode', 'opencode2'] + settings.agentStatusHooksEnabled = false rejectHooks = true const waiters = Array.from({ length: 4 }, () => manager.ensureForDistro('fixture')) beforeSources.release() @@ -321,6 +319,7 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet expect(hookCount).toBe(3) expect(requests).toHaveLength(1) expect(manager.getOpenCodeOverlayDir('fixture')).toBe(previous) + settings.agentStatusHooksEnabled = true await manager.ensureForDistro('fixture') expect(hookCount).toBe(3) now += REINSTALL_MIN_INTERVAL_MS + 1 @@ -328,8 +327,8 @@ describe.skipIf(process.platform === 'win32')('WSL public-manager settings lifet pendingRequest.release() await manager.ensureForDistro('fixture') expect(hookCount).toBe(4) - expect(requests.at(-1)).toMatchObject({ opencodePluginSource: '', opencode2PluginSource: '' }) - expect(manager.getOpenCodeOverlayDir('fixture')).toBeNull() + expect(requests.at(-1)).toMatchObject(sources) + expect(manager.getOpenCodeOverlayDir('fixture')).toBe(previous) }) it('keeps a disposed generation from publishing into a reconnected manager', async () => { diff --git a/src/main/agent-hooks/wsl-opencode-settings.test.ts b/src/main/agent-hooks/wsl-opencode-settings.test.ts index c2a260c6d1f3..565839d3c008 100644 --- a/src/main/agent-hooks/wsl-opencode-settings.test.ts +++ b/src/main/agent-hooks/wsl-opencode-settings.test.ts @@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { TuiAgent } from '../../shared/tui-agent' import { RelayDispatcher } from '../../wsl-guest/dispatcher' import { PluginOverlayManager } from '../../wsl-guest/plugin-overlay' import { createInstallPluginsHandler } from '../../wsl-guest/wsl-install-plugins-handler' @@ -19,7 +18,7 @@ let home: string let dispatcher: RelayDispatcher let mux: SshChannelMultiplexer let state: WslRelayDistroState -let settings: { disabledTuiAgents: TuiAgent[] } +let settings: { agentStatusHooksEnabled: boolean } let params: Record[] const sources = { opencodePluginSource: '// v1', @@ -40,7 +39,7 @@ beforeEach(() => { const xdg = join(home, 'xdg') mkdirSync(join(xdg, 'opencode'), { recursive: true }) writeFileSync(join(xdg, 'opencode', 'opencode.json'), '{"model":"guest-fixture"}') - settings = { disabledTuiAgents: [] } + settings = { agentStatusHooksEnabled: true } params = [] const callbacks: ((data: Buffer) => void)[] = [] dispatcher = new RelayDispatcher((data) => { @@ -84,30 +83,23 @@ afterEach(() => { }) describe('WSL settings over the existing plugin RPC into real fixture overlays', () => { - const cases: TuiAgent[][] = [[], ['opencode'], ['opencode2'], ['opencode', 'opencode2']] - it.each(cases.map((disabled) => ({ disabled })))( - 'honors disabled $disabled', - async ({ disabled }) => { - settings.disabledTuiAgents = disabled - const result = await requestGuestOpenCodeOverlayDir(mux, deps, 'fixture') - for (const agent of ['opencode', 'opencode2'] as const) { - const key = agent === 'opencode' ? 'opencodePluginSource' : 'opencode2PluginSource' - expect(params[0][key]).toBe(disabled.includes(agent) ? '' : sources[key]) + it.each([true, false])('honors agentStatusHooksEnabled %s', async (enabled) => { + settings.agentStatusHooksEnabled = enabled + const result = await requestGuestOpenCodeOverlayDir(mux, deps, 'fixture') + expect(params[0].opencodePluginSource).toBe(enabled ? sources.opencodePluginSource : '') + expect(params[0].opencode2PluginSource).toBe(enabled ? sources.opencode2PluginSource : '') + if (result.kind === 'dir') { + expect(enabled).toBe(true) + const dir = result.dir ?? result.dir2 + if (!dir) { + throw new Error('missing overlay') } - if (result.kind === 'dir') { - expect(Boolean(result.dir)).toBe(!disabled.includes('opencode')) - expect(Boolean(result.dir2)).toBe(!disabled.includes('opencode2')) - const dir = result.dir ?? result.dir2 - if (!dir) { - throw new Error('missing overlay') - } - expect(readFileSync(join(dir, 'opencode.json'), 'utf8')).toBe('{"model":"guest-fixture"}') - } else { - expect(disabled).toEqual(['opencode', 'opencode2']) - } - expect(params[0].piExtensionSource).toBe('// pi') + expect(readFileSync(join(dir, 'opencode.json'), 'utf8')).toBe('{"model":"guest-fixture"}') + } else { + expect(enabled).toBe(false) } - ) + expect(params[0].piExtensionSource).toBe('// pi') + }) it('revokes cached sources before the reinstall interval and re-enables the same service', async () => { await runWslRelayGuestInstall(deps, state, mux, home) @@ -117,10 +109,9 @@ describe('WSL settings over the existing plugin RPC into real fixture overlays', } const path = join(dir, 'plugins', 'orca-opencode-status.js') writeFileSync(path, '// installed sentinel') - settings.disabledTuiAgents = ['opencode'] + settings.agentStatusHooksEnabled = false await maybeRerunWslRelayGuestInstall(deps, state) expect(state.opencodeOverlayDir).toBeUndefined() - expect(state.opencode2OverlayDir).toBeTruthy() expect(readFileSync(path, 'utf8')).toBe('// installed sentinel') const omitted = await requestGuestOpenCodeOverlayDir( mux, @@ -128,7 +119,7 @@ describe('WSL settings over the existing plugin RPC into real fixture overlays', 'fixture' ) expect(omitted.kind === 'dir' ? omitted.dir : undefined).toBeUndefined() - settings.disabledTuiAgents = [] + settings.agentStatusHooksEnabled = true await maybeRerunWslRelayGuestInstall(deps, state) expect(state.opencodeOverlayDir).toBe(dir) expect(existsSync(path)).toBe(true) diff --git a/src/main/agent-launch/startup-agent-worktree-create.test.ts b/src/main/agent-launch/startup-agent-worktree-create.test.ts index 8b39d1090508..b72112ccf1f5 100644 --- a/src/main/agent-launch/startup-agent-worktree-create.test.ts +++ b/src/main/agent-launch/startup-agent-worktree-create.test.ts @@ -200,9 +200,7 @@ describe('createWorktreeWithStartupAgent', () => { }) it('surfaces a create failure unchanged', async () => { - const failure = new Error( - 'Selected agent is disabled. Choose an enabled agent before creating.' - ) + const failure = new Error('Branch app already exists.') const { create } = harness({ createError: failure }) await expect(create({ ...BASE, startupAgent: 'claude' })).rejects.toBe(failure) diff --git a/src/main/claude-accounts/claude-launch-routing.test.ts b/src/main/claude-accounts/claude-launch-routing.test.ts index d8fc2a0b3cb4..c1754ce65304 100644 --- a/src/main/claude-accounts/claude-launch-routing.test.ts +++ b/src/main/claude-accounts/claude-launch-routing.test.ts @@ -48,8 +48,7 @@ function coveredAccount() { claudeManagedAccounts: [account], activeClaudeManagedAccountId: 'a', activeClaudeManagedAccountIdsByRuntime: undefined, - agentStatusHooksEnabled: false, - disabledTuiAgents: [] + agentStatusHooksEnabled: false } const router = new ClaudeProfileRouter({ getSettings: () => settings, diff --git a/src/main/claude-accounts/claude-profile-router.test.ts b/src/main/claude-accounts/claude-profile-router.test.ts index a24f4e4d5fa9..3e53f05d1559 100644 --- a/src/main/claude-accounts/claude-profile-router.test.ts +++ b/src/main/claude-accounts/claude-profile-router.test.ts @@ -75,8 +75,7 @@ function fixture(env: NodeJS.ProcessEnv = {}) { activeClaudeManagedAccountId: 'a', activeClaudeManagedAccountIdsByRuntime: undefined, // Hooks off: setup must not probe a real Claude here. - agentStatusHooksEnabled: false, - disabledTuiAgents: [] + agentStatusHooksEnabled: false } // Stands in for the worker; like it, stays pending until settled and marks the folder last. const setup: { calls: number; outcome: ClaudeProfileSetupReport['outcome']; settle: () => void } = diff --git a/src/main/claude-accounts/claude-profile-router.ts b/src/main/claude-accounts/claude-profile-router.ts index 3a7d3f81f42d..e567eed7b7f0 100644 --- a/src/main/claude-accounts/claude-profile-router.ts +++ b/src/main/claude-accounts/claude-profile-router.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs' import { homedir } from 'node:os' import { dirname, join } from 'node:path' -import { isAgentStatusHooksEnabledForAgent } from '../../shared/agent-status-hooks-setting' +import { isAgentStatusHooksEnabled } from '../../shared/agent-status-hooks-setting' import { CLAUDE_INJECTED_CONFIG_DIR_ENV, CLAUDE_PROFILE_POINTER_ENV, @@ -48,7 +48,6 @@ export type ClaudeProfileRouterSettings = Pick< | 'activeClaudeManagedAccountId' | 'activeClaudeManagedAccountIdsByRuntime' | 'agentStatusHooksEnabled' - | 'disabledTuiAgents' > /** @@ -260,7 +259,7 @@ export class ClaudeProfileRouter { private async runSetup(profile: ClaudeProfileDescriptor): Promise { await this.envReady - const hooks = isAgentStatusHooksEnabledForAgent(this.args.getSettings(), 'claude') + const hooks = isAgentStatusHooksEnabled(this.args.getSettings()) const claudeVersion = hooks ? await probeRecentClaudeCliVersion(resolveClaudeCommand()) : null const report = await (this.args.runSetup ?? runClaudeProfileSetupInWorker)({ dataRoot: this.args.dataRoot, diff --git a/src/main/claude-accounts/claude-profile-wsl-router.test.ts b/src/main/claude-accounts/claude-profile-wsl-router.test.ts index be520cac3eeb..1e5ac7ef0817 100644 --- a/src/main/claude-accounts/claude-profile-wsl-router.test.ts +++ b/src/main/claude-accounts/claude-profile-wsl-router.test.ts @@ -72,8 +72,7 @@ function fixture() { claudeManagedAccounts: [account('a')], activeClaudeManagedAccountId: null, activeClaudeManagedAccountIdsByRuntime: { host: null, wsl }, - agentStatusHooksEnabled: false, - disabledTuiAgents: [] + agentStatusHooksEnabled: false } const setup = { calls: 0, fail: false, gate: Promise.resolve() } const router = new ClaudeWslProfileRouter({ diff --git a/src/main/claude/claude-structured-launch-account-routing.test.ts b/src/main/claude/claude-structured-launch-account-routing.test.ts index eda02246f191..9e21793c61dd 100644 --- a/src/main/claude/claude-structured-launch-account-routing.test.ts +++ b/src/main/claude/claude-structured-launch-account-routing.test.ts @@ -25,8 +25,7 @@ it('launches each acquisition under the current selection, not the account it wa claudeManagedAccounts: [], activeClaudeManagedAccountId: 'a', activeClaudeManagedAccountIdsByRuntime: undefined, - agentStatusHooksEnabled: false, - disabledTuiAgents: [] + agentStatusHooksEnabled: false } const router = new ClaudeProfileRouter({ getSettings: () => settings, @@ -93,8 +92,7 @@ function routedResumeFixture() { claudeManagedAccounts: [], activeClaudeManagedAccountId: 'a', activeClaudeManagedAccountIdsByRuntime: undefined, - agentStatusHooksEnabled: false, - disabledTuiAgents: [] + agentStatusHooksEnabled: false }), dataRoot: root, userHome: join(root, 'personal'), diff --git a/src/main/ipc/pty-ipc-spawn-drivers.ts b/src/main/ipc/pty-ipc-spawn-drivers.ts index dde291ce0725..ab2819279a09 100644 --- a/src/main/ipc/pty-ipc-spawn-drivers.ts +++ b/src/main/ipc/pty-ipc-spawn-drivers.ts @@ -33,7 +33,6 @@ export function createPtyIpcSpawnDrivers(ctx: { ) => string | null, getSettings?: () => { agentStatusHooksEnabled?: boolean - disabledTuiAgents?: TuiAgent[] httpProxyUrl?: string httpProxyBypassRules?: string }, diff --git a/src/main/ipc/pty-spawn-env-terminal-basics.test.ts b/src/main/ipc/pty-spawn-env-terminal-basics.test.ts index 250000ef465f..b54c14fa7a37 100644 --- a/src/main/ipc/pty-spawn-env-terminal-basics.test.ts +++ b/src/main/ipc/pty-spawn-env-terminal-basics.test.ts @@ -1,6 +1,6 @@ import { withFreshOmpLaunch } from '../../shared/omp-fresh-launch' import { describe, expect, it, vi } from 'vitest' -import { piBuildPtyEnvMock, spawnMock } from './pty-ipc-mock-registry' +import { spawnMock } from './pty-ipc-mock-registry' import { TEST_CODEX_HOME } from './pty-ipc-test-constants' import { setupPtyIpcSuite } from './pty-ipc-test-harness' import { LocalPtyProvider } from '../providers/local-pty-provider' @@ -96,55 +96,6 @@ describe('registerPtyHandlers', () => { } ) - it('does not install managed Pi extensions when Pi is disabled', () => { - piBuildPtyEnvMock.mockClear() - - buildPtyHostEnv( - 'pty-pi-disabled', - {}, - { - isPackaged: true, - userDataPath: '/tmp/orca-user-data', - selectedCodexHomePath: null, - agentStatusHooksEnabled: true, - disabledTuiAgents: ['pi'] - } - ) - - expect(piBuildPtyEnvMock.mock.calls.map(([, , kind]) => kind)).toEqual(['omp']) - }) - - it('does not install managed OMP extensions when OMP is disabled', () => { - piBuildPtyEnvMock.mockClear() - - const env = buildPtyHostEnv( - 'pty-omp-disabled', - {}, - { - isPackaged: true, - userDataPath: '/tmp/orca-user-data', - selectedCodexHomePath: null, - launchCommand: 'omp', - launchAgent: 'omp', - agentStatusHooksEnabled: true, - disabledTuiAgents: ['omp'] - } - ) - - expect(piBuildPtyEnvMock).not.toHaveBeenCalled() - expect(env.ORCA_OMP_FRESH_CONFIG).toBe('/tmp/orca-fresh-session.yml') - }) - - it('threads disabled Pi settings through a bare PTY spawn', async () => { - piBuildPtyEnvMock.mockClear() - - await spawnAndGetEnv(undefined, undefined, undefined, () => ({ - agentStatusHooksEnabled: true, - disabledTuiAgents: ['pi'] - })) - - expect(piBuildPtyEnvMock.mock.calls.map(([, , kind]) => kind)).toEqual(['omp']) - }) it('prepares fresh OMP settings even when status hooks are disabled', () => { const env = buildPtyHostEnv( 'fresh-without-hooks', @@ -472,17 +423,6 @@ describe('registerPtyHandlers', () => { // Why: the app prepares a native pane's Codex home; only WSL panes run the preflight. expect(env.ORCA_CODEX_LAUNCH_PREFLIGHT).toBeUndefined() }) - it('does not install the Codex launch preflight when Codex hooks are disabled', async () => { - const env = await spawnAndGetEnv( - undefined, - undefined, - () => TEST_CODEX_HOME, - () => ({ agentStatusHooksEnabled: true, disabledTuiAgents: ['codex'] }) - ) - - expect(env.CODEX_HOME).toBe(TEST_CODEX_HOME) - expect(env.ORCA_CODEX_LAUNCH_PREFLIGHT).toBeUndefined() - }) it('resumes an automatic Codex session from its prepared originating home', async () => { const selectedHome = vi.fn(() => '/managed/current/home') const prepareResume = vi.fn(async () => ({ diff --git a/src/main/ipc/pty/host-env/assembly.ts b/src/main/ipc/pty/host-env/assembly.ts index 0508d3e459ce..4073f564f5df 100644 --- a/src/main/ipc/pty/host-env/assembly.ts +++ b/src/main/ipc/pty/host-env/assembly.ts @@ -17,7 +17,6 @@ import { mergePersistedWindowsPath } from '../../../pty/windows-environment-path import { resolveCodexShellLaunchPreflightCommand } from '../../../pty/codex-shell-launch-preflight' import { scheduleCodexHookReconcile } from '../../../codex/codex-hook-reconcile' import { buildConfiguredProxyEnv } from '../../../../shared/network-proxy' -import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' import type { BuildPtyHostEnvOptions } from './types' import { stripInheritedOrcaCodexHomeOverride } from './codex-home' import { @@ -68,12 +67,9 @@ export function buildPtyHostEnv( }) const shouldPrepareOmpShadow = piAgentKind === 'omp' || !hasLaunchCommand - const shouldInstallPiExtensions = - opts.agentStatusHooksEnabled && isTuiAgentEnabled('pi', opts.disabledTuiAgents) - const shouldInstallOmpExtensions = - opts.agentStatusHooksEnabled && isTuiAgentEnabled('omp', opts.disabledTuiAgents) - const shouldInstallPrimeAgentExtensions = - opts.agentStatusHooksEnabled && isTuiAgentEnabled('prime-agent', opts.disabledTuiAgents) + const shouldInstallPiExtensions = opts.agentStatusHooksEnabled + const shouldInstallOmpExtensions = opts.agentStatusHooksEnabled + const shouldInstallPrimeAgentExtensions = opts.agentStatusHooksEnabled // Why: source shadows are agent-scoped; trusting the other kind's source reintroduces Pi/OMP extension-state shadowing. const preexistingPiAgentDir = resolvePiAgentSourceDir(baseEnv, 'pi') const preexistingOmpAgentDir = @@ -239,8 +235,7 @@ export function buildPtyHostEnv( // Why: user startup files may re-export CODEX_HOME; shell-ready wrappers restore this runtime home before Codex launches. baseEnv.ORCA_CODEX_HOME = opts.selectedCodexHomePath const preflightCommand = resolveCodexShellLaunchPreflightCommand({ - hooksEnabled: - opts.agentStatusHooksEnabled && isTuiAgentEnabled('codex', opts.disabledTuiAgents), + hooksEnabled: opts.agentStatusHooksEnabled, isPackaged: opts.isPackaged, isWsl: opts.isWsl, managedHomePath: opts.selectedCodexHomePath, diff --git a/src/main/ipc/pty/host-env/opencode-config.ts b/src/main/ipc/pty/host-env/opencode-config.ts index 74aa1e46625b..7e3481234694 100644 --- a/src/main/ipc/pty/host-env/opencode-config.ts +++ b/src/main/ipc/pty/host-env/opencode-config.ts @@ -5,7 +5,6 @@ import { import { openCode2HookService, openCodeHookService } from '../../../opencode/hook-service' import { resolveOpenCodeSourceConfigDir, restoreOrStripOverlayEnv } from './pi-agent' import { selectOpenCodeHookAgent } from '../../../../shared/opencode-launch-command' -import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' import { resolveOpenCodeConfigDirectory } from '../../../../shared/opencode-config-directory' import { readSessionShellStartupEnvVar } from '../../../pty/shell-startup-env' import type { BuildPtyHostEnvOptions } from './types' @@ -43,15 +42,14 @@ export function applyOpenCodeStatusPluginEnv( config: OpenCodeSourceConfig, options: Pick< BuildPtyHostEnvOptions, - 'launchAgent' | 'agentStatusHooksEnabled' | 'disabledTuiAgents' | 'isWsl' | 'shellPath' + 'launchAgent' | 'agentStatusHooksEnabled' | 'isWsl' | 'shellPath' >, command: string | undefined ): 'opencode' | 'opencode2' | null { const agent = selectOpenCodeHookAgent( options.launchAgent, command, - (candidate) => - options.agentStatusHooksEnabled && isTuiAgentEnabled(candidate, options.disabledTuiAgents) + () => options.agentStatusHooksEnabled ) restoreOrStripOverlayEnv( env, diff --git a/src/main/ipc/pty/host-env/opencode-hook-installation.test.ts b/src/main/ipc/pty/host-env/opencode-hook-installation.test.ts index fe42ca5735c4..9dd3ea2f48a9 100644 --- a/src/main/ipc/pty/host-env/opencode-hook-installation.test.ts +++ b/src/main/ipc/pty/host-env/opencode-hook-installation.test.ts @@ -142,16 +142,12 @@ describe('OpenCode installation uses the current enabled agents', () => { expect(env.OPENCODE_CONFIG_DIR).toBeUndefined() }) - it.each([false, true])('consumer config root is untouched with hooks disabled %s', (disabled) => { + it('consumer config root is untouched with hooks disabled', () => { const consumer = join(root, 'disabled-consumer') buildPtyHostEnv( 'pane', { XDG_CONFIG_HOME: consumer }, - { - ...options, - agentStatusHooksEnabled: disabled, - disabledTuiAgents: disabled ? ['opencode', 'opencode2'] : [] - } + { ...options, agentStatusHooksEnabled: false } ) expect(existsSync(join(consumer, 'opencode'))).toBe(false) }) @@ -179,20 +175,15 @@ describe('OpenCode installation uses the current enabled agents', () => { }) const combinations = [ - { disabled: [], fallback: 'opencode' }, - { disabled: ['opencode'], fallback: 'opencode2' }, - { disabled: ['opencode2'], fallback: 'opencode' }, - { disabled: ['opencode', 'opencode2'], fallback: undefined } + { enabled: true, fallback: 'opencode' }, + { enabled: false, fallback: undefined } ] - for (const { disabled, fallback } of combinations) { - it.each(['default', 'custom'])(`bare %s config with disabled ${disabled.join(',')}`, (kind) => { + for (const { enabled, fallback } of combinations) { + it.each(['default', 'custom'])(`bare %s config with hooks ${enabled}`, (kind) => { const env = buildPtyHostEnv( 'pane', kind === 'custom' ? { OPENCODE_CONFIG_DIR: custom } : {}, - { - ...options, - disabledTuiAgents: disabled - } + { ...options, agentStatusHooksEnabled: enabled } ) expect(env.ORCA_OPENCODE_AGENT).toBe(fallback) const selected = env.OPENCODE_CONFIG_DIR ?? config @@ -205,62 +196,52 @@ describe('OpenCode installation uses the current enabled agents', () => { expect(existsSync(plugin(custom, 'opencode'))).toBe(false) } }) - it.each(['opencode', 'opencode2'] as const)( - `explicit %s with disabled ${disabled.join(',')}`, - (agent) => { - for (const selection of [ - { launchCommand: `${agent} --session fixture` }, - { launchAgent: agent } - ]) { - const env = buildPtyHostEnv( - 'pane', - {}, - { ...options, ...selection, disabledTuiAgents: disabled } - ) - const selected = disabled.includes(agent) ? undefined : agent - expect(env.ORCA_OPENCODE_AGENT).toBe(selected) - expect(existsSync(plugin(config, agent))).toBe(selected === agent) - expect(existsSync(plugin(config, agent === 'opencode' ? 'opencode2' : 'opencode'))).toBe( - false - ) - } + it.each(['opencode', 'opencode2'] as const)(`explicit %s with hooks ${enabled}`, (agent) => { + for (const selection of [ + { launchCommand: `${agent} --session fixture` }, + { launchAgent: agent } + ]) { + const env = buildPtyHostEnv( + 'pane', + {}, + { ...options, ...selection, agentStatusHooksEnabled: enabled } + ) + const selected = enabled ? agent : undefined + expect(env.ORCA_OPENCODE_AGENT).toBe(selected) + expect(existsSync(plugin(config, agent))).toBe(selected === agent) + expect(existsSync(plugin(config, agent === 'opencode' ? 'opencode2' : 'opencode'))).toBe( + false + ) } - ) + }) } it('reads enable/disable changes on the same services without deleting installed files', () => { buildPtyHostEnv('first', {}, options) const installed = plugin(config, 'opencode') writeFileSync(installed, '// already installed sentinel') - const env = buildPtyHostEnv('second', {}, { ...options, disabledTuiAgents: ['opencode'] }) - expect(env.ORCA_OPENCODE_AGENT).toBe('opencode2') + const env = buildPtyHostEnv('second', {}, { ...options, agentStatusHooksEnabled: false }) + expect(env.ORCA_OPENCODE_AGENT).toBeUndefined() expect(readFileSync(installed, 'utf8')).toBe('// already installed sentinel') buildPtyHostEnv('third', {}, options) expect(readFileSync(installed, 'utf8')).toContain('/hook/opencode') }) - it.each([true, false])( - 'restores inherited source and clears markers with hooks %s', - (enabled) => { - const first = buildPtyHostEnv('first', { OPENCODE_CONFIG_DIR: custom }, options) - const original = readFileSync(plugin(first.OPENCODE_CONFIG_DIR, 'opencode'), 'utf8') - const env = buildPtyHostEnv( - 'second', - { ...first }, - { - ...options, - agentStatusHooksEnabled: enabled, - disabledTuiAgents: ['opencode', 'opencode2'] - } - ) - expect(env.OPENCODE_CONFIG_DIR).toBe(custom) - expect(env.ORCA_OPENCODE_CONFIG_DIR).toBeUndefined() - expect(env.ORCA_OPENCODE_SOURCE_CONFIG_DIR).toBeUndefined() - expect(env.ORCA_OPENCODE_AGENT).toBeUndefined() - expect(readFileSync(plugin(first.OPENCODE_CONFIG_DIR, 'opencode'), 'utf8')).toBe(original) - expect(env.ORCA_AGENT_HOOK_PORT).toBe(enabled ? '12345' : undefined) - } - ) + it('restores inherited source and clears markers with hooks off', () => { + const first = buildPtyHostEnv('first', { OPENCODE_CONFIG_DIR: custom }, options) + const original = readFileSync(plugin(first.OPENCODE_CONFIG_DIR, 'opencode'), 'utf8') + const env = buildPtyHostEnv( + 'second', + { ...first }, + { ...options, agentStatusHooksEnabled: false } + ) + expect(env.OPENCODE_CONFIG_DIR).toBe(custom) + expect(env.ORCA_OPENCODE_CONFIG_DIR).toBeUndefined() + expect(env.ORCA_OPENCODE_SOURCE_CONFIG_DIR).toBeUndefined() + expect(env.ORCA_OPENCODE_AGENT).toBeUndefined() + expect(readFileSync(plugin(first.OPENCODE_CONFIG_DIR, 'opencode'), 'utf8')).toBe(original) + expect(env.ORCA_AGENT_HOOK_PORT).toBeUndefined() + }) it('drops an inherited overlay without a source and preserves an unrelated explicit config', () => { for (const primary of [fixture.guestOverlay, custom]) { @@ -271,7 +252,7 @@ describe('OpenCode installation uses the current enabled agents', () => { ORCA_OPENCODE_CONFIG_DIR: fixture.guestOverlay, ORCA_OPENCODE_AGENT: 'opencode' }, - { ...options, disabledTuiAgents: ['opencode', 'opencode2'] } + { ...options, agentStatusHooksEnabled: false } ) expect(env.OPENCODE_CONFIG_DIR).toBe(primary === custom ? custom : undefined) expect(env.ORCA_OPENCODE_AGENT).toBeUndefined() @@ -289,14 +270,9 @@ describe('OpenCode installation uses the current enabled agents', () => { const disabled = buildPtyHostEnv( 'wsl-disabled', {}, - { - ...options, - isWsl: true, - disabledTuiAgents: ['opencode', 'opencode2'] - } + { ...options, isWsl: true, agentStatusHooksEnabled: false } ) expect(disabled.OPENCODE_CONFIG_DIR).toBeUndefined() - expect(disabled.ORCA_AGENT_HOOK_ENDPOINT).toBe('/guest/endpoint.json') expect(existsSync(config)).toBe(false) }) }) @@ -307,10 +283,7 @@ it.each(['source', 'no-source', 'user-config'])( const prepared = buildPtyHostEnv( 'daemon-pane', {}, - { - ...options, - disabledTuiAgents: ['opencode', 'opencode2'] - } + { ...options, agentStatusHooksEnabled: false } ) vi.stubEnv('OPENCODE_CONFIG_DIR', kind === 'user-config' ? custom : fixture.guestOverlay) vi.stubEnv('ORCA_OPENCODE_CONFIG_DIR', fixture.guestOverlay) @@ -344,7 +317,7 @@ it.each([ const prepared = buildPtyHostEnv( 'daemon-pane', scenario.explicit ? { OPENCODE_CONFIG_DIR: custom } : {}, - { ...options, disabledTuiAgents: ['opencode', 'opencode2'] } + { ...options, agentStatusHooksEnabled: false } ) const previous = join(root, 'previous') mkdirSync(previous) @@ -442,31 +415,6 @@ it.each([true, false])( } ) -it.each([true, false])( - 'drops host config for disabled WSL variants with explicit primary %s', - (explicit) => { - const overlay = 'C:\\Users\\fixture\\Orca\\opencode-overlays\\old' - const source = 'C:\\Users\\fixture\\config\\opencode' - vi.stubEnv('ORCA_OPENCODE_CONFIG_DIR', overlay) - vi.stubEnv('ORCA_OPENCODE_SOURCE_CONFIG_DIR', source) - const env = buildPtyHostEnv( - 'wsl-disabled-inherited', - { - OPENCODE_CONFIG_DIR: explicit ? source : overlay, - ORCA_OPENCODE_CONFIG_DIR: overlay, - ORCA_OPENCODE_SOURCE_CONFIG_DIR: source - }, - { ...options, isWsl: true, disabledTuiAgents: ['opencode', 'opencode2'] } - ) - expect(env.OPENCODE_CONFIG_DIR).toBeUndefined() - expect(env.ORCA_OPENCODE_CONFIG_DIR).toBeUndefined() - expect(env.ORCA_OPENCODE_SOURCE_CONFIG_DIR).toBeUndefined() - expect(env.ORCA_OPENCODE_AGENT).toBeUndefined() - expect(env.ORCA_AGENT_HOOK_ENDPOINT).toBe('/guest/endpoint.json') - expect(existsSync(config)).toBe(false) - } -) - // Why: pre-1.4.209 panes exported Orca's retired /opencode-hooks/shared dir. OpenCode 2 // treats OPENCODE_CONFIG_DIR as the only config dir, so inheriting it loaded a stale plugin and hid // the user's global config. diff --git a/src/main/ipc/pty/host-env/types.ts b/src/main/ipc/pty/host-env/types.ts index 3222ddf533cc..8beecb213e93 100644 --- a/src/main/ipc/pty/host-env/types.ts +++ b/src/main/ipc/pty/host-env/types.ts @@ -29,8 +29,6 @@ export type BuildPtyHostEnvOptions = { /** Distro for WSL spawns (null = Windows default distro); drives the WSL hook relay + endpoint repoint. Only read when isWsl. */ wslDistro?: string | null agentStatusHooksEnabled: boolean - /** Per-agent opt-out; disabled agents must not receive managed extensions. */ - disabledTuiAgents?: Iterable | null networkProxySettings?: NetworkProxySettings /** Headless paired runtimes hand browser launches to the client-hosted Orca browser. */ routeBrowserOpensToClient?: boolean diff --git a/src/main/ipc/pty/ipc/spawn-env-codex.ts b/src/main/ipc/pty/ipc/spawn-env-codex.ts index 5634928b81f0..ac2a908ad45f 100644 --- a/src/main/ipc/pty/ipc/spawn-env-codex.ts +++ b/src/main/ipc/pty/ipc/spawn-env-codex.ts @@ -151,7 +151,6 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd), wslDistro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), - disabledTuiAgents: ptySettings?.disabledTuiAgents, networkProxySettings: ptySettings, routeBrowserOpensToClient: ctx.deps.runtime?.shouldRelayTerminalBrowserOpens?.(), deferGitConfigGuardToDaemon: diff --git a/src/main/ipc/pty/provider/local-configure.ts b/src/main/ipc/pty/provider/local-configure.ts index 774ac8d25944..e11e848073fd 100644 --- a/src/main/ipc/pty/provider/local-configure.ts +++ b/src/main/ipc/pty/provider/local-configure.ts @@ -94,7 +94,6 @@ export function configureLocalPtyProvider(args: { isWsl: ctx?.isWsl, wslDistro: ctx?.wslDistro ?? null, agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), - disabledTuiAgents: ptySettings?.disabledTuiAgents, networkProxySettings: ptySettings, routeBrowserOpensToClient: runtime?.shouldRelayTerminalBrowserOpens?.() }) diff --git a/src/main/ipc/pty/runtime/spawn-preflight.ts b/src/main/ipc/pty/runtime/spawn-preflight.ts index 22d33bf708d6..6466756aa102 100644 --- a/src/main/ipc/pty/runtime/spawn-preflight.ts +++ b/src/main/ipc/pty/runtime/spawn-preflight.ts @@ -258,7 +258,6 @@ export async function prepareRuntimePtySpawn( isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd), wslDistro: ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null, agentStatusHooksEnabled: isAgentStatusHooksEnabled(ptySettings), - disabledTuiAgents: ptySettings?.disabledTuiAgents, networkProxySettings: ptySettings, routeBrowserOpensToClient: ctx.deps.runtime?.shouldRelayTerminalBrowserOpens?.(), deferGitConfigGuardToDaemon: diff --git a/src/main/ipc/settings.test.ts b/src/main/ipc/settings.test.ts index 80757d55c922..ae063f8e7258 100644 --- a/src/main/ipc/settings.test.ts +++ b/src/main/ipc/settings.test.ts @@ -177,53 +177,6 @@ describe('registerSettingsHandlers', () => { expect(event.returnValue).toEqual({ terminalMainSideEffectAuthority: false }) }) - it('does not reconcile hooks when the disabled-agent set is unchanged', async () => { - const before = { - agentStatusHooksEnabled: true, - disabledTuiAgents: ['codex', 'claude'] - } - store.getSettings.mockReturnValue(before) - store.updateSettings.mockReturnValue({ - ...before, - disabledTuiAgents: ['claude', 'codex'] - }) - registerSettingsHandlers(store as never) - const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as ( - event: typeof settingsInvokeEvent, - args: { disabledTuiAgents: string[] } - ) => Promise - - await handler(settingsInvokeEvent, { disabledTuiAgents: ['claude', 'codex'] }) - - expect(applyAgentStatusHooksEnabledMock).not.toHaveBeenCalled() - }) - - it('reconciles hooks when the disabled-agent set changes', async () => { - const before = { - agentStatusHooksEnabled: true, - disabledTuiAgents: ['codex', 'claude'] - } - const updated = { - ...before, - disabledTuiAgents: ['claude'] - } - store.getSettings.mockReturnValue(before) - store.updateSettings.mockReturnValue(updated) - registerSettingsHandlers(store as never) - const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as ( - event: typeof settingsInvokeEvent, - args: { disabledTuiAgents: string[] } - ) => Promise - - await handler(settingsInvokeEvent, { disabledTuiAgents: ['claude'] }) - - expect(applyAgentStatusHooksEnabledMock).toHaveBeenCalledWith( - true, - updated, - expect.objectContaining({ shouldContinue: expect.any(Function) }) - ) - }) - it('rejects durable Active Server writes through generic settings:set', async () => { store.getSettings.mockReturnValue({ activeRuntimeEnvironmentId: null }) store.updateSettings.mockReturnValue({ activeRuntimeEnvironmentId: null }) @@ -766,14 +719,12 @@ describe('registerSettingsHandlers', () => { store.getSettings.mockReturnValue({ httpProxyUrl: '', httpProxyBypassRules: '', - agentStatusHooksEnabled: false, - disabledTuiAgents: [] + agentStatusHooksEnabled: false }) store.updateSettings.mockImplementation((args) => ({ httpProxyUrl: args.httpProxyUrl, httpProxyBypassRules: '', - agentStatusHooksEnabled: args.agentStatusHooksEnabled ?? true, - disabledTuiAgents: [] + agentStatusHooksEnabled: args.agentStatusHooksEnabled ?? true })) let releaseHookReconciliation = (): void => {} let markHookReconciliationStarted = (): void => {} diff --git a/src/main/ipc/settings.ts b/src/main/ipc/settings.ts index cd9fd873e0a3..ca3a6d272142 100644 --- a/src/main/ipc/settings.ts +++ b/src/main/ipc/settings.ts @@ -12,7 +12,7 @@ import { SETTINGS_CHANGED_WHITELIST, type SettingsChangedKey } from '../../share import type { AgentAwakeService } from '../agent-awake-service' import { sanitizeFloatingWorkspaceDirectorySetting } from './floating-workspace-directory' import { applyAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' -import { isAgentStatusHooksEnabledForAgent } from '../../shared/agent-status-hooks-setting' +import { isAgentStatusHooksEnabled } from '../../shared/agent-status-hooks-setting' import { recordManagedHookInstallFailure } from '../agent-hooks/install-telemetry' import { applyElectronProxySettings } from '../network/proxy-settings' import { applyBrowserSessionProxies } from '../browser/browser-session-proxy' @@ -29,7 +29,6 @@ import { scheduleCurrentWorktreeBaseDirectoryWatcherSync } from './worktree-base import { applyPRBotAuthorOverride } from '../../shared/pr-bot-author-overrides' import { resolveEnvironment } from '../../shared/runtime-environment-store' import { readSettingsWithRuntimeEnvironmentPreference } from './runtime-environment-preference' -import { haveSameDisabledTuiAgents } from '../../shared/tui-agent-selection' import { normalizeMobilePairingCustomAddress, normalizeMobilePairingCustomAddresses @@ -238,17 +237,15 @@ export function registerSettingsHandlers( ) } const hookSettingChanged = - ('agentStatusHooksEnabled' in sanitizedArgs && - before.agentStatusHooksEnabled !== result.agentStatusHooksEnabled) || - ('disabledTuiAgents' in sanitizedArgs && - !haveSameDisabledTuiAgents(before.disabledTuiAgents, result.disabledTuiAgents)) + 'agentStatusHooksEnabled' in sanitizedArgs && + before.agentStatusHooksEnabled !== result.agentStatusHooksEnabled if (hookSettingChanged) { try { await applyAgentStatusHooksEnabled(result.agentStatusHooksEnabled, result, { userInitiated: true, shouldHydrateShellPath: app.isPackaged, onInstallError: recordManagedHookInstallFailure, - shouldContinue: (agent) => isAgentStatusHooksEnabledForAgent(store.getSettings(), agent) + shouldContinue: () => isAgentStatusHooksEnabled(store.getSettings()) }) } catch (error) { console.warn('[settings] failed to reconcile managed agent hooks:', error) diff --git a/src/main/opencode/opencode-status-plugin-startup-refresh.test.ts b/src/main/opencode/opencode-status-plugin-startup-refresh.test.ts index da588a3f6896..80c3ccd21e60 100644 --- a/src/main/opencode/opencode-status-plugin-startup-refresh.test.ts +++ b/src/main/opencode/opencode-status-plugin-startup-refresh.test.ts @@ -28,10 +28,4 @@ describe('refreshInstalledOpenCodeStatusPlugins', () => { expect(refreshV1).not.toHaveBeenCalled() expect(refreshV2).not.toHaveBeenCalled() }) - - it('skips a disabled variant only', () => { - refreshInstalledOpenCodeStatusPlugins({ disabledTuiAgents: ['opencode2'] }) - expect(refreshV1).toHaveBeenCalledTimes(1) - expect(refreshV2).not.toHaveBeenCalled() - }) }) diff --git a/src/main/opencode/opencode-status-plugin-startup-refresh.ts b/src/main/opencode/opencode-status-plugin-startup-refresh.ts index af88a0666a13..06499c6c1c32 100644 --- a/src/main/opencode/opencode-status-plugin-startup-refresh.ts +++ b/src/main/opencode/opencode-status-plugin-startup-refresh.ts @@ -1,4 +1,4 @@ -import { isAgentStatusHooksEnabledForAgent } from '../../shared/agent-status-hooks-setting' +import { isAgentStatusHooksEnabled } from '../../shared/agent-status-hooks-setting' import type { ManagedHookDetectionSettings } from '../agent-hooks/managed-hook-detection-commands' import { openCode2HookService, openCodeHookService } from './hook-service' @@ -7,10 +7,8 @@ import { openCode2HookService, openCodeHookService } from './hook-service' export function refreshInstalledOpenCodeStatusPlugins( settings: ManagedHookDetectionSettings ): void { - if (isAgentStatusHooksEnabledForAgent(settings, 'opencode')) { + if (isAgentStatusHooksEnabled(settings)) { openCodeHookService.refreshInstalledPlugins() - } - if (isAgentStatusHooksEnabledForAgent(settings, 'opencode2')) { openCode2HookService.refreshInstalledPlugins() } } diff --git a/src/main/orcad/orcad-managed-agent-hooks.ts b/src/main/orcad/orcad-managed-agent-hooks.ts index dbdf08695fde..1ed2f05137d3 100644 --- a/src/main/orcad/orcad-managed-agent-hooks.ts +++ b/src/main/orcad/orcad-managed-agent-hooks.ts @@ -2,10 +2,7 @@ import { getAppEnvironment } from '../../shared/app-environment' import { startCodexHooks } from '../codex/codex-hook-reconcile' import { adoptLoginShellAgentHomes } from '../agent-hooks/agent-home-login-shell' import { hydrateAgentCliShellPath } from '../agent-hooks/local-agent-cli-presence' -import { - isAgentStatusHooksEnabled, - isAgentStatusHooksEnabledForAgent -} from '../agent-hooks/managed-agent-hook-controls' +import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' import { reconcileStartupManagedAgentHooks } from '../agent-hooks/managed-agent-hook-startup' import { refreshInstalledOpenCodeStatusPlugins } from '../opencode/opencode-status-plugin-startup-refresh' import type { ProfilePreferences } from '../persistence/loading-store/profile-preferences' @@ -35,7 +32,7 @@ export function startOrcadManagedAgentHooks( const shouldHydrateShellPath = getAppEnvironment().isPackaged() startCodexHooks({ pathReady: shouldHydrateShellPath ? hydrateAgentCliShellPath() : Promise.resolve(), - isEnabled: () => isAgentStatusHooksEnabledForAgent(getSettings(), 'codex'), + isEnabled: () => isAgentStatusHooksEnabled(getSettings()), // Why null: orcad launches Codex on ~/.codex; Orca-managed account homes are a desktop flow. resolveLaunchHome: () => null }) diff --git a/src/main/runtime/managed-worktree-create-telemetry.test.ts b/src/main/runtime/managed-worktree-create-telemetry.test.ts index 377237132756..3e2e941fdcf0 100644 --- a/src/main/runtime/managed-worktree-create-telemetry.test.ts +++ b/src/main/runtime/managed-worktree-create-telemetry.test.ts @@ -73,7 +73,7 @@ type RuntimeInternals = { function makeRuntime(repo: Record = gitRepo): OrcaRuntimeService { const store = { - getSettings: () => ({ disabledTuiAgents: ['codex'], workspaceDir: '/worktrees' }), + getSettings: () => ({ disabledTuiAgents: [], workspaceDir: '/worktrees' }), getProjectHostSetups: () => [] } // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every store method this create path reaches is supplied above. @@ -265,9 +265,10 @@ describe('runtime create events', () => { makeRuntime().createManagedWorktree({ repoSelector: 'repo-1', name: 'app', - startupAgent: 'codex' + startupAgent: 'opencode', + startupLaunchPreferences: { model: 'opencode/any' } }) - ).rejects.toThrow('Selected agent is disabled') + ).rejects.toThrow('The execution host cannot verify this OpenCode model launch.') expect(mocks.createLocal).not.toHaveBeenCalled() expect(mocks.track).not.toHaveBeenCalled() }) diff --git a/src/main/runtime/orca-runtime-create-agent-session.ts b/src/main/runtime/orca-runtime-create-agent-session.ts index dc8ea3f2aeeb..899a4c038117 100644 --- a/src/main/runtime/orca-runtime-create-agent-session.ts +++ b/src/main/runtime/orca-runtime-create-agent-session.ts @@ -11,7 +11,6 @@ import { parseAgentSessionOperationTimestamp } from '../../shared/agent-session-host-authority' import { createHash } from 'node:crypto' -import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-inputs' import { buildExecutionHostAgentStartupPlan } from '../opencode/opencode-model-startup-plan' import type { RuntimeTerminalCreate } from '../../shared/runtime-types' @@ -127,9 +126,6 @@ export class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithGetAgentSe ) .digest('base64url') const settings = this.store!.getSettings() - if (!isTuiAgentEnabled(request.agent, settings.disabledTuiAgents)) { - throw new Error('Selected agent is disabled. Choose an enabled agent before creating.') - } const startupArgs = resolveAgentStartupPlanInputs({ agent: request.agent, settings, diff --git a/src/main/runtime/orca-runtime-create-managed-worktree.ts b/src/main/runtime/orca-runtime-create-managed-worktree.ts index a651ef3265bb..eba8453438f6 100644 --- a/src/main/runtime/orca-runtime-create-managed-worktree.ts +++ b/src/main/runtime/orca-runtime-create-managed-worktree.ts @@ -2,7 +2,6 @@ import { OrcaRuntimeWithGetWorktreeTerminalProvisioningHost } from './orca-runtime-get-worktree-terminal-provisioning-host' import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktree-create-types' import type { CreateWorktreeResult } from '../../shared/worktree/create-types' -import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' import { isFolderRepo } from '../../shared/repo-kind' import { resolveWorktreeCreateRoute } from '../worktree-create-execution-host-route' import { ExecutionHostNotDispatchableError } from '../providers/execution-host-provider-dispatch' @@ -50,20 +49,6 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork const repo = await this.resolveRepoSelector(args.repoSelector) const createSettings = this.store.getSettings() const requestedAgent = args.startupAgent ?? args.createdWithAgent - const requestedAgentEnabled = - requestedAgent !== undefined - ? isTuiAgentEnabled(requestedAgent, createSettings.disabledTuiAgents) - : false - if ((args.startup || args.startupAgent) && requestedAgent && !requestedAgentEnabled) { - throw new Error('Selected agent is disabled. Choose an enabled agent before creating.') - } - if ( - args.startup && - args.startupDraftPaste && - !isTuiAgentEnabled(args.startupDraftPaste.agent, createSettings.disabledTuiAgents) - ) { - throw new Error('Selected agent is disabled. Choose an enabled agent before creating.') - } const agentStartup = resolveWorktreeCreateAgentStartup(args, (...inputs) => this.buildStartupForAgent(repo, ...inputs) ) @@ -80,9 +65,7 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork const effectiveStartupFollowup = agentStartup?.followup const effectiveCreatedWithAgent = args.startup ? args.createdWithAgent - : (agentStartup?.agent ?? - draftStartup?.agent ?? - (requestedAgentEnabled ? requestedAgent : undefined)) + : (agentStartup?.agent ?? draftStartup?.agent ?? requestedAgent) const effectiveDraftPaste = args.startupDraftPaste ?? draftStartup?.draftPaste // Match IPC routing: executionHostId-only SSH repos must not create locally. const createRoute = resolveWorktreeCreateRoute(repo) diff --git a/src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts b/src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts index 5aa3ba9195a6..246d643c4ce2 100644 --- a/src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts +++ b/src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts @@ -10,7 +10,6 @@ import type { RuntimeEnsureAgentSessionResult } from '../../shared/agent-session-host-authority' import { canonicalizeAgentSessionIdentity } from './agent-session-claim-identity' -import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-terminal-shell' import { buildAgentResumeStartupPlan } from '../../shared/tui-agent-startup' import { OrchestrationError } from './orchestration/orchestration-error' @@ -122,9 +121,6 @@ export class OrcaRuntimeWithGetAgentSessionExecutionNamespace extends OrcaRuntim canonicalWorktreeId: workspace.id }) const settings = this.store.getSettings() - if (!isTuiAgentEnabled(request.agent, settings.disabledTuiAgents)) { - throw new Error('Selected agent is disabled. Choose an enabled agent before resuming.') - } const platform = this.getAgentLaunchPlatformForWorkspace(workspace) // Why: `workspace.repo` is display metadata and may be a row from another host; the launch // shape must match the PTY route this scope already resolved. diff --git a/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts b/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts index 567cbe6896d5..ee6a62f74a76 100644 --- a/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts +++ b/src/main/runtime/orca-runtime-get-terminal-interactive-wait.ts @@ -13,8 +13,6 @@ import { parsePaneKey } from '../../shared/stable-pane-id' import type { ExactWorkerProviderSession } from '../../shared/orchestration-worker-output' import { selectExactWorkerProviderSession } from './orchestration/worker-provider-session' import type { TuiAgent } from '../../shared/tui-agent' -import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' -import { OrchestrationError } from './orchestration/orchestration-error' import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-terminal-shell' import { resolveStartupShell, type AgentStartupShell } from '../../shared/tui-agent-startup-shell' import { isTuiAgent } from '../../shared/tui-agent-config' @@ -280,16 +278,9 @@ export class OrcaRuntimeWithGetTerminalInteractiveWait extends OrcaRuntimeWithAd } } - validateOrchestrationAgentLauncher(agent: TuiAgent): void { - const settings = this.store?.getSettings() - if (!settings) { + validateOrchestrationAgentLauncher(): void { + if (!this.store?.getSettings()) { throw new Error('runtime_unavailable') } - if (!isTuiAgentEnabled(agent, settings.disabledTuiAgents)) { - throw new OrchestrationError( - 'agent_unconfigured', - `Agent launcher ${agent} is disabled or unavailable.` - ) - } } } diff --git a/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts b/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts index 019103542244..2d15023b51ad 100644 --- a/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts +++ b/src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts @@ -180,8 +180,8 @@ export class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh extends OrcaRu agent: tab.launchAgent }) } catch { - // Why: a disabled or unresolvable agent must not make the tab - // untappable; fall back to the plain-shell materialize. + // Why: an unresolvable agent must not make the tab untappable; + // fall back to the plain-shell materialize. } } try { diff --git a/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts b/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts index 095ee58a37f6..7e1ab995b4ca 100644 --- a/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts +++ b/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts @@ -4,7 +4,6 @@ import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-termi import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' import type { TuiAgent } from '../../shared/tui-agent' import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume' -import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' import { buildAgentStartupPlan } from '../../shared/tui-agent-startup' import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-inputs' @@ -43,9 +42,6 @@ export class OrcaRuntimeWithResolveMobileSessionTerminalCommand extends OrcaRunt throw new Error('runtime_unavailable') } const settings = this.store.getSettings() - if (!isTuiAgentEnabled(opts.agent, settings.disabledTuiAgents)) { - throw new Error('Selected agent is disabled. Choose an enabled agent before creating.') - } const startupPlan = buildAgentStartupPlan({ ...resolveAgentStartupPlanInputs({ agent: opts.agent, diff --git a/src/main/runtime/orca-runtime-stop-requested-pty-ids.ts b/src/main/runtime/orca-runtime-stop-requested-pty-ids.ts index a959249383d8..3cd84cba2ec2 100644 --- a/src/main/runtime/orca-runtime-stop-requested-pty-ids.ts +++ b/src/main/runtime/orca-runtime-stop-requested-pty-ids.ts @@ -28,7 +28,7 @@ import { RuntimeManagedWorktreeQueries } from './runtime-managed-worktree-querie import { RuntimePtyForegroundAgent } from './runtime-pty-foreground-agent' import { OpenCodeRunLifetimeStatus } from './opencode-run-lifetime-status' import { readLocalPtyForegroundCommandLine } from './local-pty-foreground-command-line' -import { isAgentStatusHooksEnabledForAgent } from '../../shared/agent-status-hooks-setting' +import { isAgentStatusHooksEnabled } from '../../shared/agent-status-hooks-setting' import { RuntimeTerminalAgentStatusQuery } from './runtime-terminal-agent-status-query' import type { OrchestrationDb } from './orchestration/db' import { OrchestrationMailboxOwner } from './orchestration/mailbox-owner' @@ -174,8 +174,7 @@ export class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWithRuntimeId // Why: SSH and WSL foregrounds live on another host or in the guest. return !!pty && !pty.connectionId && !pty.wslDistro && !this.wslDistroByPtyId.has(ptyId) }, - isStatusEnabled: (agent) => - isAgentStatusHooksEnabledForAgent(this.store?.getSettings?.(), agent), + isStatusEnabled: () => isAgentStatusHooksEnabled(this.store?.getSettings?.()), readForegroundProcessName: async (ptyId) => { const read = await this.ptyForegroundAgent.read(ptyId) return read?.available ? read.process : null diff --git a/src/main/runtime/orca-runtime-test-scenario-builders.spec.ts b/src/main/runtime/orca-runtime-test-scenario-builders.spec.ts index 45030b7c972e..d08128a068b4 100644 --- a/src/main/runtime/orca-runtime-test-scenario-builders.spec.ts +++ b/src/main/runtime/orca-runtime-test-scenario-builders.spec.ts @@ -282,7 +282,9 @@ function makePostRevealWorkerRecoveryHarness( } } -function makePendingAgentTabActivationRuntime(opts: { disabledTuiAgents?: string[] } = {}): { +function makePendingAgentTabActivationRuntime( + opts: { agentDefaultArgs?: Record } = {} +): { runtime: RuntimeService spawn: ReturnType } { @@ -309,11 +311,12 @@ function makePendingAgentTabActivationRuntime(opts: { disabledTuiAgents?: string } }) ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a complete test store with only getSettings replaced; the spread loses its declared type. const runtime = new OrcaRuntimeService({ ...runtimeStore, getSettings: () => ({ ...store.getSettings(), - disabledTuiAgents: opts.disabledTuiAgents ?? [] + ...(opts.agentDefaultArgs ? { agentDefaultArgs: opts.agentDefaultArgs } : {}) }) } as never) runtime.setPtyController({ diff --git a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-08.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-08.spec.ts index 4ac0faf4d586..29df18420981 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-08.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-08.spec.ts @@ -60,9 +60,9 @@ describe('OrcaRuntimeService', () => { expect(spawn.mock.calls[0]![0].command).toBeUndefined() }) - it('falls back to a plain shell when the pending tab agent is disabled', async () => { + it('falls back to a plain shell when the pending tab agent launch cannot be built', async () => { const { runtime, spawn } = makePendingAgentTabActivationRuntime({ - disabledTuiAgents: ['claude'] + agentDefaultArgs: { claude: '--model "unclosed' } }) const activated = await runtime.activateMobileSessionTab( @@ -76,12 +76,7 @@ describe('OrcaRuntimeService', () => { expect.objectContaining({ sessionId: 'serve-dead-pty', worktreeId: TEST_WORKTREE_ID }) ) expect(spawn.mock.calls[0]![0].command).toBeUndefined() - // Why: the disabled-agent fallback keeps the tab's agent identity; only the startup command is skipped. - expect(activated.tabs[0]).toMatchObject({ - type: 'terminal', - status: 'ready', - launchAgent: 'claude' - }) + expect(activated.tabs[0]).toMatchObject({ status: 'ready', launchAgent: 'claude' }) }) it('collapses duplicate mobile terminal entries when renderer and headless leaf ids diverge for the same pty', async () => { diff --git a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-12.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-12.spec.ts index 92b29a8b4496..63a51f0d5e15 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-12.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-12.spec.ts @@ -218,32 +218,6 @@ describe('OrcaRuntimeService', () => { ]) }) - it('rejects disabled mobile session agent launches before spawning', async () => { - const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent' }) - const runtime = new OrcaRuntimeService({ - ...store, - getSettings: () => ({ - ...store.getSettings(), - disabledTuiAgents: ['codex'], - agentCmdOverrides: {} - }) - } as never) - runtime.setPtyController({ - spawn, - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) - - await expect( - runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { - agent: 'codex' - }) - ).rejects.toThrow('Selected agent is disabled') - expect(spawn).not.toHaveBeenCalled() - }) - it('validates mobile terminal insertion anchors before resolving agent launch commands', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-agent' }) const runtime = new OrcaRuntimeService({ diff --git a/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts b/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts index 9f9909c7ec4d..33c4ba5c8aad 100644 --- a/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts +++ b/src/main/runtime/orca-runtime-tests/paired-settings.spec.ts @@ -278,8 +278,7 @@ describe('OrcaRuntimeService', () => { electronMocks.app.isPackaged = true let settings = { ...store.getSettings(), - agentStatusHooksEnabled: true, - disabledTuiAgents: ['codex', 'claude'] + agentStatusHooksEnabled: true } const updateSettings = vi.fn((updates: Partial) => { settings = { ...settings, ...updates } @@ -291,14 +290,14 @@ describe('OrcaRuntimeService', () => { updateSettings } as never) - await runtime.updateClientSettings({ disabledTuiAgents: ['claude', 'codex'] }) + await runtime.updateClientSettings({ agentStatusHooksEnabled: true }) expect(applyAgentStatusHooksEnabledMock).not.toHaveBeenCalled() - await runtime.updateClientSettings({ disabledTuiAgents: ['claude'] }) + await runtime.updateClientSettings({ agentStatusHooksEnabled: false }) expect(applyAgentStatusHooksEnabledMock).toHaveBeenCalledOnce() expect(applyAgentStatusHooksEnabledMock).toHaveBeenCalledWith( - true, - expect.objectContaining({ disabledTuiAgents: ['claude'] }), + false, + expect.objectContaining({ agentStatusHooksEnabled: false }), expect.objectContaining({ shouldContinue: expect.any(Function), shouldHydrateShellPath: true @@ -309,8 +308,7 @@ describe('OrcaRuntimeService', () => { it('serializes paired-client hook reconciliation and reads current settings', async () => { let settings = { ...store.getSettings(), - agentStatusHooksEnabled: true, - disabledTuiAgents: ['codex', 'claude'] + agentStatusHooksEnabled: true } const updateSettings = vi.fn((updates: Partial) => { settings = { ...settings, ...updates } @@ -326,9 +324,9 @@ describe('OrcaRuntimeService', () => { updateSettings } as never) - const first = runtime.updateClientSettings({ disabledTuiAgents: ['claude'] }) + const first = runtime.updateClientSettings({ agentStatusHooksEnabled: false }) await vi.waitFor(() => expect(applyAgentStatusHooksEnabledMock).toHaveBeenCalledOnce()) - const second = runtime.updateClientSettings({ disabledTuiAgents: [] }) + const second = runtime.updateClientSettings({ agentStatusHooksEnabled: true }) expect(applyAgentStatusHooksEnabledMock).toHaveBeenCalledOnce() const firstOptions = applyAgentStatusHooksEnabledMock.mock.calls[0]?.[2] @@ -340,7 +338,7 @@ describe('OrcaRuntimeService', () => { expect(applyAgentStatusHooksEnabledMock).toHaveBeenCalledTimes(2) expect(applyAgentStatusHooksEnabledMock).toHaveBeenLastCalledWith( true, - expect.objectContaining({ disabledTuiAgents: [] }), + expect.objectContaining({ agentStatusHooksEnabled: true }), expect.objectContaining({ shouldContinue: expect.any(Function) }) ) }) diff --git a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-02.spec.ts index d568281d00e9..d8e40fa0ac58 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-02.spec.ts @@ -784,30 +784,6 @@ describe('OrcaRuntimeService', () => { expect(spawn).not.toHaveBeenCalled() }) - it('rejects a startupAgent create for a disabled agent', async () => { - const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) - const runtimeStore = { - ...store, - getSettings: () => ({ - ...store.getSettings(), - disabledTuiAgents: ['cursor' as const], - agentCmdOverrides: {} - }) - } - const runtime = new OrcaRuntimeService(runtimeStore) - runtime.setPtyController({ - spawn, - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - - await expect( - runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { startupAgent: 'cursor' }) - ).rejects.toThrow(/disabled/) - expect(spawn).not.toHaveBeenCalled() - }) - it('quotes local Windows bare agent command defaults for cmd.exe terminal creates', async () => { setPlatform('win32') const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) diff --git a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-03.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-03.spec.ts index 794aafdd9815..5ee6a1dbb383 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-03.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-03.spec.ts @@ -160,9 +160,9 @@ describe('OrcaRuntimeService', () => { expect(spawnCall?.env?.ORCA_AGENT_LAUNCH_TOKEN).toBeUndefined() }) - it('keeps disabled bare agent command terminal creates unchanged', async () => { + it('launches an agent hidden in Settings when it is asked for explicitly', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) - const runtimeStore = { + const runtime = new OrcaRuntimeService({ ...store, getSettings: () => ({ ...store.getSettings(), @@ -171,8 +171,7 @@ describe('OrcaRuntimeService', () => { agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, agentDefaultEnv: {} }) - } - const runtime = new OrcaRuntimeService(runtimeStore) + }) runtime.setPtyController({ spawn, write: () => true, @@ -180,15 +179,13 @@ describe('OrcaRuntimeService', () => { getForegroundProcess: async () => null }) - await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { - command: 'codex' - }) + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { startupAgent: 'codex' }) + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { command: 'codex' }) - const spawnCall = spawn.mock.calls[0]?.[0] as - | { command?: string; env?: Record } - | undefined - expect(spawnCall?.command).toBe('codex') - expect(spawnCall?.env?.ORCA_AGENT_LAUNCH_TOKEN).toBeUndefined() + expect(spawn.mock.calls.map((call) => call[0].command)).toEqual([ + "codex '--dangerously-bypass-approvals-and-sandbox'", + "codex '--dangerously-bypass-approvals-and-sandbox'" + ]) }) it('sends Settings agent defaults through renderer-backed bare agent terminal creates', async () => { diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-03.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-03.spec.ts index 3a978bcbb562..9282657c20cf 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-03.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-03.spec.ts @@ -2,7 +2,6 @@ import { getDefaultWorkspaceSession } from '../../../shared/constants' import { describe, expect, it, onTestFinished, vi } from 'vitest' import { OrcaRuntimeService, - addWorktree, computeWorktreePathMock, createSetupRunnerScript, detectInstalledAgentsWithShellPathHydrationMock, @@ -733,36 +732,6 @@ describe('OrcaRuntimeService', () => { ) }) - it('rejects explicit startup commands for disabled selected agents', async () => { - const runtimeStore = { - ...store, - getSettings: () => ({ - ...store.getSettings(), - disabledTuiAgents: ['codex' as const] - }) - } - const runtime = new OrcaRuntimeService(runtimeStore as never) - const spawn = vi.fn().mockResolvedValue({ id: 'pty-disabled-startup' }) - runtime.setPtyController({ - spawn, - write: () => true, - kill: () => true, - getForegroundProcess: async () => null - }) - - await expect( - runtime.createManagedWorktree({ - repoSelector: TEST_REPO_ID, - name: 'disabled-startup', - startup: { command: 'codex' }, - createdWithAgent: 'codex' - }) - ).rejects.toThrow('Selected agent is disabled. Choose an enabled agent before creating.') - - expect(spawn).not.toHaveBeenCalled() - expect(addWorktree).not.toHaveBeenCalled() - }) - it('launches explicit startup agents with prompts for CLI-created worktrees', async () => { const metaById: Record = {} const runtimeStore = { diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts index c145d17bedf5..df70ab0467c1 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts @@ -162,7 +162,7 @@ describe('OrcaRuntimeService', () => { } }) - it('records the resolved fallback agent when the requested startup draft agent is disabled', async () => { + it('records the detected agent when the default startup draft agent is hidden', async () => { detectInstalledAgentsWithShellPathHydrationMock.mockResolvedValue(['claude']) const metaById: Record = {} const runtimeStore = { @@ -220,7 +220,6 @@ describe('OrcaRuntimeService', () => { repoSelector: TEST_REPO_ID, name: 'runtime-fallback-draft', startupDraft: 'https://github.com/stablyai/orca/issues/456', - createdWithAgent: 'codex', activate: true }) diff --git a/src/main/runtime/rpc/methods/agent-hooks.test.ts b/src/main/runtime/rpc/methods/agent-hooks.test.ts index f78e7709d6ee..43fc54704c82 100644 --- a/src/main/runtime/rpc/methods/agent-hooks.test.ts +++ b/src/main/runtime/rpc/methods/agent-hooks.test.ts @@ -32,12 +32,10 @@ function prepareMethod() { return method } -function runtimeWithSettings(enabled = true, disabledTuiAgents: string[] = []): OrcaRuntimeService { +function runtimeWithSettings(enabled = true): OrcaRuntimeService { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these handlers read only getClientSettings from the runtime. return { - getClientSettings: vi.fn(() => ({ - agentStatusHooksEnabled: enabled, - disabledTuiAgents - })) + getClientSettings: vi.fn(() => ({ agentStatusHooksEnabled: enabled })) } as unknown as OrcaRuntimeService } @@ -67,10 +65,7 @@ describe('agent hook RPC methods', () => { }) }) - it.each([ - [false, []], - [true, ['codex']] - ])('does not install when hooks are disabled (%s, %j)', async (enabled, disabledTuiAgents) => { + it('does not install when hooks are disabled', async () => { const method = prepareMethod() const params = method.params!.parse({ codexHome: '/home/jin/.local/share/orca/codex-runtime-home/home', @@ -79,7 +74,7 @@ describe('agent hook RPC methods', () => { }) await expect( - method.handler(params, { runtime: runtimeWithSettings(enabled, disabledTuiAgents) }) + method.handler(params, { runtime: runtimeWithSettings(false) }) ).resolves.toBeNull() expect(installForRuntimeHomeSerializedMock).not.toHaveBeenCalled() }) diff --git a/src/main/runtime/rpc/methods/agent-hooks.ts b/src/main/runtime/rpc/methods/agent-hooks.ts index 388ba3736b78..430f0f7c9af5 100644 --- a/src/main/runtime/rpc/methods/agent-hooks.ts +++ b/src/main/runtime/rpc/methods/agent-hooks.ts @@ -1,4 +1,4 @@ -import { isAgentStatusHooksEnabledForAgent } from '../../../../shared/agent-status-hooks-setting' +import { isAgentStatusHooksEnabled } from '../../../../shared/agent-status-hooks-setting' import { prepareManagedWslCodexHomeBeforeShellLaunch } from '../../../codex/managed-wsl-home-shell-preflight' import { defineMethod } from '../core' import { PrepareCodexForWslPaneParams } from '../../../../shared/rpc-contract/agent-hooks-params' @@ -18,7 +18,7 @@ export const AGENT_HOOK_METHODS = [ ORCA_CODEX_HOME: params.orcaCodexHome, WSL_DISTRO_NAME: params.wslDistro }, - hooksEnabled: isAgentStatusHooksEnabledForAgent(runtime.getClientSettings(), 'codex') + hooksEnabled: isAgentStatusHooksEnabled(runtime.getClientSettings()) }) } }) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.ts index b9e39abb859d..f28222872305 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.ts @@ -156,7 +156,7 @@ function resolveWorkerStartAgent(args: { throw new OrchestrationError('agent_unconfigured', args.missingAgentMessage) } if (agent) { - args.runtime.validateOrchestrationAgentLauncher(agent) + args.runtime.validateOrchestrationAgentLauncher() return { agent, launch: resolveWorkerLaunchPreferences({ diff --git a/src/main/runtime/runtime-agent-launch-resolution.test.ts b/src/main/runtime/runtime-agent-launch-resolution.test.ts new file mode 100644 index 000000000000..3787128ec7c5 --- /dev/null +++ b/src/main/runtime/runtime-agent-launch-resolution.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest' +import { resolveBareAgentLaunchCommand } from './runtime-agent-launch-resolution' + +describe('resolveBareAgentLaunchCommand', () => { + it("lets an override claim another agent's default command", () => { + expect( + resolveBareAgentLaunchCommand({ + command: 'opencode', + settings: { agentCmdOverrides: { opencode2: 'opencode' } }, + platform: 'darwin', + isRemote: false + }) + ).toBe('opencode2') + }) +}) diff --git a/src/main/runtime/runtime-agent-launch-resolution.ts b/src/main/runtime/runtime-agent-launch-resolution.ts index 9f5ddd4bd0a6..35505e84a69e 100644 --- a/src/main/runtime/runtime-agent-launch-resolution.ts +++ b/src/main/runtime/runtime-agent-launch-resolution.ts @@ -5,8 +5,11 @@ import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resum import type { ClaudeAgentTeamsMode } from '../../shared/claude-agent-teams-tmux-compat' import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime' import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path' -import { getTuiAgentLaunchCommand, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' -import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' +import { + getTuiAgentLaunchCommand, + isTuiAgent, + TUI_AGENT_CONFIG +} from '../../shared/tui-agent-config' export function mergeTerminalEnvDeletionKeys( first: readonly string[] | undefined, @@ -52,7 +55,6 @@ export function resolveBareAgentLaunchCommand(args: { command: string | undefined settings: { agentCmdOverrides?: Partial> | null - disabledTuiAgents?: Iterable | null } platform: NodeJS.Platform isRemote: boolean @@ -63,23 +65,23 @@ export function resolveBareAgentLaunchCommand(args: { } const cmdOverrides = args.settings.agentCmdOverrides ?? {} - for (const agent of Object.keys(TUI_AGENT_CONFIG) as TuiAgent[]) { - if (!isTuiAgentEnabled(agent, args.settings.disabledTuiAgents)) { - continue - } - const override = cmdOverrides[agent]?.trim() - const defaultLaunchCommand = getTuiAgentLaunchCommand(TUI_AGENT_CONFIG[agent], args.platform, { - isRemote: args.isRemote - }) - const launchCommands = override ? [defaultLaunchCommand, override] : [defaultLaunchCommand] - if ( - launchCommands.some((candidate) => command === normalizeAgentLaunchCommandForMatch(candidate)) - ) { - return agent - } - } - - return null + const agents = Object.keys(TUI_AGENT_CONFIG).filter(isTuiAgent) + // `command` is non-empty, so an empty override never matches. + const matches = (candidate: string | undefined): boolean => + candidate !== undefined && command === normalizeAgentLaunchCommandForMatch(candidate) + // Why overrides first: defaults are unique, so a collision is an override naming another agent's + // command, which says which agent the user means. Agents hidden in Settings match like any other. + return ( + agents.find((agent) => matches(cmdOverrides[agent])) ?? + agents.find((agent) => + matches( + getTuiAgentLaunchCommand(TUI_AGENT_CONFIG[agent], args.platform, { + isRemote: args.isRemote + }) + ) + ) ?? + null + ) } export function inferCapturedClaudeAgentTeamsMode( diff --git a/src/main/runtime/runtime-agent-terminal-startup.ts b/src/main/runtime/runtime-agent-terminal-startup.ts index 68932fc00587..8ef033fcb4d0 100644 --- a/src/main/runtime/runtime-agent-terminal-startup.ts +++ b/src/main/runtime/runtime-agent-terminal-startup.ts @@ -2,7 +2,6 @@ import type { SessionOptionValue } from '../../shared/native-chat-session-option import type { RuntimeStore } from './runtime-store-contract' import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types' import type { TerminalCreateOptions } from './runtime-terminal-contracts' -import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' import { resolveBareAgentLaunchCommand } from './runtime-agent-launch-resolution' import { planExecutionHostStartupWithPromptCandidate } from '../opencode/opencode-model-startup-plan' import { agentPromptRidesLaunchCommand } from '../../shared/tui-agent-startup' @@ -25,9 +24,6 @@ export async function buildRuntimeAgentTerminalStartupOptions( // Why: `workspace.repo` is display metadata and may be a row from another host; the launch // shape must match the PTY route this scope already resolved. const isRemote = Boolean(workspace.connectionId) - if (opts.startupAgent && !isTuiAgentEnabled(opts.startupAgent, settings.disabledTuiAgents)) { - throw new Error(`Agent ${opts.startupAgent} is disabled. Choose an enabled agent.`) - } const agent = opts.startupAgent ?? resolveBareAgentLaunchCommand({ diff --git a/src/main/runtime/runtime-client-settings.ts b/src/main/runtime/runtime-client-settings.ts index 5ca04d127e38..ae11b69f07c5 100644 --- a/src/main/runtime/runtime-client-settings.ts +++ b/src/main/runtime/runtime-client-settings.ts @@ -8,7 +8,6 @@ import { MAX_QUICK_COMMANDS, type TerminalQuickCommandMutation } from '../../shared/terminal-quick-commands' -import { haveSameDisabledTuiAgents } from '../../shared/tui-agent-selection' import { normalizeSourceControlAiSettings } from '../../shared/source-control-ai' import { SOURCE_CONTROL_LAUNCH_ACTION_IDS, @@ -22,7 +21,7 @@ import type { ExecutionHostId } from '../../shared/execution-host' import type { TerminalQuickCommand } from '../../shared/terminal-quick-command-types' import { recordManagedHookInstallFailure } from '../agent-hooks/install-telemetry' import { applyAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' -import { isAgentStatusHooksEnabledForAgent } from '../../shared/agent-status-hooks-setting' +import { isAgentStatusHooksEnabled } from '../../shared/agent-status-hooks-setting' import type { RuntimeStore } from './runtime-store-contract' export type RuntimeClientSettings = Pick< @@ -155,18 +154,14 @@ export class RuntimeClientSettingsController { if (!this.store?.getSettings || !this.store.updateSettings) { throw new Error('runtime_unavailable') } - const beforeSettings = this.store.getSettings() - const before = beforeSettings.agentStatusHooksEnabled !== false + const before = this.store.getSettings().agentStatusHooksEnabled !== false this.store.updateSettings(updates, { notifyListeners: true }) - const settings = this.store.getSettings() if (updates.worktreeVisibilityDefaults !== undefined) { this.notifyReposChanged?.() } if ( - (typeof updates.agentStatusHooksEnabled === 'boolean' && - before !== updates.agentStatusHooksEnabled) || - (updates.disabledTuiAgents !== undefined && - !haveSameDisabledTuiAgents(beforeSettings.disabledTuiAgents, settings.disabledTuiAgents)) + typeof updates.agentStatusHooksEnabled === 'boolean' && + before !== updates.agentStatusHooksEnabled ) { await this.reconcileManagedAgentHooks() } @@ -235,9 +230,9 @@ export class RuntimeClientSettingsController { await applyAgentStatusHooksEnabled(settings.agentStatusHooksEnabled !== false, settings, { shouldHydrateShellPath: getAppEnvironment().isPackaged(), onInstallError: recordManagedHookInstallFailure, - shouldContinue: (agent) => { + shouldContinue: () => { const current = this.store?.getSettings() - return current !== undefined && isAgentStatusHooksEnabledForAgent(current, agent) + return current !== undefined && isAgentStatusHooksEnabled(current) } }) }) diff --git a/src/main/runtime/runtime-worktree-agent-startup.test.ts b/src/main/runtime/runtime-worktree-agent-startup.test.ts index 1751b82a2a2e..e77f27bcf5bb 100644 --- a/src/main/runtime/runtime-worktree-agent-startup.test.ts +++ b/src/main/runtime/runtime-worktree-agent-startup.test.ts @@ -302,8 +302,10 @@ describe('resolveWorktreeStartupDraftAgent', () => { ...(requestedAgent ? { requestedAgent } : {}) }) - it('returns an enabled requested agent without detecting', async () => { - await expect(resolve({ defaultTuiAgent: 'claude' }, 'codex')).resolves.toBe('codex') + it('returns the requested agent without detecting, even one hidden in Settings', async () => { + await expect( + resolve({ defaultTuiAgent: 'claude', disabledTuiAgents: ['codex'] }, 'codex') + ).resolves.toBe('codex') expect(mocks.detectInstalledAgentsWithShellPathHydration).not.toHaveBeenCalled() }) @@ -312,17 +314,6 @@ describe('resolveWorktreeStartupDraftAgent', () => { expect(mocks.detectInstalledAgentsWithShellPathHydration).not.toHaveBeenCalled() }) - // The requested agent replaces the default rather than preceding it, so a disabled request goes - // straight to detection — the order the create has always used. - it('detects instead of using the default when the requested agent is disabled', async () => { - mocks.detectInstalledAgentsWithShellPathHydration.mockResolvedValue(['codex', 'gemini']) - - await expect( - resolve({ defaultTuiAgent: 'claude', disabledTuiAgents: ['codex'] }, 'codex') - ).resolves.toBe('gemini') - expect(mocks.detectInstalledAgentsWithShellPathHydration).toHaveBeenCalledTimes(1) - }) - it('starts no agent when the default is blank', async () => { await expect(resolve({ defaultTuiAgent: 'blank' })).resolves.toBeNull() expect(mocks.detectInstalledAgentsWithShellPathHydration).not.toHaveBeenCalled() diff --git a/src/main/runtime/runtime-worktree-agent-startup.ts b/src/main/runtime/runtime-worktree-agent-startup.ts index 72b083b82a7e..6bafc55543bc 100644 --- a/src/main/runtime/runtime-worktree-agent-startup.ts +++ b/src/main/runtime/runtime-worktree-agent-startup.ts @@ -39,12 +39,16 @@ type StartupEnvironment = { } /** The agent a linked draft starts: the requested one, else the default, else one detected on the - * host that runs it. `null` means the draft starts no agent. */ + * host that runs it. `null` means the draft starts no agent. Only the automatic picks skip agents + * hidden in Settings. */ export async function resolveWorktreeStartupDraftAgent( environment: Pick & { requestedAgent?: TuiAgent } ): Promise { const { repo, settings } = environment - const preferredAgent = environment.requestedAgent ?? settings.defaultTuiAgent + if (environment.requestedAgent) { + return environment.requestedAgent + } + const preferredAgent = settings.defaultTuiAgent // Why: `blank` is an explicit shell-only preference, so linked drafts must not auto-pick an agent. if (preferredAgent === 'blank') { return null @@ -148,9 +152,6 @@ export function buildWorktreeStartupForAgent( followup?: WorktreeStartupFollowup } { const { agent, repo, settings } = environment - if (!isTuiAgentEnabled(agent, settings.disabledTuiAgents)) { - throw new Error('Selected agent is disabled. Choose an enabled agent before creating.') - } const planInputs = resolveAgentStartupPlanInputs({ agent, settings, diff --git a/src/main/startup/codex-hook-startup-wiring.test.ts b/src/main/startup/codex-hook-startup-wiring.test.ts index f142448b98ef..9335168d9db3 100644 --- a/src/main/startup/codex-hook-startup-wiring.test.ts +++ b/src/main/startup/codex-hook-startup-wiring.test.ts @@ -61,7 +61,7 @@ import { initializeReadyRuntimeServices } from './main-process-ready-runtime' beforeEach(() => { fixture.startCodexHooks.mockClear() - fixture.settings.disabledTuiAgents = [] + fixture.settings.agentStatusHooksEnabled = true }) describe('Codex hook startup', () => { @@ -86,12 +86,12 @@ describe('Codex hook startup', () => { expect(ready).toBe(true) }) - it("reads Codex's per-agent hook setting each time it is asked", async () => { + it('reads the hook setting each time it is asked', async () => { await initializeReadyRuntimeServices() const { isEnabled } = fixture.startCodexHooks.mock.calls[0]![0] expect(isEnabled()).toBe(true) - fixture.settings.disabledTuiAgents = ['codex'] + fixture.settings.agentStatusHooksEnabled = false expect(isEnabled()).toBe(false) }) }) diff --git a/src/main/startup/codex-launch-per-agent-hook-opt-out.test.ts b/src/main/startup/codex-launch-hook-switch.test.ts similarity index 89% rename from src/main/startup/codex-launch-per-agent-hook-opt-out.test.ts rename to src/main/startup/codex-launch-hook-switch.test.ts index 2942a7ff7135..af94fba403c3 100644 --- a/src/main/startup/codex-launch-per-agent-hook-opt-out.test.ts +++ b/src/main/startup/codex-launch-hook-switch.test.ts @@ -3,9 +3,9 @@ import type { GlobalSettings } from '../../shared/global-settings-types' import type { VerifiedCodexResumeSource } from '../codex/codex-session-resume-preparation' /** - * Turning Codex off in the per-agent hook settings removes Orca's Codex hook - * entry. Launch prep and session resume must read that same opt-out, or the - * next Codex launch writes the entry straight back. + * Turning agent status hooks off removes Orca's Codex hook entry. Launch prep + * and session resume must read that same switch, or the next Codex launch + * writes the entry straight back. */ const SYSTEM_HOME = '/home/user/.codex' const ACCOUNT_HOME = '/accounts/one/.codex' @@ -73,23 +73,13 @@ const HOOK_SETTINGS: readonly { codexHooksOn: boolean }[] = [ { - name: 'Codex turned off per agent', - settings: { agentStatusHooksEnabled: true, disabledTuiAgents: ['codex'] }, - codexHooksOn: false - }, - { - name: 'every agent on', - settings: { agentStatusHooksEnabled: true, disabledTuiAgents: [] }, - codexHooksOn: true - }, - { - name: 'another agent turned off', - settings: { agentStatusHooksEnabled: true, disabledTuiAgents: ['claude'] }, + name: 'the global switch on', + settings: { agentStatusHooksEnabled: true }, codexHooksOn: true }, { name: 'the global switch off', - settings: { agentStatusHooksEnabled: false, disabledTuiAgents: [] }, + settings: { agentStatusHooksEnabled: false }, codexHooksOn: false } ] @@ -117,7 +107,7 @@ function resumeFrom(homePath: string): Promise { }) } -describe('Codex launch prep honours the per-agent hook opt-out', () => { +describe('Codex launch prep honours the hook off switch', () => { beforeEach(() => { vi.clearAllMocks() mocks.isHostSystemDefaultRealHomeSelected.mockReturnValue(false) @@ -197,7 +187,7 @@ describe('Codex launch prep honours the per-agent hook opt-out', () => { ) it("never makes a plain terminal or a structured launch wait for Codex's hook hashes", async () => { - mocks.settings = { agentStatusHooksEnabled: true, disabledTuiAgents: [] } + mocks.settings = { agentStatusHooksEnabled: true } mocks.prepareForCodexLaunchAsync.mockResolvedValue(ACCOUNT_HOME) await prepareCodexRuntimeHomeForLaunch() diff --git a/src/main/startup/codex-launch-preparation.ts b/src/main/startup/codex-launch-preparation.ts index 716b4830ef88..1b289f84b053 100644 --- a/src/main/startup/codex-launch-preparation.ts +++ b/src/main/startup/codex-launch-preparation.ts @@ -2,7 +2,7 @@ import type { CodexHomeLaunchContext } from '../ipc/pty' import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection' import { codexHookService } from '../codex/hook-service' import { getDefaultWslDistro } from '../wsl' -import { isAgentStatusHooksEnabledForAgent } from '../agent-hooks/managed-agent-hook-controls' +import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' import { reconcileCodexHooksForLaunch } from '../codex/codex-hook-reconcile' import { mainProcessState as state } from './main-process-state' @@ -34,8 +34,7 @@ export async function prepareCodexRuntimeHomeForLaunch( target?.runtime === 'wsl' ? { runtime: 'wsl' as const, wslDistro: target.wslDistro?.trim() || getDefaultWslDistro() } : target - const isHooksEnabled = (): boolean => - isAgentStatusHooksEnabledForAgent(state.store?.getSettings(), 'codex') + const isHooksEnabled = (): boolean => isAgentStatusHooksEnabled(state.store?.getSettings()) const hooksEnabled = isHooksEnabled() try { // Why: honor the persisted off switch so post-startup launches can't reinstall removed hooks. diff --git a/src/main/startup/codex-session-resume-daemon-guard.test.ts b/src/main/startup/codex-session-resume-daemon-guard.test.ts index 934408db8c76..c504c62bb501 100644 --- a/src/main/startup/codex-session-resume-daemon-guard.test.ts +++ b/src/main/startup/codex-session-resume-daemon-guard.test.ts @@ -30,7 +30,7 @@ vi.mock('../codex/codex-hook-reconcile', () => ({ reconcileCodexHooksForLaunch: mocks.reconcileCodexHooksForLaunch })) vi.mock('../agent-hooks/managed-agent-hook-controls', () => ({ - isAgentStatusHooksEnabledForAgent: () => mocks.hooksEnabled + isAgentStatusHooksEnabled: () => mocks.hooksEnabled })) vi.mock('../codex/codex-home-paths', async (importOriginal) => ({ ...(await importOriginal()), diff --git a/src/main/startup/codex-session-resume-launch.ts b/src/main/startup/codex-session-resume-launch.ts index cd7a6619851b..ffcf91fe478e 100644 --- a/src/main/startup/codex-session-resume-launch.ts +++ b/src/main/startup/codex-session-resume-launch.ts @@ -13,7 +13,7 @@ import { ManagedCodexHomeTemporarilyUnavailableError } from '../codex-accounts/h import { codexHookService } from '../codex/hook-service' import { reconcileCodexHooksForLaunch } from '../codex/codex-hook-reconcile' import { ensureCodexDaemonSocketGuard } from '../codex/codex-config-mirror' -import { isAgentStatusHooksEnabledForAgent } from '../agent-hooks/managed-agent-hook-controls' +import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' import { getOrcaManagedCodexHomePath, getSystemCodexHomePath } from '../codex/codex-home-paths' import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' import { mainProcessState as state } from './main-process-state' @@ -111,8 +111,7 @@ export async function prepareCodexPinnedLaunchHome( ): Promise { const isSystemHome = normalizeRuntimePathForComparison(home) === normalizeRuntimePathForComparison(systemHomePath) - const isHooksEnabled = (): boolean => - isAgentStatusHooksEnabledForAgent(state.store?.getSettings(), 'codex') + const isHooksEnabled = (): boolean => isAgentStatusHooksEnabled(state.store?.getSettings()) try { if (isSystemHome) { // Why bounded: the resume waits only briefly for a reconcile; it runs on ~/.codex whatever the selection. diff --git a/src/main/startup/main-process-pty-startup.ts b/src/main/startup/main-process-pty-startup.ts index 20136d4e9d53..4a11bfb23c8b 100644 --- a/src/main/startup/main-process-pty-startup.ts +++ b/src/main/startup/main-process-pty-startup.ts @@ -14,10 +14,7 @@ import { } from '../codex/codex-pane-account-registry' import { reconcileRetainedCodexHookHomes } from '../codex/retained-codex-hook-state' import { codexHookService } from '../codex/hook-service' -import { - isAgentStatusHooksEnabled, - isAgentStatusHooksEnabledForAgent -} from '../agent-hooks/managed-agent-hook-controls' +import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' import { agentHookServer } from '../agent-hooks/server' import { createLocalTmuxManagedPtyResolver } from '../agent-hooks/local-tmux-managed-pty' import { @@ -155,7 +152,7 @@ export function startTerminalRuntimeStartupServices(): WindowsDesktopStartupServ if (hasRetainedManagedHostPane) { void reconcileRetainedCodexHookHomes({ hookService: codexHookService, - hooksEnabled: isAgentStatusHooksEnabledForAgent(settings, 'codex'), + hooksEnabled: isAgentStatusHooksEnabled(settings), runtimeHomePaths: state.codexRuntimeHome.getRetainedHostCodexHookHomePaths(livePtyIds) }).catch((error) => console.warn('[codex-hook-service] retained Codex home reconcile failed:', error) diff --git a/src/main/startup/main-process-ready-runtime.ts b/src/main/startup/main-process-ready-runtime.ts index e0e93f920e1d..0f921d455446 100644 --- a/src/main/startup/main-process-ready-runtime.ts +++ b/src/main/startup/main-process-ready-runtime.ts @@ -15,7 +15,7 @@ import { handleGpuChildCrash } from './gpu-lifecycle' import { isGpuFallbackCrashCandidate } from '../crash-reporting/gpu-crash-fallback-decision' import { startCodexHooks } from '../codex/codex-hook-reconcile' import { hydrateAgentCliShellPath } from '../agent-hooks/local-agent-cli-presence' -import { isAgentStatusHooksEnabledForAgent } from '../agent-hooks/managed-agent-hook-controls' +import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' import { reconcileStartupManagedAgentHooks } from '../agent-hooks/managed-agent-hook-startup' import { shouldInstallManagedHooks } from './configure-process' import { mainProcessState as state } from './main-process-state' @@ -104,7 +104,7 @@ export async function initializeReadyRuntimeServices(): Promise { // and launches find the reconcile in flight before CLI detection ends. startCodexHooks({ pathReady: app.isPackaged ? hydrateAgentCliShellPath() : Promise.resolve(), - isEnabled: () => isAgentStatusHooksEnabledForAgent(store.getSettings(), 'codex'), + isEnabled: () => isAgentStatusHooksEnabled(store.getSettings()), resolveLaunchHome: () => { if (!state.codexRuntimeHome) { throw new Error('Codex runtime home service is not initialized') diff --git a/src/renderer/src/components/automations/automation-save-action.ts b/src/renderer/src/components/automations/automation-save-action.ts index 6c5c961ab65a..51c87fbbf1d7 100644 --- a/src/renderer/src/components/automations/automation-save-action.ts +++ b/src/renderer/src/components/automations/automation-save-action.ts @@ -1,5 +1,4 @@ import { toast } from 'sonner' -import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' import { isValidAutomationCronSchedule, isValidAutomationSchedule @@ -18,8 +17,7 @@ import type { AutomationSaveContext } from './automation-save-context' /** Validates editor input then delegates the provider-specific save transaction. */ export function createAutomationSaveAction(context: AutomationSaveContext) { return async function saveAutomation(now = Date.now()): Promise { - const { store, local, destinationForm, destination } = context - const { settings } = store + const { local, destinationForm, destination } = context const { draft, editingAutomationId, @@ -72,19 +70,6 @@ export function createAutomationSaveAction(context: AutomationSaveContext) { ) return } - if ( - editingAutomationId === null && - !isHermesSave && - !isTuiAgentEnabled(draft.agentId, settings?.disabledTuiAgents) - ) { - toast.error( - translate( - 'auto.components.automations.AutomationsPage.2360ffc956', - 'Choose an enabled agent before saving.' - ) - ) - return - } if (!isHermesSave) { const extrasError = draftExtraAgentArgsNeedFreshSession(draft) ? translate( diff --git a/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts b/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts index f72a249b9c7b..9ddb6e5d4fcd 100644 --- a/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts +++ b/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts @@ -10,7 +10,8 @@ const mocks = vi.hoisted(() => ({ vi.mock('@/store', () => ({ useAppStore: { getState: () => ({ - settings: null, + // Hiding an agent only removes it from pickers; an explicit launch still starts it. + settings: { disabledTuiAgents: ['codex'] }, getKnownWorktreeById: mocks.getKnownWorktreeById, setActiveWorktree: mocks.setActiveWorktree }) diff --git a/src/renderer/src/components/dashboard/launch-dashboard-agent.ts b/src/renderer/src/components/dashboard/launch-dashboard-agent.ts index 43ff278bd7cd..30ddb82590a2 100644 --- a/src/renderer/src/components/dashboard/launch-dashboard-agent.ts +++ b/src/renderer/src/components/dashboard/launch-dashboard-agent.ts @@ -3,14 +3,13 @@ import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import { useAppStore } from '@/store' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' import type { DashboardSpawnAgentArgs } from '../../../../shared/dashboard-snapshot' -import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' /** Starts the requested agent through the same host-aware tab path as Quick Launch. */ export function launchDashboardAgent({ worktreeId, agent }: DashboardSpawnAgentArgs): boolean { const state = useAppStore.getState() const executionHostId = getExecutionHostIdForWorktree(state, worktreeId) const worktree = state.getKnownWorktreeById(worktreeId, executionHostId) - if (!worktree || !isTuiAgentEnabled(agent, state.settings?.disabledTuiAgents)) { + if (!worktree) { return false } state.setActiveWorktree(worktreeId, executionHostId) diff --git a/src/renderer/src/components/right-sidebar/buildSourceControlAgentDeliveryPlan.ts b/src/renderer/src/components/right-sidebar/buildSourceControlAgentDeliveryPlan.ts index cca65a8bb5c0..403595b6c974 100644 --- a/src/renderer/src/components/right-sidebar/buildSourceControlAgentDeliveryPlan.ts +++ b/src/renderer/src/components/right-sidebar/buildSourceControlAgentDeliveryPlan.ts @@ -34,7 +34,6 @@ export function buildSourceControlAgentDeliveryPlan({ commandInput, agentArgs, detectedAgents, - disabledAgents: settings?.disabledTuiAgents, cmdOverrides: settings?.agentCmdOverrides, terminalWindowsShell: settings?.terminalWindowsShell, platform: launchPlatform, diff --git a/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts b/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts index a16e37230739..9bca4d847850 100644 --- a/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts +++ b/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts @@ -13,7 +13,6 @@ import { pickSourceControlLaunchAgent, readSourceControlLaunchRecipeAgentId } from '../../../../../../shared/source-control-launch-agent-selection' -import { isTuiAgentEnabled } from '../../../../../../shared/tui-agent-selection' import type { SourceControlActionRecipe, SourceControlLaunchActionId @@ -136,11 +135,7 @@ export async function launchSourceControlRecoveryAgentWithDefault({ const detectedAgents = await ensureDetectedAgentsForWorktree(store, activeWorktreeId) const savedAgent = readSourceControlLaunchRecipeAgentId(savedRecipe) - if ( - savedAgent && - (!detectedAgents.includes(savedAgent) || - !isTuiAgentEnabled(savedAgent, store.settings?.disabledTuiAgents)) - ) { + if (savedAgent && !detectedAgents.includes(savedAgent)) { toast.error(copy.savedAgentUnavailable) return false } diff --git a/src/renderer/src/hooks/composer-state/composer-name-ownership.test.ts b/src/renderer/src/hooks/composer-state/composer-name-ownership.test.ts index 8af6786ee362..b24ccf9fdd7e 100644 --- a/src/renderer/src/hooks/composer-state/composer-name-ownership.test.ts +++ b/src/renderer/src/hooks/composer-state/composer-name-ownership.test.ts @@ -78,7 +78,6 @@ function useNameOwnership(draft?: { name: string; linkedWorkItem: LinkedWorkItem ...common, baseBranch: 'main', compareBaseRef: undefined, - disabledTuiAgents: [], effectiveLinkedPR: null, decisions, fallbackCreatureName: 'otter', diff --git a/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts b/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts index 2bb1003e7448..42cc55ab3415 100644 --- a/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts +++ b/src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts @@ -18,7 +18,6 @@ export function useComposerSubmitOrchestration( clearNewWorkspaceDraft: target.composerTargetStore.clearNewWorkspaceDraft, createFolderWorkspace: target.composerTargetStore.createFolderWorkspace, decisions: target.composerTargetStore.decisions, - disabledTuiAgents: target.workspaceIdentityState.disabledTuiAgents, folderCreateDisabled: source.composerNavigationActions.folderCreateDisabled, folderSourceRepos: target.runtimeTargetSelection.folderSourceRepos, folderTargetIsRemote: target.runtimeTargetSelection.folderTargetIsRemote, @@ -54,7 +53,6 @@ export function useComposerSubmitOrchestration( baseBranch: target.workspaceIdentityState.baseBranch, branchNameOverride: target.workspaceIdentityState.branchNameOverride, compareBaseRef: target.workspaceIdentityState.compareBaseRef, - disabledTuiAgents: target.workspaceIdentityState.disabledTuiAgents, effectiveLinkedPR: target.derivedComposerState.effectiveLinkedPR, decisions: target.composerTargetStore.decisions, fallbackCreatureName: target.derivedComposerState.fallbackCreatureName, diff --git a/src/renderer/src/hooks/composer-state/folder-submit-orchestration.ts b/src/renderer/src/hooks/composer-state/folder-submit-orchestration.ts index 036177a4a73c..5c75d2b4ab5b 100644 --- a/src/renderer/src/hooks/composer-state/folder-submit-orchestration.ts +++ b/src/renderer/src/hooks/composer-state/folder-submit-orchestration.ts @@ -5,7 +5,6 @@ type FolderSubmitOrchestrationInput = Pick< | 'clearNewWorkspaceDraft' | 'createFolderWorkspace' | 'decisions' - | 'disabledTuiAgents' | 'folderCreateDisabled' | 'folderSourceRepos' | 'folderTargetIsRemote' @@ -29,7 +28,6 @@ type FolderSubmitOrchestrationInput = Pick< import { useCallback } from 'react' import type { TuiAgent } from '../../../../shared/tui-agent' import { settleComposerSubmit } from '@/lib/composer-submit-cancellation' -import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' import { submitFolderWorkspaceCreate } from '@/components/sidebar/folder-workspace-composer-submit' import { resolveTuiAgentLaunchArgs, @@ -47,7 +45,6 @@ export function useFolderSubmitOrchestration(input: FolderSubmitOrchestrationInp clearNewWorkspaceDraft, createFolderWorkspace, decisions, - disabledTuiAgents, folderCreateDisabled, folderSourceRepos, folderTargetIsRemote, @@ -93,10 +90,7 @@ export function useFolderSubmitOrchestration(input: FolderSubmitOrchestrationInp const smartGitHubMetadata = smartGitHubResolution.kind === 'none' ? null : smartGitHubResolution const submitLinkedWorkItem = smartGitHubMetadata?.linkedWorkItem ?? linkedWorkItem - const agent = - requestedAgent && isTuiAgentEnabled(requestedAgent, disabledTuiAgents) - ? requestedAgent - : null + const agent = requestedAgent if (isSubmissionCancelled()) { return } @@ -158,7 +152,6 @@ export function useFolderSubmitOrchestration(input: FolderSubmitOrchestrationInp clearNewWorkspaceDraft, createFolderWorkspace, canResolveFolderSmartGitHubSubmit, - disabledTuiAgents, folderCreateDisabled, folderTargetIsRemote, folderTargetRuntimeEnvironmentId, diff --git a/src/renderer/src/hooks/composer-state/quick-submit-source-preparation.ts b/src/renderer/src/hooks/composer-state/quick-submit-source-preparation.ts index 407bfb9e658c..64bc11b3f8d2 100644 --- a/src/renderer/src/hooks/composer-state/quick-submit-source-preparation.ts +++ b/src/renderer/src/hooks/composer-state/quick-submit-source-preparation.ts @@ -5,7 +5,6 @@ type QuickSubmitSourcePreparationInput = Pick< | 'baseBranch' | 'branchNameOverride' | 'compareBaseRef' - | 'disabledTuiAgents' | 'effectiveLinkedPR' | 'decisions' | 'fallbackCreatureName' @@ -18,7 +17,6 @@ type QuickSubmitSourcePreparationInput = Pick< > import { useCallback } from 'react' -import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' import { getLinkedWorkItemWorkspaceName } from '@/lib/new-workspace' import type { TuiAgent } from '../../../../shared/tui-agent' import type { PendingSmartGitHubSubmitResolution } from './source-selection-decisions' @@ -28,7 +26,6 @@ export function useQuickSubmitSourcePreparation(input: QuickSubmitSourcePreparat baseBranch, branchNameOverride, compareBaseRef, - disabledTuiAgents, effectiveLinkedPR, decisions, fallbackCreatureName, @@ -52,10 +49,7 @@ export function useQuickSubmitSourcePreparation(input: QuickSubmitSourcePreparat ? linkedWorkItem : smartGitHubResolution.linkedWorkItem - const agent = - requestedAgent && isTuiAgentEnabled(requestedAgent, disabledTuiAgents) - ? requestedAgent - : null + const agent = requestedAgent const submitLinkedIssueNumber = smartGitHubResolution.kind === 'none' @@ -152,7 +146,6 @@ export function useQuickSubmitSourcePreparation(input: QuickSubmitSourcePreparat [ baseBranch, branchNameOverride, - disabledTuiAgents, effectiveLinkedPR, fallbackCreatureName, linkedGitLabMR, diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 2f8812801cee..2ba3f0df3b54 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -932,7 +932,6 @@ "3f0ea9aa0d": "Could not build the agent launch command.", "46f1a2c9bd": "Command input is empty.", "8eb541cc83": "The selected agent was not detected on this workspace host.", - "b96e091fc9": "The selected agent is disabled in Settings.", "a7ac8717c7": "Choose an agent before starting." } } @@ -16779,7 +16778,6 @@ "08efc3ae12": "Hermes automation updated.", "e431bb85d4": "Choose a workspace on the same host as this Hermes automation.", "32534e7c9c": "Choose an available workspace before saving.", - "2360ffc956": "Choose an enabled agent before saving.", "6e91dab317": "Enter a valid advanced schedule before saving.", "64bdb2304f": "Pick a supported schedule before saving.", "2430fecf53": "Choose a run location and enter a prompt before saving.", @@ -19013,7 +19011,6 @@ "startSession": "Start New Session", "starting": "Starting…", "noContext": "No session context is available to continue in a new session.", - "agentDisabled": "{{agent}} is disabled in Agent settings.", "agentUnavailable": "{{agent}} was not detected on this workspace host.", "sent": "Session context sent to {{agent}} in a new session.", "deliveryFailed": "The new {{agent}} session started, but its context could not be sent.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 2c7e4f05eda9..6c60a3cf8f1c 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -749,7 +749,6 @@ "3f0ea9aa0d": "No se pudo generar el comando de inicio del agente.", "46f1a2c9bd": "La entrada del comando está vacía.", "8eb541cc83": "No se detectó el agente seleccionado en este host de espacio de trabajo.", - "b96e091fc9": "El agente seleccionado está deshabilitado en Ajustes.", "a7ac8717c7": "Elige un agente antes de comenzar." } } @@ -16506,7 +16505,6 @@ "08efc3ae12": "Automatización Hermes actualizada.", "e431bb85d4": "Elija un espacio de trabajo en el mismo host que esta automatización de Hermes.", "32534e7c9c": "Elija un espacio de trabajo disponible antes de guardar.", - "2360ffc956": "Elija un agente habilitado antes de guardar.", "6e91dab317": "Ingrese un horario avanzado válido antes de guardar.", "64bdb2304f": "Elija un horario admitido antes de guardar.", "2430fecf53": "Elija una ubicación de ejecución e ingrese un mensaje antes de guardar.", @@ -18698,7 +18696,6 @@ "startSession": "Iniciar sesión nueva", "starting": "Iniciando…", "noContext": "No hay contexto de sesión disponible para continuar en una sesión nueva.", - "agentDisabled": "{{agent}} está deshabilitado en la configuración de agentes.", "agentUnavailable": "No se detectó {{agent}} en el host de este espacio de trabajo.", "sent": "El contexto de la sesión se envió a {{agent}} en una sesión nueva.", "deliveryFailed": "La nueva sesión de {{agent}} se inició, pero no se pudo enviar su contexto.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 04f622973aba..442a9f6e2d37 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -850,7 +850,6 @@ "3f0ea9aa0d": "Impossible de construire la commande de lancement de l'agent.", "46f1a2c9bd": "La saisie de commande est vide.", "8eb541cc83": "L'agent sélectionné n'a pas été détecté sur l'hôte de cet espace de travail.", - "b96e091fc9": "L'agent sélectionné est désactivé dans les Paramètres.", "a7ac8717c7": "Choisissez un agent avant de démarrer." } } @@ -16583,7 +16582,6 @@ "08efc3ae12": "Automatisation Hermes mise à jour.", "e431bb85d4": "Choisissez un espace de travail sur le même hôte que cette automatisation Hermes.", "32534e7c9c": "Choisissez un espace de travail disponible avant d'enregistrer.", - "2360ffc956": "Choisissez un agent activé avant d'enregistrer.", "6e91dab317": "Saisissez une planification avancée valide avant d'enregistrer.", "64bdb2304f": "Choisissez une planification prise en charge avant d'enregistrer.", "2430fecf53": "Choisissez un lieu d'exécution et saisissez un prompt avant d'enregistrer.", @@ -18800,7 +18798,6 @@ "startSession": "Démarrer une nouvelle session", "starting": "Démarrage…", "noContext": "Aucun contexte de session disponible pour continuer dans une nouvelle session.", - "agentDisabled": "{{agent}} est désactivé dans les paramètres des Agents.", "agentUnavailable": "{{agent}} n'a pas été détecté sur cet hôte d'espace de travail.", "sent": "Contexte de session envoyé à {{agent}} dans une nouvelle session.", "deliveryFailed": "La nouvelle session {{agent}} a démarré, mais son contexte n'a pas pu être envoyé.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index a632ba19b243..469d2519575c 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -749,7 +749,6 @@ "3f0ea9aa0d": "Agent 起動コマンドを構築できませんでした。", "46f1a2c9bd": "コマンド入力が空です。", "8eb541cc83": "選択した Agent はこのワークスペースホストで検出されませんでした。", - "b96e091fc9": "選択した Agent は設定で無効になっています。", "a7ac8717c7": "開始する前に、Agent を選択してください。" } } @@ -16506,7 +16505,6 @@ "08efc3ae12": "Hermes オートメーションが更新されました。", "e431bb85d4": "この Hermes オートメーションと同じホスト上のワークスペースを選択します。", "32534e7c9c": "保存する前に、使用可能なワークスペースを選択してください。", - "2360ffc956": "保存する前に、有効な Agent を選択してください。", "6e91dab317": "保存する前に、有効な詳細スケジュールを入力してください。", "64bdb2304f": "保存する前に、サポートされているスケジュールを選択してください。", "2430fecf53": "実行場所を選択し、保存する前にプロンプ​​トを入力します。", @@ -18733,7 +18731,6 @@ "startSession": "新規セッションを開始", "starting": "開始中…", "noContext": "新規セッションで続けるためのコンテキストがありません。", - "agentDisabled": "Agent 設定で {{agent}} が無効になっています。", "agentUnavailable": "このワークスペースホストで {{agent}} が検出されませんでした。", "sent": "セッションコンテキストを新規 {{agent}} セッションに送信しました。", "deliveryFailed": "新規 {{agent}} セッションは開始しましたが、コンテキストを送信できませんでした。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 4e9aac6cd485..564c24dded11 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -749,7 +749,6 @@ "3f0ea9aa0d": "agent 시작 명령을 만들 수 없습니다.", "46f1a2c9bd": "명령 입력이 비어 있습니다.", "8eb541cc83": "선택한 agent가 이 워크스페이스 호스트에서 감지되지 않았습니다.", - "b96e091fc9": "선택한 agent는 설정에서 비활성화되어 있습니다.", "a7ac8717c7": "시작하기 전에 agent를 선택하세요." } } @@ -16506,7 +16505,6 @@ "08efc3ae12": "Hermes 자동화가 업데이트되었습니다.", "e431bb85d4": "이 Hermes 자동화와 동일한 호스트에서 워크스페이스를 선택하세요.", "32534e7c9c": "저장하기 전에 사용 가능한 워크스페이스를 선택하세요.", - "2360ffc956": "저장하기 전에 활성화된 agent를 선택하세요.", "6e91dab317": "저장하기 전에 유효한 고급 일정을 입력하세요.", "64bdb2304f": "저장하기 전에 지원되는 일정을 선택하세요.", "2430fecf53": "실행 위치를 선택하고 저장하기 전에 프롬프트를 입력하세요.", @@ -18733,7 +18731,6 @@ "startSession": "새 세션 시작", "starting": "시작 중…", "noContext": "새 세션에서 계속할 세션 컨텍스트가 없습니다.", - "agentDisabled": "에이전트 설정에서 {{agent}}이(가) 비활성화되어 있습니다.", "agentUnavailable": "이 워크스페이스 호스트에서 {{agent}}을(를) 찾지 못했습니다.", "sent": "세션 컨텍스트를 새 {{agent}} 세션으로 보냈습니다.", "deliveryFailed": "새 {{agent}} 세션은 시작되었지만 컨텍스트를 보내지 못했습니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 0592f647928e..dec2d856f069 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -749,7 +749,6 @@ "3f0ea9aa0d": "无法构建智能体启动命令。", "46f1a2c9bd": "提示词输入为空。", "8eb541cc83": "此工作区主机上未检测到所选智能体。", - "b96e091fc9": "选定的智能体在“设置”中被禁用。", "a7ac8717c7": "在开始之前选择一个智能体。" } } @@ -16520,7 +16519,6 @@ "08efc3ae12": "Hermes 自动化已更新。", "e431bb85d4": "选择与此 Hermes 自动化位于同一主机上的工作区。", "32534e7c9c": "保存前选择一个可用的工作区。", - "2360ffc956": "保存前选择已启用的智能体。", "6e91dab317": "保存前输入有效的高级计划。", "64bdb2304f": "保存前选择支持的计划。", "2430fecf53": "选择运行位置并在保存前输入提示。", @@ -18712,7 +18710,6 @@ "startSession": "启动新会话", "starting": "正在启动…", "noContext": "没有可用于在新会话中继续的上下文。", - "agentDisabled": "{{agent}} 已在智能体设置中停用。", "agentUnavailable": "未在此工作区主机上检测到 {{agent}}。", "sent": "已将会话上下文发送到新的 {{agent}} 会话。", "deliveryFailed": "新的 {{agent}} 会话已启动,但无法发送上下文。", diff --git a/src/renderer/src/lib/fix-checks-agent-launch.ts b/src/renderer/src/lib/fix-checks-agent-launch.ts index f2e78e715519..d0133a218aeb 100644 --- a/src/renderer/src/lib/fix-checks-agent-launch.ts +++ b/src/renderer/src/lib/fix-checks-agent-launch.ts @@ -21,7 +21,6 @@ import { DEFAULT_SOURCE_CONTROL_ACTION_COMMAND_TEMPLATES, renderSourceControlActionCommandTemplate } from '../../../shared/source-control-ai-actions' -import { isTuiAgentEnabled } from '../../../shared/tui-agent-selection' import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { TuiAgent } from '../../../shared/tui-agent' import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../../shared/workspace-source' @@ -53,13 +52,6 @@ async function detectAgentsForConnection( : await store.ensureDetectedAgents() } -function isAgentAvailable(agent: TuiAgent, detectedAgents: TuiAgent[]): boolean { - return ( - detectedAgents.includes(agent) && - isTuiAgentEnabled(agent, useAppStore.getState().settings?.disabledTuiAgents) - ) -} - async function resolveSavedAgentOverride( savedAgent: TuiAgent | null | undefined, connectionId: string | null | undefined @@ -68,7 +60,7 @@ async function resolveSavedAgentOverride( return { kind: 'launch-default' } } const detectedAgents = await detectAgentsForConnection(connectionId) - if (!isAgentAvailable(savedAgent, detectedAgents)) { + if (!detectedAgents.includes(savedAgent)) { toast.error( translate( 'auto.lib.fix.checks.agent.launch.4c7f783a7a', @@ -86,7 +78,7 @@ async function pickExistingWorktreeAgent( ): Promise { const detectedAgents = await ensureDetectedAgentsForWorktree(useAppStore.getState(), worktreeId) if (savedAgent) { - if (isAgentAvailable(savedAgent, detectedAgents)) { + if (detectedAgents.includes(savedAgent)) { return savedAgent } toast.error( diff --git a/src/renderer/src/lib/launch-agent-session-continuation.ts b/src/renderer/src/lib/launch-agent-session-continuation.ts index 080830781f78..d52449df77d3 100644 --- a/src/renderer/src/lib/launch-agent-session-continuation.ts +++ b/src/renderer/src/lib/launch-agent-session-continuation.ts @@ -4,7 +4,6 @@ import { ensureDetectedAgentsForWorktree } from '@/lib/agent-detection-target-in import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import { useAppStore } from '@/store' -import { isTuiAgentEnabled } from '../../../shared/tui-agent-selection' import type { LaunchSource } from '../../../shared/telemetry-events' import type { TuiAgent } from '../../../shared/tui-agent' import { translate } from '@/i18n/i18n' @@ -25,19 +24,7 @@ export async function detectAgentSessionContinuationAgents( } async function ensureAgentAvailable(agent: TuiAgent, worktreeId: string): Promise { - const state = useAppStore.getState() const label = getAgentLabel(agent) - if (!isTuiAgentEnabled(agent, state.settings?.disabledTuiAgents)) { - toast.error( - translate( - 'components.agentSessionContinuation.agentDisabled', - '{{agent}} is disabled in Agent settings.', - { agent: label } - ) - ) - return false - } - let detectedAgents: TuiAgent[] try { detectedAgents = await detectAgentSessionContinuationAgents(worktreeId) diff --git a/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts b/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts index 40e04c61e5c8..7a974ffe938a 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts @@ -1,7 +1,7 @@ import type { TuiAgent } from '../../../shared/tui-agent' import type { AppState } from '@/store/types' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' -import { isTuiAgentEnabled, pickTuiAgent } from '../../../shared/tui-agent-selection' +import { pickTuiAgent } from '../../../shared/tui-agent-selection' import { buildDirectWorkItemAgentStartupPlan } from '@/lib/launch-work-item-direct-agent' import type { AgentSessionLaunchPlan } from '@/lib/agent-session-launch-plan' import { resolveSourceControlLaunchPlatform } from '@/lib/source-control-launch-platform' @@ -60,9 +60,7 @@ export async function resolveDirectWorkItemAgent(args: { if (args.agentOverride !== undefined) { return { agent: args.agentOverride, - unavailable: - !detectedAgents.includes(args.agentOverride) || - !isTuiAgentEnabled(args.agentOverride, args.latestStore.settings?.disabledTuiAgents) + unavailable: !detectedAgents.includes(args.agentOverride) } } return { diff --git a/src/renderer/src/lib/launch-work-item-direct.test.ts b/src/renderer/src/lib/launch-work-item-direct.test.ts index 6874e4f9d312..954d6154d6a4 100644 --- a/src/renderer/src/lib/launch-work-item-direct.test.ts +++ b/src/renderer/src/lib/launch-work-item-direct.test.ts @@ -701,40 +701,6 @@ describe('launchWorkItemDirect', () => { expect(pasteDraftWhenAgentReady).not.toHaveBeenCalled() }) - it('does not launch a disabled saved agent even when another agent is available', async () => { - mocks.ensureDetectedAgents.mockResolvedValue(['codex', 'claude']) - mocks.store.settings = { - defaultTuiAgent: 'claude', - disabledTuiAgents: ['codex'], - agentCmdOverrides: {} - } - const { launchWorkItemDirect } = await import('./launch-work-item-direct') - - await expect( - launchWorkItemDirect({ - item: { - title: 'Fix failing checks', - url: 'https://github.com/acme/repo/pull/1', - type: 'issue', - number: 1, - pasteContent: 'Fix the failing checks.' - }, - repoId: 'repo-1', - openModalFallback: mocks.openModalFallback, - launchSource: 'task_page', - agentOverride: 'codex', - promptDelivery: 'submit-after-ready' - }) - ).resolves.toBe(false) - - expect(mocks.createWorktree).toHaveBeenCalled() - expect(mocks.updateWorktreeMeta).not.toHaveBeenCalled() - expect(mocks.pasteDraftWhenAgentReady).not.toHaveBeenCalled() - expect(mocks.toastError).toHaveBeenCalledWith( - 'Selected agent is not available in the created workspace.' - ) - }) - it('plans direct SSH workspace agent startup for the remote host platform', async () => { mocks.getConnectionId.mockReturnValue('ssh-1') mocks.ensureRemoteDetectedAgents.mockResolvedValue(['pi']) diff --git a/src/renderer/src/lib/source-control-agent-action-launch-check.test.ts b/src/renderer/src/lib/source-control-agent-action-launch-check.test.ts index 390af35ebe53..eda2c8f63607 100644 --- a/src/renderer/src/lib/source-control-agent-action-launch-check.test.ts +++ b/src/renderer/src/lib/source-control-agent-action-launch-check.test.ts @@ -15,16 +15,6 @@ describe('checkSourceControlAgentActionLaunch', () => { }) }) - it('rejects disabled agents', () => { - expect( - checkSourceControlAgentActionLaunch({ - ...BASE, - detectedAgents: ['codex'], - disabledAgents: ['codex'] - }) - ).toEqual({ ok: false, error: 'The selected agent is disabled in Settings.' }) - }) - it('rejects agents not detected on the current host', () => { expect( checkSourceControlAgentActionLaunch({ ...BASE, agent: 'claude', detectedAgents: ['codex'] }) diff --git a/src/renderer/src/lib/source-control-agent-action-launch-check.ts b/src/renderer/src/lib/source-control-agent-action-launch-check.ts index cd45284f2000..2c009a0f2410 100644 --- a/src/renderer/src/lib/source-control-agent-action-launch-check.ts +++ b/src/renderer/src/lib/source-control-agent-action-launch-check.ts @@ -1,6 +1,5 @@ import { buildAgentStartupPlan, planAgentCliArgsSuffix } from '@/lib/tui-agent-startup' import { CLIENT_PLATFORM } from '@/lib/new-workspace' -import { isTuiAgentEnabled } from '../../../shared/tui-agent-selection' import type { TuiAgent } from '../../../shared/tui-agent' import { translate } from '@/i18n/i18n' import { resolveLocalWindowsAgentStartupShell } from '../../../shared/windows-terminal-shell' @@ -16,7 +15,6 @@ export function checkSourceControlAgentActionLaunch(args: { agent: TuiAgent | null commandInput: string detectedAgents: TuiAgent[] - disabledAgents?: TuiAgent[] cmdOverrides?: Partial> agentArgs?: string | null platform?: NodeJS.Platform @@ -35,15 +33,6 @@ export function checkSourceControlAgentActionLaunch(args: { ) } } - if (!isTuiAgentEnabled(agent, args.disabledAgents)) { - return { - ok: false, - error: translate( - 'auto.lib.source.control.agent.action.plan.b96e091fc9', - 'The selected agent is disabled in Settings.' - ) - } - } if (!args.detectedAgents.includes(agent)) { return { ok: false, diff --git a/src/shared/agent-status-hooks-setting.ts b/src/shared/agent-status-hooks-setting.ts index 1c2c49feb0c5..d24f138235a5 100644 --- a/src/shared/agent-status-hooks-setting.ts +++ b/src/shared/agent-status-hooks-setting.ts @@ -1,11 +1,4 @@ import type { GlobalSettings } from './global-settings-types' -import type { TuiAgent } from './tui-agent' -import { isTuiAgentEnabled } from './tui-agent-selection' - -export type AgentStatusHooksSettings = - | Partial> - | null - | undefined // Why shared: the CLI compiles shared/ itself, so its per-launch Codex preflight needs no out/main entry. export function isAgentStatusHooksEnabled( @@ -13,13 +6,3 @@ export function isAgentStatusHooksEnabled( ): boolean { return settings?.agentStatusHooksEnabled !== false } - -// Why: turning an agent off removes its hooks, so any install that reads only the global switch writes them back. -export function isAgentStatusHooksEnabledForAgent( - settings: AgentStatusHooksSettings, - agent: TuiAgent -): boolean { - return ( - isAgentStatusHooksEnabled(settings) && isTuiAgentEnabled(agent, settings?.disabledTuiAgents) - ) -} diff --git a/src/shared/orchestration-dispatch-refusal-contract.ts b/src/shared/orchestration-dispatch-refusal-contract.ts index b764067402b0..d0cc600e882f 100644 --- a/src/shared/orchestration-dispatch-refusal-contract.ts +++ b/src/shared/orchestration-dispatch-refusal-contract.ts @@ -67,7 +67,7 @@ function taskNotStartableNextSteps(detail: TaskNotStartableDetail): string[] { } // Why: the old five-name example read as an allowlist (#15125); derive from the field detection keys on so it cannot drift. -// Not filtered by `disabledTuiAgents` — that gates Orca's launchers, not detection, so a hand-started disabled agent still injects. +// Not filtered by `disabledTuiAgents` — that only hides agents from pickers, so a hand-started hidden agent still injects. const RECOGNIZED_AGENT_PROCESS_NAMES = [ ...new Set(Object.values(TUI_AGENT_CONFIG).map((config) => config.expectedProcess)) ].sort() diff --git a/src/shared/source-control-launch-agent-selection.ts b/src/shared/source-control-launch-agent-selection.ts index 4a408b2ffc64..1b2cc630f57e 100644 --- a/src/shared/source-control-launch-agent-selection.ts +++ b/src/shared/source-control-launch-agent-selection.ts @@ -22,10 +22,11 @@ export function pickSourceControlLaunchAgent(args: { detectedAgents: TuiAgent[] disabledAgents?: TuiAgent[] }): TuiAgent | null { - const enabledAgents = filterEnabledTuiAgents(args.detectedAgents, args.disabledAgents) - if (args.savedAgent && enabledAgents.includes(args.savedAgent)) { + // A saved agent is the user's explicit choice; hiding only skips agents in the automatic pick. + if (args.savedAgent && args.detectedAgents.includes(args.savedAgent)) { return args.savedAgent } + const enabledAgents = filterEnabledTuiAgents(args.detectedAgents, args.disabledAgents) if ( args.defaultAgent && args.defaultAgent !== 'blank' && diff --git a/src/shared/tui-agent-selection.test.ts b/src/shared/tui-agent-selection.test.ts index c92f5e62d376..50f106b845e2 100644 --- a/src/shared/tui-agent-selection.test.ts +++ b/src/shared/tui-agent-selection.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - haveSameDisabledTuiAgents, - normalizeDisabledTuiAgents, - pickTuiAgent -} from './tui-agent-selection' +import { normalizeDisabledTuiAgents, pickTuiAgent } from './tui-agent-selection' describe('pickTuiAgent', () => { it('uses an installed preferred agent', () => { @@ -34,11 +30,3 @@ describe('normalizeDisabledTuiAgents', () => { ]) }) }) - -describe('haveSameDisabledTuiAgents', () => { - it('compares the normalized disabled-agent sets', () => { - expect(haveSameDisabledTuiAgents(['codex', 'claude'], ['claude', 'codex'])).toBe(true) - expect(haveSameDisabledTuiAgents(['codex', 'unknown'], ['codex'])).toBe(true) - expect(haveSameDisabledTuiAgents(['codex'], ['claude'])).toBe(false) - }) -}) diff --git a/src/shared/tui-agent-selection.ts b/src/shared/tui-agent-selection.ts index 810a3886db58..b7e0d18356ba 100644 --- a/src/shared/tui-agent-selection.ts +++ b/src/shared/tui-agent-selection.ts @@ -89,12 +89,6 @@ export function normalizeDisabledTuiAgents(value: unknown): TuiAgent[] { return [...seen] } -export function haveSameDisabledTuiAgents(left: unknown, right: unknown): boolean { - const leftSet = new Set(normalizeDisabledTuiAgents(left)) - const rightSet = new Set(normalizeDisabledTuiAgents(right)) - return leftSet.size === rightSet.size && [...leftSet].every((agent) => rightSet.has(agent)) -} - export function isTuiAgentEnabled(agent: TuiAgent, disabled?: Iterable | null): boolean { return !normalizeDisabledTuiAgents(disabled).includes(agent) } From be58e77c502822796cffac2b956c6c40470b4172 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:46:15 -0700 Subject: [PATCH 2/6] fix(agent-launch): saved hidden agents start from the Source Control dialog - The Source Control action dialog and its auto-start check only whether the saved or selected agent is installed; the picker stays filtered. - Checks panel AI actions stay available when a saved agent is installed even if every detected agent is hidden. - Typed agent commands keep main's match order; only the hidden-agent skip is gone, so users who hide nothing see no change. - Leftovers: the offline CLI hook path, the OpenCode hook selector and the hook installer no longer carry per-agent hidden state. --- src/cli/handlers/agent-hooks.ts | 8 ++-- .../managed-agent-hook-controls.ts | 12 ++---- src/main/ipc/pty/host-env/opencode-config.ts | 2 +- .../profile-state-offline-settings.test.ts | 5 +-- .../profile-state-offline-settings.ts | 16 ++------ .../mobile-session-tabs-part-08.spec.ts | 7 +++- .../runtime-agent-launch-resolution.test.ts | 15 -------- .../runtime-agent-launch-resolution.ts | 38 ++++++++----------- .../dashboard/launch-dashboard-agent.test.ts | 3 +- .../SourceControlAgentActionDialog.test.tsx | 6 ++- .../use-checks-panel-comment-resolution.tsx | 13 +++++++ ...rce-control-agent-action-dialog-support.ts | 13 +++---- ...eSavedSourceControlAgentActionAutoStart.ts | 7 +--- .../useSourceControlAgentActionDialog.ts | 6 +-- src/shared/opencode-launch-command.test.ts | 13 ++++--- src/shared/opencode-launch-command.ts | 12 +++--- ...rce-control-launch-agent-selection.test.ts | 14 +++++++ 17 files changed, 90 insertions(+), 100 deletions(-) delete mode 100644 src/main/runtime/runtime-agent-launch-resolution.test.ts diff --git a/src/cli/handlers/agent-hooks.ts b/src/cli/handlers/agent-hooks.ts index 135c651e6a67..bea7f1ecaf1a 100644 --- a/src/cli/handlers/agent-hooks.ts +++ b/src/cli/handlers/agent-hooks.ts @@ -44,7 +44,7 @@ function legacyProfileStateLocation(): ProfileStateOfflineLocation { } async function readHookSettingsFromDisk(): Promise< - Pick + Pick > { const { acquireProfileStateRuntimeAdmission } = await import('../../main/persistence/profile-state/profile-state-access.js') @@ -57,7 +57,7 @@ async function readHookSettingsFromDisk(): Promise< } async function readAdmittedHookSettingsFromDisk(): Promise< - Pick + Pick > { const { readAgentHookSettingsFromProfileState } = await import('../../main/persistence/profile-state/profile-state-offline-settings.js') @@ -68,7 +68,7 @@ async function readAdmittedHookSettingsFromDisk(): Promise< async function updateEnabledOnDisk(enabled: boolean): Promise<{ settingsPath: string - settings: Pick + settings: Pick }> { const { assertOfflineProfileStateMutationRuntime } = await import('../../main/persistence/profile-state/profile-state-offline-settings.js') @@ -86,7 +86,7 @@ async function updateEnabledOnDisk(enabled: boolean): Promise<{ async function updateAdmittedEnabledOnDisk(enabled: boolean): Promise<{ settingsPath: string - settings: Pick + settings: Pick }> { const { updateAgentHookSettingsFromProfileState, readAgentHookSettingsFromProfileState } = await import('../../main/persistence/profile-state/profile-state-offline-settings.js') diff --git a/src/main/agent-hooks/managed-agent-hook-controls.ts b/src/main/agent-hooks/managed-agent-hook-controls.ts index d0cc0c4fae63..762d7cd61767 100644 --- a/src/main/agent-hooks/managed-agent-hook-controls.ts +++ b/src/main/agent-hooks/managed-agent-hook-controls.ts @@ -26,7 +26,7 @@ type InstallOptions = { userInitiated?: boolean shouldHydrateShellPath?: boolean onInstallError?: (agent: AgentHookTarget, error: unknown) => void - shouldContinue?: (agent: AgentHookTarget) => boolean + shouldContinue?: () => boolean agents?: readonly AgentHookTarget[] } @@ -147,7 +147,7 @@ export async function installManagedAgentHooks( const results: AgentHookInstallStatus[] = [] for (const entry of installers) { const [agent] = entry - if (options.shouldContinue && !options.shouldContinue(agent)) { + if (options.shouldContinue && !options.shouldContinue()) { results.push( skippedStatus( agent, @@ -182,15 +182,9 @@ export async function installManagedAgentHooks( return results } -export async function removeManagedAgentHooks( - options: RemoveOptions = {} -): Promise { - const allowed = options.agents ? new Set(options.agents) : null +export async function removeManagedAgentHooks(): Promise { const results: AgentHookInstallStatus[] = [] for (const [agent, remove] of MANAGED_AGENT_HOOK_REMOVERS) { - if (allowed !== null && !allowed.has(agent)) { - continue - } try { results.push(await remove()) } catch (error) { diff --git a/src/main/ipc/pty/host-env/opencode-config.ts b/src/main/ipc/pty/host-env/opencode-config.ts index 7e3481234694..788ba1b56de8 100644 --- a/src/main/ipc/pty/host-env/opencode-config.ts +++ b/src/main/ipc/pty/host-env/opencode-config.ts @@ -49,7 +49,7 @@ export function applyOpenCodeStatusPluginEnv( const agent = selectOpenCodeHookAgent( options.launchAgent, command, - () => options.agentStatusHooksEnabled + options.agentStatusHooksEnabled ) restoreOrStripOverlayEnv( env, diff --git a/src/main/persistence/profile-state/profile-state-offline-settings.test.ts b/src/main/persistence/profile-state/profile-state-offline-settings.test.ts index 6a8d83470205..17e061950346 100644 --- a/src/main/persistence/profile-state/profile-state-offline-settings.test.ts +++ b/src/main/persistence/profile-state/profile-state-offline-settings.test.ts @@ -56,10 +56,7 @@ describe.each(['json', 'sqlite'] as const)('offline settings %s updates', (backe expect(updateAgentHookSettingsFromProfileState(location, false)).toEqual({ settingsPath: location.databaseFile, - settings: { - agentCmdOverrides: { claude: 'claude --model opus' }, - disabledTuiAgents: ['codex', 'future-agent', 'codex'] - } + settings: { agentCmdOverrides: { claude: 'claude --model opus' } } }) const persisted = authority.readSerializedState() expect(hasProfileStateAuthorityMarker(location.databaseFile)).toBe(true) diff --git a/src/main/persistence/profile-state/profile-state-offline-settings.ts b/src/main/persistence/profile-state/profile-state-offline-settings.ts index 708424cd6367..8b2f55122806 100644 --- a/src/main/persistence/profile-state/profile-state-offline-settings.ts +++ b/src/main/persistence/profile-state/profile-state-offline-settings.ts @@ -2,7 +2,6 @@ import { existsSync, readFileSync } from 'node:fs' import { homedir } from 'node:os' import type { GlobalSettings } from '../../../shared/global-settings-types' import { getDefaultPersistedState } from '../../../shared/constants' -import { normalizeDisabledTuiAgents } from '../../../shared/tui-agent-selection' import { profileStateJsonMatchesAcceptance } from './profile-state-documents' import { isRecord, parseProfileStateRoot } from './profile-state-document-validation' import { @@ -26,13 +25,10 @@ export type ProfileStateOfflineLocation = { profileId: string } -export type AgentHookSettings = Pick< - GlobalSettings, - 'agentStatusHooksEnabled' | 'disabledTuiAgents' -> +export type AgentHookSettings = Pick export type AgentHookSettingsUpdate = { - settings: Pick + settings: Pick settingsPath: string } @@ -139,10 +135,7 @@ function projectAgentHookSettings( ([, value]) => typeof value === 'string' ) ) - : {}, - disabledTuiAgents: Array.isArray(settings.disabledTuiAgents) - ? settings.disabledTuiAgents.filter((value) => typeof value === 'string') - : [] + : {} } } @@ -186,7 +179,6 @@ function readAgentHookSettingsFromJson(dataFile: string): AgentHookSettings { function readAgentHookSettingsFromSettingsValue(value: unknown): AgentHookSettings { const settings = isRecord(value) ? value : {} return { - agentStatusHooksEnabled: settings.agentStatusHooksEnabled !== false, - disabledTuiAgents: normalizeDisabledTuiAgents(settings.disabledTuiAgents) + agentStatusHooksEnabled: settings.agentStatusHooksEnabled !== false } } diff --git a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-08.spec.ts b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-08.spec.ts index 29df18420981..70bdb12d40cc 100644 --- a/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-08.spec.ts +++ b/src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-08.spec.ts @@ -76,7 +76,12 @@ describe('OrcaRuntimeService', () => { expect.objectContaining({ sessionId: 'serve-dead-pty', worktreeId: TEST_WORKTREE_ID }) ) expect(spawn.mock.calls[0]![0].command).toBeUndefined() - expect(activated.tabs[0]).toMatchObject({ status: 'ready', launchAgent: 'claude' }) + // Why: the fallback keeps the tab's agent identity; only the startup command is skipped. + expect(activated.tabs[0]).toMatchObject({ + type: 'terminal', + status: 'ready', + launchAgent: 'claude' + }) }) it('collapses duplicate mobile terminal entries when renderer and headless leaf ids diverge for the same pty', async () => { diff --git a/src/main/runtime/runtime-agent-launch-resolution.test.ts b/src/main/runtime/runtime-agent-launch-resolution.test.ts deleted file mode 100644 index 3787128ec7c5..000000000000 --- a/src/main/runtime/runtime-agent-launch-resolution.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { resolveBareAgentLaunchCommand } from './runtime-agent-launch-resolution' - -describe('resolveBareAgentLaunchCommand', () => { - it("lets an override claim another agent's default command", () => { - expect( - resolveBareAgentLaunchCommand({ - command: 'opencode', - settings: { agentCmdOverrides: { opencode2: 'opencode' } }, - platform: 'darwin', - isRemote: false - }) - ).toBe('opencode2') - }) -}) diff --git a/src/main/runtime/runtime-agent-launch-resolution.ts b/src/main/runtime/runtime-agent-launch-resolution.ts index 35505e84a69e..9835bf19b2ce 100644 --- a/src/main/runtime/runtime-agent-launch-resolution.ts +++ b/src/main/runtime/runtime-agent-launch-resolution.ts @@ -5,11 +5,7 @@ import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resum import type { ClaudeAgentTeamsMode } from '../../shared/claude-agent-teams-tmux-compat' import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime' import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path' -import { - getTuiAgentLaunchCommand, - isTuiAgent, - TUI_AGENT_CONFIG -} from '../../shared/tui-agent-config' +import { getTuiAgentLaunchCommand, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' export function mergeTerminalEnvDeletionKeys( first: readonly string[] | undefined, @@ -65,23 +61,21 @@ export function resolveBareAgentLaunchCommand(args: { } const cmdOverrides = args.settings.agentCmdOverrides ?? {} - const agents = Object.keys(TUI_AGENT_CONFIG).filter(isTuiAgent) - // `command` is non-empty, so an empty override never matches. - const matches = (candidate: string | undefined): boolean => - candidate !== undefined && command === normalizeAgentLaunchCommandForMatch(candidate) - // Why overrides first: defaults are unique, so a collision is an override naming another agent's - // command, which says which agent the user means. Agents hidden in Settings match like any other. - return ( - agents.find((agent) => matches(cmdOverrides[agent])) ?? - agents.find((agent) => - matches( - getTuiAgentLaunchCommand(TUI_AGENT_CONFIG[agent], args.platform, { - isRemote: args.isRemote - }) - ) - ) ?? - null - ) + // Hiding an agent in Settings only hides it from pickers; its typed command still starts it. + for (const agent of Object.keys(TUI_AGENT_CONFIG) as TuiAgent[]) { + const override = cmdOverrides[agent]?.trim() + const defaultLaunchCommand = getTuiAgentLaunchCommand(TUI_AGENT_CONFIG[agent], args.platform, { + isRemote: args.isRemote + }) + const launchCommands = override ? [defaultLaunchCommand, override] : [defaultLaunchCommand] + if ( + launchCommands.some((candidate) => command === normalizeAgentLaunchCommandForMatch(candidate)) + ) { + return agent + } + } + + return null } export function inferCapturedClaudeAgentTeamsMode( diff --git a/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts b/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts index 9ddb6e5d4fcd..f72a249b9c7b 100644 --- a/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts +++ b/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts @@ -10,8 +10,7 @@ const mocks = vi.hoisted(() => ({ vi.mock('@/store', () => ({ useAppStore: { getState: () => ({ - // Hiding an agent only removes it from pickers; an explicit launch still starts it. - settings: { disabledTuiAgents: ['codex'] }, + settings: null, getKnownWorktreeById: mocks.getKnownWorktreeById, setActiveWorktree: mocks.setActiveWorktree }) diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx index 379eb23f6e01..6de9788f3288 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx @@ -200,6 +200,8 @@ describe('SourceControlAgentActionDialog', () => { setLocalRuntimeCapabilitiesForTests(null) }) it('hides the dialog and auto-starts once when the saved global launch recipe matches', async () => { + // Hiding the saved agent in Settings only removes it from the picker; it still auto-starts. + resetStore(settingsWithGlobalRecipe(undefined, ['codex'])) renderControlledDialog() expect(container.textContent).not.toContain('Launch agent') await vi.waitFor(() => expect(mocks.onStart).toHaveBeenCalledTimes(1)) @@ -312,7 +314,9 @@ describe('SourceControlAgentActionDialog', () => { renderControlledDialog() expect(container.textContent).not.toContain('Launch agent') await vi.waitFor(() => - expect(container.textContent?.toLowerCase()).toContain('not enabled or was not detected') + expect(container.textContent?.toLowerCase()).toContain( + 'was not detected on this workspace host' + ) ) expect(mocks.onStart).not.toHaveBeenCalled() expect(container.textContent).toContain('Launch agent') diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx index b3babe0c0093..6c27cd042e2e 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx @@ -2,6 +2,7 @@ import { forgeOwnerHostIdForWorkspace } from '@/runtime/forge-credential-target' import { useCallback, useEffect } from 'react' import { toast } from 'sonner' import { pickDefaultSourceControlAgent } from '../SourceControl' +import { resolveSourceControlLaunchAgentScope } from '../../../../../shared/source-control-launch-agent-selection' import { markPRCommentThreadResolved, restorePRCommentThreadSnapshot @@ -151,8 +152,20 @@ export function useChecksPanelCommentResolution(model: ChecksPanelCommentResolut const commentsDisabledReason = canTargetPRComments ? undefined : 'Commenting requires a GitHub PR repository target.' + // A saved action agent launches even when hidden from pickers, so it keeps the actions usable. + const savedAgentDetected = (['fixChecks', 'resolveConflicts', 'resolveComments'] as const).some( + (actionId) => { + const agent = resolveSourceControlLaunchAgentScope({ + settings, + repo, + actionId + }).effectiveAgentId + return agent !== null && Boolean(detectedAgentIds?.includes(agent)) + } + ) const noEnabledAgentKnown = detectedAgentIds != null && + !savedAgentDetected && pickDefaultSourceControlAgent( settings?.defaultTuiAgent, detectedAgentIds, diff --git a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts index 6c376fb8ae5e..dc2112e63104 100644 --- a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts +++ b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts @@ -1,19 +1,16 @@ import { getAgentCatalog } from '@/lib/agent-catalog' -import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' import type { TuiAgent } from '../../../../shared/tui-agent' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' import { translate } from '@/i18n/i18n' import type { SourceControlAgentActionDeliveryPlanState } from './SourceControlAgentActionDialogForm' import type { SourceControlAgentScopeNote } from './source-control-agent-action-dialog-result' -export function isSourceControlAgentDetectedAndEnabled( +// Hiding an agent in Settings only removes it from the picker; a saved or chosen agent still runs. +export function isSourceControlAgentDetected( agent: TuiAgent | null, - detectedAgents: TuiAgent[], - disabledAgents: TuiAgent[] | undefined + detectedAgents: TuiAgent[] ): boolean { - return Boolean( - agent && detectedAgents.includes(agent) && isTuiAgentEnabled(agent, disabledAgents) - ) + return Boolean(agent && detectedAgents.includes(agent)) } export function buildSourceControlAgentSaveTargets(repoId?: string | null): { @@ -90,7 +87,7 @@ export function buildSourceControlAgentStatusCopy(args: { detecting } = args if (selectedAgentUnavailable) { - return `${getAgentCatalog().find((entry) => entry.id === selectedAgent)?.label ?? selectedAgent} is not enabled or was not detected on this workspace host.` + return `${getAgentCatalog().find((entry) => entry.id === selectedAgent)?.label ?? selectedAgent} was not detected on this workspace host.` } if (connectionUnavailable) { return 'Unable to resolve the workspace connection.' diff --git a/src/renderer/src/components/right-sidebar/useSavedSourceControlAgentActionAutoStart.ts b/src/renderer/src/components/right-sidebar/useSavedSourceControlAgentActionAutoStart.ts index 0d582ca508ca..7d3422cc198d 100644 --- a/src/renderer/src/components/right-sidebar/useSavedSourceControlAgentActionAutoStart.ts +++ b/src/renderer/src/components/right-sidebar/useSavedSourceControlAgentActionAutoStart.ts @@ -6,7 +6,7 @@ import type { import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { Repo } from '../../../../shared/repo-types' import type { TuiAgent } from '../../../../shared/tui-agent' -import { isSourceControlAgentDetectedAndEnabled } from './source-control-agent-action-dialog-support' +import { isSourceControlAgentDetected } from './source-control-agent-action-dialog-support' import { sourceControlActionRecipeMatchesTarget } from './source-control-action-recipe-match' type SavedSourceControlAgentActionTargetValue = 'repo' | 'global' @@ -33,7 +33,6 @@ type UseSavedSourceControlAgentActionAutoStartArgs = { detecting: boolean isStarting: boolean detectedAgents: TuiAgent[] - disabledAgents: TuiAgent[] | undefined onAutoStart: (args: { detectedAgents: TuiAgent[] saveTargetValue: SavedSourceControlAgentActionTargetValue @@ -147,7 +146,6 @@ export function useSavedSourceControlAgentActionAutoStart({ detecting, isStarting, detectedAgents, - disabledAgents, onAutoStart }: UseSavedSourceControlAgentActionAutoStartArgs): SavedSourceControlAgentActionAutoStartResult { const autoStartedOpenCycleRef = useRef(0) @@ -244,7 +242,7 @@ export function useSavedSourceControlAgentActionAutoStart({ selectedAgent !== savedAgentId || !trimmedCommandInput || connectionUnavailable || - !isSourceControlAgentDetectedAndEnabled(savedAgentId, detectedAgents, disabledAgents) + !isSourceControlAgentDetected(savedAgentId, detectedAgents) ) { revealDialog() return @@ -270,7 +268,6 @@ export function useSavedSourceControlAgentActionAutoStart({ detectedAgents, detectionReady, detecting, - disabledAgents, isStarting, matchedSavedReceiptTargetValue, onAutoStart, diff --git a/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts b/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts index cf029a0007d2..c955692288b3 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts +++ b/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts @@ -20,7 +20,7 @@ import { buildSourceControlAgentSaveTargets, buildSourceControlAgentScopeNote, buildSourceControlAgentStatusCopy, - isSourceControlAgentDetectedAndEnabled + isSourceControlAgentDetected } from './source-control-agent-action-dialog-support' import { useSourceControlAgentActionStart } from './useSourceControlAgentActionStart' @@ -166,8 +166,7 @@ export function useSourceControlAgentActionDialog({ [enabledDetectedAgents, selectedAgent] ) const selectedAgentUnavailable = Boolean( - selectedAgent && - !isSourceControlAgentDetectedAndEnabled(selectedAgent, detectedAgents, disabledAgents) + selectedAgent && !isSourceControlAgentDetected(selectedAgent, detectedAgents) ) const hasEnabledAgents = enabledDetectedAgents.length > 0 const commandInput = renderSourceControlActionCommandTemplate(commandTemplate, { @@ -257,7 +256,6 @@ export function useSourceControlAgentActionDialog({ detecting, isStarting, detectedAgents, - disabledAgents, onAutoStart: ({ detectedAgents: agentsForLaunch, saveTargetValue: matchedTargetValue }) => startWithDetectedAgents({ detectedAgents: agentsForLaunch, diff --git a/src/shared/opencode-launch-command.test.ts b/src/shared/opencode-launch-command.test.ts index d9457de42e04..f14e251cfcbb 100644 --- a/src/shared/opencode-launch-command.test.ts +++ b/src/shared/opencode-launch-command.test.ts @@ -19,13 +19,14 @@ describe('OpenCode hook selection', () => { 'opencode --session fixture', '"C:\\tools\\opencode.exe" --session fixture', '/usr/local/bin/opencode' - ])('preserves disabled v1 identity for %s', (command) => { - expect(selectOpenCodeHookAgent(undefined, command, (agent) => agent === 'opencode2')).toBeNull() + ])('reads the v1 identity from %s', (command) => { + expect(selectOpenCodeHookAgent(undefined, command, true)).toBe('opencode') + }) + it('selects nothing when status hooks are off', () => { + expect(selectOpenCodeHookAgent('opencode2', 'opencode2', false)).toBeNull() }) it('trusts the supplied identity for wrapped or renamed commands', () => { - expect(selectOpenCodeHookAgent('opencode', 'opencode2', () => true)).toBe('opencode') - expect( - selectOpenCodeHookAgent('opencode2', 'my-wrapper', (agent) => agent === 'opencode2') - ).toBe('opencode2') + expect(selectOpenCodeHookAgent('opencode', 'opencode2', true)).toBe('opencode') + expect(selectOpenCodeHookAgent('opencode2', 'my-wrapper', true)).toBe('opencode2') }) }) diff --git a/src/shared/opencode-launch-command.ts b/src/shared/opencode-launch-command.ts index 92c4896b094c..24f79f23b20b 100644 --- a/src/shared/opencode-launch-command.ts +++ b/src/shared/opencode-launch-command.ts @@ -15,17 +15,17 @@ export function isOpenCode2LaunchCommand(launchCommand: string | undefined): boo export function selectOpenCodeHookAgent( launchAgent: TuiAgent | undefined, launchCommand: string | undefined, - isEnabled: (agent: 'opencode' | 'opencode2') => boolean + hooksEnabled: boolean ): 'opencode' | 'opencode2' | null { + if (!hooksEnabled) { + return null + } const explicit = launchAgent === 'opencode' || launchAgent === 'opencode2' ? launchAgent : launchAgent === undefined ? openCodeCommandAgent(launchCommand) : null - // An explicit version selects its own integration; disabling it never substitutes the other. - if (explicit) { - return isEnabled(explicit) ? explicit : null - } - return isEnabled('opencode') ? 'opencode' : isEnabled('opencode2') ? 'opencode2' : null + // An explicit version selects its own integration. + return explicit ?? 'opencode' } diff --git a/src/shared/source-control-launch-agent-selection.test.ts b/src/shared/source-control-launch-agent-selection.test.ts index 1862122d8715..ecff6149e5bc 100644 --- a/src/shared/source-control-launch-agent-selection.test.ts +++ b/src/shared/source-control-launch-agent-selection.test.ts @@ -4,10 +4,24 @@ import type { GlobalSettings } from './global-settings-types' import type { Repo } from './repo-types' import type { TuiAgent } from './tui-agent' import { + pickSourceControlLaunchAgent, resolveSourceControlLaunchAgentScope, summarizeReposOverridingActionRecipe } from './source-control-launch-agent-selection' +describe('pickSourceControlLaunchAgent', () => { + it('keeps a detected saved agent that is hidden from pickers', () => { + expect( + pickSourceControlLaunchAgent({ + savedAgent: 'codex', + defaultAgent: 'claude', + detectedAgents: ['claude', 'codex'], + disabledAgents: ['codex'] + }) + ).toBe('codex') + }) +}) + function settingsWithGlobalResolveAgent(agentId: TuiAgent): GlobalSettings { const base = getDefaultSettings('/tmp') return { From f3bcd8a4eeee22606590e1fd229697cd49b7c455 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:33:49 -0700 Subject: [PATCH 3/6] fix(agent-launch): hide the no-agents line while a hidden saved agent is selected Also drops the WSL guest install's now-pointless settings copy and the offline CLI test's leftover hidden-list expectation. --- src/cli/runtime-client-deferral.test.ts | 5 +---- src/main/agent-hooks/wsl-hook-relay-guest-install.ts | 3 +-- .../source-control-agent-action-dialog-support.ts | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/cli/runtime-client-deferral.test.ts b/src/cli/runtime-client-deferral.test.ts index c5cadcfcec71..2080e7576b9c 100644 --- a/src/cli/runtime-client-deferral.test.ts +++ b/src/cli/runtime-client-deferral.test.ts @@ -133,10 +133,7 @@ describe('RuntimeClient module-graph deferral', () => { expect( applyAgentStatusHooksEnabledMock, `${argv.join(' ')} hook application` - ).toHaveBeenCalledExactlyOnceWith(false, { - agentCmdOverrides: {}, - disabledTuiAgents: [] - }) + ).toHaveBeenCalledExactlyOnceWith(false, { agentCmdOverrides: {} }) } else { expect( applyAgentStatusHooksEnabledMock, diff --git a/src/main/agent-hooks/wsl-hook-relay-guest-install.ts b/src/main/agent-hooks/wsl-hook-relay-guest-install.ts index e2ed66df418d..6779a9415aa1 100644 --- a/src/main/agent-hooks/wsl-hook-relay-guest-install.ts +++ b/src/main/agent-hooks/wsl-hook-relay-guest-install.ts @@ -103,8 +103,7 @@ async function installGuestHooksAndPlugins( if (state.mux !== mux || mux.isDisposed()) { return } - const currentSettings = deps.managedHookSettings() - const settings = currentSettings && { ...currentSettings } + const settings = deps.managedHookSettings() const settingsKey = openCodePluginSettingsKey(settings) state.lastAttemptOpenCodeSettings = settingsKey const overlay = await requestGuestOpenCodeOverlayDir( diff --git a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts index dc2112e63104..efbc7b566153 100644 --- a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts +++ b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts @@ -92,7 +92,7 @@ export function buildSourceControlAgentStatusCopy(args: { if (connectionUnavailable) { return 'Unable to resolve the workspace connection.' } - if (!hasEnabledAgents && !detecting) { + if (!selectedAgent && !hasEnabledAgents && !detecting) { return 'No enabled agents were detected on this workspace host.' } return null From a04b81b10a7a5281469fb50a881aafb8ab04c78c Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:45:03 -0700 Subject: [PATCH 4/6] feat(settings): agent switch reads Shown / Hidden Hiding an agent only removes it from menus and pickers, so the Settings switch and the notices that mention it say shown/hidden instead of enabled/disabled, in every language. --- config/scripts/locale-value-overrides.mjs | 4 ++-- src/cli/specs/orchestration-worker-specs.ts | 2 +- .../AgentSessionContinuationDialog.tsx | 2 +- .../SourceControlAgentActionDialogForm.tsx | 2 +- .../use-checks-panel-comment-resolution.tsx | 2 +- ...source-control-agent-action-dialog-support.ts | 2 +- .../source-control/ai/recovery-launch.ts | 2 +- .../src/components/settings/AgentCatalogRow.tsx | 8 ++++---- .../src/components/settings/AgentsPane.test.tsx | 8 ++++---- .../src/components/tab-bar/QuickLaunchButton.tsx | 2 +- src/renderer/src/i18n/locales/en.json | 16 ++++++++-------- src/renderer/src/i18n/locales/es.json | 16 ++++++++-------- src/renderer/src/i18n/locales/fr.json | 16 ++++++++-------- src/renderer/src/i18n/locales/ja.json | 16 ++++++++-------- src/renderer/src/i18n/locales/ko.json | 16 ++++++++-------- src/renderer/src/i18n/locales/zh.json | 16 ++++++++-------- src/renderer/src/lib/fix-checks-agent-launch.ts | 2 +- 17 files changed, 66 insertions(+), 66 deletions(-) diff --git a/config/scripts/locale-value-overrides.mjs b/config/scripts/locale-value-overrides.mjs index 2e8547f057c6..80fd9cec9016 100644 --- a/config/scripts/locale-value-overrides.mjs +++ b/config/scripts/locale-value-overrides.mjs @@ -340,8 +340,8 @@ export const LOCALE_VALUE_OVERRIDES = { 'No local ports detected': '未检测到本地端口', 'No ports detected': '未检测到端口', 'No speech detected.': '未检测到语音。', - 'No enabled AI agent was detected on this workspace host.': - '此工作区主机上未检测到已启用的 AI 智能体。', + 'No AI agent shown in Settings was detected on this workspace host.': + '此工作区主机上未检测到设置中显示的 AI 智能体。', 'The selected agent was not detected on this workspace host.': '此工作区主机上未检测到所选智能体。', 'No agents detected on your PATH. Pick one to install later, or continue with a blank terminal.': diff --git a/src/cli/specs/orchestration-worker-specs.ts b/src/cli/specs/orchestration-worker-specs.ts index 9123009f9de0..7c9f24b3d5f1 100644 --- a/src/cli/specs/orchestration-worker-specs.ts +++ b/src/cli/specs/orchestration-worker-specs.ts @@ -35,7 +35,7 @@ export const ORCHESTRATION_WORKER_COMMAND_SPECS: CommandSpec[] = [ notes: [ 'Current and existing worktrees never rerun setup; a fresh agent terminal is created unless --terminal is explicit.', 'When reusing --terminal, pass --worktree for that terminal; current means the coordinator worktree.', - '--agent takes an Orca agent id enabled on the worker server, such as claude, codex, cursor, antigravity, muse, zcode, opencode, or opencode2.', + '--agent takes an Orca agent id on the worker server, such as claude, codex, cursor, antigravity, muse, zcode, opencode, or opencode2.', '--model supports Claude, Codex, Cursor, Antigravity, and Muse opaque provider model ids; --effort requires --model. OMP accepts --model only; --effort is unsupported. Neither can combine with --terminal. OpenCode model selection requires an existing worktree, a verified execution-host CLI, and an available model; effort is unsupported. Other agents, including zcode, launch with the model from their own config.', 'New worktrees use agent-first creation and default --setup to run. Repository start-immediately runs setup beside the agent; wait-for-setup gates agent readiness and task input.', 'Creation flags (--name, --repo, --base-branch, --display-name, --comment, --setup) are rejected for current/existing worktrees. Use exact --repo on the selected server; project/host convenience routing remains on worktree create.', diff --git a/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx b/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx index 9183212d8147..6312d981388f 100644 --- a/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx +++ b/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx @@ -221,7 +221,7 @@ export function AgentSessionContinuationDialog({

{translate( 'components.agentSessionContinuation.noAgents', - 'No enabled Agents were detected on this workspace host.' + 'No Agents shown in Settings were detected on this workspace host.' )}

) : null} diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx index cc0a2707de9f..9b0df5680ccc 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx @@ -169,7 +169,7 @@ export function SourceControlAgentActionDialogForm({ ) : translate( 'auto.components.right.sidebar.SourceControlAgentActionDialogForm.1d47db9bf0', - 'No enabled agents' + 'No agents to show' )} {onOpenSettings ? ( diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx index 6c27cd042e2e..ad2362855994 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx @@ -174,7 +174,7 @@ export function useChecksPanelCommentResolution(model: ChecksPanelCommentResolut const aiActionDisabledReason = !activeWorktreeId ? 'Select a workspace before launching an AI action.' : noEnabledAgentKnown - ? 'No enabled AI agents. Configure agents in Settings.' + ? 'No AI agents are shown. Choose which agents to show in Settings.' : undefined useEffect(() => { if (!sourceControlAiActionsVisible) { diff --git a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts index efbc7b566153..ff217684d327 100644 --- a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts +++ b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts @@ -93,7 +93,7 @@ export function buildSourceControlAgentStatusCopy(args: { return 'Unable to resolve the workspace connection.' } if (!selectedAgent && !hasEnabledAgents && !detecting) { - return 'No enabled agents were detected on this workspace host.' + return 'No agents shown in Settings were detected on this workspace host.' } return null } diff --git a/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts b/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts index 9bca4d847850..2d00cd608903 100644 --- a/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts +++ b/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts @@ -57,7 +57,7 @@ export function getDefaultSourceControlRecoveryLaunchCopy( ), noEnabledAgent: translate( 'auto.components.right.sidebar.source.control.ai.recovery.launch.9bbd9077a2', - 'No enabled AI agents. Configure agents in Settings.' + 'No AI agents are shown. Choose which agents to show in Settings.' ), launchCommandUnavailable: translate( 'auto.components.right.sidebar.source.control.ai.recovery.launch.5540ff50cc', diff --git a/src/renderer/src/components/settings/AgentCatalogRow.tsx b/src/renderer/src/components/settings/AgentCatalogRow.tsx index d9f6c926c399..5c3e1fa3f7bf 100644 --- a/src/renderer/src/components/settings/AgentCatalogRow.tsx +++ b/src/renderer/src/components/settings/AgentCatalogRow.tsx @@ -37,18 +37,18 @@ export function AgentAvailabilityControl({ }} ariaLabel={translate( 'auto.components.settings.AgentsPane.1c9a9679ec', - '{{value0}} availability', + '{{value0}} visibility', { value0: label } )} size="sm" options={[ { value: 'enabled', - label: translate('auto.components.settings.AgentsPane.d4d2a45d63', 'Enabled') + label: translate('auto.components.settings.AgentsPane.d4d2a45d63', 'Shown') }, { value: 'disabled', - label: translate('auto.components.settings.AgentsPane.8dc0192e48', 'Disabled') + label: translate('auto.components.settings.AgentsPane.8dc0192e48', 'Hidden') } ]} /> @@ -113,7 +113,7 @@ export function AgentCatalogRow({ {label} {!isEnabled && ( - {translate('auto.components.settings.AgentsPane.8dc0192e48', 'Disabled')} + {translate('auto.components.settings.AgentsPane.8dc0192e48', 'Hidden')} )} diff --git a/src/renderer/src/components/settings/AgentsPane.test.tsx b/src/renderer/src/components/settings/AgentsPane.test.tsx index 3970d5493d15..2fea6a9f2261 100644 --- a/src/renderer/src/components/settings/AgentsPane.test.tsx +++ b/src/renderer/src/components/settings/AgentsPane.test.tsx @@ -546,9 +546,9 @@ describe('AgentsPane', () => { disabledTuiAgents: ['claude'] }) - expect(markup).toContain('aria-label="Claude availability"') - expect(markup).toContain('Enabled') - expect(markup).toContain('Disabled') + expect(markup).toContain('aria-label="Claude visibility"') + expect(markup).toContain('Shown') + expect(markup).toContain('Hidden') expect(markup).not.toContain('Shown in launch and default choices.') expect(markup).not.toContain('Install to use in launch and default choices.') expect(markup).not.toContain('Hidden from launch and default choices.') @@ -570,7 +570,7 @@ describe('AgentsPane', () => { } expect(props.value).toBe('enabled') - expect(props.ariaLabel).toBe('Claude availability') + expect(props.ariaLabel).toBe('Claude visibility') props.onChange('enabled') expect(onSetEnabled).not.toHaveBeenCalled() diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx index 8622e567160e..d4963271cc30 100644 --- a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx +++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx @@ -243,7 +243,7 @@ function QuickLaunchAgentMenuItemsInner({ : detectedIds && detectedIds.length > 0 ? translate( 'auto.components.tab.bar.QuickLaunchButton.8dea9b5cdf', - 'No enabled agents' + 'No agents to show' ) : translate( 'auto.components.tab.bar.QuickLaunchButton.e518f544b1', diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 2ba3f0df3b54..a0bf82df3edb 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -799,7 +799,7 @@ "822bf52295": "Unable to resolve the workspace launch platform.", "dfb4dd7c00": "Unable to find the workspace attached to these checks.", "9f00d7df0c": "Fix checks prompt is empty. Update Source Control AI settings.", - "2ebf794906": "No enabled AI agent was detected on this workspace host.", + "2ebf794906": "No AI agent shown in Settings was detected on this workspace host.", "4c7f783a7a": "Saved checks agent is not available on this workspace host." } } @@ -3654,7 +3654,7 @@ "ec2adf093e": "Launch {{value0}} in a new terminal", "465e432ef1": "Could not build launch command for {{value0}}.", "e518f544b1": "No agents detected", - "8dea9b5cdf": "No enabled agents", + "8dea9b5cdf": "No agents to show", "needsServerUpdate": "Update Orca on this server to list this workspace’s agents", "hostUnreachable": "Can’t reach this workspace’s host to list its agents" }, @@ -6988,13 +6988,13 @@ "f9f127d664": "Override the binary path or name, and edit the default launch arguments or environment for this agent.", "f95b5c79b8": "Install", "fe4d630c94": "Docs", - "8dc0192e48": "Disabled", + "8dc0192e48": "Hidden", "df123171d1": "Not installed", "c8794e622e": "Detected", "5200dac9da": "Reset", "2e45ca29b6": "Command", - "d4d2a45d63": "Enabled", - "1c9a9679ec": "{{value0}} availability", + "d4d2a45d63": "Shown", + "1c9a9679ec": "{{value0}} visibility", "0d9e293a02": "Refresh", "c9b33eb5c0": "Refreshing…", "13647f9f80": "Re-read your shell PATH and re-detect installed agents", @@ -13258,7 +13258,7 @@ "15c5d85706": "Agent", "3e8f21954f": "error", "74168d7ada": "idle", - "1d47db9bf0": "No enabled agents", + "1d47db9bf0": "No agents to show", "c7ff8cef11": "Detecting agents...", "b0da3a4d3e": "Launch recipe already saved", "bff4795a6d": "Change the agent, arguments, or prompt template to update the saved recipe.", @@ -13657,7 +13657,7 @@ "empty": "Commit failure prompt is empty. Update Source Control AI settings." }, "d481ab22f9": "Saved AI agent is unavailable. Use Customize launch to choose another agent.", - "9bbd9077a2": "No enabled AI agents. Configure agents in Settings.", + "9bbd9077a2": "No AI agents are shown. Choose which agents to show in Settings.", "5540ff50cc": "Could not build the agent launch command.", "216f762bd7": "Unable to resolve the workspace connection.", "success": "Started an AI agent for the {{value0}} failure." @@ -19001,7 +19001,7 @@ "selectAgent": "Select an Agent", "detectingAgents": "Detecting Agents on this workspace host…", "detectionFailed": "Could not detect Agents on this workspace host.", - "noAgents": "No enabled Agents were detected on this workspace host.", + "noAgents": "No Agents shown in Settings were detected on this workspace host.", "context": "Context", "modeFocused": "Focused handoff (Recommended)", "modeFocusedDescription": "Uses the latest status and current workspace, reading older transcript details only when needed.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 6c60a3cf8f1c..ae90ed9764cb 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -616,7 +616,7 @@ "822bf52295": "No se puede resolver la plataforma de inicio del espacio de trabajo.", "dfb4dd7c00": "No se encontró el espacio de trabajo adjunto a estos checks.", "9f00d7df0c": "El prompt de corrección de checks está vacío. Actualiza los ajustes de Source Control AI.", - "2ebf794906": "No se detectó ningún agente de IA habilitado en este host de espacio de trabajo.", + "2ebf794906": "No se detectó en este host de espacio de trabajo ningún agente de IA mostrado en Configuración.", "4c7f783a7a": "El agente guardado para checks no está disponible en este host de espacio de trabajo." } } @@ -3415,7 +3415,7 @@ "ec2adf093e": "Iniciar {{value0}} en un nuevo terminal", "465e432ef1": "No se pudo generar el comando de inicio para {{value0}}.", "e518f544b1": "No se detectaron agentes", - "8dea9b5cdf": "No hay agentes habilitados", + "8dea9b5cdf": "No hay agentes para mostrar", "needsServerUpdate": "Actualiza Orca en este servidor para ver los agentes de este espacio de trabajo" }, "RecentTabSwitcher": { @@ -6719,13 +6719,13 @@ "f9f127d664": "Override de la ruta o nombre del binario, y edición de argumentos de lanzamiento o entorno predeterminados para este agent.", "f95b5c79b8": "Instalar", "fe4d630c94": "Documentación", - "8dc0192e48": "Desactivado", + "8dc0192e48": "Oculto", "df123171d1": "No instalado", "c8794e622e": "Detectado", "5200dac9da": "Reiniciar", "2e45ca29b6": "Comando", - "d4d2a45d63": "Activado", - "1c9a9679ec": "{{value0}} disponibilidad", + "d4d2a45d63": "Visible", + "1c9a9679ec": "Visibilidad de {{value0}}", "0d9e293a02": "Refrescar", "c9b33eb5c0": "Actualizando…", "13647f9f80": "Vuelve a leer PATH de tu shell y vuelve a detectar los Agents instalados", @@ -12963,7 +12963,7 @@ "15c5d85706": "Agente", "3e8f21954f": "error", "74168d7ada": "idle", - "1d47db9bf0": "No hay agentes habilitados", + "1d47db9bf0": "No hay agentes para mostrar", "c7ff8cef11": "Detectando agentes...", "b0da3a4d3e": "La receta de lanzamiento ya está guardada", "bff4795a6d": "Cambia el agente, los argumentos o la plantilla del prompt para actualizar la receta guardada.", @@ -13361,7 +13361,7 @@ "empty": "El prompt de fallo de commit está vacío. Actualiza la configuración de AI de Control de Código." }, "d481ab22f9": "El agente de AI guardado no está disponible. Usa Inicio personalizado para elegir otro agente.", - "9bbd9077a2": "No hay agentes de AI habilitados. Configura agentes en Configuración.", + "9bbd9077a2": "No se muestra ningún agente de IA. Elige qué agentes mostrar en Configuración.", "5540ff50cc": "No se pudo construir el comando de inicio del agente.", "216f762bd7": "No se pudo resolver la conexión del espacio de trabajo.", "success": "Se inició un agente de AI para el fallo de {{value0}}." @@ -18686,7 +18686,7 @@ "selectAgent": "Selecciona un agente", "detectingAgents": "Detectando agentes en el host de este espacio de trabajo…", "detectionFailed": "No se pudieron detectar agentes en el host de este espacio de trabajo.", - "noAgents": "No se detectaron agentes habilitados en el host de este espacio de trabajo.", + "noAgents": "No se detectaron en el host de este espacio de trabajo agentes mostrados en Configuración.", "context": "Contexto", "modeFocused": "Traspaso enfocado (recomendado)", "modeFocusedDescription": "Usa el estado más reciente y el espacio de trabajo actual, y consulta detalles anteriores solo cuando hacen falta.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 442a9f6e2d37..bc7834d91aa5 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -717,7 +717,7 @@ "822bf52295": "Impossible de résoudre la plateforme de lancement de l'espace de travail.", "dfb4dd7c00": "Impossible de trouver l'espace de travail associé à ces vérifications.", "9f00d7df0c": "Le prompt de correction des checks est vide. Mettez à jour les paramètres d'IA du contrôle de code source.", - "2ebf794906": "Aucun agent IA activé n'a été détecté sur l'hôte de cet espace de travail.", + "2ebf794906": "Aucun agent IA affiché dans les paramètres n'a été détecté sur l'hôte de cet espace de travail.", "4c7f783a7a": "L'agent de checks enregistré n'est pas disponible sur l'hôte de cet espace de travail." } } @@ -3520,7 +3520,7 @@ "ec2adf093e": "Lancer {{value0}} dans un nouveau terminal", "465e432ef1": "Impossible de construire la commande de lancement pour {{value0}}.", "e518f544b1": "Aucun agent détecté", - "8dea9b5cdf": "Aucun agent activé", + "8dea9b5cdf": "Aucun agent à afficher", "needsServerUpdate": "Mettez à jour Orca sur ce serveur pour lister les agents de cet espace de travail" }, "RecentTabSwitcher": { @@ -6829,13 +6829,13 @@ "f9f127d664": "Remplacez le chemin ou le nom du binaire, et modifiez les arguments de lancement ou l'environnement par défaut de cet agent.", "f95b5c79b8": "Installer", "fe4d630c94": "Docs", - "8dc0192e48": "Désactivé", + "8dc0192e48": "Masqué", "df123171d1": "Non installé", "c8794e622e": "Détecté", "5200dac9da": "Réinitialiser", "2e45ca29b6": "Commande", - "d4d2a45d63": "Activé", - "1c9a9679ec": "Disponibilité de {{value0}}", + "d4d2a45d63": "Affiché", + "1c9a9679ec": "Visibilité de {{value0}}", "0d9e293a02": "Actualiser", "c9b33eb5c0": "Actualisation…", "13647f9f80": "Relire le PATH de votre shell et redétecter les agents installés", @@ -13069,7 +13069,7 @@ "15c5d85706": "Agent", "3e8f21954f": "error", "74168d7ada": "idle", - "1d47db9bf0": "Aucun agent activé", + "1d47db9bf0": "Aucun agent à afficher", "c7ff8cef11": "Détection des agents...", "b0da3a4d3e": "Recette de lancement déjà enregistrée", "bff4795a6d": "Modifiez l'agent, les arguments ou le modèle de prompt pour mettre à jour la recette enregistrée.", @@ -13467,7 +13467,7 @@ "empty": "Le prompt d'échec de commit est vide. Mettez à jour les paramètres IA du contrôle de code source." }, "d481ab22f9": "L'agent IA enregistré est indisponible. Utilisez « Personnaliser le lancement » pour choisir un autre agent.", - "9bbd9077a2": "Aucun agent IA activé. Configurez les agents dans les paramètres.", + "9bbd9077a2": "Aucun agent IA n'est affiché. Choisissez les agents à afficher dans les paramètres.", "5540ff50cc": "Impossible de construire la commande de lancement de l'agent.", "216f762bd7": "Impossible de résoudre la connexion de l'espace de travail.", "success": "Un agent IA a été lancé pour l'échec de {{value0}}." @@ -18788,7 +18788,7 @@ "selectAgent": "Sélectionner un Agent", "detectingAgents": "Détection des Agents sur cet hôte d'espace de travail…", "detectionFailed": "Impossible de détecter des Agents sur cet hôte d'espace de travail.", - "noAgents": "Aucun Agent activé n'a été détecté sur cet hôte d'espace de travail.", + "noAgents": "Aucun Agent affiché dans les paramètres n'a été détecté sur cet hôte d'espace de travail.", "context": "Contexte", "modeFocused": "Transfert ciblé (recommandé)", "modeFocusedDescription": "Utilise le statut le plus récent et l'espace de travail actuel, en ne lisant les détails anciens de la transcription qu'en cas de besoin.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 469d2519575c..b6859bfc6f86 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -616,7 +616,7 @@ "822bf52295": "ワークスペース起動プラットフォームを解決できません。", "dfb4dd7c00": "これらのチェックに関連付けられたワークスペースが見つかりません。", "9f00d7df0c": "チェックプロンプトが空です。ソース管理 AI 設定を更新してください。", - "2ebf794906": "このワークスペースホストでは有効な AI Agent が検出されませんでした。", + "2ebf794906": "[設定] で表示している AI Agent がこのワークスペースホストで検出されませんでした。", "4c7f783a7a": "保存されたチェック Agent は、このワークスペースホストでは使用できません。" } } @@ -3415,7 +3415,7 @@ "ec2adf093e": "新規ターミナルで {{value0}} を起動", "465e432ef1": "{{value0}} の起動コマンドを作成できませんでした。", "e518f544b1": "Agent が検出されませんでした", - "8dea9b5cdf": "有効な Agent がありません", + "8dea9b5cdf": "表示する Agent がありません", "needsServerUpdate": "このワークスペースの Agent を表示するには、このサーバーの Orca を更新してください" }, "RecentTabSwitcher": { @@ -6704,13 +6704,13 @@ "f9f127d664": "バイナリのパスまたは名前を上書きし、この Agent の既定の起動引数や環境変数を編集します。", "f95b5c79b8": "インストール", "fe4d630c94": "ドキュメント", - "8dc0192e48": "無効", + "8dc0192e48": "非表示", "df123171d1": "未インストール", "c8794e622e": "検出されました", "5200dac9da": "リセット", "2e45ca29b6": "コマンド", - "d4d2a45d63": "有効", - "1c9a9679ec": "{{value0}} の利用可否", + "d4d2a45d63": "表示", + "1c9a9679ec": "{{value0}} の表示", "0d9e293a02": "更新", "c9b33eb5c0": "更新中…", "13647f9f80": "シェルの PATH を再読み取り、インストールされている Agent を再検出します", @@ -12955,7 +12955,7 @@ "15c5d85706": "Agent", "3e8f21954f": "エラー", "74168d7ada": "idle", - "1d47db9bf0": "有効な Agent がありません", + "1d47db9bf0": "表示する Agent がありません", "c7ff8cef11": "Agent を検出中…", "013c9ac04a": "保存対象", "1bb611240f": "Orca の既定のプロンプトを使うには、{basePrompt} を指定します。", @@ -13361,7 +13361,7 @@ "empty": "コミット失敗プロンプトが空です。ソース管理の AI 設定を更新してください。" }, "d481ab22f9": "保存された AI Agent は使用できません。[起動のカスタマイズ] から別の Agent を選択してください。", - "9bbd9077a2": "有効な AI Agent がありません。[設定] で Agent を構成してください。", + "9bbd9077a2": "表示されている AI Agent がありません。[設定] で表示する Agent を選択してください。", "5540ff50cc": "Agent 起動コマンドを構築できませんでした。", "216f762bd7": "ワークスペースの接続を解決できません。", "success": "{{value0}}の失敗に対してAIAgent を起動しました。" @@ -18721,7 +18721,7 @@ "selectAgent": "Agent を選択", "detectingAgents": "このワークスペースホストで Agent を検出中…", "detectionFailed": "このワークスペースホストで Agent を検出できませんでした。", - "noAgents": "このワークスペースホストで有効な Agent が検出されませんでした。", + "noAgents": "[設定] で表示している Agent がこのワークスペースホストで検出されませんでした。", "context": "コンテキスト", "modeFocused": "要点を引き継ぐ(推奨)", "modeFocusedDescription": "最新の状態と現在のワークスペースを使い、必要な場合だけ過去の詳細を読みます。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 564c24dded11..4ad7fc1c487d 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -616,7 +616,7 @@ "822bf52295": "워크스페이스 시작 플랫폼을 확인할 수 없습니다.", "dfb4dd7c00": "이러한 검사에 연결된 워크스페이스를 찾을 수 없습니다.", "9f00d7df0c": "검사 프롬프트가 비어 있습니다. 소스 제어 AI 설정을 업데이트하세요.", - "2ebf794906": "이 워크스페이스 호스트에서는 활성화된 AI agent가 감지되지 않았습니다.", + "2ebf794906": "설정에서 표시한 AI 에이전트가 이 워크스페이스 호스트에서 감지되지 않았습니다.", "4c7f783a7a": "이 워크스페이스 호스트에서는 저장된 확인 agent를 사용할 수 없습니다." } } @@ -3415,7 +3415,7 @@ "ec2adf093e": "새 terminal에서 {{value0}} 실행", "465e432ef1": "{{value0}}에 대한 실행 명령을 작성할 수 없습니다.", "e518f544b1": "감지된 agents가 없습니다.", - "8dea9b5cdf": "활성화된 agents가 없습니다.", + "8dea9b5cdf": "표시할 에이전트가 없습니다.", "needsServerUpdate": "이 워크스페이스의 에이전트를 보려면 이 서버의 Orca를 업데이트하세요" }, "RecentTabSwitcher": { @@ -6704,13 +6704,13 @@ "f9f127d664": "이 agent의 바이너리 경로나 이름을 재정의하고 기본 실행 인수 또는 환경을 편집합니다.", "f95b5c79b8": "설치", "fe4d630c94": "문서", - "8dc0192e48": "비활성", + "8dc0192e48": "숨김", "df123171d1": "설치되지 않음", "c8794e622e": "감지됨", "5200dac9da": "재설정", "2e45ca29b6": "명령", - "d4d2a45d63": "활성화됨", - "1c9a9679ec": "{{value0}} 사용 가능 여부", + "d4d2a45d63": "표시", + "1c9a9679ec": "{{value0}} 표시 여부", "0d9e293a02": "새로고침", "c9b33eb5c0": "새로고침 중…", "13647f9f80": "셸 PATH를 다시 ​​읽고 설치된 agents를 다시 검색하십시오.", @@ -12955,7 +12955,7 @@ "15c5d85706": "에이전트", "3e8f21954f": "오류", "74168d7ada": "idle", - "1d47db9bf0": "활성화된 agents가 없습니다.", + "1d47db9bf0": "표시할 에이전트가 없습니다.", "c7ff8cef11": "agents 감지 중...", "013c9ac04a": "저장 대상", "1bb611240f": "Orca 기본 프롬프트를 사용하려면 {basePrompt}를 사용하세요.", @@ -13361,7 +13361,7 @@ "empty": "커밋 실패 프롬프트가 비어 있습니다. 소스 컨트롤 AI 설정을 업데이트하세요." }, "d481ab22f9": "저장된 AI 에이전트를 사용할 수 없습니다. 사용자 지정 시작에서 다른 에이전트를 선택하세요.", - "9bbd9077a2": "활성화된 AI 에이전트가 없습니다. 설정에서 에이전트를 구성하세요.", + "9bbd9077a2": "표시된 AI 에이전트가 없습니다. 설정에서 표시할 에이전트를 선택하세요.", "5540ff50cc": "에이전트 시작 명령을 구성할 수 없습니다.", "216f762bd7": "워크트리 연결을 확인할 수 없습니다.", "success": "{{value0}} 실패에 대한 AI 에이전트가 시작되었습니다." @@ -18721,7 +18721,7 @@ "selectAgent": "에이전트 선택", "detectingAgents": "이 워크스페이스 호스트에서 에이전트를 찾는 중…", "detectionFailed": "이 워크스페이스 호스트에서 에이전트를 찾을 수 없습니다.", - "noAgents": "이 워크스페이스 호스트에서 활성화된 에이전트를 찾지 못했습니다.", + "noAgents": "설정에서 표시한 에이전트를 이 워크스페이스 호스트에서 찾지 못했습니다.", "context": "컨텍스트", "modeFocused": "핵심 컨텍스트 전달(권장)", "modeFocusedDescription": "최신 상태와 현재 워크스페이스를 사용하고 필요한 경우에만 이전 세부 정보를 읽습니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index dec2d856f069..2a0698ef9858 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -616,7 +616,7 @@ "822bf52295": "无法解析工作区启动平台。", "dfb4dd7c00": "无法找到附加到这些检查的工作区。", "9f00d7df0c": "检查提示为空。请更新源代码管理 AI 设置。", - "2ebf794906": "此工作区主机上未检测到已启用的 AI 智能体。", + "2ebf794906": "此工作区主机上未检测到设置中显示的 AI 智能体。", "4c7f783a7a": "保存的检查智能体在此工作区主机上不可用。" } } @@ -3415,7 +3415,7 @@ "ec2adf093e": "在新终端中启动 {{value0}}", "465e432ef1": "无法为 {{value0}} 构建启动命令。", "e518f544b1": "未检测到智能体", - "8dea9b5cdf": "没有启用智能体", + "8dea9b5cdf": "没有可显示的智能体", "needsServerUpdate": "请更新此服务器上的 Orca 以列出此工作区的智能体" }, "RecentTabSwitcher": { @@ -6704,13 +6704,13 @@ "f9f127d664": "覆盖二进制路径或名称,并编辑此智能体的默认启动参数或环境。", "f95b5c79b8": "安装", "fe4d630c94": "文档", - "8dc0192e48": "已禁用", + "8dc0192e48": "隐藏", "df123171d1": "未安装", "c8794e622e": "已检测", "5200dac9da": "重置", "2e45ca29b6": "命令", - "d4d2a45d63": "启用", - "1c9a9679ec": "{{value0}} 可用性", + "d4d2a45d63": "显示", + "1c9a9679ec": "{{value0}} 可见性", "0d9e293a02": "刷新", "c9b33eb5c0": "刷新中…", "13647f9f80": "重新读取 shell PATH 并重新检测已安装的智能体", @@ -12969,7 +12969,7 @@ "15c5d85706": "智能体", "3e8f21954f": "错误", "74168d7ada": "空闲", - "1d47db9bf0": "没有启用智能体", + "1d47db9bf0": "没有可显示的智能体", "c7ff8cef11": "检测剂...", "013c9ac04a": "保存范围", "1bb611240f": "使用 {basePrompt} 作为 Orca 的默认提示词。", @@ -13375,7 +13375,7 @@ "empty": "提交失败提示词为空。请更新源代码控制 AI 设置。" }, "d481ab22f9": "已保存的 AI 智能体不可用。请使用「自定义启动」选择其他智能体。", - "9bbd9077a2": "没有已启用的 AI 智能体。请在设置中配置智能体。", + "9bbd9077a2": "没有显示的 AI 智能体。请在设置中选择要显示的智能体。", "5540ff50cc": "无法构建智能体启动命令。", "216f762bd7": "无法解析工作区连接。", "success": "已为 {{value0}} 失败启动 AI 智能体。" @@ -18700,7 +18700,7 @@ "selectAgent": "选择智能体", "detectingAgents": "正在此工作区主机上检测智能体…", "detectionFailed": "无法检测此工作区主机上的智能体。", - "noAgents": "未在此工作区主机上检测到已启用的智能体。", + "noAgents": "未在此工作区主机上检测到设置中显示的智能体。", "context": "上下文", "modeFocused": "聚焦交接(推荐)", "modeFocusedDescription": "使用最新进度和当前工作区,仅在需要时读取更早的会话细节。", diff --git a/src/renderer/src/lib/fix-checks-agent-launch.ts b/src/renderer/src/lib/fix-checks-agent-launch.ts index d0133a218aeb..78734f8755bf 100644 --- a/src/renderer/src/lib/fix-checks-agent-launch.ts +++ b/src/renderer/src/lib/fix-checks-agent-launch.ts @@ -99,7 +99,7 @@ async function pickExistingWorktreeAgent( toast.error( translate( 'auto.lib.fix.checks.agent.launch.2ebf794906', - 'No enabled AI agent was detected on this workspace host.' + 'No AI agent shown in Settings was detected on this workspace host.' ) ) } From bf618582d30f7a51d3285d8c2c02b9d76164abe1 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:01:24 -0700 Subject: [PATCH 5/6] fix(agent-launch): name both causes and the next step in no-agent notices The AI buttons, checks panel and Fix checks say no agent is both installed and shown, and offer installing or showing one; Quick Launch says all agents are hidden. Locale pins keep the new label and ja Shown stable under regeneration. --- config/scripts/locale-key-overrides.mjs | 8 +++++--- config/scripts/locale-value-overrides.mjs | 4 ++-- .../checks-panel/use-checks-panel-comment-resolution.tsx | 2 +- .../right-sidebar/source-control/ai/recovery-launch.ts | 2 +- src/renderer/src/components/tab-bar/QuickLaunchButton.tsx | 2 +- src/renderer/src/i18n/locales/en.json | 6 +++--- src/renderer/src/i18n/locales/es.json | 6 +++--- src/renderer/src/i18n/locales/fr.json | 6 +++--- src/renderer/src/i18n/locales/ja.json | 6 +++--- src/renderer/src/i18n/locales/ko.json | 6 +++--- src/renderer/src/i18n/locales/zh.json | 8 ++++---- src/renderer/src/lib/fix-checks-agent-launch.ts | 2 +- 12 files changed, 30 insertions(+), 28 deletions(-) diff --git a/config/scripts/locale-key-overrides.mjs b/config/scripts/locale-key-overrides.mjs index 175961b88cf8..2e9148b19600 100644 --- a/config/scripts/locale-key-overrides.mjs +++ b/config/scripts/locale-key-overrides.mjs @@ -243,10 +243,12 @@ const BASE_LOCALE_KEY_OVERRIDES = { ja: 'コマンド' }, 'auto.components.settings.AgentsPane.1c9a9679ec': { - ko: '{{value0}} 사용 가능 여부', - zh: '{{value0}} 可用性', - ja: '{{value0}} の利用可否' + ko: '{{value0}} 표시 여부', + zh: '{{value0}} 可见性', + ja: '{{value0}} の表示' }, + // The Shown / Hidden switch pairs as 表示 / 非表示; the glossary's 表示中 reads as a status. + 'auto.components.settings.AgentsPane.d4d2a45d63': { ja: '表示' }, 'auto.components.settings.AgentsPane.ed3e110e61': { ko: '감지됨', zh: '已检测', diff --git a/config/scripts/locale-value-overrides.mjs b/config/scripts/locale-value-overrides.mjs index 80fd9cec9016..0cf796c3a276 100644 --- a/config/scripts/locale-value-overrides.mjs +++ b/config/scripts/locale-value-overrides.mjs @@ -340,8 +340,8 @@ export const LOCALE_VALUE_OVERRIDES = { 'No local ports detected': '未检测到本地端口', 'No ports detected': '未检测到端口', 'No speech detected.': '未检测到语音。', - 'No AI agent shown in Settings was detected on this workspace host.': - '此工作区主机上未检测到设置中显示的 AI 智能体。', + 'No AI agent on this workspace host is both installed and shown in Settings. Install one, or show one in Settings.': + '此工作区主机上没有既已安装又在设置中显示的 AI 智能体。请安装一个,或在设置中显示一个。', 'The selected agent was not detected on this workspace host.': '此工作区主机上未检测到所选智能体。', 'No agents detected on your PATH. Pick one to install later, or continue with a blank terminal.': diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx index ad2362855994..0d56012c4ef1 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx @@ -174,7 +174,7 @@ export function useChecksPanelCommentResolution(model: ChecksPanelCommentResolut const aiActionDisabledReason = !activeWorktreeId ? 'Select a workspace before launching an AI action.' : noEnabledAgentKnown - ? 'No AI agents are shown. Choose which agents to show in Settings.' + ? 'No AI agent is both installed and shown in Settings. Install one, or show one in Settings.' : undefined useEffect(() => { if (!sourceControlAiActionsVisible) { diff --git a/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts b/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts index 2d00cd608903..a9b5a915afb3 100644 --- a/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts +++ b/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts @@ -57,7 +57,7 @@ export function getDefaultSourceControlRecoveryLaunchCopy( ), noEnabledAgent: translate( 'auto.components.right.sidebar.source.control.ai.recovery.launch.9bbd9077a2', - 'No AI agents are shown. Choose which agents to show in Settings.' + 'No AI agent is both installed and shown in Settings. Install one, or show one in Settings.' ), launchCommandUnavailable: translate( 'auto.components.right.sidebar.source.control.ai.recovery.launch.5540ff50cc', diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx index d4963271cc30..2ee55fc5e50b 100644 --- a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx +++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx @@ -243,7 +243,7 @@ function QuickLaunchAgentMenuItemsInner({ : detectedIds && detectedIds.length > 0 ? translate( 'auto.components.tab.bar.QuickLaunchButton.8dea9b5cdf', - 'No agents to show' + 'All agents are hidden. Show one in Settings.' ) : translate( 'auto.components.tab.bar.QuickLaunchButton.e518f544b1', diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index a0b80323fc68..15bc83a8512a 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -799,7 +799,7 @@ "822bf52295": "Unable to resolve the workspace launch platform.", "dfb4dd7c00": "Unable to find the workspace attached to these checks.", "9f00d7df0c": "Fix checks prompt is empty. Update Source Control AI settings.", - "2ebf794906": "No AI agent shown in Settings was detected on this workspace host.", + "2ebf794906": "No AI agent on this workspace host is both installed and shown in Settings. Install one, or show one in Settings.", "4c7f783a7a": "Saved checks agent is not available on this workspace host." } } @@ -3654,7 +3654,7 @@ "ec2adf093e": "Launch {{value0}} in a new terminal", "465e432ef1": "Could not build launch command for {{value0}}.", "e518f544b1": "No agents detected", - "8dea9b5cdf": "No agents to show", + "8dea9b5cdf": "All agents are hidden. Show one in Settings.", "needsServerUpdate": "Update Orca on this server to list this workspace’s agents", "hostUnreachable": "Can’t reach this workspace’s host to list its agents" }, @@ -13668,7 +13668,7 @@ "empty": "Commit failure prompt is empty. Update Source Control AI settings." }, "d481ab22f9": "Saved AI agent is unavailable. Use Customize launch to choose another agent.", - "9bbd9077a2": "No AI agents are shown. Choose which agents to show in Settings.", + "9bbd9077a2": "No AI agent is both installed and shown in Settings. Install one, or show one in Settings.", "5540ff50cc": "Could not build the agent launch command.", "216f762bd7": "Unable to resolve the workspace connection.", "success": "Started an AI agent for the {{value0}} failure." diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 9ac81456e3f4..1f0534822681 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -616,7 +616,7 @@ "822bf52295": "No se puede resolver la plataforma de inicio del espacio de trabajo.", "dfb4dd7c00": "No se encontró el espacio de trabajo adjunto a estos checks.", "9f00d7df0c": "El prompt de corrección de checks está vacío. Actualiza los ajustes de Source Control AI.", - "2ebf794906": "No se detectó en este host de espacio de trabajo ningún agente de IA mostrado en Configuración.", + "2ebf794906": "Ningún agente de IA de este host de espacio de trabajo está instalado y mostrado en Configuración a la vez. Instala uno o muestra uno en Configuración.", "4c7f783a7a": "El agente guardado para checks no está disponible en este host de espacio de trabajo." } } @@ -3415,7 +3415,7 @@ "ec2adf093e": "Iniciar {{value0}} en un nuevo terminal", "465e432ef1": "No se pudo generar el comando de inicio para {{value0}}.", "e518f544b1": "No se detectaron agentes", - "8dea9b5cdf": "No hay agentes para mostrar", + "8dea9b5cdf": "Todos los agentes están ocultos. Muestra uno en Configuración.", "needsServerUpdate": "Actualiza Orca en este servidor para ver los agentes de este espacio de trabajo" }, "RecentTabSwitcher": { @@ -13372,7 +13372,7 @@ "empty": "El prompt de fallo de commit está vacío. Actualiza la configuración de AI de Control de Código." }, "d481ab22f9": "El agente de AI guardado no está disponible. Usa Inicio personalizado para elegir otro agente.", - "9bbd9077a2": "No se muestra ningún agente de IA. Elige qué agentes mostrar en Configuración.", + "9bbd9077a2": "Ningún agente de IA está instalado y mostrado en Configuración a la vez. Instala uno o muestra uno en Configuración.", "5540ff50cc": "No se pudo construir el comando de inicio del agente.", "216f762bd7": "No se pudo resolver la conexión del espacio de trabajo.", "success": "Se inició un agente de AI para el fallo de {{value0}}." diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 5b61b1e0acf5..96e79fba72e0 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -717,7 +717,7 @@ "822bf52295": "Impossible de résoudre la plateforme de lancement de l'espace de travail.", "dfb4dd7c00": "Impossible de trouver l'espace de travail associé à ces vérifications.", "9f00d7df0c": "Le prompt de correction des checks est vide. Mettez à jour les paramètres d'IA du contrôle de code source.", - "2ebf794906": "Aucun agent IA affiché dans les paramètres n'a été détecté sur l'hôte de cet espace de travail.", + "2ebf794906": "Aucun agent IA de l'hôte de cet espace de travail n'est à la fois installé et affiché dans les paramètres. Installez-en un ou affichez-en un dans les paramètres.", "4c7f783a7a": "L'agent de checks enregistré n'est pas disponible sur l'hôte de cet espace de travail." } } @@ -3520,7 +3520,7 @@ "ec2adf093e": "Lancer {{value0}} dans un nouveau terminal", "465e432ef1": "Impossible de construire la commande de lancement pour {{value0}}.", "e518f544b1": "Aucun agent détecté", - "8dea9b5cdf": "Aucun agent à afficher", + "8dea9b5cdf": "Tous les agents sont masqués. Affichez-en un dans les paramètres.", "needsServerUpdate": "Mettez à jour Orca sur ce serveur pour lister les agents de cet espace de travail" }, "RecentTabSwitcher": { @@ -13478,7 +13478,7 @@ "empty": "Le prompt d'échec de commit est vide. Mettez à jour les paramètres IA du contrôle de code source." }, "d481ab22f9": "L'agent IA enregistré est indisponible. Utilisez « Personnaliser le lancement » pour choisir un autre agent.", - "9bbd9077a2": "Aucun agent IA n'est affiché. Choisissez les agents à afficher dans les paramètres.", + "9bbd9077a2": "Aucun agent IA n'est à la fois installé et affiché dans les paramètres. Installez-en un ou affichez-en un dans les paramètres.", "5540ff50cc": "Impossible de construire la commande de lancement de l'agent.", "216f762bd7": "Impossible de résoudre la connexion de l'espace de travail.", "success": "Un agent IA a été lancé pour l'échec de {{value0}}." diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index f07d16bdc652..46d9fd693d6b 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -616,7 +616,7 @@ "822bf52295": "ワークスペース起動プラットフォームを解決できません。", "dfb4dd7c00": "これらのチェックに関連付けられたワークスペースが見つかりません。", "9f00d7df0c": "チェックプロンプトが空です。ソース管理 AI 設定を更新してください。", - "2ebf794906": "[設定] で表示している AI Agent がこのワークスペースホストで検出されませんでした。", + "2ebf794906": "このワークスペースホストには、インストール済みで [設定] で表示している AI Agent がありません。いずれかをインストールするか、[設定] で表示してください。", "4c7f783a7a": "保存されたチェック Agent は、このワークスペースホストでは使用できません。" } } @@ -3415,7 +3415,7 @@ "ec2adf093e": "新規ターミナルで {{value0}} を起動", "465e432ef1": "{{value0}} の起動コマンドを作成できませんでした。", "e518f544b1": "Agent が検出されませんでした", - "8dea9b5cdf": "表示する Agent がありません", + "8dea9b5cdf": "すべての Agent が非表示です。[設定] でいずれかを表示してください。", "needsServerUpdate": "このワークスペースの Agent を表示するには、このサーバーの Orca を更新してください" }, "RecentTabSwitcher": { @@ -13372,7 +13372,7 @@ "empty": "コミット失敗プロンプトが空です。ソース管理の AI 設定を更新してください。" }, "d481ab22f9": "保存された AI Agent は使用できません。[起動のカスタマイズ] から別の Agent を選択してください。", - "9bbd9077a2": "表示されている AI Agent がありません。[設定] で表示する Agent を選択してください。", + "9bbd9077a2": "インストール済みで [設定] で表示している AI Agent がありません。いずれかをインストールするか、[設定] で表示してください。", "5540ff50cc": "Agent 起動コマンドを構築できませんでした。", "216f762bd7": "ワークスペースの接続を解決できません。", "success": "{{value0}}の失敗に対してAIAgent を起動しました。" diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index ea3f5928f5a2..985cbab0bce8 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -616,7 +616,7 @@ "822bf52295": "워크스페이스 시작 플랫폼을 확인할 수 없습니다.", "dfb4dd7c00": "이러한 검사에 연결된 워크스페이스를 찾을 수 없습니다.", "9f00d7df0c": "검사 프롬프트가 비어 있습니다. 소스 제어 AI 설정을 업데이트하세요.", - "2ebf794906": "설정에서 표시한 AI 에이전트가 이 워크스페이스 호스트에서 감지되지 않았습니다.", + "2ebf794906": "이 워크스페이스 호스트에는 설치되어 있고 설정에서 표시된 AI 에이전트가 없습니다. 하나를 설치하거나 설정에서 하나를 표시하세요.", "4c7f783a7a": "이 워크스페이스 호스트에서는 저장된 확인 agent를 사용할 수 없습니다." } } @@ -3415,7 +3415,7 @@ "ec2adf093e": "새 terminal에서 {{value0}} 실행", "465e432ef1": "{{value0}}에 대한 실행 명령을 작성할 수 없습니다.", "e518f544b1": "감지된 agents가 없습니다.", - "8dea9b5cdf": "표시할 에이전트가 없습니다.", + "8dea9b5cdf": "모든 에이전트가 숨겨져 있습니다. 설정에서 하나를 표시하세요.", "needsServerUpdate": "이 워크스페이스의 에이전트를 보려면 이 서버의 Orca를 업데이트하세요" }, "RecentTabSwitcher": { @@ -13372,7 +13372,7 @@ "empty": "커밋 실패 프롬프트가 비어 있습니다. 소스 컨트롤 AI 설정을 업데이트하세요." }, "d481ab22f9": "저장된 AI 에이전트를 사용할 수 없습니다. 사용자 지정 시작에서 다른 에이전트를 선택하세요.", - "9bbd9077a2": "표시된 AI 에이전트가 없습니다. 설정에서 표시할 에이전트를 선택하세요.", + "9bbd9077a2": "설치되어 있고 설정에서 표시된 AI 에이전트가 없습니다. 하나를 설치하거나 설정에서 하나를 표시하세요.", "5540ff50cc": "에이전트 시작 명령을 구성할 수 없습니다.", "216f762bd7": "워크트리 연결을 확인할 수 없습니다.", "success": "{{value0}} 실패에 대한 AI 에이전트가 시작되었습니다." diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 02e1f9535a36..88363b348e91 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -616,7 +616,7 @@ "822bf52295": "无法解析工作区启动平台。", "dfb4dd7c00": "无法找到附加到这些检查的工作区。", "9f00d7df0c": "检查提示为空。请更新源代码管理 AI 设置。", - "2ebf794906": "此工作区主机上未检测到设置中显示的 AI 智能体。", + "2ebf794906": "此工作区主机上没有既已安装又在设置中显示的 AI 智能体。请安装一个,或在设置中显示一个。", "4c7f783a7a": "保存的检查智能体在此工作区主机上不可用。" } } @@ -3415,7 +3415,7 @@ "ec2adf093e": "在新终端中启动 {{value0}}", "465e432ef1": "无法为 {{value0}} 构建启动命令。", "e518f544b1": "未检测到智能体", - "8dea9b5cdf": "没有可显示的智能体", + "8dea9b5cdf": "所有智能体都已隐藏。请在设置中显示一个。", "needsServerUpdate": "请更新此服务器上的 Orca 以列出此工作区的智能体" }, "RecentTabSwitcher": { @@ -13386,7 +13386,7 @@ "empty": "提交失败提示词为空。请更新源代码控制 AI 设置。" }, "d481ab22f9": "已保存的 AI 智能体不可用。请使用「自定义启动」选择其他智能体。", - "9bbd9077a2": "没有显示的 AI 智能体。请在设置中选择要显示的智能体。", + "9bbd9077a2": "没有既已安装又在设置中显示的 AI 智能体。请安装一个,或在设置中显示一个。", "5540ff50cc": "无法构建智能体启动命令。", "216f762bd7": "无法解析工作区连接。", "success": "已为 {{value0}} 失败启动 AI 智能体。" @@ -18711,7 +18711,7 @@ "selectAgent": "选择智能体", "detectingAgents": "正在此工作区主机上检测智能体…", "detectionFailed": "无法检测此工作区主机上的智能体。", - "noAgents": "未在此工作区主机上检测到设置中显示的智能体。", + "noAgents": "此工作区主机上没有找到设置中显示的智能体。", "context": "上下文", "modeFocused": "聚焦交接(推荐)", "modeFocusedDescription": "使用最新进度和当前工作区,仅在需要时读取更早的会话细节。", diff --git a/src/renderer/src/lib/fix-checks-agent-launch.ts b/src/renderer/src/lib/fix-checks-agent-launch.ts index 78734f8755bf..d2c46b7cb290 100644 --- a/src/renderer/src/lib/fix-checks-agent-launch.ts +++ b/src/renderer/src/lib/fix-checks-agent-launch.ts @@ -99,7 +99,7 @@ async function pickExistingWorktreeAgent( toast.error( translate( 'auto.lib.fix.checks.agent.launch.2ebf794906', - 'No AI agent shown in Settings was detected on this workspace host.' + 'No AI agent on this workspace host is both installed and shown in Settings. Install one, or show one in Settings.' ) ) } From eecf9da3662adb328fbe090b7bf3711f316f1952 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:06:26 -0700 Subject: [PATCH 6/6] fix(checks-panel): gate each AI action on its own saved agent A saved hidden agent for Fix checks no longer enables Resolve conflicts and Resolve comments, whose launches have no agent to start. --- .../checks-panel/active-content-props.ts | 3 +- .../checks-panel/active-content.tsx | 11 ++-- .../panel-content-rendering.test.tsx | 3 +- ...e-checks-panel-comment-resolution.test.tsx | 49 +++++++++++++++++ .../use-checks-panel-comment-resolution.tsx | 54 +++++++++---------- 5 files changed, 86 insertions(+), 34 deletions(-) create mode 100644 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.test.tsx diff --git a/src/renderer/src/components/right-sidebar/checks-panel/active-content-props.ts b/src/renderer/src/components/right-sidebar/checks-panel/active-content-props.ts index 12da56b47bf3..1614da3b0f5d 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/active-content-props.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel/active-content-props.ts @@ -55,11 +55,12 @@ export type ChecksPanelActiveContentModel = Pick< > & Pick< ChecksPanelCommentResolutionState, - | 'aiActionDisabledReason' | 'canTargetPRComments' | 'commentsDisabledReason' + | 'fixChecksDisabledReason' | 'handleResolve' | 'resolveCommentsWithAIDisabledReason' + | 'resolveConflictsDisabledReason' > & Pick< ChecksPanelAiAcknowledgementState, diff --git a/src/renderer/src/components/right-sidebar/checks-panel/active-content.tsx b/src/renderer/src/components/right-sidebar/checks-panel/active-content.tsx index a19899b69b92..0fc0e277029e 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/active-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/active-content.tsx @@ -48,7 +48,6 @@ export function ChecksPanelActiveContent({ activeWorktree, activeWorktreeId, agentComposerState, - aiActionDisabledReason, canTargetPRComments, checks, checksError, @@ -64,6 +63,7 @@ export function ChecksPanelActiveContent({ consumeClaimedCommentResolutionAfterDeliveryRef, detachedHeadDisplay, editingTitle, + fixChecksDisabledReason, getGitLabProjectRef, handleAddPRComment, handleCancelEdit, @@ -96,6 +96,7 @@ export function ChecksPanelActiveContent({ refreshHostedReviewAfterMutation, repo, resolveCommentsWithAIDisabledReason, + resolveConflictsDisabledReason, saveLaunchActionDefault, setAgentComposerState, setChecksPanelContentRef, @@ -222,12 +223,12 @@ export function ChecksPanelActiveContent({ checks={checks} isResolvingConflictsWithAI={isResolvingConflictsWithAI} onResolveConflictsWithAI={() => void handleResolveConflictsWithAI()} - resolveConflictsDisabled={Boolean(aiActionDisabledReason)} - resolveConflictsDisabledReason={aiActionDisabledReason} + resolveConflictsDisabled={Boolean(resolveConflictsDisabledReason)} + resolveConflictsDisabledReason={resolveConflictsDisabledReason} isFixingChecksWithAI={isFixingChecksWithAI} onFixChecksWithAI={() => void handleFixChecksWithAI()} - fixChecksDisabled={Boolean(aiActionDisabledReason)} - fixChecksDisabledReason={aiActionDisabledReason} + fixChecksDisabled={Boolean(fixChecksDisabledReason)} + fixChecksDisabledReason={fixChecksDisabledReason} /> )} {activeConflictReview && ( diff --git a/src/renderer/src/components/right-sidebar/checks-panel/panel-content-rendering.test.tsx b/src/renderer/src/components/right-sidebar/checks-panel/panel-content-rendering.test.tsx index bdb154300d84..a8d5263358eb 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/panel-content-rendering.test.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/panel-content-rendering.test.tsx @@ -115,7 +115,6 @@ describe('checks panel concrete content', () => { activeWorktree: null, activeWorktreeId: 'worktree-1', agentComposerState: null, - aiActionDisabledReason: undefined, canTargetPRComments: false, checks: [], checksError: null, @@ -131,6 +130,7 @@ describe('checks panel concrete content', () => { consumeClaimedCommentResolutionAfterDeliveryRef: { current: vi.fn() }, detachedHeadDisplay: null, editingTitle: false, + fixChecksDisabledReason: undefined, getGitLabProjectRef: vi.fn(() => null), handleAddPRComment: vi.fn(), handleCancelEdit: vi.fn(), @@ -163,6 +163,7 @@ describe('checks panel concrete content', () => { repo: null, refreshHostedReviewAfterMutation: vi.fn(), resolveCommentsWithAIDisabledReason: undefined, + resolveConflictsDisabledReason: undefined, saveLaunchActionDefault: vi.fn(), setAgentComposerState: vi.fn(), setChecksPanelContentRef: vi.fn(), diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.test.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.test.tsx new file mode 100644 index 000000000000..90f171d35fba --- /dev/null +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.test.tsx @@ -0,0 +1,49 @@ +// @vitest-environment happy-dom + +import { cleanup, renderHook } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' +import { useChecksPanelCommentResolution } from './use-checks-panel-comment-resolution' + +afterEach(cleanup) + +const NO_AGENT_REASON = + 'No AI agent is both installed and shown in Settings. Install one, or show one in Settings.' + +describe('useChecksPanelCommentResolution', () => { + it('keeps only the action with a saved hidden agent usable when every agent is hidden', () => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture supplies every field the hook reads; handlers are never called. + const model = { + activeGitLabReview: null, + activeReview: { provider: 'github', number: 42 }, + activeWorktree: null, + activeWorktreeId: 'wt-1', + branch: 'feature', + claimedCommentResolutionRef: { current: null }, + commentResolutionAckBusy: false, + commentResolutionLaunchAcceptedRef: { current: false }, + commentsLoading: false, + detectedAgentIds: ['claude', 'codex'], + isCurrentAsyncResult: () => true, + pendingCommentResolutionRef: { current: null }, + pr: null, + prCacheKey: 'pr-42', + prNumber: 42, + repo: { + id: 'repo-1', + path: '/repo', + sourceControlAi: { actionOverrides: { fixChecks: { agentId: 'codex' } } } + }, + resolveReviewThread: async () => true, + setAgentComposerState: () => {}, + setComments: () => {}, + settings: { defaultTuiAgent: null, disabledTuiAgents: ['claude', 'codex'] }, + sourceControlAiActionsVisible: true + } as unknown as Parameters[0] + + const { result } = renderHook(() => useChecksPanelCommentResolution(model)) + + expect(result.current.fixChecksDisabledReason).toBeUndefined() + expect(result.current.resolveConflictsDisabledReason).toBe(NO_AGENT_REASON) + expect(result.current.resolveCommentsWithAIDisabledReason).toBe(NO_AGENT_REASON) + }) +}) diff --git a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx index 0d56012c4ef1..a4a43813116a 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-comment-resolution.tsx @@ -1,8 +1,11 @@ import { forgeOwnerHostIdForWorkspace } from '@/runtime/forge-credential-target' import { useCallback, useEffect } from 'react' import { toast } from 'sonner' -import { pickDefaultSourceControlAgent } from '../SourceControl' -import { resolveSourceControlLaunchAgentScope } from '../../../../../shared/source-control-launch-agent-selection' +import { + pickSourceControlLaunchAgent, + resolveSourceControlLaunchAgentScope +} from '../../../../../shared/source-control-launch-agent-selection' +import type { SourceControlActionId } from '../../../../../shared/source-control-ai-actions' import { markPRCommentThreadResolved, restorePRCommentThreadSnapshot @@ -152,30 +155,27 @@ export function useChecksPanelCommentResolution(model: ChecksPanelCommentResolut const commentsDisabledReason = canTargetPRComments ? undefined : 'Commenting requires a GitHub PR repository target.' - // A saved action agent launches even when hidden from pickers, so it keeps the actions usable. - const savedAgentDetected = (['fixChecks', 'resolveConflicts', 'resolveComments'] as const).some( - (actionId) => { - const agent = resolveSourceControlLaunchAgentScope({ - settings, - repo, - actionId - }).effectiveAgentId - return agent !== null && Boolean(detectedAgentIds?.includes(agent)) + // A saved agent launches even when hidden from pickers, so it keeps only its own action usable. + const aiActionDisabledReason = (actionId: SourceControlActionId): string | undefined => { + if (!activeWorktreeId) { + return 'Select a workspace before launching an AI action.' } - ) - const noEnabledAgentKnown = - detectedAgentIds != null && - !savedAgentDetected && - pickDefaultSourceControlAgent( - settings?.defaultTuiAgent, - detectedAgentIds, - settings?.disabledTuiAgents - ) == null - const aiActionDisabledReason = !activeWorktreeId - ? 'Select a workspace before launching an AI action.' - : noEnabledAgentKnown + const noAgentToLaunch = + detectedAgentIds != null && + pickSourceControlLaunchAgent({ + savedAgent: resolveSourceControlLaunchAgentScope({ settings, repo, actionId }) + .effectiveAgentId, + defaultAgent: settings?.defaultTuiAgent, + detectedAgents: detectedAgentIds, + disabledAgents: settings?.disabledTuiAgents + }) == null + return noAgentToLaunch ? 'No AI agent is both installed and shown in Settings. Install one, or show one in Settings.' : undefined + } + const fixChecksDisabledReason = aiActionDisabledReason('fixChecks') + const resolveConflictsDisabledReason = aiActionDisabledReason('resolveConflicts') + const resolveCommentsAgentDisabledReason = aiActionDisabledReason('resolveComments') useEffect(() => { if (!sourceControlAiActionsVisible) { setAgentComposerState(null) @@ -195,8 +195,8 @@ export function useChecksPanelCommentResolution(model: ChecksPanelCommentResolut ? 'Still finishing the previous comment launch.' : commentsLoading ? 'Comments are still loading.' - : aiActionDisabledReason - ? aiActionDisabledReason + : resolveCommentsAgentDisabledReason + ? resolveCommentsAgentDisabledReason : !activeReview ? 'Open a PR or MR before launching an AI action.' : !repo @@ -211,8 +211,8 @@ export function useChecksPanelCommentResolution(model: ChecksPanelCommentResolut canTargetPRComments, commentsDisabledReason, detectedAgentsForAI: detectedAgentIds, - noEnabledAgentKnown, - aiActionDisabledReason, + fixChecksDisabledReason, + resolveConflictsDisabledReason, resolveCommentsWithAIDisabledReason } }