@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
JavaScript
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