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