capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
741 lines • 36.5 kB
JavaScript
import React, { useState, useEffect } from 'react';
import { Box, useApp, useInput, Static } from 'ink';
import { ChatMessage } from './ChatMessage.js';
import { InputBox } from './InputBox.js';
import { StatusBar } from './StatusBar.js';
import { CommandPalette } from './CommandPalette.js';
import { LoadingAnimation } from './LoadingAnimation.js';
import { ModelSelector } from './ModelSelector.js';
import { ProviderSelector } from './ProviderSelector.js';
import { EditConfirmation } from './EditConfirmation.js';
import { BashConfirmation } from './BashConfirmation.js';
import { KeyConfiguration } from './KeyConfiguration.js';
import { ChatSelector } from './ChatSelector.js';
import { ChatDeleter } from './ChatDeleter.js';
import { SubAgentConfig } from './SubAgentConfig.js';
import { OrchestratorConfig } from './OrchestratorConfig.js';
import { ActivationUI } from './ActivationUI.js';
import { ResizeHandler } from './ResizeHandler.js';
import { chatService } from '../../services/chat.js';
import { contextManager } from '../../services/context.js';
import { stateService } from '../../services/state.js';
import { toolExecutor } from '../../tools/executor.js';
import { editConfirmationService } from '../../services/edit-confirmation.js';
import { bashConfirmationService } from '../../services/bash-confirmation.js';
import { toolResultsService } from '../../services/tool-results.js';
import { authService } from '../../services/auth.js';
import { v4 as uuidv4 } from 'uuid';
import { getLogo } from '../capsule-logo.js';
import { executeCommand } from '../commands/index.js';
import chalk from 'chalk';
import { configManager } from '../../core/config.js';
const 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': 'Sub-Agents',
'bash': 'Bash',
'git': 'Git',
'web_fetch': 'Web Fetch',
'google_search': 'Google Search'
};
return displayNames[toolName] || toolName;
};
export const Window = () => {
const { exit } = useApp();
const [messages, setMessages] = useState([]);
const [currentInput, setCurrentInput] = useState('');
const [showCommandPalette, setShowCommandPalette] = useState(false);
const [mode, setMode] = useState(stateService.getMode());
const [isProcessing, setIsProcessing] = useState(false);
const [processingStartTime, setProcessingStartTime] = useState();
const [streamedTokens, setStreamedTokens] = useState(0);
const [showModelSelector, setShowModelSelector] = useState(false);
const [showProviderSelector, setShowProviderSelector] = useState(false);
const [selectorData, setSelectorData] = useState(null);
const [editConfirmationData, setEditConfirmationData] = useState(null);
const [bashConfirmationData, setBashConfirmationData] = useState(null);
const [showKeyConfiguration, setShowKeyConfiguration] = useState(false);
const [keyConfigData, setKeyConfigData] = useState(null);
const [showChatSelector, setShowChatSelector] = useState(false);
const [chatSelectorData, setChatSelectorData] = useState(null);
const [showChatDeleter, setShowChatDeleter] = useState(false);
const [chatDeleterData, setChatDeleterData] = useState(null);
const [showSubAgentConfig, setShowSubAgentConfig] = useState(false);
const [subAgentConfigData, setSubAgentConfigData] = useState(null);
const [showOrchestratorConfig, setShowOrchestratorConfig] = useState(false);
const [orchestratorConfigData, setOrchestratorConfigData] = useState(null);
const [showActivationUI, setShowActivationUI] = useState(false);
const [isPasting, setIsPasting] = useState(false);
const [resizeKey, setResizeKey] = useState(0);
useInput((input, key) => {
if (key.ctrl && input === 'c') {
exit();
}
if (key.escape) {
if (isProcessing) {
chatService.abort();
setIsProcessing(false);
setProcessingStartTime(undefined);
setStreamedTokens(0);
}
setShowCommandPalette(false);
setShowModelSelector(false);
setShowProviderSelector(false);
setShowKeyConfiguration(false);
setShowChatSelector(false);
setShowChatDeleter(false);
setShowSubAgentConfig(false);
setShowOrchestratorConfig(false);
if (editConfirmationData) {
editConfirmationData.onCancel();
setEditConfirmationData(null);
}
if (bashConfirmationData) {
bashConfirmationData.onCancel();
setBashConfirmationData(null);
}
}
if (key.shift && key.tab) {
const modes = ['agent', 'plan', 'orchestrator', 'auto'];
const currentIndex = modes.indexOf(mode);
const nextIndex = (currentIndex + 1) % modes.length;
const newMode = modes[nextIndex];
setMode(newMode);
stateService.setMode(newMode);
const modeMessage = {
id: uuidv4(),
type: 'system',
content: `Mode switched to ${newMode}`,
timestamp: new Date()
};
setMessages(prev => [...prev, modeMessage]);
}
});
useEffect(() => {
const handleProgress = (_executionId, progress) => {
if (progress.details?.agentId) {
const agentMessage = {
id: uuidv4(),
type: 'sub-agent-start',
content: progress.message,
timestamp: new Date(),
metadata: {
agentId: progress.details.agentId,
agentStatus: progress.details.status,
toolName: 'Sub-Agent'
}
};
setMessages(prev => {
const existingIndex = prev.findIndex(msg => msg.type === 'sub-agent-start' && msg.metadata?.agentId === progress.details.agentId);
if (existingIndex >= 0) {
const updated = [...prev];
updated[existingIndex] = agentMessage;
return updated;
}
else {
return [...prev, agentMessage];
}
});
}
};
toolExecutor.on('execution:progress', handleProgress);
return () => {
toolExecutor.off('execution:progress', handleProgress);
};
}, []);
useEffect(() => {
const handleEditConfirmation = (data) => {
setEditConfirmationData(data);
};
editConfirmationService.on('showEditConfirmation', handleEditConfirmation);
return () => {
editConfirmationService.off('showEditConfirmation', handleEditConfirmation);
};
}, []);
useEffect(() => {
const handleBashConfirmation = (data) => {
setBashConfirmationData(data);
};
bashConfirmationService.on('showBashConfirmation', handleBashConfirmation);
return () => {
bashConfirmationService.off('showBashConfirmation', handleBashConfirmation);
};
}, []);
useEffect(() => {
const context = contextManager.getCurrentContext();
const licenseStatus = stateService.getLicenseStatus();
const config = configManager.getConfig();
const hasOpenRouterKey = !!config.providers.openrouter?.apiKey;
if (context.messages.length === 0) {
const welcomeMessage = {
id: uuidv4(),
type: 'system',
content: getLogo() + '\n\nWelcome to Capsule CLI! Type a message or / for commands.',
timestamp: new Date()
};
const messages = [welcomeMessage];
if (licenseStatus && !licenseStatus.isAuthenticated) {
const activationMessage = {
id: uuidv4(),
type: 'system',
content: 'activation-setup',
timestamp: new Date()
};
messages.push(activationMessage);
}
else if (!hasOpenRouterKey) {
const setupMessage = {
id: uuidv4(),
type: 'system',
content: 'openrouter-setup',
timestamp: new Date()
};
messages.push(setupMessage);
}
setMessages(messages);
}
else {
const loadedMessages = context.messages.map(msg => ({
id: uuidv4(),
type: msg.role,
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
timestamp: new Date()
}));
setMessages(loadedMessages);
}
}, []);
const handleInputChange = (input) => {
setCurrentInput(input);
if (input.startsWith('/')) {
setShowCommandPalette(true);
}
else {
setShowCommandPalette(false);
}
};
const handlePasteStart = () => {
setIsPasting(true);
};
const handlePasteEnd = () => {
setIsPasting(false);
};
const handleSubmit = async (input) => {
if (!input.trim()) {
return;
}
if (editConfirmationData || bashConfirmationData) {
return;
}
if (input.startsWith('/')) {
return;
}
const licenseStatus = stateService.getLicenseStatus();
if (licenseStatus && !licenseStatus.isAuthenticated) {
const errorMessage = {
id: uuidv4(),
type: 'error',
content: chalk.yellow('Activation required. Use /activate command to activate your license.'),
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
return;
}
setCurrentInput('');
const userMessage = {
id: uuidv4(),
type: 'user',
content: input,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setIsProcessing(true);
setProcessingStartTime(new Date());
setStreamedTokens(0);
try {
let assistantMessage = {
id: uuidv4(),
type: 'assistant',
content: '',
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMessage]);
const stream = chatService.stream(input, { mode });
let fullContent = '';
for await (const chunk of stream) {
if (chunk.delta) {
fullContent += chunk.delta;
setMessages(prev => prev.map(msg => msg.id === assistantMessage.id
? { ...msg, content: fullContent }
: msg));
}
if (chunk.toolCall) {
if (fullContent.trim()) {
const currentAssistantMessage = {
id: uuidv4(),
type: 'assistant',
content: fullContent.trim(),
timestamp: new Date()
};
setMessages(prev => [...prev.slice(0, -1), currentAssistantMessage]);
fullContent = '';
}
const displayName = getToolDisplayName(chunk.toolCall.name);
const toolCallMessage = {
id: uuidv4(),
type: 'tool-call',
content: chunk.toolCall.arguments ? JSON.stringify(chunk.toolCall.arguments) : '',
timestamp: new Date(),
metadata: {
toolName: displayName,
originalParams: chunk.toolCall.arguments
}
};
setMessages(prev => [...prev, toolCallMessage]);
let toolFailed = false;
try {
const execution = await toolExecutor.execute(chunk.toolCall);
let resultContent = '';
if (execution.result?.error) {
resultContent = execution.result.error;
}
else if (execution.result?.output) {
const output = execution.result.output;
if (typeof output === 'string') {
resultContent = output;
}
else if (output.files && output.directories) {
const dirs = output.directories || [];
const files = output.files || [];
resultContent = [...dirs, ...files].join('\n') || 'Empty directory';
}
else if (output.content) {
resultContent = output.content;
}
else if (output.display) {
resultContent = JSON.stringify(output);
}
else if (output.summary && output.agents) {
resultContent = output.summary;
const subAgentResultMessage = {
id: uuidv4(),
type: 'sub-agent-result',
content: '',
timestamp: new Date(),
metadata: {
agentTasks: output.agents
}
};
setMessages(prev => [...prev, subAgentResultMessage]);
}
else if (output.preview) {
const header = `Updated ${output.path} with ${output.replacements} replacement${output.replacements > 1 ? 's' : ''}`;
resultContent = `${header}\n${output.preview}`;
}
else if (output.path && output.linesChanged !== undefined) {
resultContent = `Updated ${output.path} (${output.linesChanged} lines changed)`;
}
else if (output.path && output.created) {
resultContent = JSON.stringify(output);
}
else if (output.path) {
resultContent = `File operation completed: ${output.path}`;
}
else {
resultContent = JSON.stringify(output, null, 2);
}
}
else {
resultContent = 'No output';
}
setMessages(prev => prev.map(msg => msg.id === toolCallMessage.id
? {
...msg,
type: 'tool-result',
content: resultContent,
metadata: {
...msg.metadata,
success: execution.result?.success || false,
toolName: displayName,
error: execution.result?.error
}
}
: msg));
toolResultsService.setToolResult(chunk.toolCall.id, {
call_id: chunk.toolCall.id,
output: execution.result?.output || execution.result?.error || 'Unknown error',
success: execution.state === 'completed',
name: chunk.toolCall.name
});
}
catch (toolError) {
toolFailed = true;
setMessages(prev => prev.map(msg => msg.id === toolCallMessage.id
? {
...msg,
type: 'tool-result',
content: toolError.message,
metadata: {
...msg.metadata,
success: false,
toolName: displayName,
error: toolError.message
}
}
: msg));
toolResultsService.setToolResult(chunk.toolCall.id, {
call_id: chunk.toolCall.id,
output: { error: toolError.message },
success: false,
name: chunk.toolCall.name
});
}
if (!toolFailed) {
const newAssistantMessage = {
id: uuidv4(),
type: 'assistant',
content: '',
timestamp: new Date()
};
setMessages(prev => [...prev, newAssistantMessage]);
assistantMessage = newAssistantMessage;
}
}
if (chunk.usage) {
setStreamedTokens(chunk.usage.totalTokens || 0);
}
}
setMessages(prev => {
if (prev.length > 0) {
const lastMsg = prev[prev.length - 1];
if (lastMsg.type === 'assistant' && lastMsg.content.trim() === '') {
return prev.slice(0, -1);
}
}
return prev;
});
}
catch (error) {
const errorMessage = {
id: uuidv4(),
type: 'error',
content: error.message,
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
}
finally {
setIsProcessing(false);
setProcessingStartTime(undefined);
setStreamedTokens(0);
}
};
const handleModelSelect = (model) => {
stateService.setModel(model);
const message = {
id: uuidv4(),
type: 'system',
content: `Model changed to ${model}`,
timestamp: new Date()
};
setMessages(prev => [...prev, message]);
setShowModelSelector(false);
setSelectorData(null);
};
const handleProviderSelect = async (provider) => {
const previousModel = stateService.getModel();
stateService.setProvider(provider);
const newModel = stateService.getModel();
let content = `Provider changed to ${provider}`;
if (previousModel !== newModel) {
const { getModelDisplayName } = await import('../utils/model-display.js');
content += ` (model: ${getModelDisplayName(newModel)})`;
}
const message = {
id: uuidv4(),
type: 'system',
content,
timestamp: new Date()
};
setMessages(prev => [...prev, message]);
setShowProviderSelector(false);
setSelectorData(null);
};
const handleCommandSelect = async (command) => {
setShowCommandPalette(false);
setCurrentInput('');
try {
const result = await executeCommand(command);
if (result.message) {
const messageType = result.success ? 'system' : 'error';
const message = {
id: uuidv4(),
type: messageType,
content: result.message,
timestamp: new Date()
};
setMessages(prev => [...prev, message]);
}
if (result.showActivationUI) {
setShowActivationUI(true);
return;
}
switch (result.action) {
case 'exit':
exit();
break;
case 'clear': {
const config = configManager.getConfig();
const hasOpenRouterKey = !!config.providers.openrouter?.apiKey;
const licenseStatus = stateService.getLicenseStatus();
const clearMessage = {
id: uuidv4(),
type: 'system',
content: getLogo() + '\n\nChat cleared. Type a message or / for commands.',
timestamp: new Date()
};
const messages = [clearMessage];
if (licenseStatus && !licenseStatus.isAuthenticated) {
const activationMessage = {
id: uuidv4(),
type: 'system',
content: 'activation-setup',
timestamp: new Date()
};
messages.push(activationMessage);
}
else if (!hasOpenRouterKey) {
const setupMessage = {
id: uuidv4(),
type: 'system',
content: 'openrouter-setup',
timestamp: new Date()
};
messages.push(setupMessage);
}
setMessages(messages);
break;
}
case 'new': {
const config = configManager.getConfig();
const hasOpenRouterKey = !!config.providers.openrouter?.apiKey;
const licenseStatus = stateService.getLicenseStatus();
const newContext = contextManager.createNewContext();
contextManager.setCurrentContext(newContext.id);
const newChatMessage = {
id: uuidv4(),
type: 'system',
content: getLogo() + '\n\nNew chat started. Type a message or / for commands.',
timestamp: new Date()
};
const messages = [newChatMessage];
if (licenseStatus && !licenseStatus.isAuthenticated) {
const activationMessage = {
id: uuidv4(),
type: 'system',
content: 'activation-setup',
timestamp: new Date()
};
messages.push(activationMessage);
}
else if (!hasOpenRouterKey) {
const setupMessage = {
id: uuidv4(),
type: 'system',
content: 'openrouter-setup',
timestamp: new Date()
};
messages.push(setupMessage);
}
setMessages(messages);
break;
}
case 'none':
if (result.data?.type === 'model-select') {
setSelectorData(result.data);
setShowModelSelector(true);
}
else if (result.data?.type === 'provider-select') {
setSelectorData(result.data);
setShowProviderSelector(true);
}
else if (result.data?.type === 'configure-keys') {
setKeyConfigData(result.data);
setShowKeyConfiguration(true);
}
else if (result.data?.type === 'chat-select') {
setChatSelectorData(result.data);
setShowChatSelector(true);
}
else if (result.data?.type === 'chat-delete') {
setChatDeleterData(result.data);
setShowChatDeleter(true);
}
else if (result.data?.type === 'subagent-config') {
setSubAgentConfigData(result.data);
setShowSubAgentConfig(true);
}
else if (result.data?.type === 'orchestrator-config') {
setOrchestratorConfigData(result.data);
setShowOrchestratorConfig(true);
}
else if (result.data?.type === 'openrouter-setup') {
const setupMessage = {
id: uuidv4(),
type: 'system',
content: 'openrouter-setup',
timestamp: new Date()
};
setMessages(prev => [...prev, setupMessage]);
}
break;
}
}
catch (error) {
const errorMessage = {
id: uuidv4(),
type: 'error',
content: error.message,
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
}
};
const stats = contextManager.getContextStats();
const model = stateService.getModel();
const provider = stateService.getProvider();
const tokenLimit = contextManager.getTokenLimit(model);
const completedMessages = isProcessing && messages.length > 0
? messages.slice(0, -1)
: messages;
const streamingMessage = isProcessing && messages.length > 0
? messages[messages.length - 1]
: null;
return (React.createElement(ResizeHandler, { onResize: () => setResizeKey(prev => prev + 1) },
React.createElement(Box, { key: resizeKey, flexDirection: "column" },
React.createElement(Box, { flexDirection: "column" },
React.createElement(Static, { items: completedMessages }, (message) => (React.createElement(ChatMessage, { key: message.id, type: message.type, content: message.content, timestamp: message.timestamp, metadata: message.metadata, provider: provider }))),
streamingMessage && (React.createElement(ChatMessage, { type: streamingMessage.type, content: streamingMessage.content, timestamp: streamingMessage.timestamp, metadata: streamingMessage.metadata, provider: provider }))),
React.createElement(Box, { flexDirection: "column" },
editConfirmationData ? (React.createElement(Box, { key: "edit-confirmation", width: "100%", marginBottom: 1 },
React.createElement(EditConfirmation, { filePath: editConfirmationData.filePath, oldContent: editConfirmationData.oldContent, newContent: editConfirmationData.newContent, onConfirm: () => {
editConfirmationData.onConfirm();
setEditConfirmationData(null);
}, onCancel: () => {
editConfirmationData.onCancel();
setEditConfirmationData(null);
} }))) : null,
bashConfirmationData ? (React.createElement(Box, { key: "bash-confirmation", width: "100%", marginBottom: 1 },
React.createElement(BashConfirmation, { command: bashConfirmationData.command, workingDirectory: bashConfirmationData.workingDirectory, isDangerous: bashConfirmationData.isDangerous, onConfirm: () => {
bashConfirmationData.onConfirm();
setBashConfirmationData(null);
}, onCancel: () => {
bashConfirmationData.onCancel();
setBashConfirmationData(null);
} }))) : null,
showCommandPalette && (React.createElement(CommandPalette, { query: currentInput, onSelect: handleCommandSelect, onClose: () => setShowCommandPalette(false), provider: provider })),
showModelSelector && selectorData && (React.createElement(ModelSelector, { models: selectorData.models, currentModel: selectorData.currentModel, provider: selectorData.provider, onSelect: handleModelSelect, onClose: () => {
setShowModelSelector(false);
setSelectorData(null);
} })),
showProviderSelector && selectorData && (React.createElement(ProviderSelector, { providers: selectorData.providers, currentProvider: selectorData.currentProvider, onSelect: handleProviderSelect, onClose: () => {
setShowProviderSelector(false);
setSelectorData(null);
} })),
showKeyConfiguration && keyConfigData && (React.createElement(KeyConfiguration, { providers: keyConfigData.providers, onClose: () => {
setShowKeyConfiguration(false);
setKeyConfigData(null);
} })),
showChatSelector && chatSelectorData && (React.createElement(ChatSelector, { contexts: chatSelectorData.contexts, currentContextId: chatSelectorData.currentContextId, onSelect: (contextId) => {
contextManager.switchContext(contextId);
const switchMessage = {
id: uuidv4(),
type: 'system',
content: 'Chat context switched successfully',
timestamp: new Date()
};
setMessages([switchMessage]);
setShowChatSelector(false);
setChatSelectorData(null);
}, onClose: () => {
setShowChatSelector(false);
setChatSelectorData(null);
} })),
showChatDeleter && chatDeleterData && (React.createElement(ChatDeleter, { contexts: chatDeleterData.contexts, onDelete: (contextId) => {
const deleted = contextManager.deleteContext(contextId);
if (deleted) {
setChatDeleterData((prev) => ({
...prev,
contexts: prev.contexts.filter((ctx) => ctx.id !== contextId)
}));
const remainingChats = chatDeleterData.contexts.filter((ctx) => ctx.id !== contextId);
if (remainingChats.length === 0) {
const deleteMessage = {
id: uuidv4(),
type: 'system',
content: 'All chats deleted successfully',
timestamp: new Date()
};
setMessages(prev => [...prev, deleteMessage]);
setShowChatDeleter(false);
setChatDeleterData(null);
}
}
}, onClose: () => {
setShowChatDeleter(false);
setChatDeleterData(null);
} })),
showSubAgentConfig && subAgentConfigData && (React.createElement(SubAgentConfig, { currentModel: subAgentConfigData.currentModel, currentProvider: subAgentConfigData.currentProvider, currentDefaults: subAgentConfigData.currentDefaults, saveAsDefault: true, onConfirm: (model, provider) => {
const message = {
id: uuidv4(),
type: 'system',
content: `Sub-agent defaults saved: ${model} on ${provider}`,
timestamp: new Date()
};
setMessages(prev => [...prev, message]);
setShowSubAgentConfig(false);
setSubAgentConfigData(null);
}, onCancel: () => {
setShowSubAgentConfig(false);
setSubAgentConfigData(null);
} })),
showOrchestratorConfig && orchestratorConfigData && (React.createElement(OrchestratorConfig, { currentPreferences: orchestratorConfigData.currentPreferences, availableModels: orchestratorConfigData.availableModels, onClose: () => {
const message = {
id: uuidv4(),
type: 'system',
content: 'Orchestrator preferences saved',
timestamp: new Date()
};
setMessages(prev => [...prev, message]);
setShowOrchestratorConfig(false);
setOrchestratorConfigData(null);
} })),
showActivationUI && (React.createElement(ActivationUI, { onClose: () => setShowActivationUI(false), onSuccess: async (result) => {
const newStatus = await authService.getStatus();
stateService.setLicenseStatus(newStatus);
const successMessage = {
id: uuidv4(),
type: 'system',
content: chalk.green('✓ Activation successful!\n') +
chalk.gray(`Email: ${result.email}\n`) +
chalk.gray(`Tier: ${result.tier.toUpperCase()}\n`) +
chalk.cyan('\nYou can now use all Capsule features!'),
timestamp: new Date()
};
setMessages(prev => [...prev, successMessage]);
setShowActivationUI(false);
} })),
!editConfirmationData && !bashConfirmationData && !showKeyConfiguration && !showModelSelector && !showProviderSelector && !showChatSelector && !showChatDeleter && !showSubAgentConfig && !showOrchestratorConfig && !showActivationUI && (React.createElement(React.Fragment, null,
React.createElement(LoadingAnimation, { provider: provider, isLoading: isProcessing, startTime: processingStartTime, tokenCount: streamedTokens }),
React.createElement(InputBox, { value: currentInput, onSubmit: handleSubmit, onChange: handleInputChange, placeholder: isProcessing ? "Processing..." : "Type a message or / for commands", onPasteStart: handlePasteStart, onPasteEnd: handlePasteEnd, provider: provider }),
React.createElement(StatusBar, { mode: mode, model: model, provider: provider, tokenCount: stats.tokenCount, tokenLimit: tokenLimit, isPasting: isPasting })))))));
};
//# sourceMappingURL=Window.js.map