diff --git a/apps/claude-sdk-cli/src/AgentMessageHandler.ts b/apps/claude-sdk-cli/src/AgentMessageHandler.ts index 29fff60..6661095 100644 --- a/apps/claude-sdk-cli/src/AgentMessageHandler.ts +++ b/apps/claude-sdk-cli/src/AgentMessageHandler.ts @@ -144,7 +144,6 @@ export class AgentMessageHandler { } case 'tool_approval_request': this.#layout.transitionBlock('tools'); - this.#layout.appendStreaming(`${formatToolSummary(msg.name, msg.input, this.#cwd, this.#store)}\n`); if (!this.#usageBeforeTools) { this.#usageBeforeTools = this.#lastUsage; } @@ -213,10 +212,14 @@ export class AgentMessageHandler { } this.#respond(msg.requestId, approved); this.#layout.removePendingTool(msg.requestId); + const summary = formatToolSummary(msg.name, msg.input, this.#cwd, this.#store); + this.#layout.appendStreaming(`${summary} ${approved ? '✅' : '❌'}\n`); } catch (err) { this.#logger.error('Error', err); this.#respond(msg.requestId, false); this.#layout.removePendingTool(msg.requestId); + const catchSummary = formatToolSummary(msg.name, msg.input, this.#cwd, this.#store); + this.#layout.appendStreaming(`${catchSummary} 💥\n`); } } } diff --git a/apps/claude-sdk-cli/test/AgentMessageHandler.spec.ts b/apps/claude-sdk-cli/test/AgentMessageHandler.spec.ts index 5018ac7..b3cc76f 100644 --- a/apps/claude-sdk-cli/test/AgentMessageHandler.spec.ts +++ b/apps/claude-sdk-cli/test/AgentMessageHandler.spec.ts @@ -1,4 +1,6 @@ +import type { AnyToolDefinition } from '@shellicar/claude-sdk'; import { describe, expect, it, vi } from 'vitest'; +import { z } from 'zod'; import { AgentMessageHandler, type AgentMessageHandlerOptions } from '../src/AgentMessageHandler.js'; import type { AppLayout } from '../src/AppLayout.js'; import { logger } from '../src/logger.js'; @@ -235,6 +237,66 @@ describe('AgentMessageHandler — tool_error', () => { }); }); +// --------------------------------------------------------------------------- +// tool_approval_request +// --------------------------------------------------------------------------- + +function makeTool(name: string, operation: AnyToolDefinition['operation']): AnyToolDefinition { + return { + name, + description: 'test', + operation, + input_schema: z.object({}), + input_examples: [], + handler: async () => ({}), + } as unknown as AnyToolDefinition; +} + +describe('AgentMessageHandler — tool_approval_request', () => { + it('transitions to tools block', () => { + const layout = makeLayout(); + makeHandler(layout).handle({ type: 'tool_approval_request', requestId: 'r1', name: 'Unknown', input: {} }); + const expected = 'tools'; + const actual = vi.mocked(layout.transitionBlock).mock.calls[0]?.[0]; + expect(actual).toBe(expected); + }); + + it('records auto-denied decision synchronously when tool is not registered', () => { + const layout = makeLayout(); + // empty tools → getPermission returns Deny for any unknown tool + makeHandler(layout).handle({ type: 'tool_approval_request', requestId: 'r1', name: 'Unknown', input: {} }); + const text = vi.mocked(layout.appendStreaming).mock.calls[0]?.[0] ?? ''; + expect(text).toContain('❌'); + }); + + it('records auto-approved decision synchronously for a read tool', () => { + const layout = makeLayout(); + const handler = makeHandler(layout, { tools: [makeTool('Find', 'read')] }); + handler.handle({ type: 'tool_approval_request', requestId: 'r1', name: 'Find', input: {} }); + const text = vi.mocked(layout.appendStreaming).mock.calls[0]?.[0] ?? ''; + expect(text).toContain('✅'); + }); + + it('records manual approval after user input for a delete tool', async () => { + const layout = makeLayout(); // requestApproval resolves true by default + const handler = makeHandler(layout, { tools: [makeTool('DeleteFile', 'delete')] }); + handler.handle({ type: 'tool_approval_request', requestId: 'r1', name: 'DeleteFile', input: {} }); + await Promise.resolve(); + const text = vi.mocked(layout.appendStreaming).mock.calls[0]?.[0] ?? ''; + expect(text).toContain('✅'); + }); + + it('records manual denial after user input for a delete tool', async () => { + const layout = makeLayout(); + vi.mocked(layout.requestApproval).mockResolvedValue(false); + const handler = makeHandler(layout, { tools: [makeTool('DeleteFile', 'delete')] }); + handler.handle({ type: 'tool_approval_request', requestId: 'r1', name: 'DeleteFile', input: {} }); + await Promise.resolve(); + const text = vi.mocked(layout.appendStreaming).mock.calls[0]?.[0] ?? ''; + expect(text).toContain('❌'); + }); +}); + // --------------------------------------------------------------------------- // message_usage // ---------------------------------------------------------------------------