capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
1,228 lines • 63.6 kB
JavaScript
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