mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Fix cursor-agent notification spam on shell tool hooks (#4835)
This commit is contained in:
@@ -3894,24 +3894,24 @@ describe('Cursor hook normalization', () => {
|
||||
expect(result?.payload.interrupted).toBe(true)
|
||||
})
|
||||
|
||||
it('beforeShellExecution maps to waiting with the pending command as toolInput', () => {
|
||||
it('beforeShellExecution maps to working with the pending command as toolInput', () => {
|
||||
const result = _internals.normalizeHookPayload(
|
||||
'cursor',
|
||||
buildBody({ hook_event_name: 'beforeShellExecution', command: 'rm -rf /tmp/foo' }),
|
||||
'production'
|
||||
)
|
||||
expect(result?.payload.state).toBe('waiting')
|
||||
expect(result?.payload.state).toBe('working')
|
||||
expect(result?.payload.toolName).toBe('Shell')
|
||||
expect(result?.payload.toolInput).toBe('rm -rf /tmp/foo')
|
||||
})
|
||||
|
||||
it('beforeMCPExecution maps to waiting', () => {
|
||||
it('beforeMCPExecution maps to working', () => {
|
||||
const result = _internals.normalizeHookPayload(
|
||||
'cursor',
|
||||
buildBody({ hook_event_name: 'beforeMCPExecution', tool_name: 'fetch', url: 'https://x' }),
|
||||
'production'
|
||||
)
|
||||
expect(result?.payload.state).toBe('waiting')
|
||||
expect(result?.payload.state).toBe('working')
|
||||
expect(result?.payload.toolName).toBe('fetch')
|
||||
})
|
||||
|
||||
@@ -4010,6 +4010,37 @@ describe('Cursor hook normalization', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('tool-heavy turn keeps working across shell and generic tool hooks until stop', () => {
|
||||
_internals.normalizeHookPayload(
|
||||
'cursor',
|
||||
buildBody({ hook_event_name: 'beforeSubmitPrompt', prompt: 'run checks' }),
|
||||
'production'
|
||||
)
|
||||
const shell = _internals.normalizeHookPayload(
|
||||
'cursor',
|
||||
buildBody({ hook_event_name: 'beforeShellExecution', command: 'pnpm test' }),
|
||||
'production'
|
||||
)
|
||||
expect(shell?.payload.state).toBe('working')
|
||||
const tool = _internals.normalizeHookPayload(
|
||||
'cursor',
|
||||
buildBody({
|
||||
hook_event_name: 'preToolUse',
|
||||
tool_name: 'Read',
|
||||
tool_input: { file_path: '/repo/src/app.ts' }
|
||||
}),
|
||||
'production'
|
||||
)
|
||||
expect(tool?.payload.state).toBe('working')
|
||||
const stop = _internals.normalizeHookPayload(
|
||||
'cursor',
|
||||
buildBody({ hook_event_name: 'stop', status: 'completed' }),
|
||||
'production'
|
||||
)
|
||||
expect(stop?.payload.state).toBe('done')
|
||||
expect(stop?.payload.prompt).toBe('run checks')
|
||||
})
|
||||
|
||||
it('beforeSubmitPrompt clears the cached tool state from a prior turn', () => {
|
||||
_internals.normalizeHookPayload(
|
||||
'cursor',
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
// - preToolUse/postToolUse/postToolUseFailure: in-flight tool preview
|
||||
// between submit and stop — without these the pane appears idle for the
|
||||
// entire duration of a long tool-heavy turn
|
||||
// - beforeShellExecution / beforeMCPExecution: approval prompts (→ waiting)
|
||||
// - beforeShellExecution / beforeMCPExecution: shell/MCP tool preview (→ working)
|
||||
// - afterAgentResponse: carries the final composed reply text so the
|
||||
// dashboard can surface it on done
|
||||
// sessionStart / sessionEnd are intentionally NOT subscribed — cursor-agent
|
||||
|
||||
@@ -723,6 +723,89 @@ describe('agent completion coordinator', () => {
|
||||
expect(dispatchCompletion).toHaveBeenCalledWith(agentType)
|
||||
})
|
||||
|
||||
it('notifies once after a Cursor tool-heavy turn, not on each shell hook', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const coordinator = createAgentCompletionCoordinator({
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
getPtyId: () => 'pty-1',
|
||||
getSettings: () => null,
|
||||
inspectProcess: vi.fn(),
|
||||
dispatchCompletion,
|
||||
isLive: () => true
|
||||
})
|
||||
|
||||
const turn = {
|
||||
prompt: 'fix the bug',
|
||||
agentType: 'cursor' as const
|
||||
}
|
||||
|
||||
coordinator.observeHookStatus({ state: 'working', ...turn })
|
||||
coordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
...turn,
|
||||
toolName: 'Shell',
|
||||
toolInput: 'pnpm test'
|
||||
})
|
||||
coordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
...turn,
|
||||
toolName: 'Read',
|
||||
toolInput: '/repo/src/app.ts'
|
||||
})
|
||||
coordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
...turn,
|
||||
toolName: 'Shell',
|
||||
toolInput: 'git status'
|
||||
})
|
||||
|
||||
expect(dispatchCompletion).not.toHaveBeenCalled()
|
||||
|
||||
coordinator.observeHookStatus({ state: 'done', ...turn, lastAssistantMessage: 'Fixed.' })
|
||||
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS)
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('would spam Cursor notifications if shell hooks still mapped to waiting', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const coordinator = createAgentCompletionCoordinator({
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
getPtyId: () => 'pty-1',
|
||||
getSettings: () => null,
|
||||
inspectProcess: vi.fn(),
|
||||
dispatchCompletion,
|
||||
isLive: () => true
|
||||
})
|
||||
|
||||
const turn = {
|
||||
prompt: 'fix the bug',
|
||||
agentType: 'cursor' as const
|
||||
}
|
||||
|
||||
coordinator.observeHookStatus({ state: 'working', ...turn })
|
||||
coordinator.observeHookStatus({
|
||||
state: 'waiting',
|
||||
...turn,
|
||||
toolName: 'Shell',
|
||||
toolInput: 'pnpm test'
|
||||
})
|
||||
coordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
...turn,
|
||||
toolName: 'Read',
|
||||
toolInput: '/repo/src/app.ts'
|
||||
})
|
||||
coordinator.observeHookStatus({
|
||||
state: 'waiting',
|
||||
...turn,
|
||||
toolName: 'Shell',
|
||||
toolInput: 'git status'
|
||||
})
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('keeps a generic title completion pending long enough for the first remote inspection', async () => {
|
||||
const inspection = createDeferred<RuntimeTerminalProcessInspection>()
|
||||
const dispatchCompletion = vi.fn()
|
||||
|
||||
@@ -318,6 +318,45 @@ describe('agent hook completion notifications', () => {
|
||||
expect(dispatchTerminalNotification).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not notify on each Cursor shell tool hook during a working turn', async () => {
|
||||
const { observeAgentHookCompletionForNotification } =
|
||||
await import('./agent-hook-completion-notifications')
|
||||
|
||||
observeAgentHookCompletionForNotification({
|
||||
paneKey,
|
||||
worktreeId: 'wt-1',
|
||||
payload: {
|
||||
state: 'working',
|
||||
prompt: 'fix the bug',
|
||||
agentType: 'cursor'
|
||||
}
|
||||
})
|
||||
observeAgentHookCompletionForNotification({
|
||||
paneKey,
|
||||
worktreeId: 'wt-1',
|
||||
payload: {
|
||||
state: 'working',
|
||||
prompt: 'fix the bug',
|
||||
agentType: 'cursor',
|
||||
toolName: 'Shell',
|
||||
toolInput: 'pnpm test'
|
||||
}
|
||||
})
|
||||
observeAgentHookCompletionForNotification({
|
||||
paneKey,
|
||||
worktreeId: 'wt-1',
|
||||
payload: {
|
||||
state: 'working',
|
||||
prompt: 'fix the bug',
|
||||
agentType: 'cursor',
|
||||
toolName: 'Read',
|
||||
toolInput: '/repo/src/app.ts'
|
||||
}
|
||||
})
|
||||
|
||||
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')
|
||||
|
||||
@@ -2343,7 +2343,12 @@ function normalizeCursorEvent(
|
||||
eventName === 'sessionStart' ||
|
||||
eventName === 'preToolUse' ||
|
||||
eventName === 'postToolUse' ||
|
||||
eventName === 'postToolUseFailure'
|
||||
eventName === 'postToolUseFailure' ||
|
||||
// Why: these fire for every shell/MCP invocation as pre-execution gates,
|
||||
// not only when the user is blocked on approval. Treat them like PreToolUse
|
||||
// so a tool-heavy turn does not spam waiting-state notifications.
|
||||
eventName === 'beforeShellExecution' ||
|
||||
eventName === 'beforeMCPExecution'
|
||||
? 'working'
|
||||
: eventName === 'afterAgentResponse'
|
||||
? previousStatus?.state === 'done' && previousStatus.agentType === 'cursor'
|
||||
@@ -2351,9 +2356,7 @@ function normalizeCursorEvent(
|
||||
: 'working'
|
||||
: eventName === 'stop' || eventName === 'sessionEnd'
|
||||
? 'done'
|
||||
: eventName === 'beforeShellExecution' || eventName === 'beforeMCPExecution'
|
||||
? 'waiting'
|
||||
: null
|
||||
: null
|
||||
|
||||
if (!stateName) {
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user