diff --git a/src/server/__tests__/conversations.test.ts b/src/server/__tests__/conversations.test.ts index 1abd1605..3332d7f7 100644 --- a/src/server/__tests__/conversations.test.ts +++ b/src/server/__tests__/conversations.test.ts @@ -272,7 +272,13 @@ describe('ConversationService', () => { pendingOutbound: [], stderrLines: [], sdkMessages: [], - pendingPermissionRequests: new Map(), + pendingPermissionRequests: new Map([ + ['req-1', { + toolName: 'ExitPlanMode', + input: {}, + permissionSuggestions: [], + }], + ]), }) const result = svc.respondToPermission( @@ -294,6 +300,48 @@ describe('ConversationService', () => { }, }, }) + expect((sent[0] as any).response.response.interrupt).toBeUndefined() + }) + + it('should interrupt the active turn when desktop denies a tool permission', () => { + const svc = new ConversationService() + const sent: unknown[] = [] + + ;(svc as any).sessions.set('session-1', { + proc: null, + outputCallbacks: [], + workDir: process.cwd(), + sdkToken: 'token', + sdkSocket: { + send(data: string) { + sent.push(JSON.parse(data)) + }, + }, + pendingOutbound: [], + stderrLines: [], + sdkMessages: [], + pendingPermissionRequests: new Map([ + ['req-1', { + toolName: 'Bash', + input: { command: 'rm temp.txt' }, + permissionSuggestions: [], + }], + ]), + }) + + const result = svc.respondToPermission('session-1', 'req-1', false) + + expect(result).toBe(true) + expect(sent[0]).toMatchObject({ + type: 'control_response', + response: { + response: { + behavior: 'deny', + message: 'User denied via UI', + interrupt: true, + }, + }, + }) }) it('should resolve a permission mode request only after the CLI confirms the change', async () => { diff --git a/src/server/services/conversationService.ts b/src/server/services/conversationService.ts index f0e286f8..2dd134fb 100644 --- a/src/server/services/conversationService.ts +++ b/src/server/services/conversationService.ts @@ -622,7 +622,15 @@ export class ConversationService { } : {}), } - : { behavior: 'deny', message: denyMessage || 'User denied via UI' }, + : { + behavior: 'deny', + message: denyMessage || 'User denied via UI', + // Rejecting ExitPlanMode means "keep planning"; other desktop + // denials stop the current agent turn and wait for user input. + ...(pendingRequest?.toolName !== 'ExitPlanMode' + ? { interrupt: true } + : {}), + }, }, }) }