diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index b2b86ec33d08..75852aa86add 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/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 2e8547f057c6..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 enabled AI agent 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/config/scripts/vitest-sqlite-runtime-files.mjs b/config/scripts/vitest-sqlite-runtime-files.mjs index cd058eb9c5b4..af1e37d40132 100644 --- a/config/scripts/vitest-sqlite-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-runtime-files.mjs @@ -268,7 +268,6 @@ export const SQLITE_RUNTIME_INCLUDE = [ 'tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts', 'tests/e2e/cross-version-wire/submission-positions-downgrade.unit.test.ts', 'tests/e2e/folder-upgrade-identity-persistence.unit.test.ts', - 'tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts', 'tests/e2e/structured-agent-session-read-owner.unit.test.ts', 'tests/e2e/structured-chat-owner-status-activation.unit.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/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/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/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..762d7cd61767 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 = { @@ -36,7 +26,7 @@ type InstallOptions = { userInitiated?: boolean shouldHydrateShellPath?: boolean onInstallError?: (agent: AgentHookTarget, error: unknown) => void - shouldContinue?: (agent: AgentHookTarget) => boolean + shouldContinue?: () => boolean agents?: readonly AgentHookTarget[] } @@ -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,21 +141,13 @@ 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)) { + if (options.shouldContinue && !options.shouldContinue()) { results.push( skippedStatus( agent, @@ -203,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) { @@ -256,21 +229,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..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,11 +103,7 @@ async function installGuestHooksAndPlugins( if (state.mux !== mux || mux.isDisposed()) { return } - const currentSettings = deps.managedHookSettings() - const settings = currentSettings && { - ...currentSettings, - disabledTuiAgents: currentSettings.disabledTuiAgents?.slice() - } + const settings = deps.managedHookSettings() 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 247d676da8fc..65a2c08d8f30 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 1043bd4551e4..e350d8d542b2 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 14c15704fd97..0209d201fab8 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' @@ -97,55 +97,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', @@ -473,17 +424,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..788ba1b56de8 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/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/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..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 @@ -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,7 +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. + // Why: the fallback keeps the tab's agent identity; only the startup command is skipped. expect(activated.tabs[0]).toMatchObject({ type: 'terminal', status: 'ready', 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.ts b/src/main/runtime/runtime-agent-launch-resolution.ts index 9f5ddd4bd0a6..9835bf19b2ce 100644 --- a/src/main/runtime/runtime-agent-launch-resolution.ts +++ b/src/main/runtime/runtime-agent-launch-resolution.ts @@ -6,7 +6,6 @@ import type { ClaudeAgentTeamsMode } from '../../shared/claude-agent-teams-tmux- 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' export function mergeTerminalEnvDeletionKeys( first: readonly string[] | undefined, @@ -52,7 +51,6 @@ export function resolveBareAgentLaunchCommand(args: { command: string | undefined settings: { agentCmdOverrides?: Partial> | null - disabledTuiAgents?: Iterable | null } platform: NodeJS.Platform isRemote: boolean @@ -63,10 +61,8 @@ export function resolveBareAgentLaunchCommand(args: { } const cmdOverrides = args.settings.agentCmdOverrides ?? {} + // 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[]) { - if (!isTuiAgentEnabled(agent, args.settings.disabledTuiAgents)) { - continue - } const override = cmdOverrides[agent]?.trim() const defaultLaunchCommand = getTuiAgentLaunchCommand(TUI_AGENT_CONFIG[agent], args.platform, { isRemote: args.isRemote 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-agent-launch-settings-durability.test.ts b/src/main/runtime/runtime-client-agent-launch-settings-durability.test.ts index 0ea3f8f018cc..3504c6215aa8 100644 --- a/src/main/runtime/runtime-client-agent-launch-settings-durability.test.ts +++ b/src/main/runtime/runtime-client-agent-launch-settings-durability.test.ts @@ -1,11 +1,7 @@ import { describe, expect, it, vi } from 'vitest' -import { - deferred, - fixture -} from '../persistence/loading-store/profile-state-delayed-authority-fixture' +import { fixture } from '../persistence/loading-store/profile-state-delayed-authority-fixture' import { RuntimeClientSettingsController } from './runtime-client-settings' -const hooks = vi.hoisted(() => ({ apply: vi.fn(async () => {}) })) vi.mock('../telemetry/client', () => ({ track: vi.fn() })) vi.mock('../telemetry/cohort-classifier', () => ({ getCohortAtEmit: () => ({ nth_repo_added: 2 }) @@ -14,79 +10,8 @@ vi.mock('../ssh/ssh-config-parser', () => ({ loadUserSshConfig: () => ({ hosts: [] }), sshConfigHostsToTargets: () => [] })) -vi.mock('../agent-hooks/managed-agent-hook-controls', () => ({ - applyAgentStatusHooksEnabled: hooks.apply -})) describe('durable execution-host launch settings', () => { - it('acknowledges a committed availability edit and its queued follow-up while hooks are pending', async () => { - const { store, readState } = await fixture() - const controller = new RuntimeClientSettingsController(store) - const started = deferred() - const finish = deferred() - hooks.apply.mockImplementationOnce(async () => { - started.resolve() - await finish.promise - }) - let acknowledged = false - let followupAcknowledged = false - const saving = controller - .mutateAgentLaunch({ type: 'availability', agent: 'claude', enabled: false }) - .then((settings) => { - acknowledged = true - return settings - }) - const followup = saving - .then(() => - controller.mutateAgentLaunch({ type: 'arguments', agent: 'codex', value: '--followup' }) - ) - .then(() => { - followupAcknowledged = true - }) - try { - await started.promise - expect(readState().settings.disabledTuiAgents).toContain('claude') - await vi.waitFor(() => { - expect(acknowledged).toBe(true) - expect(followupAcknowledged).toBe(true) - }) - expect(readState().settings.agentDefaultArgs.codex).toBe('--followup') - } finally { - finish.resolve() - await saving - await followup - } - }) - - it('reports hook failure separately without rejecting durable settings or blocking another save', async () => { - const { store, readState } = await fixture() - const controller = new RuntimeClientSettingsController(store) - const error = new Error('hook-filesystem-unavailable') - const reported = vi.spyOn(console, 'error').mockImplementation(() => {}) - hooks.apply.mockRejectedValueOnce(error) - try { - await expect( - controller.mutateAgentLaunch({ type: 'availability', agent: 'claude', enabled: false }) - ).resolves.toMatchObject({ disabledTuiAgents: ['claude'] }) - await controller.mutateAgentLaunch({ - type: 'arguments', - agent: 'codex', - value: '--after-failure' - }) - expect(readState().settings.agentDefaultArgs.codex).toBe('--after-failure') - await vi.waitFor(() => - expect(reported).toHaveBeenCalledWith( - '[agent-hooks] Failed to reconcile managed hooks after saving launch settings:', - error - ) - ) - await controller.mutateAgentLaunch({ type: 'availability', agent: 'claude', enabled: true }) - expect(readState().settings.disabledTuiAgents).not.toContain('claude') - } finally { - reported.mockRestore() - } - }) - it('acknowledges a settings change only after the real database write finishes', async () => { const { store, authority, readState } = await fixture() const controller = new RuntimeClientSettingsController(store) diff --git a/src/main/runtime/runtime-client-settings.ts b/src/main/runtime/runtime-client-settings.ts index 68a2169980c6..e9c82f3831de 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' import { agentLaunchSettingsMutationUpdates, @@ -165,18 +164,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() } @@ -209,12 +204,7 @@ export class RuntimeClientSettingsController { applySettings(updates, { notifyListeners: true }) const applied = store.getSettings() return { - value: { - settings: projectAgentLaunchSettings(applied), - reconcileHooks: - updates.disabledTuiAgents !== undefined && - !haveSameDisabledTuiAgents(before.disabledTuiAgents, applied.disabledTuiAgents) - }, + value: projectAgentLaunchSettings(applied), rollback: () => { const rollback = agentLaunchSettingsRollbackUpdates( before, @@ -228,15 +218,7 @@ export class RuntimeClientSettingsController { } } }) - if (committed.reconcileHooks) { - void this.reconcileManagedAgentHooks().catch((error) => { - console.error( - '[agent-hooks] Failed to reconcile managed hooks after saving launch settings:', - error - ) - }) - } - return committed.settings + return committed } updateTerminalQuickCommands(mutation: TerminalQuickCommandMutation): TerminalQuickCommand[] { @@ -294,9 +276,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 f60345640ac7..3351f5e13cfe 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/host-env/types' 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 64f7ea41f386..549f47fc76f9 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/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/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.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/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/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/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/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 b3babe0c0093..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,7 +1,11 @@ import { forgeOwnerHostIdForWorkspace } from '@/runtime/forge-credential-target' import { useCallback, useEffect } from 'react' import { toast } from 'sonner' -import { pickDefaultSourceControlAgent } from '../SourceControl' +import { + pickSourceControlLaunchAgent, + resolveSourceControlLaunchAgentScope +} from '../../../../../shared/source-control-launch-agent-selection' +import type { SourceControlActionId } from '../../../../../shared/source-control-ai-actions' import { markPRCommentThreadResolved, restorePRCommentThreadSnapshot @@ -151,18 +155,27 @@ export function useChecksPanelCommentResolution(model: ChecksPanelCommentResolut const commentsDisabledReason = canTargetPRComments ? undefined : 'Commenting requires a GitHub PR repository target.' - const noEnabledAgentKnown = - detectedAgentIds != null && - pickDefaultSourceControlAgent( - settings?.defaultTuiAgent, - detectedAgentIds, - settings?.disabledTuiAgents - ) == null - const aiActionDisabledReason = !activeWorktreeId - ? 'Select a workspace before launching an AI action.' - : noEnabledAgentKnown - ? 'No enabled AI agents. Configure agents in Settings.' + // 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 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) @@ -182,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 @@ -198,8 +211,8 @@ export function useChecksPanelCommentResolution(model: ChecksPanelCommentResolut canTargetPRComments, commentsDisabledReason, detectedAgentsForAI: detectedAgentIds, - noEnabledAgentKnown, - aiActionDisabledReason, + fixChecksDisabledReason, + resolveConflictsDisabledReason, resolveCommentsWithAIDisabledReason } } 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..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 @@ -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,13 +87,13 @@ 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.' } - if (!hasEnabledAgents && !detecting) { - return 'No enabled agents were detected on this workspace host.' + if (!selectedAgent && !hasEnabledAgents && !detecting) { + 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 a16e37230739..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 @@ -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 @@ -58,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 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', @@ -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/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/renderer/src/components/settings/AgentCatalogRow.tsx b/src/renderer/src/components/settings/AgentCatalogRow.tsx index 5197cae394f3..e5557e972c35 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') } ]} /> @@ -117,7 +117,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 c0f9e8796917..79cbd9f86a90 100644 --- a/src/renderer/src/components/settings/AgentsPane.test.tsx +++ b/src/renderer/src/components/settings/AgentsPane.test.tsx @@ -544,9 +544,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.') @@ -568,7 +568,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..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 enabled agents' + 'All agents are hidden. Show one in Settings.' ) : translate( 'auto.components.tab.bar.QuickLaunchButton.e518f544b1', 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 9494d8958ead..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 enabled AI agent 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." } } @@ -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." } } @@ -3655,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": "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" }, @@ -6989,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", @@ -13270,7 +13269,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.", @@ -13669,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 enabled AI agents. Configure agents 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." @@ -16790,7 +16789,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.", @@ -19014,7 +19012,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.", @@ -19024,7 +19022,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 57d481de1747..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ó ningún agente de IA habilitado en este host de espacio de trabajo.", + "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." } } @@ -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." } } @@ -3416,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": "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": { @@ -6720,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", @@ -12975,7 +12974,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.", @@ -13373,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 hay agentes de AI habilitados. Configura agentes 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}}." @@ -16517,7 +16516,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.", @@ -18699,7 +18697,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.", @@ -18709,7 +18707,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 39212ceb7104..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 activé 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." } } @@ -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." } } @@ -3521,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": "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": { @@ -6830,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", @@ -13081,7 +13080,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.", @@ -13479,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 activé. Configurez les agents 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}}." @@ -16594,7 +16593,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.", @@ -18801,7 +18799,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.", @@ -18811,7 +18809,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 cb52eb3bcaf7..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 は、このワークスペースホストでは使用できません。" } } @@ -749,7 +749,6 @@ "3f0ea9aa0d": "Agent 起動コマンドを構築できませんでした。", "46f1a2c9bd": "コマンド入力が空です。", "8eb541cc83": "選択した Agent はこのワークスペースホストで検出されませんでした。", - "b96e091fc9": "選択した Agent は設定で無効になっています。", "a7ac8717c7": "開始する前に、Agent を選択してください。" } } @@ -3416,7 +3415,7 @@ "ec2adf093e": "新規ターミナルで {{value0}} を起動", "465e432ef1": "{{value0}} の起動コマンドを作成できませんでした。", "e518f544b1": "Agent が検出されませんでした", - "8dea9b5cdf": "有効な Agent がありません", + "8dea9b5cdf": "すべての Agent が非表示です。[設定] でいずれかを表示してください。", "needsServerUpdate": "このワークスペースの Agent を表示するには、このサーバーの Orca を更新してください" }, "RecentTabSwitcher": { @@ -6705,13 +6704,13 @@ "f9f127d664": "バイナリのパスまたは名前を上書きし、この Agent の既定の起動引数や環境変数を編集します。", "f95b5c79b8": "インストール", "fe4d630c94": "ドキュメント", - "8dc0192e48": "無効", + "8dc0192e48": "非表示", "df123171d1": "未インストール", "c8794e622e": "検出されました", "5200dac9da": "リセット", "2e45ca29b6": "コマンド", - "d4d2a45d63": "有効", - "1c9a9679ec": "{{value0}} の利用可否", + "d4d2a45d63": "表示", + "1c9a9679ec": "{{value0}} の表示", "0d9e293a02": "更新", "c9b33eb5c0": "更新中…", "13647f9f80": "シェルの PATH を再読み取り、インストールされている Agent を再検出します", @@ -12967,7 +12966,7 @@ "15c5d85706": "Agent", "3e8f21954f": "エラー", "74168d7ada": "idle", - "1d47db9bf0": "有効な Agent がありません", + "1d47db9bf0": "表示する Agent がありません", "c7ff8cef11": "Agent を検出中…", "013c9ac04a": "保存対象", "1bb611240f": "Orca の既定のプロンプトを使うには、{basePrompt} を指定します。", @@ -13373,7 +13372,7 @@ "empty": "コミット失敗プロンプトが空です。ソース管理の AI 設定を更新してください。" }, "d481ab22f9": "保存された AI Agent は使用できません。[起動のカスタマイズ] から別の Agent を選択してください。", - "9bbd9077a2": "有効な AI Agent がありません。[設定] で Agent を構成してください。", + "9bbd9077a2": "インストール済みで [設定] で表示している AI Agent がありません。いずれかをインストールするか、[設定] で表示してください。", "5540ff50cc": "Agent 起動コマンドを構築できませんでした。", "216f762bd7": "ワークスペースの接続を解決できません。", "success": "{{value0}}の失敗に対してAIAgent を起動しました。" @@ -16517,7 +16516,6 @@ "08efc3ae12": "Hermes オートメーションが更新されました。", "e431bb85d4": "この Hermes オートメーションと同じホスト上のワークスペースを選択します。", "32534e7c9c": "保存する前に、使用可能なワークスペースを選択してください。", - "2360ffc956": "保存する前に、有効な Agent を選択してください。", "6e91dab317": "保存する前に、有効な詳細スケジュールを入力してください。", "64bdb2304f": "保存する前に、サポートされているスケジュールを選択してください。", "2430fecf53": "実行場所を選択し、保存する前にプロンプ​​トを入力します。", @@ -18734,7 +18732,7 @@ "selectAgent": "Agent を選択", "detectingAgents": "このワークスペースホストで Agent を検出中…", "detectionFailed": "このワークスペースホストで Agent を検出できませんでした。", - "noAgents": "このワークスペースホストで有効な Agent が検出されませんでした。", + "noAgents": "[設定] で表示している Agent がこのワークスペースホストで検出されませんでした。", "context": "コンテキスト", "modeFocused": "要点を引き継ぐ(推奨)", "modeFocusedDescription": "最新の状態と現在のワークスペースを使い、必要な場合だけ過去の詳細を読みます。", @@ -18744,7 +18742,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 4aa83f430d06..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 agent가 감지되지 않았습니다.", + "2ebf794906": "이 워크스페이스 호스트에는 설치되어 있고 설정에서 표시된 AI 에이전트가 없습니다. 하나를 설치하거나 설정에서 하나를 표시하세요.", "4c7f783a7a": "이 워크스페이스 호스트에서는 저장된 확인 agent를 사용할 수 없습니다." } } @@ -749,7 +749,6 @@ "3f0ea9aa0d": "agent 시작 명령을 만들 수 없습니다.", "46f1a2c9bd": "명령 입력이 비어 있습니다.", "8eb541cc83": "선택한 agent가 이 워크스페이스 호스트에서 감지되지 않았습니다.", - "b96e091fc9": "선택한 agent는 설정에서 비활성화되어 있습니다.", "a7ac8717c7": "시작하기 전에 agent를 선택하세요." } } @@ -3416,7 +3415,7 @@ "ec2adf093e": "새 terminal에서 {{value0}} 실행", "465e432ef1": "{{value0}}에 대한 실행 명령을 작성할 수 없습니다.", "e518f544b1": "감지된 agents가 없습니다.", - "8dea9b5cdf": "활성화된 agents가 없습니다.", + "8dea9b5cdf": "모든 에이전트가 숨겨져 있습니다. 설정에서 하나를 표시하세요.", "needsServerUpdate": "이 워크스페이스의 에이전트를 보려면 이 서버의 Orca를 업데이트하세요" }, "RecentTabSwitcher": { @@ -6705,13 +6704,13 @@ "f9f127d664": "이 agent의 바이너리 경로나 이름을 재정의하고 기본 실행 인수 또는 환경을 편집합니다.", "f95b5c79b8": "설치", "fe4d630c94": "문서", - "8dc0192e48": "비활성", + "8dc0192e48": "숨김", "df123171d1": "설치되지 않음", "c8794e622e": "감지됨", "5200dac9da": "재설정", "2e45ca29b6": "명령", - "d4d2a45d63": "활성화됨", - "1c9a9679ec": "{{value0}} 사용 가능 여부", + "d4d2a45d63": "표시", + "1c9a9679ec": "{{value0}} 표시 여부", "0d9e293a02": "새로고침", "c9b33eb5c0": "새로고침 중…", "13647f9f80": "셸 PATH를 다시 ​​읽고 설치된 agents를 다시 검색하십시오.", @@ -12967,7 +12966,7 @@ "15c5d85706": "에이전트", "3e8f21954f": "오류", "74168d7ada": "idle", - "1d47db9bf0": "활성화된 agents가 없습니다.", + "1d47db9bf0": "표시할 에이전트가 없습니다.", "c7ff8cef11": "agents 감지 중...", "013c9ac04a": "저장 대상", "1bb611240f": "Orca 기본 프롬프트를 사용하려면 {basePrompt}를 사용하세요.", @@ -13373,7 +13372,7 @@ "empty": "커밋 실패 프롬프트가 비어 있습니다. 소스 컨트롤 AI 설정을 업데이트하세요." }, "d481ab22f9": "저장된 AI 에이전트를 사용할 수 없습니다. 사용자 지정 시작에서 다른 에이전트를 선택하세요.", - "9bbd9077a2": "활성화된 AI 에이전트가 없습니다. 설정에서 에이전트를 구성하세요.", + "9bbd9077a2": "설치되어 있고 설정에서 표시된 AI 에이전트가 없습니다. 하나를 설치하거나 설정에서 하나를 표시하세요.", "5540ff50cc": "에이전트 시작 명령을 구성할 수 없습니다.", "216f762bd7": "워크트리 연결을 확인할 수 없습니다.", "success": "{{value0}} 실패에 대한 AI 에이전트가 시작되었습니다." @@ -16517,7 +16516,6 @@ "08efc3ae12": "Hermes 자동화가 업데이트되었습니다.", "e431bb85d4": "이 Hermes 자동화와 동일한 호스트에서 워크스페이스를 선택하세요.", "32534e7c9c": "저장하기 전에 사용 가능한 워크스페이스를 선택하세요.", - "2360ffc956": "저장하기 전에 활성화된 agent를 선택하세요.", "6e91dab317": "저장하기 전에 유효한 고급 일정을 입력하세요.", "64bdb2304f": "저장하기 전에 지원되는 일정을 선택하세요.", "2430fecf53": "실행 위치를 선택하고 저장하기 전에 프롬프트를 입력하세요.", @@ -18734,7 +18732,7 @@ "selectAgent": "에이전트 선택", "detectingAgents": "이 워크스페이스 호스트에서 에이전트를 찾는 중…", "detectionFailed": "이 워크스페이스 호스트에서 에이전트를 찾을 수 없습니다.", - "noAgents": "이 워크스페이스 호스트에서 활성화된 에이전트를 찾지 못했습니다.", + "noAgents": "설정에서 표시한 에이전트를 이 워크스페이스 호스트에서 찾지 못했습니다.", "context": "컨텍스트", "modeFocused": "핵심 컨텍스트 전달(권장)", "modeFocusedDescription": "최신 상태와 현재 워크스페이스를 사용하고 필요한 경우에만 이전 세부 정보를 읽습니다.", @@ -18744,7 +18742,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 cb86cd624c68..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": "保存的检查智能体在此工作区主机上不可用。" } } @@ -749,7 +749,6 @@ "3f0ea9aa0d": "无法构建智能体启动命令。", "46f1a2c9bd": "提示词输入为空。", "8eb541cc83": "此工作区主机上未检测到所选智能体。", - "b96e091fc9": "选定的智能体在“设置”中被禁用。", "a7ac8717c7": "在开始之前选择一个智能体。" } } @@ -3416,7 +3415,7 @@ "ec2adf093e": "在新终端中启动 {{value0}}", "465e432ef1": "无法为 {{value0}} 构建启动命令。", "e518f544b1": "未检测到智能体", - "8dea9b5cdf": "没有启用智能体", + "8dea9b5cdf": "所有智能体都已隐藏。请在设置中显示一个。", "needsServerUpdate": "请更新此服务器上的 Orca 以列出此工作区的智能体" }, "RecentTabSwitcher": { @@ -6705,13 +6704,13 @@ "f9f127d664": "覆盖二进制路径或名称,并编辑此智能体的默认启动参数或环境。", "f95b5c79b8": "安装", "fe4d630c94": "文档", - "8dc0192e48": "已禁用", + "8dc0192e48": "隐藏", "df123171d1": "未安装", "c8794e622e": "已检测", "5200dac9da": "重置", "2e45ca29b6": "命令", - "d4d2a45d63": "启用", - "1c9a9679ec": "{{value0}} 可用性", + "d4d2a45d63": "显示", + "1c9a9679ec": "{{value0}} 可见性", "0d9e293a02": "刷新", "c9b33eb5c0": "刷新中…", "13647f9f80": "重新读取 shell PATH 并重新检测已安装的智能体", @@ -12981,7 +12980,7 @@ "15c5d85706": "智能体", "3e8f21954f": "错误", "74168d7ada": "空闲", - "1d47db9bf0": "没有启用智能体", + "1d47db9bf0": "没有可显示的智能体", "c7ff8cef11": "检测剂...", "013c9ac04a": "保存范围", "1bb611240f": "使用 {basePrompt} 作为 Orca 的默认提示词。", @@ -13387,7 +13386,7 @@ "empty": "提交失败提示词为空。请更新源代码控制 AI 设置。" }, "d481ab22f9": "已保存的 AI 智能体不可用。请使用「自定义启动」选择其他智能体。", - "9bbd9077a2": "没有已启用的 AI 智能体。请在设置中配置智能体。", + "9bbd9077a2": "没有既已安装又在设置中显示的 AI 智能体。请安装一个,或在设置中显示一个。", "5540ff50cc": "无法构建智能体启动命令。", "216f762bd7": "无法解析工作区连接。", "success": "已为 {{value0}} 失败启动 AI 智能体。" @@ -16531,7 +16530,6 @@ "08efc3ae12": "Hermes 自动化已更新。", "e431bb85d4": "选择与此 Hermes 自动化位于同一主机上的工作区。", "32534e7c9c": "保存前选择一个可用的工作区。", - "2360ffc956": "保存前选择已启用的智能体。", "6e91dab317": "保存前输入有效的高级计划。", "64bdb2304f": "保存前选择支持的计划。", "2430fecf53": "选择运行位置并在保存前输入提示。", @@ -18713,7 +18711,7 @@ "selectAgent": "选择智能体", "detectingAgents": "正在此工作区主机上检测智能体…", "detectionFailed": "无法检测此工作区主机上的智能体。", - "noAgents": "未在此工作区主机上检测到已启用的智能体。", + "noAgents": "此工作区主机上没有找到设置中显示的智能体。", "context": "上下文", "modeFocused": "聚焦交接(推荐)", "modeFocusedDescription": "使用最新进度和当前工作区,仅在需要时读取更早的会话细节。", @@ -18723,7 +18721,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..d2c46b7cb290 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( @@ -107,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 on this workspace host is both installed and shown in Settings. Install one, or show one in Settings.' ) ) } 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/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/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.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 { 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 6656b8daa924..ac44cc6ed4f6 100644 --- a/src/shared/tui-agent-selection.ts +++ b/src/shared/tui-agent-selection.ts @@ -90,12 +90,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) } diff --git a/tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts b/tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts deleted file mode 100644 index 590d9936c469..000000000000 --- a/tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -// @vitest-environment happy-dom -import { createElement } from 'react' -import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { - deferred, - fixture -} from '../../src/main/persistence/loading-store/profile-state-delayed-authority-fixture' -import { RuntimeClientSettingsController } from '../../src/main/runtime/runtime-client-settings' -import { HostAgentLaunchSettings } from '../../src/renderer/src/components/settings/HostAgentLaunchSettings' -import { TooltipProvider } from '../../src/renderer/src/components/ui/tooltip' -import { getAgentCatalog } from '../../src/renderer/src/lib/agent-catalog' - -const transport = vi.hoisted(() => ({ read: vi.fn(), mutate: vi.fn() })) -const hooks = vi.hoisted(() => ({ apply: vi.fn(async () => {}) })) -vi.mock('../../src/main/telemetry/client', () => ({ track: vi.fn() })) -vi.mock('../../src/main/telemetry/cohort-classifier', () => ({ - getCohortAtEmit: () => ({ nth_repo_added: 2 }) -})) -vi.mock('../../src/main/ssh/ssh-config-parser', () => ({ - loadUserSshConfig: () => ({ hosts: [] }), - sshConfigHostsToTargets: () => [] -})) -vi.mock('../../src/main/agent-hooks/managed-agent-hook-controls', () => ({ - applyAgentStatusHooksEnabled: hooks.apply -})) -vi.mock('../../src/renderer/src/components/settings/agent-launch-settings-transport', () => ({ - readHostAgentLaunchSettings: transport.read, - mutateHostAgentLaunchSettings: transport.mutate -})) -vi.mock('@/hooks/useDetectedAgents', () => ({ - useDetectedAgents: () => ({ - detectedIds: ['claude'], - detectionFailed: false, - isRefreshing: false, - refresh: vi.fn() - }) -})) - -afterEach(cleanup) -describe('host form and committed-settings contract', () => { - it('commits a following form edit while availability hook bookkeeping is still pending', async () => { - const { store, readState } = await fixture() - const controller = new RuntimeClientSettingsController(store) - await controller.mutateAgentLaunch({ - type: 'arguments', - agent: 'claude', - value: '--host-marker' - }) - transport.read.mockResolvedValue(controller.getAgentLaunch()) - transport.mutate.mockImplementation((_owner, mutation) => - controller.mutateAgentLaunch(mutation) - ) - const started = deferred() - const finish = deferred() - hooks.apply.mockImplementationOnce(async () => { - started.resolve() - await finish.promise - }) - render( - createElement( - TooltipProvider, - {}, - createElement(HostAgentLaunchSettings, { - environmentId: 'ssh-host', - pairingRevision: 17, - hostName: 'SSH host', - renderPermissions: () => createElement('span') - }) - ) - ) - await screen.findByDisplayValue('--host-marker') - const label = getAgentCatalog().find((agent) => agent.id === 'claude')?.label - fireEvent.click( - within(screen.getByRole('radiogroup', { name: `${label} availability` })).getByRole('radio', { - name: 'Disabled' - }) - ) - try { - await started.promise - expect(readState().settings.disabledTuiAgents).toContain('claude') - const args = screen.getByDisplayValue('--host-marker') - fireEvent.change(args, { target: { value: '--unblocked-followup' } }) - fireEvent.blur(args) - await waitFor(() => - expect(readState().settings.agentDefaultArgs.claude).toBe('--unblocked-followup') - ) - await waitFor(() => - expect(screen.getByDisplayValue('--unblocked-followup')).toHaveProperty('readOnly', false) - ) - expect(transport.mutate).toHaveBeenCalledTimes(2) - expect(screen.queryByRole('alert')).toBeNull() - } finally { - finish.resolve() - await waitFor(() => - expect(readState().settings.agentDefaultArgs.claude).toBe('--unblocked-followup') - ) - } - }) -})