@@ -15,6 +15,7 @@ Textual widgets responsible for UI rendering:
1515- ** Message widgets** : SystemMessage, UserMessage, AgentMessage, ToolMessage
1616- ** UserInput** : Handles user text input and submission
1717- ** ThinkingIndicator** : Shows when agent is processing
18+ - ** ToolPermissionPrompt** : Interactive widget for approving/denying tool execution requests
1819
1920### System Layer
2021
@@ -26,6 +27,9 @@ Manages the conversation loop with Claude SDK:
2627- Emits AgentMessageType events (STREAM_EVENT, ASSISTANT, RESULT)
2728- Manages session persistence via session_id
2829- Supports dynamic MCP server inference and loading
30+ - Implements ` _can_use_tool ` callback for interactive tool permission requests
31+ - Uses ` permission_lock ` (asyncio.Lock) to serialize parallel permission requests
32+ - Manages ` permission_response_queue ` for user responses to tool permission prompts
2933
3034#### MCP Server Inference (` system/mcp_inference.py ` )
3135Intelligently determines which MCP servers are needed for each query:
@@ -42,19 +46,23 @@ Routes agent messages to appropriate UI components:
4246- Controls thinking indicator state
4347- Manages scroll-to-bottom behavior
4448- Displays system messages (e.g., MCP server connection notifications)
49+ - Detects tool permission requests and shows ToolPermissionPrompt
50+ - Manages UI transitions between UserInput and ToolPermissionPrompt
4551
4652#### Actions (` system/actions.py ` )
4753Centralizes all user-initiated actions and controls:
4854- ** quit()** : Exits the application
4955- ** query(user_input)** : Sends user query to agent loop queue
50- - ** interrupt()** : Stops streaming mid-execution by setting interrupt flag and calling SDK interrupt
56+ - ** interrupt()** : Stops streaming mid-execution by setting interrupt flag and calling SDK interrupt (ignores ESC when tool permission prompt is visible)
5157- ** new()** : Starts new conversation by sending NEW_CONVERSATION control command
58+ - ** respond_to_tool_permission(response)** : Handles tool permission responses, manages UI state transitions between permission prompt and user input
5259- Manages UI state (thinking indicator, chat history clearing)
53- - Directly accesses agent_loop internals (query_queue, client, interrupting flag)
60+ - Directly accesses agent_loop internals (query_queue, client, interrupting flag, permission_response_queue )
5461
5562Actions are triggered via:
5663- Keybindings in app.py (ESC → action_interrupt, Ctrl+N → action_new)
5764- Text commands in user_input.py ("exit", "clear")
65+ - Component events (ToolPermissionPrompt.on_input_submitted → respond_to_tool_permission)
5866
5967### Utils Layer
6068
@@ -211,6 +219,103 @@ mcp_servers:
211219 # ... rest of config
212220```
213221
222+ ## Tool Permission System
223+
224+ The application implements interactive tool permission requests that allow users to approve or deny tool execution in real-time.
225+
226+ ### Components
227+
228+ #### ToolPermissionPrompt (` components/tool_permission_prompt.py ` )
229+ Textual widget that displays permission requests to the user:
230+ - Shows tool name with MCP server info
231+ - Provides input field for user response
232+ - Supports Enter (approve), ESC (deny), or custom text responses
233+
234+ ### Permission Flow
235+
236+ ```
237+ Tool Execution Request (from Claude SDK)
238+ ↓
239+ AgentLoop._can_use_tool (callback with permission_lock acquired)
240+ ↓
241+ Emit SYSTEM AgentMessage with tool_permission_request data
242+ ↓
243+ MessageBus._handle_system detects permission request
244+ ↓
245+ Show ToolPermissionPrompt, hide UserInput
246+ ↓
247+ User Response:
248+ - Enter (or "yes") → Approve
249+ - ESC (or "no") → Deny
250+ - Custom text → Send to Claude as alternative instruction
251+ ↓
252+ Actions.respond_to_tool_permission(response)
253+ ↓
254+ Put response on permission_response_queue
255+ ↓
256+ Hide ToolPermissionPrompt, show UserInput
257+ ↓
258+ AgentLoop._can_use_tool receives response
259+ ↓
260+ Return PermissionResultAllow or PermissionResultDeny
261+ ↓
262+ Next tool permission request (if multiple tools called)
263+ ```
264+
265+ ### Serialization with Permission Lock
266+
267+ When multiple tools request permission in parallel, a ` permission_lock ` (asyncio.Lock) ensures they are handled sequentially:
268+
269+ 1 . First tool acquires lock → Shows prompt → Waits for response → Releases lock
270+ 2 . Second tool acquires lock → Shows prompt → Waits for response → Releases lock
271+ 3 . Third tool acquires lock → Shows prompt → Waits for response → Releases lock
272+
273+ This prevents race conditions where multiple prompts would overwrite each other and ensures each tool gets a dedicated user response.
274+
275+ ### Permission Responses
276+
277+ The ` _can_use_tool ` callback returns typed permission results:
278+
279+ ** Approve (CONFIRM)** :
280+ ``` python
281+ return PermissionResultAllow(
282+ behavior = " allow" ,
283+ updated_input = tool_input,
284+ )
285+ ```
286+
287+ ** Deny (DENY)** :
288+ ``` python
289+ return PermissionResultDeny(
290+ behavior = " deny" ,
291+ message = " User denied permission" ,
292+ interrupt = True ,
293+ )
294+ ```
295+
296+ ** Custom Response** :
297+ ``` python
298+ return PermissionResultDeny(
299+ behavior = " deny" ,
300+ message = user_response, # Alternative instruction sent to Claude
301+ interrupt = True ,
302+ )
303+ ```
304+
305+ ### ESC Key Handling
306+
307+ When ToolPermissionPrompt is visible, the ESC key is intercepted:
308+ - ` Actions.interrupt() ` checks ` permission_prompt.is_visible `
309+ - If visible, returns early without interrupting the agent
310+ - ToolPermissionPrompt's ` on_key ` handler processes ESC to deny the tool
311+ - If not visible, ESC performs normal interrupt behavior
312+
313+ ### System Messages
314+
315+ Permission denials generate system messages in the chat:
316+ - ** Denied** : ` "Permission denied for {tool_name}" `
317+ - ** Custom response** : ` "Custom response for {tool_name}: {user_response}" `
318+
214319## User Commands
215320
216321### Text Commands
@@ -219,7 +324,7 @@ mcp_servers:
219324
220325### Keybindings
221326- ** Ctrl+C** : Quit application
222- - ** ESC** : Interrupt streaming response
327+ - ** ESC** : Interrupt streaming response (or deny tool permission if prompt visible)
223328- ** Ctrl+N** : Start new conversation
224329
225330## Session Management
@@ -274,3 +379,5 @@ SDK reconnects to previous session with full history
274379- Control commands are queued alongside user queries to ensure proper task ordering
275380- Agent loop processes both strings (user queries) and ControlCommands from the same queue
276381- Interrupt flag is checked on each streaming message to enable immediate stop
382+ - Tool permission requests are serialized via asyncio.Lock to handle parallel tool calls sequentially
383+ - Permission responses use typed SDK objects (PermissionResultAllow, PermissionResultDeny) rather than plain dictionaries
0 commit comments