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

331 lines 12.4 kB
import React from 'react'; import { parseInput } from '../../command-parser.js'; import { commandRegistry } from '../../commands.js'; import { CopilotLogin } from '../../commands/copilot-login.js'; import BashProgress from '../../components/bash-progress.js'; import { ErrorMessage, InfoMessage, SuccessMessage, } from '../../components/message-box.js'; import { DELAY_COMMAND_COMPLETE_MS } from '../../constants.js'; import { CheckpointManager } from '../../services/checkpoint-manager.js'; import { executeBashCommand, formatBashResultForLLM } from '../../tools/execute-bash.js'; import { handleCompactCommand } from './handlers/compact-handler.js'; import { handleContextMaxCommand } from './handlers/context-max-handler.js'; import { handleCommandCreate, handleScheduleCreate, handleScheduleStart, } from './handlers/create-handler.js'; import { handleResumeCommand } from './handlers/session-handler.js'; // Re-export for consumers that import parseContextLimit from here export { parseContextLimit } from './handlers/context-max-handler.js'; /** Command names that require special handling in the app */ const SPECIAL_COMMANDS = { CLEAR: 'clear', MODEL: 'model', PROVIDER: 'provider', MODEL_DATABASE: 'model-database', SETUP_PROVIDERS: 'setup-providers', SETUP_MCP: 'setup-mcp', SETTINGS: 'settings', STATUS: 'status', CHECKPOINT: 'checkpoint', EXPLORER: 'explorer', IDE: 'ide', }; /** Checkpoint subcommands */ const CHECKPOINT_SUBCOMMANDS = { LOAD: 'load', RESTORE: 'restore', }; /** * Extracts error message from an unknown error */ function getErrorMessage(error, fallback = 'Unknown error') { return error instanceof Error ? error.message : fallback; } /** * Handles bash commands prefixed with ! */ async function handleBashCommand(bashCommand, options) { const { onAddToChatQueue, setLiveComponent, setIsToolExecuting, onCommandComplete, getNextComponentKey, setMessages, messages, } = options; setIsToolExecuting(true); try { const { executionId, promise } = executeBashCommand(bashCommand); setLiveComponent(React.createElement(BashProgress, { key: `bash-progress-live-${getNextComponentKey()}`, executionId, command: bashCommand, isLive: true, })); const result = await promise; setLiveComponent(null); onAddToChatQueue(React.createElement(BashProgress, { key: `bash-progress-complete-${getNextComponentKey()}`, executionId, command: bashCommand, completedState: result, })); const llmContext = formatBashResultForLLM(result); if (llmContext) { const userMessage = { role: 'user', content: `Bash command output:\n\`\`\`\n$ ${bashCommand}\n${llmContext}\n\`\`\``, }; setMessages([...messages, userMessage]); } } catch (error) { setLiveComponent(null); onAddToChatQueue(React.createElement(ErrorMessage, { key: `bash-error-${getNextComponentKey()}`, message: `Error executing command: ${getErrorMessage(error, String(error))}`, })); } finally { setIsToolExecuting(false); onCommandComplete?.(); } } /** * Handles custom user-defined commands. * Returns true if a custom command was found and handled. */ async function handleCustomCommand(message, commandName, options) { const { customCommandCache, customCommandLoader, customCommandExecutor, onHandleChatMessage, onCommandComplete, } = options; const customCommand = customCommandCache.get(commandName) || customCommandLoader?.getCommand(commandName); if (!customCommand) { return false; } const args = message .slice(commandName.length + 2) .trim() .split(/\s+/) .filter(arg => arg); const processedPrompt = customCommandExecutor?.execute(customCommand, args); if (processedPrompt) { await onHandleChatMessage(processedPrompt); } else { onCommandComplete?.(); } return true; } /** * Handles special commands that need app state access (/clear, /model, etc.) * Returns true if a special command was handled. */ async function handleSpecialCommand(commandName, options) { const { onClearMessages, onEnterModelSelectionMode, onEnterProviderSelectionMode, onEnterModelDatabaseMode, onEnterConfigWizardMode, onEnterSettingsMode, onEnterMcpWizardMode, onEnterExplorerMode, onShowStatus, onCommandComplete, onAddToChatQueue, getNextComponentKey, } = options; switch (commandName) { case SPECIAL_COMMANDS.CLEAR: await onClearMessages(); onAddToChatQueue(React.createElement(SuccessMessage, { key: `clear-success-${getNextComponentKey()}`, message: 'Chat cleared.', hideBox: true, })); setTimeout(() => onCommandComplete?.(), DELAY_COMMAND_COMPLETE_MS); return true; case SPECIAL_COMMANDS.MODEL: onEnterModelSelectionMode(); onCommandComplete?.(); return true; case SPECIAL_COMMANDS.PROVIDER: onEnterProviderSelectionMode(); onCommandComplete?.(); return true; case SPECIAL_COMMANDS.MODEL_DATABASE: onEnterModelDatabaseMode(); onCommandComplete?.(); return true; case SPECIAL_COMMANDS.SETUP_PROVIDERS: onEnterConfigWizardMode(); onCommandComplete?.(); return true; case SPECIAL_COMMANDS.SETUP_MCP: onEnterMcpWizardMode(); onCommandComplete?.(); return true; case SPECIAL_COMMANDS.SETTINGS: onEnterSettingsMode(); onCommandComplete?.(); return true; case SPECIAL_COMMANDS.STATUS: onShowStatus(); setTimeout(() => onCommandComplete?.(), DELAY_COMMAND_COMPLETE_MS); return true; case SPECIAL_COMMANDS.EXPLORER: onEnterExplorerMode(); onCommandComplete?.(); return true; case SPECIAL_COMMANDS.IDE: options.onEnterIdeSelectionMode(); onCommandComplete?.(); return true; default: return false; } } /** * Handles interactive checkpoint load command. * Returns true if checkpoint load was handled. */ async function handleCheckpointLoad(commandParts, options) { const { onAddToChatQueue, onEnterCheckpointLoadMode, onCommandComplete, getNextComponentKey, messages, } = options; const isCheckpointLoad = commandParts[0] === SPECIAL_COMMANDS.CHECKPOINT && (commandParts[1] === CHECKPOINT_SUBCOMMANDS.LOAD || commandParts[1] === CHECKPOINT_SUBCOMMANDS.RESTORE) && commandParts.length === 2; if (!isCheckpointLoad) { return false; } try { const manager = new CheckpointManager(); const checkpoints = await manager.listCheckpoints(); if (checkpoints.length === 0) { onAddToChatQueue(React.createElement(InfoMessage, { key: `checkpoint-info-${getNextComponentKey()}`, message: 'No checkpoints available. Create one with /checkpoint create [name]', hideBox: true, })); onCommandComplete?.(); return true; } onEnterCheckpointLoadMode(checkpoints, messages.length); return true; } catch (error) { onAddToChatQueue(React.createElement(ErrorMessage, { key: `checkpoint-error-${getNextComponentKey()}`, message: `Failed to list checkpoints: ${getErrorMessage(error)}`, hideBox: true, })); onCommandComplete?.(); return true; } } /** * Handles /copilot-login as a live component. * Returns true if handled. */ function handleCopilotLogin(commandParts, options) { if (commandParts[0] !== 'copilot-login') { return false; } const { setLiveComponent, setIsToolExecuting, onAddToChatQueue, onCommandComplete, getNextComponentKey, } = options; const providerName = commandParts[1]?.trim() || 'GitHub Copilot'; setIsToolExecuting(true); setLiveComponent(React.createElement(CopilotLogin, { key: `copilot-login-live-${getNextComponentKey()}`, providerName, onDone: result => { setLiveComponent(null); setIsToolExecuting(false); if (result.success) { onAddToChatQueue(React.createElement(SuccessMessage, { key: `copilot-login-done-${getNextComponentKey()}`, message: `Logged in. Credential saved for "${providerName}".`, hideBox: true, })); } else { onAddToChatQueue(React.createElement(ErrorMessage, { key: `copilot-login-error-${getNextComponentKey()}`, message: result.error ?? 'Login failed.', })); } onCommandComplete?.(); }, })); return true; } /** * Handles built-in commands via the command registry. */ async function handleBuiltInCommand(message, options) { const { onAddToChatQueue, onCommandComplete, getNextComponentKey, messages } = options; const totalTokens = messages.reduce((sum, msg) => sum + options.getMessageTokens(msg), 0); const result = await commandRegistry.execute(message.slice(1), messages, { provider: options.provider, model: options.model, tokens: totalTokens, getMessageTokens: options.getMessageTokens, }); if (!result) { onCommandComplete?.(); return; } if (React.isValidElement(result)) { queueMicrotask(() => { onAddToChatQueue(result); }); setTimeout(() => { onCommandComplete?.(); }, DELAY_COMMAND_COMPLETE_MS); return; } if (typeof result === 'string' && result.trim()) { queueMicrotask(() => { onAddToChatQueue(React.createElement(InfoMessage, { key: `command-result-${getNextComponentKey()}`, message: result, hideBox: true, })); }); setTimeout(() => { onCommandComplete?.(); }, DELAY_COMMAND_COMPLETE_MS); return; } onCommandComplete?.(); } /** * Handles slash commands (prefixed with /). */ async function handleSlashCommand(message, options) { const commandName = message.slice(1).split(/\s+/)[0]; if (await handleCustomCommand(message, commandName, options)) { return; } const commandParts = message.slice(1).trim().split(/\s+/); if (await handleCompactCommand(commandParts, options)) return; if (await handleContextMaxCommand(commandParts, options)) return; if (await handleScheduleStart(commandParts, options)) return; if (await handleScheduleCreate(commandParts, options)) return; if (await handleCommandCreate(commandParts, options)) return; if (await handleSpecialCommand(commandName, options)) return; if (await handleCheckpointLoad(commandParts, options)) return; if (await handleResumeCommand(commandParts, options)) return; if (handleCopilotLogin(commandParts, options)) return; await handleBuiltInCommand(message, options); } /** * Main entry point for handling user message submission. * Routes messages to appropriate handlers based on their type. */ export async function handleMessageSubmission(message, options) { const parsedInput = parseInput(message); if (parsedInput.isBashCommand && parsedInput.bashCommand) { await handleBashCommand(parsedInput.bashCommand, options); return; } if (message.startsWith('/')) { await handleSlashCommand(message, options); return; } await options.onHandleChatMessage(message); } export function createClearMessagesHandler(setMessages, client) { return async () => { setMessages([]); if (client) { await client.clearContext(); } }; } //# sourceMappingURL=app-util.js.map