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

135 lines 5.73 kB
import React from 'react'; import { isNonInteractiveModeComplete } from '../app/helpers.js'; import { TIMEOUT_EXECUTION_MAX_MS, TIMEOUT_OUTPUT_FLUSH_MS } from '../constants.js'; import { setCurrentMode as setCurrentModeContext } from '../context/mode-context.js'; import { getLogger } from '../utils/logging/index.js'; import { getShutdownManager } from '../utils/shutdown/index.js'; // Buffer factor ensures we allow enough time for the provider response plus overhead // Doubling is used because provider timeouts often refer to individual request segments // or initial socket connection, while this execution timeout covers the entire // multi-step process including output flushing and initial setup. const TIMEOUT_BUFFER_FACTOR = 2; /** * Calculates the effective timeout based on provider configuration. * * @param client - The LLM client which may have provider-level timeouts * @returns Final timeout in milliseconds */ export function calculateEffectiveTimeout(client) { let effectiveTimeout = TIMEOUT_EXECUTION_MAX_MS; if (client) { const configuredTimeout = client.getTimeout(); if (configuredTimeout === -1) { // -1 means no timeout effectiveTimeout = Number.MAX_SAFE_INTEGER; } else if (configuredTimeout !== undefined) { // If the provider timeout is longer than the default 5 mins, // we should allow it to run for at least that long with a safety buffer. // We use a buffer because the overall execution includes overhead beyond // the raw provider request time. effectiveTimeout = Math.max(TIMEOUT_EXECUTION_MAX_MS, configuredTimeout * TIMEOUT_BUFFER_FACTOR); } } return effectiveTimeout; } /** * Handles non-interactive mode logic: * - Automatically submits prompt when ready * - Sets auto-accept mode * - Monitors completion and exits when done */ export function useNonInteractiveMode({ nonInteractivePrompt, mcpInitialized, client, appState, setDevelopmentMode, handleMessageSubmit, }) { const [nonInteractiveSubmitted, setNonInteractiveSubmitted] = React.useState(false); const [startTime] = React.useState(Date.now()); // Auto-submit prompt when ready React.useEffect(() => { if (nonInteractivePrompt && mcpInitialized && client && !nonInteractiveSubmitted) { setNonInteractiveSubmitted(true); // Set auto-accept mode for non-interactive execution // 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 setDevelopmentMode('auto-accept'); setCurrentModeContext('auto-accept'); // Submit the prompt void handleMessageSubmit(nonInteractivePrompt); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ nonInteractivePrompt, mcpInitialized, client, nonInteractiveSubmitted, handleMessageSubmit, setDevelopmentMode, ]); // Exit when processing is complete React.useEffect(() => { if (nonInteractivePrompt && nonInteractiveSubmitted) { // Calculate timeout based on provider config if available const effectiveTimeout = calculateEffectiveTimeout(client); const { shouldExit, reason } = isNonInteractiveModeComplete(appState, startTime, effectiveTimeout); if (shouldExit) { const logger = getLogger(); if (reason === 'timeout') { logger.error('Non-interactive mode timed out'); } else if (reason === 'error') { logger.error('Non-interactive mode encountered errors'); } else if (reason === 'tool-approval') { // Exit with error code when tool approval is required // Error message already printed by useChatHandler } // Wait a bit to ensure all output is flushed const code = reason === 'error' || reason === 'tool-approval' ? 1 : 0; const timer = setTimeout(() => { void getShutdownManager().gracefulShutdown(code); }, TIMEOUT_OUTPUT_FLUSH_MS); return () => clearTimeout(timer); } } }, [ nonInteractivePrompt, nonInteractiveSubmitted, appState, startTime, client, ]); // Compute loading message const nonInteractiveLoadingMessage = React.useMemo(() => { if (!nonInteractivePrompt) { return null; } // Don't show loading message when conversation is complete (about to exit) if (appState.isConversationComplete) { return null; } if (!mcpInitialized || !client) { return 'Waiting for MCP servers...'; } const pendingToolCallCount = 0; // This would need to be passed if available if (appState.isToolExecuting || appState.isToolConfirmationMode || pendingToolCallCount > 0) { return 'Waiting for tooling...'; } return 'Waiting for chat to complete...'; }, [ nonInteractivePrompt, appState.isConversationComplete, mcpInitialized, client, appState.isToolExecuting, appState.isToolConfirmationMode, ]); return { nonInteractiveSubmitted, nonInteractiveLoadingMessage, }; } //# sourceMappingURL=useNonInteractiveMode.js.map