UNPKG

capsule-ai-cli

Version:

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

741 lines 36.5 kB
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