UNPKG

capsule-ai-cli

Version:

The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing

1,228 lines 63.6 kB
import chalk from 'chalk'; import { authService } from '../services/auth.js'; import { promptForAuth } from './auth-prompt.js'; import { authCommand, logoutCommand } from '../commands/auth.js'; import { statusCommand } from '../commands/status.js'; import { stateService } from '../services/state.js'; import inquirer from 'inquirer'; import { contextManager } from '../services/context.js'; import { chatService } from '../services/chat.js'; import { configManager } from '../core/config.js'; import { providerRegistry } from '../providers/base.js'; import { registerBuiltinTools } from '../tools/builtin/index.js'; import { toolExecutor } from '../tools/executor.js'; import { FileHandler } from '../services/file-handler.js'; import path from 'path'; import { CAPSULE_CORP_BIG } from './capsule-corp-logo.js'; import { terminalController, TerminalController } from './modules/terminal-controller.js'; import { TextProcessor } from './modules/text-processor.js'; import { animationController } from './modules/animation-controller.js'; import { conversationRenderer } from './modules/conversation-renderer.js'; import { scrollManager } from './modules/scroll-manager.js'; import { suggestionEngine } from './modules/suggestion-engine.js'; import { inputHandler } from './modules/input-handler.js'; import { layoutManager } from './modules/layout-manager.js'; import { commandProcessor } from './modules/command-processor.js'; import { uiStateManager } from './modules/ui-state-manager.js'; export var Mode; (function (Mode) { Mode["CHAT"] = "chat"; Mode["AGENT"] = "agent"; Mode["PLAN"] = "plan"; Mode["FUSION"] = "fusion"; })(Mode || (Mode = {})); class InteractivePrompt { currentMode = Mode.CHAT; isProcessingRequest = false; pendingToolCount = 0; currentAssistantContent = ''; recentToolCalls = []; MAX_TOOL_HISTORY = 10; showToolCallsPanel = false; isAutoMode = false; lastEscTime = 0; ESC_DOUBLE_PRESS_TIMEOUT = 500; constructor() { suggestionEngine.setCommands(commandProcessor.getCommands()); this.currentMode = stateService.getMode(); this.setupInputHandlers(); this.setupCommandHandlers(); this.setupToolHandlers(); this.setupUIStateHandlers(); } setupInputHandlers() { inputHandler.on('input-changed', (input) => { if (!this.isProcessingRequest) { suggestionEngine.handleInputChange(input); } this.redrawScreen(); }); inputHandler.on('submit', async (input) => { if (!this.isProcessingRequest) { await this.handleInput(input); } }); inputHandler.on('tab', () => { if (!this.isProcessingRequest) { this.handleTab(); } }); inputHandler.on('key', (event) => { this.handleSpecialKeys(event); }); inputHandler.on('paste-start', () => { this.redrawScreen(); }); inputHandler.on('paste-end', () => { this.redrawScreen(); }); inputHandler.on('multi-line-start', () => { console.log(chalk.dim(' [Multi-line mode - Press Enter twice to send]')); }); inputHandler.on('multi-line-end', () => { this.redrawScreen(); }); inputHandler.on('escape-pressed', () => { this.handleEscape(); }); inputHandler.on('history-up-empty', () => { scrollManager.scrollUp(3); this.redrawScreen(); }); inputHandler.on('history-up', () => { inputHandler.handleHistoryUp(); }); inputHandler.on('history-down', () => { inputHandler.handleHistoryDown(); }); inputHandler.on('page-up', () => { scrollManager.pageUp(); this.redrawScreen(); }); inputHandler.on('page-down', () => { scrollManager.pageDown(); this.redrawScreen(); }); inputHandler.on('cursor-left', () => { }); inputHandler.on('cursor-right', () => { }); inputHandler.on('up-arrow', () => { this.handleUpArrow(); }); inputHandler.on('down-arrow', () => { this.handleDownArrow(); }); inputHandler.on('mouse-up', () => { this.handleUpArrow(); }); inputHandler.on('mouse-down', () => { this.handleDownArrow(); }); } setupCommandHandlers() { commandProcessor.on('command', async (_data) => { }); } setupToolHandlers() { toolExecutor.on('execution:start', (execution) => { uiStateManager.startToolExecution(execution.id); const displayName = this.getToolDisplayName(execution.toolName); uiStateManager.addToolProgressOutput(`Starting ${displayName}...`, execution.id, execution.toolName); }); toolExecutor.on('execution:progress', (executionId, progress) => { if (progress?.message) { uiStateManager.addToolProgressOutput(progress.message, executionId); } }); toolExecutor.on('execution:complete', (execution) => { this.handleToolComplete(execution); }); toolExecutor.on('execution:error', (execution) => { this.handleToolError(execution); }); } setupUIStateHandlers() { uiStateManager.on('console-output-intercepted', (_output, _stream) => { }); uiStateManager.on('state-changed', (state) => { if (state.mode === 'normal' && state.bufferedOutput.length > 0) { const bufferedOutput = uiStateManager.flushBufferedOutput(); for (const output of bufferedOutput) { const formattedItem = uiStateManager.formatBufferedOutput(output); if (formattedItem) { conversationRenderer.addItem(formattedItem); } } this.redrawScreen(); } }); } handleSpecialKeys(event) { if (event.name === 'ctrl-c') { this.cleanup(); console.log(chalk.yellow('\n\nGoodbye!')); process.exit(0); } if (event.name === 'ctrl-up') { scrollManager.scrollUp(1); this.redrawScreen(); return; } if (event.name === 'ctrl-down') { scrollManager.scrollDown(1); this.redrawScreen(); return; } if (event.name === 'ctrl-r') { this.showToolCallsPanel = !this.showToolCallsPanel; if (this.showToolCallsPanel) { this.displayToolCallsPanel(); } else { console.log(chalk.dim('Tool calls panel closed. Press Ctrl+R to reopen.')); } this.redrawScreen(); } if (event.name === 'shift-tab') { if (!this.isProcessingRequest) { this.handleModeToggle(); } } } handleTab() { const input = inputHandler.getInputBuffer(); if (!input.startsWith('/')) { return; } const suggestion = suggestionEngine.getSelectedSuggestion(); if (suggestion) { inputHandler.setInputBuffer(suggestion.cmd); suggestionEngine.clearSuggestions(); return; } const commonPrefix = suggestionEngine.getCommonPrefix(input); if (commonPrefix && commonPrefix.length > input.length) { inputHandler.setInputBuffer(commonPrefix); } } handleEscape() { const now = Date.now(); if (this.isProcessingRequest) { chatService.abort(); this.isProcessingRequest = false; animationController.stop(); console.log(chalk.yellow('✗ Request interrupted')); this.redrawScreen(); this.lastEscTime = now; return; } if (now - this.lastEscTime < this.ESC_DOUBLE_PRESS_TIMEOUT) { this.handleGoBack(); this.lastEscTime = 0; } else { this.lastEscTime = now; } } handleUpArrow() { const input = inputHandler.getInputBuffer(); if (input.length === 0) { scrollManager.scrollUp(3); this.redrawScreen(); return; } if (this.isProcessingRequest) { scrollManager.scrollUp(3); this.redrawScreen(); return; } if (suggestionEngine.isShowingSuggestions()) { if (suggestionEngine.selectPrevious()) { this.redrawScreen(); } return; } inputHandler.emit('history-up'); } handleDownArrow() { const input = inputHandler.getInputBuffer(); if (input.length === 0) { scrollManager.scrollDown(3); this.redrawScreen(); return; } if (this.isProcessingRequest) { scrollManager.scrollDown(3); this.redrawScreen(); return; } if (suggestionEngine.isShowingSuggestions()) { if (suggestionEngine.selectNext()) { this.redrawScreen(); } return; } inputHandler.emit('history-down'); } handleModeToggle() { import('../services/edit-confirmation.js').then(({ editConfirmationService }) => { const modeSequence = ['chat', 'agent', 'plan', 'fusion', 'auto-accept']; let currentIndex = -1; if (this.isAutoMode) { currentIndex = 4; } else { const modeMap = { [Mode.CHAT]: 0, [Mode.AGENT]: 1, [Mode.PLAN]: 2, [Mode.FUSION]: 3 }; currentIndex = modeMap[this.currentMode] ?? 0; } const nextIndex = (currentIndex + 1) % modeSequence.length; const nextModeName = modeSequence[nextIndex]; if (nextModeName === 'auto-accept') { this.isAutoMode = true; editConfirmationService.setAutoApprove(true); } else { this.isAutoMode = false; editConfirmationService.setAutoApprove(false); const modeValues = { 'chat': Mode.CHAT, 'agent': Mode.AGENT, 'plan': Mode.PLAN, 'fusion': Mode.FUSION }; this.currentMode = modeValues[nextModeName] || Mode.CHAT; stateService.setMode(this.currentMode); } this.redrawScreen(); }); } redrawScreen() { const { columns } = TerminalController.getSize(); const state = inputHandler.getState(); const suggestions = suggestionEngine.getState(); const layout = layoutManager.calculateLayout({ showingSuggestions: suggestions.isShowing && !this.isProcessingRequest, suggestionCount: suggestions.count, multiLineMode: state.isMultiLine, inputLineCount: this.calculateInputLines(state), isSlashCommand: state.input.startsWith('/') }); const displayLines = conversationRenderer.getDisplayLines(columns); scrollManager.updateDimensions(displayLines.length, layout.messageAreaHeight); terminalController.clearAndResetScreen(); this.drawMessages(displayLines, layout); this.drawFixedUI(layout); } calculateInputLines(state) { if (state.isPasting) { return 1; } if (state.isMultiLine) { return state.multiLineBuffer.length + 1; } return 1; } drawMessages(displayLines, layout) { const visibleRange = scrollManager.getVisibleRange(); const visibleLines = displayLines.slice(visibleRange.start, visibleRange.end); for (let idx = 0; idx < layout.messageAreaHeight; idx++) { terminalController.clearLineAndWrite(idx + 1, ''); if (idx < visibleLines.length) { let line = visibleLines[idx]; if (scrollManager.calculateScrollbar().hasScrollbar) { line = TextProcessor.fitText(line, layout.terminalWidth - 2); } process.stdout.write(line); } if (scrollManager.calculateScrollbar().hasScrollbar) { const scrollChar = scrollManager.renderScrollbarCharacter(idx); terminalController.writeAt(layout.terminalWidth, idx + 1, scrollChar); } } } drawFixedUI(layout) { terminalController.hideCursor(); if (!animationController.isRunning()) { terminalController.clearLineAndWrite(layout.animationRow, ''); } this.drawInputBox(layout); if (suggestionEngine.isShowingSuggestions()) { this.drawSuggestions(layout); } this.drawFooter(layout); this.positionCursor(layout); terminalController.showCursor(); } drawInputBox(layout) { const state = inputHandler.getState(); terminalController.clearLineAndWrite(layout.inputTopRow, chalk.gray('╭' + '─'.repeat(layout.terminalWidth - 2) + '╮')); const inputLines = this.getInputDisplayLines(state, layout.terminalWidth - 4); for (let i = 0; i < layout.inputHeight; i++) { const row = layout.inputContentRow + i; terminalController.clearLineAndWrite(row, ''); if (i < inputLines.length) { const line = inputLines[i]; const contentWidth = layout.terminalWidth - 4; const truncatedLine = TextProcessor.fitText(line, contentWidth); const padding = ' '.repeat(Math.max(0, contentWidth - TextProcessor.getVisibleLength(truncatedLine))); process.stdout.write(chalk.gray('│ ') + truncatedLine + padding + chalk.gray(' │')); } else { process.stdout.write(chalk.gray('│ ' + ' '.repeat(layout.terminalWidth - 4) + '│')); } } terminalController.clearLineAndWrite(layout.inputBottomRow, chalk.gray('╰' + '─'.repeat(layout.terminalWidth - 2) + '╯')); } getInputDisplayLines(state, width) { const prompt = state.isMultiLine ? chalk.dim('... ') : '> '; const pasteMatch = state.input.match(/\[Pasted text #(\d+) \+(.+)\]/); if (pasteMatch) { return [prompt + state.input]; } if (state.isMultiLine) { const lines = []; state.multiLineBuffer.forEach((line, index) => { const linePrompt = index === 0 ? '> ' : chalk.dim('... '); lines.push(...TextProcessor.wrapText(linePrompt + line, width)); }); lines.push(...TextProcessor.wrapText(prompt + state.input, width)); return lines; } let display = prompt + state.input; if (TextProcessor.getVisibleLength(display) > width) { display = TextProcessor.truncate(display, width, { position: 'start' }); } return [display]; } drawSuggestions(layout) { const input = inputHandler.getInputBuffer(); const suggestions = suggestionEngine.renderSuggestions(layout.suggestionHeight, layout.terminalWidth, input); suggestions.forEach((line, index) => { const row = layout.suggestionStartRow + index; if (row < layout.footerRow) { terminalController.clearLineAndWrite(row, line); } }); } drawFooter(layout) { const stats = contextManager.getContextStats(); const model = stateService.getModel(); const limit = contextManager.getTokenLimit(model); const percentage = Math.round((stats.tokenCount / limit) * 100); const modeText = { [Mode.CHAT]: 'chat mode', [Mode.AGENT]: 'agent mode', [Mode.PLAN]: 'plan mode', [Mode.FUSION]: 'fusion mode' }[this.currentMode] || 'chat mode'; let contextText; const tokenInfo = `${stats.tokenCount.toLocaleString()}/${(limit / 1000).toFixed(0)}k tokens (${percentage}%)`; if (percentage > 90) { contextText = chalk.red(`[${tokenInfo}] ⚠`); } else if (percentage > 80) { contextText = chalk.yellow(`[${tokenInfo}] ⚠`); } else { contextText = chalk.dim(`[${tokenInfo}]`); } const modeColor = { [Mode.CHAT]: chalk.cyan, [Mode.AGENT]: chalk.yellow, [Mode.PLAN]: chalk.blue, [Mode.FUSION]: chalk.magenta }[this.currentMode] || chalk.cyan; let leftContent; if (inputHandler.isPastingMode()) { leftContent = `${chalk.yellow.bold('⬇ Pasting...')} ${chalk.gray('|')} ${chalk.green(model)}`; } else if (this.isProcessingRequest) { const scrollHint = scrollManager.canScroll('up') || scrollManager.canScroll('down') ? chalk.dim(' [↑↓ to scroll]') : ''; leftContent = `${modeColor(modeText)} ${chalk.gray('|')} ${chalk.green(model)}${scrollHint}`; } else { leftContent = `${modeColor(modeText)} ${chalk.gray('|')} ${chalk.green(model)}`; } const footerLayout = layoutManager.calculateFooterLayout(layout.terminalWidth); const leftTruncated = TextProcessor.truncate(leftContent, footerLayout.maxLeftContentWidth); const rightTruncated = TextProcessor.truncate(contextText, footerLayout.maxRightContentWidth); const leftLen = TextProcessor.getVisibleLength(leftTruncated); const rightLen = TextProcessor.getVisibleLength(rightTruncated); const padding = Math.max(2, layout.terminalWidth - leftLen - rightLen); terminalController.clearLineAndWrite(layout.footerRow, leftTruncated + ' '.repeat(padding) + rightTruncated); } positionCursor(layout) { const state = inputHandler.getState(); const prompt = state.isMultiLine ? chalk.dim('... ') : '> '; const promptLength = TextProcessor.getVisibleLength(prompt); const inputLength = TextProcessor.getVisibleLength(state.input); const cursorPos = layoutManager.calculateCursorPosition(layout.inputContentRow, layout.inputHeight, promptLength + inputLength, layout.terminalHeight, layout.terminalWidth); terminalController.moveCursorTo(cursorPos.col, cursorPos.row); } async handleInput(input) { let actualInput = input; const pasteMatch = input.match(/\[Pasted text #(\d+) \+(.+)\]/); if (pasteMatch) { const pasteIndex = parseInt(pasteMatch[1]); const pastedText = inputHandler.getPastedText(pasteIndex); if (pastedText) { actualInput = pastedText; } } suggestionEngine.clearSuggestions(); stateService.addToHistory(actualInput); if (commandProcessor.isCommand(actualInput)) { await this.handleSlashCommand(actualInput); } else { await this.handleModeInput(actualInput); } this.redrawScreen(); } async handleSlashCommand(input) { const result = await commandProcessor.processCommand(input); if (!result.success) { conversationRenderer.addItem({ type: 'error', content: result.message || 'Command failed', timestamp: new Date() }); return; } switch (result.action) { case 'exit': this.cleanup(); console.log(chalk.yellow('\nGoodbye!')); process.exit(0); case 'help': conversationRenderer.addItem({ type: 'system', content: commandProcessor.getHelpText(), timestamp: new Date() }); break; case 'clear': conversationRenderer.clearBuffer(); await this.displayWelcomeHeader(); this.redrawScreen(); break; case 'new': const newContext = contextManager.createNewContext(); contextManager.setCurrentContext(newContext.id); conversationRenderer.clearBuffer(); await this.displayWelcomeHeader(); scrollManager.resetScroll(); this.redrawScreen(); break; default: await this.handleComplexCommand(result.action, result.data); } } async handleComplexCommand(action, data) { switch (action) { case 'model': await this.handleModelCommand(); break; case 'provider': await this.handleProviderCommand(); break; case 'keys': await this.handleKeysCommand(); break; case 'chats': await this.handleChatsCommand(); break; case 'context': this.handleContextCommand(); break; case 'compact': await this.handleCompactCommand(); break; case 'stats': this.handleStatsCommand(); break; case 'cost': this.handleCostCommand(); break; case 'auth': await this.handleAuthCommand(data.args); break; case 'logout': await this.handleLogoutCommand(); break; case 'status': await this.handleStatusCommand(); break; } } async handleModeInput(input) { conversationRenderer.addItem({ type: 'user', content: input, timestamp: new Date() }); const provider = stateService.getProvider(); const model = stateService.getModel(); animationController.setProvider(provider); animationController.setModel(model); const contextStats = contextManager.getContextStats(); const inputTokens = Math.ceil(input.length / 4); const systemTokens = 50; const promptTokens = contextStats.tokenCount + inputTokens + systemTokens; const state = inputHandler.getState(); const suggestions = suggestionEngine.getState(); const layout = layoutManager.calculateLayout({ showingSuggestions: suggestions.isShowing, suggestionCount: suggestions.count, multiLineMode: state.isMultiLine, inputLineCount: state.multiLineBuffer.length + 1, isSlashCommand: state.input.startsWith('/') }); animationController.setRenderPosition(layout.animationRow); animationController.start({ provider, model, initialTokens: promptTokens }); this.isProcessingRequest = true; try { const messageContent = await this.processFileReferences(input); this.currentAssistantContent = ''; let assistantItem = { type: 'assistant', content: '', timestamp: new Date() }; conversationRenderer.addItem(assistantItem); const stream = chatService.stream(messageContent, { mode: this.currentMode }); for await (const chunk of stream) { if (!this.isProcessingRequest) break; if (chunk.toolCall) { this.handleToolCall(chunk.toolCall); } else if (chunk.delta) { this.currentAssistantContent += this.formatDelta(chunk.delta); conversationRenderer.updateLastAssistantMessage(this.currentAssistantContent); this.redrawScreen(); if (scrollManager.shouldAutoscroll()) { scrollManager.scrollToBottom(); this.redrawScreen(); } } if (chunk.usage) { animationController.updateTokens(chunk.usage.totalTokens); } } } catch (error) { if (error.name !== 'AbortError') { conversationRenderer.addItem({ type: 'error', content: error.message, timestamp: new Date() }); } } finally { this.isProcessingRequest = false; animationController.stop(); this.redrawScreen(); } } async processFileReferences(input) { const filePaths = FileHandler.extractFilePaths(input); if (filePaths.length === 0) { return input; } const textContent = FileHandler.removeFileReferences(input); const contentParts = []; if (textContent.trim()) { contentParts.push({ type: 'text', text: textContent }); } for (const filePath of filePaths) { try { const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath); conversationRenderer.addItem({ type: 'tool-call', content: filePath, metadata: { toolName: 'Read' }, timestamp: new Date() }); const fileContent = await FileHandler.loadFile(resolvedPath); contentParts.push(fileContent); this.addToolCall({ name: 'file_read', arguments: { path: filePath }, timestamp: new Date() }); } catch (error) { conversationRenderer.addItem({ type: 'tool-result', content: error.message, metadata: { success: false }, timestamp: new Date() }); } } return contentParts.length > 0 ? contentParts : input; } handleToolCall(toolCall) { this.pendingToolCount++; if (this.pendingToolCount === 1) { animationController.updateMessage(`Running ${this.pendingToolCount} tool${this.pendingToolCount > 1 ? 's' : ''}...`); } const displayName = this.getToolDisplayName(toolCall.name); const params = this.formatToolParams(toolCall); conversationRenderer.addItem({ type: 'tool-call', content: params, metadata: { toolName: displayName }, timestamp: new Date() }); this.addToolCall({ name: toolCall.name, arguments: toolCall.arguments || {}, timestamp: new Date() }); } handleToolComplete(execution) { uiStateManager.endToolExecution(execution.id); const bufferedOutput = uiStateManager.flushBufferedOutput(); for (const output of bufferedOutput) { const formattedItem = uiStateManager.formatBufferedOutput(output); if (formattedItem) { conversationRenderer.addItem(formattedItem); } } const toolCall = this.recentToolCalls.find(call => call.name === execution.toolName && !call.result && (execution.startTime.getTime() - call.timestamp.getTime()) < 60000); if (toolCall && execution.result) { toolCall.result = execution.result.output || execution.result.error || execution.result; } if (this.pendingToolCount > 0) { this.pendingToolCount--; if (this.pendingToolCount > 0) { animationController.updateMessage(`Running ${this.pendingToolCount} tool${this.pendingToolCount > 1 ? 's' : ''}...`); } else { animationController.updateMessage(null); } } if (execution.state === 'completed' && execution.result?.success) { if ((execution.toolName === 'file_edit' || execution.toolName === 'file_edit_with_confirmation') && execution.result.output) { const result = execution.result.output; const diffData = { filePath: result.path || '', oldText: execution.parameters?.oldText || '', newText: execution.parameters?.newText || '', lineNumber: this.findLineNumber(result.preview || '', execution.parameters?.oldText || ''), additions: result.linesChanged || 1, deletions: result.linesChanged || 1 }; conversationRenderer.addItem({ type: 'tool-result', content: `Successfully edited ${result.path}`, metadata: { success: true, toolName: 'Edit', diffData }, timestamp: new Date() }); } else { const outputContent = execution.result.output || execution.result; const content = typeof outputContent === 'string' ? outputContent : JSON.stringify(outputContent, null, 2); conversationRenderer.addItem({ type: 'tool-result', content: content, metadata: { success: true }, timestamp: new Date() }); } } else if (execution.state === 'error' || !execution.result?.success) { conversationRenderer.addItem({ type: 'tool-result', content: execution.result?.error || 'Failed', metadata: { success: false }, timestamp: new Date() }); } this.redrawScreen(); } handleToolError(execution) { uiStateManager.endToolExecution(execution.id); const bufferedOutput = uiStateManager.flushBufferedOutput(); for (const output of bufferedOutput) { const formattedItem = uiStateManager.formatBufferedOutput(output); if (formattedItem) { conversationRenderer.addItem(formattedItem); } } conversationRenderer.addItem({ type: 'tool-result', content: execution.result?.error || 'Failed', metadata: { success: false }, timestamp: new Date() }); if (this.pendingToolCount > 0) { this.pendingToolCount--; } this.redrawScreen(); } formatDelta(delta) { let formatted = delta; formatted = conversationRenderer.formatMessageContent(formatted); return formatted; } getToolDisplayName(toolName) { const displayNames = { 'file_read': 'Read', 'file_write': 'Write', 'file_write_with_confirmation': 'Write', 'file_edit': 'Edit', 'file_edit_with_confirmation': 'Edit', 'file_list': 'List', 'search': 'Search', 'grep': 'Search', 'find': 'Search', 'todo_list': 'Todo', 'task_spawn': 'Task', 'bash': 'Bash' }; return displayNames[toolName] || toolName; } formatToolParams(toolCall) { const args = toolCall.arguments; if (!args) return ''; if (toolCall.name === 'file_read' || toolCall.name === 'file_write' || toolCall.name === 'file_write_with_confirmation' || toolCall.name === 'file_edit' || toolCall.name === 'file_edit_with_confirmation') { return args.path || args.file_path || 'no path'; } else if (toolCall.name === 'file_list') { return args.path || args.directory || '.'; } else if (toolCall.name === 'bash') { return args.command || 'no command'; } else if (toolCall.name === 'task_spawn') { return args.description || args.task || 'no description'; } else if (toolCall.name === 'todo_list') { return args.action || 'list'; } const firstParam = Object.entries(args).find(([k, v]) => k !== 'encoding' && k !== 'timeout' && v !== null && v !== undefined); if (firstParam) { const [, value] = firstParam; if (typeof value === 'string' && value.length > 50) { return value.substring(0, 47) + '...'; } return String(value); } return ''; } addToolCall(toolCall) { this.recentToolCalls.push(toolCall); if (this.recentToolCalls.length > this.MAX_TOOL_HISTORY) { this.recentToolCalls.shift(); } } displayToolCallsPanel() { if (this.recentToolCalls.length === 0) { console.log(chalk.yellow('\n📋 No recent tool calls to display')); console.log(chalk.dim('Tool calls will appear here as you use them.\n')); return; } console.log(); console.log(chalk.cyan('📋 Recent Tool Calls & Results')); console.log(chalk.dim('─'.repeat(60))); console.log(chalk.dim('Press Ctrl+R again to close this panel\n')); this.recentToolCalls.slice(-5).reverse().forEach((call, index) => { const displayName = this.getToolDisplayName(call.name); console.log(chalk.green(`${index + 1}. ${displayName}`)); console.log(chalk.gray(` └─ ${call.name}(${JSON.stringify(call.arguments)})`)); if (call.result) { console.log(chalk.blue(` Tool Result:`)); const resultStr = JSON.stringify(call.result, null, 2); const lines = resultStr.split('\n'); const maxLines = 10; if (lines.length > maxLines) { lines.slice(0, maxLines).forEach(line => { console.log(chalk.gray(` ${line}`)); }); console.log(chalk.gray(` ... (${lines.length - maxLines} more lines)`)); } else { lines.forEach(line => { console.log(chalk.gray(` ${line}`)); }); } } else { console.log(chalk.dim(` Result: (pending)`)); } console.log(); }); console.log(chalk.dim('─'.repeat(60))); console.log(); } handleGoBack() { const messages = contextManager.getCurrentContext().messages; let lastUserIndex = -1; for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === 'user') { lastUserIndex = i; break; } } if (lastUserIndex === -1) { console.log(chalk.yellow('\nNo messages to go back to.\n')); return; } const messagesToRemove = messages.length - lastUserIndex; for (let i = 0; i < messagesToRemove; i++) { contextManager.getCurrentContext().messages.pop(); } const ctx = contextManager.getCurrentContext(); ctx.metadata.lastModified = new Date(); console.log(chalk.green(`\n✓ Went back ${messagesToRemove} message${messagesToRemove > 1 ? 's' : ''}\n`)); } async displayWelcomeHeader() { const gradient = await import('gradient-string').then(m => m.default); const logoGradient = gradient([ '#FF8C00', '#FFD700', '#00CED1' ]); let welcomeContent = logoGradient.multiline(CAPSULE_CORP_BIG) + '\n\n'; const auth = await authService.getStatus(); if (!auth.isAuthenticated) { welcomeContent += chalk.yellow('⚠️ Not authenticated - Use /auth to authenticate') + '\n'; } else { const tierColor = auth.tier === 'super' ? chalk.cyan : chalk.green; const devBadge = auth.email === 'dev@capsule.local' ? chalk.magenta(' [DEV MODE]') : ''; welcomeContent += chalk.dim(`${tierColor(auth.tier?.toUpperCase() || '')} | ${auth.email}${devBadge}`) + '\n'; if (auth.tier === 'super' && auth.remainingRUs !== undefined) { welcomeContent += chalk.dim(`RUs: ${auth.remainingRUs.toLocaleString()}`) + '\n'; } } welcomeContent += '\n' + chalk.dim('Type /help for commands, ↑↓ for history, Shift+Enter for multi-line, Enter×2 to send'); conversationRenderer.addItem({ type: 'system', content: welcomeContent, timestamp: new Date(), metadata: { isWelcomeHeader: true } }); } cleanup() { terminalController.cleanup(); animationController.stop(); uiStateManager.reset(); } async start(_options) { terminalController.setupInteractiveMode(); terminalController.onResize(() => { this.redrawScreen(); }); await this.initializeProviders(); registerBuiltinTools(); const currentContext = contextManager.getCurrentContext(); if (currentContext.messages.length > 0) { for (const msg of currentContext.messages) { if (msg.role === 'user') { conversationRenderer.addItem({ type: 'user', content: typeof msg.content === 'string' ? msg.content : '[multimodal content]', timestamp: new Date() }); } else if (msg.role === 'assistant') { conversationRenderer.addItem({ type: 'assistant', content: typeof msg.content === 'string' ? msg.content : '[multimodal content]', timestamp: new Date(), metadata: msg.metadata }); } } } else { await this.displayWelcomeHeader(); } scrollManager.resetScroll(); this.redrawScreen(); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); process.stdin.setEncoding('utf8'); inputHandler.setHistory(stateService.getHistory()); process.stdin.on('data', (data) => { const key = data.toString(); if (key.includes('\x1b')) { const hex = Array.from(key).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' '); const timestamp = new Date().toISOString().slice(11, 23); terminalController.writeAt(1, 1, chalk.yellow(`[${timestamp}] ESC: ${hex.substring(0, 50)}...`)); } if (key === '\u0003') { this.cleanup(); console.log(chalk.yellow('\n\nGoodbye!')); process.exit(0); } inputHandler.processInput(data); }); } async initializeProviders() { const config = configManager.getConfig(); const { ProviderFactory } = await import('../providers/factory.js'); for (const [providerName, providerConfig] of Object.entries(config.providers || {})) { if (providerConfig.apiKey && providerConfig.enabled !== false) { try { const provider = await ProviderFactory.create(providerName); providerRegistry.register(provider); } catch (error) { } } } } async handleModelCommand() { const currentProvider = stateService.getProvider(); const availableModels = stateService.getAvailableModels(currentProvider); const currentModel = stateService.getModel(); if (availableModels.length === 0) { conversationRenderer.addItem({ type: 'system', content: `⚠️ No models available for provider ${currentProvider}`, timestamp: new Date() }); return; } if (process.stdin.isTTY) { process.stdin.setRawMode(false); } terminalController.exitAlternateScreen(); try { const { selectedModel } = await inquirer.prompt([{ type: 'list', name: 'selectedModel', message: `Select a model for ${currentProvider}:`, choices: availableModels.map(model => ({ name: model === currentModel ? `${model} (current)` : model, value: model })), default: currentModel }]); if (selectedModel !== currentModel) { await chatService.switchModel(selectedModel); const oldLimit = contextManager.getTokenLimit(currentModel); const newLimit = contextManager.getTokenLimit(selectedModel); const stats = contextManager.getContextStats(); conversationRenderer.addItem({ type: 'system', content: `✓ Switched to ${selectedModel} model`, timestamp: new Date() }); if (newLimit < oldLimit && stats.tokenCount > newLimit * 0.9) { conversationRenderer.addItem({ type: 'system', content: `⚠️ Warning: Current context (${stats.tokenCount} tokens) is near the limit for ${selectedModel} (${newLimit} tokens)\nContext will be automatically truncated if needed`, timestamp: new Date() }); } } else { conversationRenderer.addItem({ type: 'system', content: 'Model unchanged', timestamp: new Date() }); } } catch (error) { conversationRenderer.addItem({ type: 'error', content: `Error: ${error.message}`, timestamp: new Date() }); } finally { if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); terminalController.enterAlternateScreen(); this.redrawScreen(); } } async handleProviderCommand() { const availableProviders = stateService.getAvailableProviders(); const currentProvider = stateService.getProvider(); const currentModel = stateService.getModel(); terminalController.exitAlternateScreen(); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } try { const { selectedProvider } = await inquirer.prompt([{ type: 'list', name: 'selectedProvider', message: 'Select an AI provider:', choices: availableProviders.map(provider => ({ name: provider === currentProvider ? `${provider} (current)` : provider, value: provider })), default: currentProvider }]); if (selectedProvider !== currentProvider) { const newProviderModels = stateService.getAvailableModels(selectedProvider); let targetModel = currentModel; if (!newProviderModels.includes(currentModel)) { const modelPrefix = currentModel.split('-')[0].toLowerCase(); targetModel = newProviderModels.find(m => m.toLowerCase().includes(modelPrefix)) || newProviderModels[0]; } await chatService.switchModel(targetModel, selectedProvider); conversationRenderer.addItem({ type: 'system', content: `✓ Switched to ${selectedProvider} provider with ${targetModel} model`, timestamp: new Date() }); } else { conversationRenderer.addItem({ type: 'system', content: 'Provider unchanged', timestamp: new Date() }); } } catch (error) { conversationRenderer.addItem({ type: 'error', content: `Error: ${error.message}`, timestamp: new Date() }); } finally { terminalController.enterAlternateScreen(); this.redrawScreen(); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); } } async handleKeysCommand() { const providers = ['openai', 'anthropic', 'google', 'cohere', 'mistral']; terminalController.exitAlternateScreen(); conversationRenderer.addItem({ type: 'system', content: '🔑 API Key Configuration\nSelect a provider to configure:', timestamp: new Date() }); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } try { const { selectedProvider } = await inquirer.prompt([{ type: 'list', name: 'selectedProvider', message: 'Which provider do you want to configure?', choices: providers.map(p => { const config = configManager.getConfig(); const hasKey = config.providers?.[p]?.apiKey ? '✓' : '✗'; const statusColor = hasKey === '✓' ? chalk.green : chalk.red; return { name: `${p} ${statusColor(hasKey)}`, value: p }; }) }]); const { apiKey } = await inquirer.prompt([{ type: 'password', name: 'apiKey', message: `Enter your ${selectedProvider} API key:`, mask: '✦', validate: (input) => { if (!input || input.trim().length === 0) { return 'API key cannot be empty'; } return true; } }]); configManager.setConfig(`providers.${selectedProvider}.apiKey`, apiKey); configManager.setConfig(`providers.${selectedProvider}.enabled`, true); try { const { ProviderFactory } = await import('../providers/factory.js'); const provider = await ProviderFactory.create(selectedProvider); providerRegistry.register(provider); let successMessage = `✓ ${selectedProvider} API key configured successfully!`; const currentProvider = stateService.getProvider(); if (!providerRegistry.get(currentProvider)) { stateService.setProvider(selectedProvider); successMessage += `\nSwitched to ${selectedProvider} as active provider`; } conversationRenderer.addItem({ type: 'system', content: successMessage, timestamp: new Date() }); } catch (error) { conversationRenderer.addItem({ type: 'error', content: `Failed to initialize ${selectedProvider}: ${error.message}`, timestamp: new Date() }); } } catch (error) { conversationRenderer.addItem({ type: 'error', content: `Error: ${error.message}`, timestamp: new Date() }); } finally { terminalController.enterAlternateScreen(); this.redrawScreen(); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); } } async handleChatsCommand() { const contexts = contextManager.listContexts(); if (contexts.length === 0) { conversationRenderer.addItem({ type: 'system', content: 'No chats available', timestamp: new Date() }); return; } const sortedContexts = contexts.sort((a, b) => b.created.getTime() - a.created.getTime()); const currentContextId = contextManager.getCurrentContext().id; let chatsText = '📋 Previous Chats:\n'; chatsText += '━'.repeat(50) + '\n'; sortedContexts.forEach((ctx, index) => { const isCurrent = ctx.id === currentContextId; const preview = this.getContextPreview(ctx.id); const timeAgo = this.getTime