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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/scripts/ci-shard-timings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion config/scripts/locale-ja-value-overrides.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 を選択してください。',
Expand Down
8 changes: 5 additions & 3 deletions config/scripts/locale-key-overrides.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: '已检测',
Expand Down
4 changes: 2 additions & 2 deletions config/scripts/locale-value-overrides.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.':
Expand Down
1 change: 0 additions & 1 deletion config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
]
8 changes: 4 additions & 4 deletions src/cli/handlers/agent-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function legacyProfileStateLocation(): ProfileStateOfflineLocation {
}

async function readHookSettingsFromDisk(): Promise<
Pick<GlobalSettings, 'agentStatusHooksEnabled' | 'disabledTuiAgents'>
Pick<GlobalSettings, 'agentStatusHooksEnabled'>
> {
const { acquireProfileStateRuntimeAdmission } =
await import('../../main/persistence/profile-state/profile-state-access.js')
Expand All @@ -57,7 +57,7 @@ async function readHookSettingsFromDisk(): Promise<
}

async function readAdmittedHookSettingsFromDisk(): Promise<
Pick<GlobalSettings, 'agentStatusHooksEnabled' | 'disabledTuiAgents'>
Pick<GlobalSettings, 'agentStatusHooksEnabled'>
> {
const { readAgentHookSettingsFromProfileState } =
await import('../../main/persistence/profile-state/profile-state-offline-settings.js')
Expand All @@ -68,7 +68,7 @@ async function readAdmittedHookSettingsFromDisk(): Promise<

async function updateEnabledOnDisk(enabled: boolean): Promise<{
settingsPath: string
settings: Pick<GlobalSettings, 'agentCmdOverrides' | 'disabledTuiAgents'>
settings: Pick<GlobalSettings, 'agentCmdOverrides'>
}> {
const { assertOfflineProfileStateMutationRuntime } =
await import('../../main/persistence/profile-state/profile-state-offline-settings.js')
Expand All @@ -86,7 +86,7 @@ async function updateEnabledOnDisk(enabled: boolean): Promise<{

async function updateAdmittedEnabledOnDisk(enabled: boolean): Promise<{
settingsPath: string
settings: Pick<GlobalSettings, 'agentCmdOverrides' | 'disabledTuiAgents'>
settings: Pick<GlobalSettings, 'agentCmdOverrides'>
}> {
const { updateAgentHookSettingsFromProfileState, readAgentHookSettingsFromProfileState } =
await import('../../main/persistence/profile-state/profile-state-offline-settings.js')
Expand Down
5 changes: 1 addition & 4 deletions src/cli/runtime-client-deferral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/cli/specs/orchestration-worker-specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
69 changes: 10 additions & 59 deletions src/main/agent-hooks/managed-agent-hook-controls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { state: 'found' }>) => void) | undefined
mocks.detect.mockImplementation(
Expand All @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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)
})
})
67 changes: 12 additions & 55 deletions src/main/agent-hooks/managed-agent-hook-controls.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -22,21 +15,18 @@ 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<GlobalSettings, 'agentCmdOverrides' | 'agentStatusHooksEnabled' | 'disabledTuiAgents'>
Pick<GlobalSettings, 'agentCmdOverrides' | 'agentStatusHooksEnabled'>
> | null

type InstallOptions = {
/** Set only for an explicit user action, never for startup reconciliation. */
userInitiated?: boolean
shouldHydrateShellPath?: boolean
onInstallError?: (agent: AgentHookTarget, error: unknown) => void
shouldContinue?: (agent: AgentHookTarget) => boolean
shouldContinue?: () => boolean
agents?: readonly AgentHookTarget[]
}

Expand All @@ -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 {
Expand Down Expand Up @@ -141,9 +130,7 @@ export async function installManagedAgentHooks(
): Promise<AgentHookInstallStatus[]> {
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] : []
})
Expand All @@ -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,
Expand Down Expand Up @@ -203,15 +182,9 @@ export async function installManagedAgentHooks(
return results
}

export async function removeManagedAgentHooks(
options: RemoveOptions = {}
): Promise<AgentHookInstallStatus[]> {
const allowed = options.agents ? new Set(options.agents) : null
export async function removeManagedAgentHooks(): Promise<AgentHookInstallStatus[]> {
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) {
Expand Down Expand Up @@ -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)
}
3 changes: 1 addition & 2 deletions src/main/agent-hooks/managed-agent-hook-startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down
8 changes: 2 additions & 6 deletions src/main/agent-hooks/managed-hook-detection-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
})

Expand Down
Loading
Loading