mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 00:03:55 +00:00
Suppress Codex auto-approval notifications
This commit is contained in:
@@ -23,6 +23,7 @@ export type AgentCompletionCoordinatorOptions = {
|
||||
dispatchCompletion: (title: string, meta?: AgentCompletionDispatchMeta) => void
|
||||
isLive: () => boolean
|
||||
shouldPollProcessCadence?: () => boolean
|
||||
shouldSuppressHookCompletion?: (payload: AgentCompletionStatusSnapshot) => boolean
|
||||
}
|
||||
|
||||
export type AgentCompletionCoordinator = {
|
||||
|
||||
@@ -1113,4 +1113,58 @@ describe('agent completion coordinator', () => {
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledWith('experimental-agent-observability')
|
||||
})
|
||||
|
||||
it('does not mutate completion state when hook completion is suppressed', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const shouldSuppressHookCompletion = vi.fn(
|
||||
(payload: { state: string }) => payload.state === 'waiting' || payload.state === 'blocked'
|
||||
)
|
||||
const coordinator = createAgentCompletionCoordinator({
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
getPtyId: () => 'pty-1',
|
||||
getSettings: () => null,
|
||||
inspectProcess: vi.fn(),
|
||||
dispatchCompletion,
|
||||
isLive: () => true,
|
||||
shouldSuppressHookCompletion
|
||||
})
|
||||
|
||||
coordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
prompt: 'implement notifications',
|
||||
agentType: 'codex'
|
||||
})
|
||||
coordinator.observeHookStatus({
|
||||
state: 'waiting',
|
||||
prompt: 'implement notifications',
|
||||
agentType: 'codex',
|
||||
toolName: 'exec_command',
|
||||
toolInput: 'git status'
|
||||
})
|
||||
|
||||
expect(dispatchCompletion).not.toHaveBeenCalled()
|
||||
expect(shouldSuppressHookCompletion).toHaveBeenCalled()
|
||||
|
||||
coordinator.observeHookStatus({
|
||||
state: 'done',
|
||||
prompt: 'implement notifications',
|
||||
agentType: 'codex',
|
||||
stateStartedAt: 1_700_000_010_000,
|
||||
lastAssistantMessage: 'Done.'
|
||||
})
|
||||
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS)
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
|
||||
expect(dispatchCompletion).toHaveBeenCalledWith(
|
||||
'codex',
|
||||
expect.objectContaining({
|
||||
source: 'hook',
|
||||
quietedHookDone: true,
|
||||
agentStatus: expect.objectContaining({
|
||||
state: 'done',
|
||||
agentType: 'codex'
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -610,6 +610,9 @@ export function createAgentCompletionCoordinator(
|
||||
}
|
||||
|
||||
function observeHookStatus(payload: AgentCompletionStatusSnapshot): void {
|
||||
if (options.shouldSuppressHookCompletion?.(payload)) {
|
||||
return
|
||||
}
|
||||
if (isRecognizedAgentType(payload.agentType)) {
|
||||
establishAgentEvidence()
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { YOLO_TUI_AGENT_ARGS } from '../../../../shared/tui-agent-permissions'
|
||||
import { createTestStore, makeTab } from '../../store/slices/store-test-helpers'
|
||||
import type { AppState } from '../../store/types'
|
||||
|
||||
let testStore: ReturnType<typeof createTestStore>
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: {
|
||||
getState: () => testStore.getState()
|
||||
}
|
||||
}))
|
||||
|
||||
import { isAutoApprovedCodexPermissionStatus } from './codex-auto-approval-notification-suppression'
|
||||
|
||||
describe('Codex auto-approval notification suppression', () => {
|
||||
beforeEach(() => {
|
||||
testStore = createTestStore()
|
||||
})
|
||||
|
||||
it('uses the production launch-config resolver for launch-token registered Codex panes', () => {
|
||||
const paneKey = 'tab-1:leaf-1'
|
||||
const launchToken = 'launch-token-1'
|
||||
const providerSession = { key: 'session_id' as const, id: 'codex-session-1' }
|
||||
|
||||
testStore.setState({
|
||||
tabsByWorktree: {
|
||||
'wt-1': [makeTab({ id: 'tab-1', worktreeId: 'wt-1' })]
|
||||
}
|
||||
} as Partial<AppState>)
|
||||
testStore.getState().registerAgentLaunchConfig(
|
||||
paneKey,
|
||||
{
|
||||
agentArgs: YOLO_TUI_AGENT_ARGS.codex ?? '',
|
||||
agentEnv: {}
|
||||
},
|
||||
{ agentType: 'codex', launchToken, tabId: 'tab-1', leafId: 'leaf-1' }
|
||||
)
|
||||
testStore
|
||||
.getState()
|
||||
.setAgentStatus(
|
||||
paneKey,
|
||||
{ state: 'waiting', prompt: 'implement notifications', agentType: 'codex' },
|
||||
'Codex',
|
||||
{ updatedAt: 10, stateStartedAt: 10 },
|
||||
{ tabId: 'tab-1', worktreeId: 'wt-1' },
|
||||
{ providerSession, launchToken }
|
||||
)
|
||||
|
||||
expect(
|
||||
isAutoApprovedCodexPermissionStatus(
|
||||
{ state: 'waiting', prompt: 'implement notifications', agentType: 'codex' },
|
||||
paneKey
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves notifications when the live status has no resolvable launch config', () => {
|
||||
const paneKey = 'tab-1:leaf-1'
|
||||
|
||||
testStore.getState().setAgentStatus(paneKey, {
|
||||
state: 'waiting',
|
||||
prompt: 'implement notifications',
|
||||
agentType: 'codex'
|
||||
})
|
||||
|
||||
expect(
|
||||
isAutoApprovedCodexPermissionStatus(
|
||||
{ state: 'waiting', prompt: 'implement notifications', agentType: 'codex' },
|
||||
paneKey
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { YOLO_TUI_AGENT_ARGS } from '../../../../shared/tui-agent-permissions'
|
||||
import type { AgentCompletionStatusSnapshot } from './agent-completion-coordinator-types'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const CODEX_AUTO_APPROVED_PERMISSION_STATES = ['waiting', 'blocked'] as const
|
||||
const CODEX_AUTO_APPROVED_ARGS = (YOLO_TUI_AGENT_ARGS.codex ?? '').trim()
|
||||
|
||||
function isCodexAutoApprovedPermissionState(
|
||||
state: AgentCompletionStatusSnapshot['state']
|
||||
): state is (typeof CODEX_AUTO_APPROVED_PERMISSION_STATES)[number] {
|
||||
return state === 'waiting' || state === 'blocked'
|
||||
}
|
||||
|
||||
export function isAutoApprovedCodexPermissionStatus(
|
||||
payload: AgentCompletionStatusSnapshot,
|
||||
paneKey: string
|
||||
): boolean {
|
||||
if (payload.agentType !== 'codex' || !isCodexAutoApprovedPermissionState(payload.state)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const state = useAppStore.getState()
|
||||
const statusEntry = state.agentStatusByPaneKey[paneKey]
|
||||
if (!statusEntry) {
|
||||
return false
|
||||
}
|
||||
|
||||
const launchConfig = state.getAgentLaunchConfigForStatusEntry(statusEntry)
|
||||
if (!launchConfig) {
|
||||
return false
|
||||
}
|
||||
|
||||
return launchConfig.agentArgs.trim() === CODEX_AUTO_APPROVED_ARGS
|
||||
}
|
||||
|
||||
export function createCodexAutoApprovalHookCompletionSuppressor(
|
||||
paneKey: string
|
||||
): (payload: AgentCompletionStatusSnapshot) => boolean {
|
||||
return (payload) => isAutoApprovedCodexPermissionStatus(payload, paneKey)
|
||||
}
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
type AgentInterruptInputIntent
|
||||
} from '../../../../shared/agent-interrupt-intent'
|
||||
import { createAgentCompletionCoordinator } from './agent-completion-coordinator'
|
||||
import { createCodexAutoApprovalHookCompletionSuppressor } from './codex-auto-approval-notification-suppression'
|
||||
import type { AgentCompletionStatusSnapshot } from './agent-completion-coordinator-types'
|
||||
import {
|
||||
markTerminalBracketedPasteInterrupted,
|
||||
@@ -1282,7 +1283,8 @@ export function connectPanePty(
|
||||
return true
|
||||
}
|
||||
return (useAppStore.getState().ptyIdsByTabId[deps.tabId] ?? []).length > 0
|
||||
}
|
||||
},
|
||||
shouldSuppressHookCompletion: createCodexAutoApprovalHookCompletionSuppressor(cacheKey)
|
||||
})
|
||||
|
||||
const onExit = (ptyId: string): void => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable max-lines -- Why: notification edge cases share one module-scoped coordinator, so keeping setup and regression cases together prevents brittle cross-file mock resets. */
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ParsedAgentStatusPayload } from '../../../shared/agent-status-types'
|
||||
import { YOLO_TUI_AGENT_ARGS } from '../../../shared/tui-agent-permissions'
|
||||
|
||||
const dispatchTerminalNotification = vi.fn()
|
||||
|
||||
@@ -24,6 +25,27 @@ type MockStoreState = {
|
||||
ptyIdsByLeafId?: Record<string, string>
|
||||
}
|
||||
>
|
||||
agentLaunchConfigByPaneKey: Record<
|
||||
string,
|
||||
{
|
||||
launchConfig: { agentArgs: string; agentEnv: Record<string, string> }
|
||||
}
|
||||
>
|
||||
agentStatusByPaneKey: Record<
|
||||
string,
|
||||
{
|
||||
state: ParsedAgentStatusPayload['state']
|
||||
prompt: string
|
||||
paneKey: string
|
||||
updatedAt: number
|
||||
stateStartedAt: number
|
||||
agentType?: ParsedAgentStatusPayload['agentType']
|
||||
stateHistory: []
|
||||
}
|
||||
>
|
||||
getAgentLaunchConfigForStatusEntry: (entry: {
|
||||
paneKey: string
|
||||
}) => { agentArgs: string; agentEnv: Record<string, string> } | undefined
|
||||
}
|
||||
|
||||
let mockStoreState: MockStoreState
|
||||
@@ -48,6 +70,24 @@ function hookStatus(state: ParsedAgentStatusPayload['state']): ParsedAgentStatus
|
||||
}
|
||||
}
|
||||
|
||||
function seedCodexPaneLaunchConfig(paneKey: string, agentArgs: string): void {
|
||||
mockStoreState.agentLaunchConfigByPaneKey[paneKey] = {
|
||||
launchConfig: {
|
||||
agentArgs,
|
||||
agentEnv: {}
|
||||
}
|
||||
}
|
||||
mockStoreState.agentStatusByPaneKey[paneKey] = {
|
||||
state: 'working',
|
||||
prompt: 'implement notifications',
|
||||
paneKey,
|
||||
updatedAt: Date.now(),
|
||||
stateStartedAt: Date.now(),
|
||||
agentType: 'codex',
|
||||
stateHistory: []
|
||||
}
|
||||
}
|
||||
|
||||
describe('agent hook completion notifications', () => {
|
||||
const paneKey = 'tab-1:11111111-1111-4111-8111-111111111111'
|
||||
|
||||
@@ -70,7 +110,11 @@ describe('agent hook completion notifications', () => {
|
||||
tabsByWorktree: {
|
||||
'wt-1': [{ id: 'tab-1', ptyId: 'pty-1' }]
|
||||
},
|
||||
terminalLayoutsByTabId: {}
|
||||
terminalLayoutsByTabId: {},
|
||||
agentLaunchConfigByPaneKey: {},
|
||||
agentStatusByPaneKey: {},
|
||||
getAgentLaunchConfigForStatusEntry: (entry) =>
|
||||
mockStoreState.agentLaunchConfigByPaneKey[entry.paneKey]?.launchConfig
|
||||
}
|
||||
})
|
||||
|
||||
@@ -414,6 +458,89 @@ describe('agent hook completion notifications', () => {
|
||||
expect(dispatchTerminalNotification).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not notify for Codex auto-approved permission requests', async () => {
|
||||
seedCodexPaneLaunchConfig(paneKey, YOLO_TUI_AGENT_ARGS.codex ?? '')
|
||||
const { observeAgentHookCompletionForNotification } =
|
||||
await import('./agent-hook-completion-notifications')
|
||||
|
||||
observeAgentHookCompletionForNotification({
|
||||
paneKey,
|
||||
worktreeId: 'wt-1',
|
||||
payload: hookStatus('working')
|
||||
})
|
||||
observeAgentHookCompletionForNotification({
|
||||
paneKey,
|
||||
worktreeId: 'wt-1',
|
||||
payload: {
|
||||
state: 'waiting',
|
||||
prompt: 'implement notifications',
|
||||
agentType: 'codex',
|
||||
toolName: 'exec_command',
|
||||
toolInput: 'git status'
|
||||
}
|
||||
})
|
||||
|
||||
expect(dispatchTerminalNotification).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still notifies for manual Codex permission requests', async () => {
|
||||
seedCodexPaneLaunchConfig(paneKey, '')
|
||||
const { observeAgentHookCompletionForNotification } =
|
||||
await import('./agent-hook-completion-notifications')
|
||||
|
||||
observeAgentHookCompletionForNotification({
|
||||
paneKey,
|
||||
worktreeId: 'wt-1',
|
||||
payload: hookStatus('working')
|
||||
})
|
||||
observeAgentHookCompletionForNotification({
|
||||
paneKey,
|
||||
worktreeId: 'wt-1',
|
||||
payload: {
|
||||
state: 'waiting',
|
||||
prompt: 'implement notifications',
|
||||
agentType: 'codex',
|
||||
toolName: 'exec_command',
|
||||
toolInput: 'git status'
|
||||
}
|
||||
})
|
||||
|
||||
expect(dispatchTerminalNotification).toHaveBeenCalledTimes(1)
|
||||
expect(dispatchTerminalNotification).toHaveBeenCalledWith(
|
||||
'wt-1',
|
||||
expect.objectContaining({
|
||||
source: 'agent-task-complete',
|
||||
paneKey,
|
||||
terminalTitle: 'codex'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('does not notify for Codex auto-approved blocked permission requests', async () => {
|
||||
seedCodexPaneLaunchConfig(paneKey, YOLO_TUI_AGENT_ARGS.codex ?? '')
|
||||
const { observeAgentHookCompletionForNotification } =
|
||||
await import('./agent-hook-completion-notifications')
|
||||
|
||||
observeAgentHookCompletionForNotification({
|
||||
paneKey,
|
||||
worktreeId: 'wt-1',
|
||||
payload: hookStatus('working')
|
||||
})
|
||||
observeAgentHookCompletionForNotification({
|
||||
paneKey,
|
||||
worktreeId: 'wt-1',
|
||||
payload: {
|
||||
state: 'blocked',
|
||||
prompt: 'implement notifications',
|
||||
agentType: 'codex',
|
||||
toolName: 'exec_command',
|
||||
toolInput: 'git status'
|
||||
}
|
||||
})
|
||||
|
||||
expect(dispatchTerminalNotification).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('suppresses an internal milestone completion when hook work resumes before quiet', async () => {
|
||||
const { observeAgentHookCompletionForNotification } =
|
||||
await import('./agent-hook-completion-notifications')
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
import type { RuntimeTerminalProcessInspection } from '@/runtime/runtime-terminal-inspection'
|
||||
import { dispatchTerminalNotification } from '@/components/terminal-pane/use-notification-dispatch'
|
||||
import { collectLeafIdsInOrder } from '@/components/terminal-pane/layout-serialization'
|
||||
import { createCodexAutoApprovalHookCompletionSuppressor } from '@/components/terminal-pane/codex-auto-approval-notification-suppression'
|
||||
|
||||
type CoordinatorEntry = {
|
||||
worktreeId: string
|
||||
@@ -159,7 +160,8 @@ function createCoordinator(paneKey: string, worktreeId: string): AgentCompletion
|
||||
...(meta?.agentStatus ? { agentStatusSnapshot: meta.agentStatus } : {})
|
||||
})
|
||||
},
|
||||
isLive: () => paneCanReceiveHookCompletion(paneKey)
|
||||
isLive: () => paneCanReceiveHookCompletion(paneKey),
|
||||
shouldSuppressHookCompletion: createCodexAutoApprovalHookCompletionSuppressor(paneKey)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1102,7 +1102,8 @@
|
||||
"importHostSetup": "导入",
|
||||
"notePasteTooLarge": "笔记字段的粘贴内容过大。",
|
||||
"reuseExistingBranch": "复用分支",
|
||||
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。"
|
||||
"reuseExistingBranchHint": "检出已有分支,而不是基于它创建新分支。",
|
||||
"createMultiple": "Create more"
|
||||
},
|
||||
"NewWorkspaceComposerModal": {
|
||||
"fa90f739a5": "创建工作区之前选择项目、工作区名称和 Agent。"
|
||||
|
||||
Reference in New Issue
Block a user