UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

253 lines 14.3 kB
import { jsx as _jsx } from "react/jsx-runtime"; import BashProgress from '../components/bash-progress.js'; import { ErrorMessage, InfoMessage } from '../components/message-box.js'; import { setCurrentMode as setCurrentModeContext } from '../context/mode-context.js'; import { getToolManager, processToolUse } from '../message-handler.js'; import { executeBashCommand, formatBashResultForLLM } from '../tools/execute-bash.js'; import { MessageBuilder } from '../utils/message-builder.js'; import { parseToolArguments } from '../utils/tool-args-parser.js'; import { createCancellationResults } from '../utils/tool-cancellation.js'; import { displayToolResult } from '../utils/tool-result-display.js'; import { getVSCodeServerSync } from '../vscode/index.js'; export function useToolHandler({ pendingToolCalls, currentToolIndex, completedToolResults, currentConversationContext, setPendingToolCalls, setCurrentToolIndex, setCompletedToolResults, setCurrentConversationContext, setIsToolConfirmationMode, setIsToolExecuting, setMessages, addToChatQueue, setLiveComponent, getNextComponentKey, resetToolConfirmationState, onProcessAssistantResponse, client: _client, currentProvider: _currentProvider, setDevelopmentMode, compactToolDisplay, }) { // Continue conversation with tool results - maintains the proper loop const continueConversationWithToolResults = async (toolResults) => { if (!currentConversationContext) { resetToolConfirmationState(); return; } // Use passed results or fallback to state (for backwards compatibility) const resultsToUse = toolResults || completedToolResults; const { messagesBeforeToolExecution, systemMessage } = currentConversationContext; // Build updated messages with tool results const builder = new MessageBuilder(messagesBeforeToolExecution); builder.addToolResults(resultsToUse); const updatedMessagesWithTools = builder.build(); setMessages(updatedMessagesWithTools); // Reset tool confirmation state since we're continuing the conversation resetToolConfirmationState(); // Continue the main conversation loop with tool results as context await onProcessAssistantResponse(systemMessage, updatedMessagesWithTools); }; // Handle tool confirmation const handleToolConfirmation = (confirmed) => { if (!confirmed) { // User cancelled - close all VS Code diffs const vscodeServer = getVSCodeServerSync(); if (vscodeServer?.hasConnections()) { vscodeServer.closeAllDiffs(); } // User cancelled - show message addToChatQueue(_jsx(InfoMessage, { message: "Tool execution cancelled by user", hideBox: true }, `tool-cancelled-${getNextComponentKey()}`)); if (!currentConversationContext) { resetToolConfirmationState(); return; } // Create cancellation results for all pending tools // This is critical to maintain conversation state integrity const cancellationResults = createCancellationResults(pendingToolCalls); const { messagesBeforeToolExecution } = currentConversationContext; // Build updated messages with cancellation results const builder = new MessageBuilder(messagesBeforeToolExecution); builder.addToolResults(cancellationResults); const updatedMessagesWithCancellation = builder.build(); setMessages(updatedMessagesWithCancellation); // Reset state to allow user to type a new message // Do NOT continue the conversation - let the user provide instructions resetToolConfirmationState(); return; } // Move to tool execution state - this allows UI to update immediately setIsToolConfirmationMode(false); setIsToolExecuting(true); // Execute tools asynchronously setImmediate(() => { void executeCurrentTool(); }); }; // Execute the current tool asynchronously const executeCurrentTool = async () => { const currentTool = pendingToolCalls[currentToolIndex]; // Check if this is an MCP tool and show appropriate messaging const toolManager = getToolManager(); if (toolManager) { const mcpInfo = toolManager.getMCPToolInfo(currentTool.function.name); if (mcpInfo.isMCPTool) { addToChatQueue(_jsx(InfoMessage, { message: `Executing MCP tool "${currentTool.function.name}" from server "${mcpInfo.serverName}"`, hideBox: true }, `mcp-tool-executing-${getNextComponentKey()}-${Date.now()}`)); } // Run validator if available const validator = toolManager.getToolValidator(currentTool.function.name); if (validator) { try { const parsedArgs = parseToolArguments(currentTool.function.arguments); const validationResult = await validator(parsedArgs); if (!validationResult.valid) { // Validation failed - show error and skip execution const errorResult = { tool_call_id: currentTool.id, role: 'tool', name: currentTool.function.name, content: validationResult.error, }; const newResults = [...completedToolResults, errorResult]; setCompletedToolResults(newResults); // Display the error addToChatQueue(_jsx(ErrorMessage, { message: validationResult.error, hideBox: true }, `tool-validation-error-${getNextComponentKey()}-${Date.now()}`)); // Move to next tool or complete the process if (currentToolIndex + 1 < pendingToolCalls.length) { setCurrentToolIndex(currentToolIndex + 1); // Return to confirmation mode for next tool setIsToolExecuting(false); setIsToolConfirmationMode(true); } else { // All tools processed, continue conversation loop with the results setIsToolExecuting(false); await continueConversationWithToolResults(newResults); } return; } } catch (validationError) { // Validation threw an error - treat as validation failure const errorResult = { tool_call_id: currentTool.id, role: 'tool', name: currentTool.function.name, content: `Validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`, }; const newResults = [...completedToolResults, errorResult]; setCompletedToolResults(newResults); addToChatQueue(_jsx(ErrorMessage, { message: `Validation error: ${String(validationError)}`, hideBox: true }, `tool-validation-error-${getNextComponentKey()}-${Date.now()}`)); // Move to next tool or complete the process if (currentToolIndex + 1 < pendingToolCalls.length) { setCurrentToolIndex(currentToolIndex + 1); setIsToolExecuting(false); setIsToolConfirmationMode(true); } else { setIsToolExecuting(false); await continueConversationWithToolResults(newResults); } return; } } } try { // Special handling for switch_mode tool if (currentTool.function.name === 'switch_mode' && setDevelopmentMode) { const parsedArgs = parseToolArguments(currentTool.function.arguments); // Actually switch the mode // Sync both React state AND global context synchronously // to prevent race conditions where tools check global context // before the useEffect in App.tsx has a chance to sync it const requestedMode = parsedArgs.mode; setDevelopmentMode(requestedMode); setCurrentModeContext(requestedMode); addToChatQueue(_jsx(InfoMessage, { message: `Development mode switched to: ${requestedMode.toUpperCase()}`, hideBox: true }, `mode-switched-${getNextComponentKey()}-${Date.now()}`)); } // Check if tool has a streaming formatter (for real-time progress) const streamingFormatter = toolManager?.getStreamingFormatter(currentTool.function.name); let result; if (streamingFormatter) { // Streaming tool (e.g., execute_bash) - handle specially const parsedArgs = parseToolArguments(currentTool.function.arguments); const commandStr = parsedArgs.command; // Start execution first to get execution ID const { executionId, promise } = executeBashCommand(commandStr); // Set as live component (renders outside Static for real-time updates) setLiveComponent(_jsx(BashProgress, { executionId: executionId, command: commandStr, isLive: true }, `streaming-tool-${currentTool.id}-${getNextComponentKey()}-${Date.now()}`)); // Wait for execution to complete const bashResult = await promise; const llmContent = formatBashResultForLLM(bashResult); result = { tool_call_id: currentTool.id, role: 'tool', name: currentTool.function.name, content: llmContent, }; // Clear live component and add static completed result to chat queue setLiveComponent(null); if (compactToolDisplay) { // In compact mode, use displayToolResult for consistent one-liner display await displayToolResult(currentTool, result, toolManager, addToChatQueue, getNextComponentKey, true); } else { addToChatQueue(_jsx(BashProgress, { executionId: executionId, command: commandStr, completedState: bashResult }, `streaming-tool-complete-${currentTool.id}-${getNextComponentKey()}-${Date.now()}`)); } } else { // Regular tool - use standard flow result = await processToolUse(currentTool); // Display the tool result await displayToolResult(currentTool, result, toolManager, addToChatQueue, getNextComponentKey, compactToolDisplay); } const newResults = [...completedToolResults, result]; setCompletedToolResults(newResults); // Move to next tool or complete the process if (currentToolIndex + 1 < pendingToolCalls.length) { setCurrentToolIndex(currentToolIndex + 1); // Return to confirmation mode for next tool setIsToolExecuting(false); setIsToolConfirmationMode(true); } else { // All tools executed, continue conversation loop with the updated results setIsToolExecuting(false); await continueConversationWithToolResults(newResults); } } catch (error) { setIsToolExecuting(false); addToChatQueue(_jsx(ErrorMessage, { message: `Tool execution error: ${String(error)}` }, `tool-exec-error-${getNextComponentKey()}`)); resetToolConfirmationState(); } }; // Handle tool confirmation cancel const handleToolConfirmationCancel = () => { // Close all VS Code diffs when user cancels const vscodeServer = getVSCodeServerSync(); if (vscodeServer?.hasConnections()) { vscodeServer.closeAllDiffs(); } addToChatQueue(_jsx(InfoMessage, { message: "Tool execution cancelled by user", hideBox: true }, `tool-cancelled-${getNextComponentKey()}`)); if (!currentConversationContext) { resetToolConfirmationState(); return; } // Create cancellation results for all pending tools // This is critical to maintain conversation state integrity const cancellationResults = createCancellationResults(pendingToolCalls); const { messagesBeforeToolExecution } = currentConversationContext; // Build updated messages with cancellation results const builder = new MessageBuilder(messagesBeforeToolExecution); builder.addToolResults(cancellationResults); const updatedMessagesWithCancellation = builder.build(); setMessages(updatedMessagesWithCancellation); // Reset state to allow user to type a new message // Do NOT continue the conversation - let the user provide instructions resetToolConfirmationState(); }; // Start tool confirmation flow const startToolConfirmationFlow = (toolCalls, messagesBeforeToolExecution, assistantMsg, systemMessage) => { setPendingToolCalls(toolCalls); setCurrentToolIndex(0); setCompletedToolResults([]); setCurrentConversationContext({ messagesBeforeToolExecution, assistantMsg, systemMessage, }); setIsToolConfirmationMode(true); }; return { handleToolConfirmation, handleToolConfirmationCancel, startToolConfirmationFlow, continueConversationWithToolResults, executeCurrentTool, }; } //# sourceMappingURL=useToolHandler.js.map