automagik-cli
Version:
Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems
448 lines (447 loc) ⢠24.5 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* Exact copy of gemini-cli App.tsx but adapted for Automagik backend
*/
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { Box, measureElement, Static, Text, useStdin, useStdout, useInput, } from 'ink';
import { StreamingState, MessageType } from './types.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { useResponsiveLayout } from './hooks/useResponsiveLayout.js';
import { useLocalAPIStream } from './hooks/useLocalAPIStream.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { Header } from './components/Header.js';
import { LoadingIndicator } from './components/LoadingIndicator.js';
import { InputPrompt } from './components/InputPrompt.js';
import { Footer } from './components/Footer.js';
import { Colors } from './colors.js';
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
import { TargetSelectionDialog } from './components/TargetSelectionDialog.js';
import { TargetTypeDialog } from './components/TargetTypeDialog.js';
import { SessionSelectionDialog } from './components/SessionSelectionDialog.js';
import { useTextBuffer } from './components/shared/text-buffer.js';
import { SessionProvider, useSession } from './contexts/SessionContext.js';
import { StreamingProvider } from './contexts/StreamingContext.js';
import { appConfig, reloadAppConfig, needsFirstRunSetup, saveSettings, initializeAppConfig } from '../config/settings.js';
import { localAPIClient } from '../config/localClient.js';
import { detectAPIServer } from '../utils/serverDetection.js';
import { ServerConfigDialog } from './components/ServerConfigDialog.js';
import { APIKeyConfigDialog } from './components/APIKeyConfigDialog.js';
import { SettingsSetupDialog } from './components/SettingsSetupDialog.js';
import { ConnectionStatus } from './components/ConnectionStatus.js';
import ansiEscapes from 'ansi-escapes';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
export const AppWrapper = (props) => {
return (_jsx(SessionProvider, { children: _jsx(StreamingProvider, { children: _jsx(App, { ...props }) }) }));
};
const App = ({ version }) => {
const { stdout } = useStdout();
const { stdin, setRawMode } = useStdin();
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
const layout = useResponsiveLayout();
// Session and history management
const { history, addMessage, clearHistory, currentSessionId, createNewSession, loadSession, setCurrentTarget, listSessions, listBackendSessions, } = useSession();
// UI state
const [staticNeedsRefresh, setStaticNeedsRefresh] = useState(false);
const [staticKey, setStaticKey] = useState(0);
const [debugMessage, setDebugMessage] = useState('');
const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false);
const ctrlCTimerRef = useRef(null);
const ctrlDTimerRef = useRef(null);
const [constrainHeight, setConstrainHeight] = useState(true);
const [footerHeight, setFooterHeight] = useState(0);
const [shellModeActive, setShellModeActive] = useState(false);
// Target selection state
const [uiState, setUiState] = useState('selecting_type');
const [selectedTargetType, setSelectedTargetType] = useState(null);
// API state
const [showStartupBanner, setShowStartupBanner] = useState(true);
const [connectionStatus, setConnectionStatus] = useState('connecting');
const [connectionError, setConnectionError] = useState('');
const [retryCount, setRetryCount] = useState(0);
const [currentServerUrl, setCurrentServerUrl] = useState(appConfig.apiBaseUrl);
const [authenticationError, setAuthenticationError] = useState('');
// First-run setup state
const [isFirstRun, setIsFirstRun] = useState(false);
const [setupConnectionError, setSetupConnectionError] = useState('');
const [selectedTarget, setSelectedTarget] = useState(null);
const [availableTargets, setAvailableTargets] = useState({ agents: [], teams: [], workflows: [] });
const refreshStatic = useCallback(() => {
stdout.write(ansiEscapes.clearTerminal);
setStaticKey((prev) => prev + 1);
}, [setStaticKey, stdout]);
// Target selection handlers
const handleTargetTypeSelect = useCallback((targetType) => {
setSelectedTargetType(targetType);
setUiState('selecting_target');
}, []);
const handleTargetSelect = useCallback((target) => {
setSelectedTarget(target);
setCurrentTarget(target);
setUiState('selecting_session');
}, [setCurrentTarget]);
const handleBackToTargetSelection = useCallback(() => {
setUiState('selecting_type');
setSelectedTargetType(null);
}, []);
const handleBackToTargetSelect = useCallback(() => {
setUiState('selecting_target');
}, []);
const handleSessionSelect = useCallback(async (sessionAction, sessionId) => {
if (sessionAction === 'new') {
createNewSession(selectedTarget || undefined);
}
else if (sessionAction === 'existing' && sessionId) {
try {
await loadSession(sessionId);
}
catch (error) {
console.error('Failed to load session:', error);
addMessage({
type: MessageType.ERROR,
text: `Failed to load session: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: Date.now(),
});
}
}
setUiState('chatting');
}, [selectedTarget, createNewSession, loadSession, addMessage]);
// Server configuration handlers
const handleServerConfigSubmit = useCallback(async (url) => {
try {
setConnectionStatus('configured');
setCurrentServerUrl(url);
// Update settings file
await saveSettings({ apiBaseUrl: url });
// Update the local client configuration
localAPIClient.setBaseUrl(url);
// Try to reconnect
await initializeAPIConnection(url);
}
catch (error) {
setConnectionError(`Failed to save configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
setConnectionStatus('failed');
}
}, []);
const handleServerConfigCancel = useCallback(() => {
process.exit(0);
}, []);
// First-run setup handlers
const handleFirstRunSetup = useCallback(async (settings) => {
try {
setSetupConnectionError('');
// Test connection first
const tempClient = localAPIClient;
tempClient.setBaseUrl(settings.apiBaseUrl);
if (settings.apiKey) {
tempClient.setApiKey(settings.apiKey);
}
const healthCheck = await tempClient.healthCheck();
if (!healthCheck.data) {
throw new Error('Cannot connect to API server');
}
// Save settings
await saveSettings({
apiBaseUrl: settings.apiBaseUrl,
apiKey: settings.apiKey,
});
// Reload config and update UI
await reloadAppConfig();
setCurrentServerUrl(settings.apiBaseUrl);
setConnectionStatus('connected');
setIsFirstRun(false);
setUiState('selecting_type');
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Connection failed';
setSetupConnectionError(errorMessage);
throw error; // Re-throw to let the dialog handle it
}
}, []);
const handleFirstRunCancel = useCallback(() => {
process.exit(0);
}, []);
// Local API streaming
const { streamingState, submitQuery, cancelStream, initError, pendingMessage, } = useLocalAPIStream(addMessage, selectedTarget, currentSessionId, setDebugMessage);
const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(streamingState);
// Text buffer setup - now using responsive layout
const inputWidth = layout.inputWidth;
const suggestionsWidth = Math.max(60, Math.floor(layout.width * 0.8));
const buffer = useTextBuffer({
initialText: '',
viewport: { height: 10, width: inputWidth },
stdin,
setRawMode,
isValidPath: () => false,
shellModeActive: false,
});
// Show startup banner for 2 seconds
useEffect(() => {
const timer = setTimeout(() => {
setShowStartupBanner(false);
}, 2000);
return () => clearTimeout(timer);
}, []);
// Initialize API connection
const initializeAPIConnection = useCallback(async (url, retry = 0) => {
try {
setConnectionStatus('connecting');
setRetryCount(retry);
// Add a small delay to ensure banner is visible on first attempt
if (retry === 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Use the updated detectAPIServer that includes authentication validation
const serverStatus = await detectAPIServer(url);
if (!serverStatus.isRunning) {
throw new Error(serverStatus.error || 'Server is not running');
}
if (!serverStatus.isAuthenticated) {
// Server is running but authentication failed
setAuthenticationError(serverStatus.authError || 'Authentication failed');
setConnectionStatus('connected'); // Connected to server but not authenticated
setUiState('configuring_api_key');
return;
}
// Server is running and authenticated, now fetch data
const [agentsResponse, teamsResponse, workflowsResponse] = await Promise.all([
localAPIClient.listAgents(),
localAPIClient.listTeams(),
localAPIClient.listWorkflows(),
]);
setAvailableTargets({
agents: agentsResponse.data || [],
teams: teamsResponse.data || [],
workflows: workflowsResponse.data || [],
});
setConnectionStatus('connected');
setConnectionError('');
setAuthenticationError('');
setRetryCount(0);
// Reset UI state to normal flow after successful connection
setUiState('selecting_type');
// Auto-select first agent for direct interface
if (agentsResponse.data && agentsResponse.data.length > 0) {
const firstAgent = agentsResponse.data[0];
setSelectedTarget({
type: 'agent',
id: firstAgent.agent_id,
name: firstAgent.name
});
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
setConnectionError(errorMessage);
// Retry up to 3 times
if (retry < 3) {
setTimeout(() => {
initializeAPIConnection(url, retry + 1);
}, 2000);
}
else {
setConnectionStatus('failed');
// Show server configuration dialog after failed attempts
setTimeout(() => {
setUiState('configuring_server');
}, 1000);
}
}
}, []);
// Check for first-run setup
useEffect(() => {
const checkFirstRun = async () => {
try {
// Initialize app config first
await initializeAppConfig();
const needsSetup = await needsFirstRunSetup();
if (needsSetup) {
setIsFirstRun(true);
setUiState('first_run_setup');
}
}
catch (error) {
console.error('Failed to check first-run setup:', error);
// Continue with normal flow on error
}
};
checkFirstRun();
}, []);
useEffect(() => {
// Skip API connection if in first-run setup
if (uiState !== 'first_run_setup') {
initializeAPIConnection(currentServerUrl);
}
}, [initializeAPIConnection, currentServerUrl]);
// API key configuration handlers
const handleAPIKeyConfigSubmit = useCallback(async (apiKey) => {
try {
// Update settings file with new API key
await saveSettings({ apiKey });
// Reload the app configuration to pick up the new API key
await reloadAppConfig();
// Update the local API client configuration (force reload)
localAPIClient.setBaseUrl(currentServerUrl);
// Clear authentication error before retrying
setAuthenticationError('');
// Reload appConfig by restarting the connection process
await initializeAPIConnection(currentServerUrl);
}
catch (error) {
setAuthenticationError(`Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}, [currentServerUrl, initializeAPIConnection]);
const handleAPIKeyConfigCancel = useCallback(() => {
process.exit(0);
}, []);
const handleExit = useCallback((pressedOnce, setPressedOnce, timerRef) => {
if (pressedOnce) {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
process.exit(0);
}
else {
setPressedOnce(true);
timerRef.current = setTimeout(() => {
setPressedOnce(false);
timerRef.current = null;
}, CTRL_EXIT_PROMPT_DURATION_MS);
}
}, []);
const isInputActive = streamingState === StreamingState.Idle && !initError && connectionStatus === 'connected' && uiState === 'chatting';
useInput((input, key) => {
let enteringConstrainHeightMode = false;
if (!constrainHeight) {
enteringConstrainHeightMode = true;
setConstrainHeight(true);
}
if (key.ctrl && (input === 'c' || input === 'C')) {
handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef);
}
else if (key.ctrl && (input === 'd' || input === 'D')) {
if (buffer.text.length > 0) {
return;
}
handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef);
}
else if (key.ctrl && input === 's' && !enteringConstrainHeightMode) {
setConstrainHeight(false);
}
});
const handleFinalSubmit = useCallback(async (submittedValue) => {
const trimmedValue = submittedValue.trim();
// Handle slash commands
if (trimmedValue.startsWith('/')) {
const command = trimmedValue.toLowerCase();
if (command === '/sessions') {
try {
const sessions = await listSessions();
const backendSessions = selectedTarget ? await listBackendSessions(selectedTarget) : [];
let sessionList = 'š **Available Sessions**\n\n';
if (sessions.length > 0) {
sessionList += '**Local Sessions:**\n';
sessions.forEach((session, index) => {
const date = new Date(session.updatedAt).toLocaleDateString();
const time = new Date(session.updatedAt).toLocaleTimeString();
const messageCount = session.metadata?.totalMessages || 0;
const target = session.metadata?.lastTarget;
const targetDisplay = target ? `${target.type}:${target.name || target.id}` : 'Unknown';
sessionList += `${index + 1}. ${session.id}\n`;
sessionList += ` š
${date} ${time} | š¬ ${messageCount} messages | šÆ ${targetDisplay}\n\n`;
});
}
if (backendSessions.length > 0) {
sessionList += '**Backend Sessions:**\n';
backendSessions.forEach((session, index) => {
sessionList += `${index + 1}. ${session.id || session.name}\n`;
sessionList += ` š Backend session\n\n`;
});
}
if (sessions.length === 0 && backendSessions.length === 0) {
sessionList += 'No sessions found.\n';
}
sessionList += '\nš” **Tips:**\n';
sessionList += '- Select a target (agent/team/workflow) to see available sessions\n';
sessionList += '- Use session selection dialog to continue existing sessions\n';
sessionList += '- Local sessions are stored in ~/.automagik-cli/sessions/\n';
addMessage({
type: MessageType.INFO,
text: sessionList,
timestamp: Date.now(),
});
}
catch (error) {
addMessage({
type: MessageType.ERROR,
text: `Failed to list sessions: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: Date.now(),
});
}
return;
}
// Handle unknown commands
addMessage({
type: MessageType.ERROR,
text: `Unknown command: ${trimmedValue}\n\nAvailable commands:\n- /sessions - List all available sessions`,
timestamp: Date.now(),
});
return;
}
// Handle regular messages
if (trimmedValue.length > 0 && selectedTarget) {
submitQuery(trimmedValue);
}
}, [submitQuery, selectedTarget, listSessions, listBackendSessions, addMessage]);
const handleClearScreen = useCallback(() => {
clearHistory();
console.clear();
refreshStatic();
}, [clearHistory, refreshStatic]);
const mainControlsRef = useRef(null);
useEffect(() => {
if (mainControlsRef.current) {
const fullFooterMeasurement = measureElement(mainControlsRef.current);
setFooterHeight(fullFooterMeasurement.height);
}
}, [terminalHeight]);
const staticExtraHeight = 3;
const availableTerminalHeight = useMemo(() => terminalHeight - footerHeight - staticExtraHeight, [terminalHeight, footerHeight]);
const mainAreaWidth = layout.maxContentWidth;
const staticAreaMaxItemHeight = Math.max(layout.height * 4, 100);
// Show startup banner first
if (showStartupBanner) {
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: layout.maxContentWidth, children: [_jsx(Header, { terminalWidth: layout.width, version: version, nightly: false }), _jsx(Box, { marginTop: 2, children: _jsxs(Text, { children: ["\uD83D\uDD17 Connecting to ", appConfig.apiBaseUrl, "..."] }) })] }));
}
// Show connection status during startup, first-run setup, server configuration, or API key configuration
if (connectionStatus === 'connecting' || connectionStatus === 'failed' || uiState === 'first_run_setup' || uiState === 'configuring_server' || uiState === 'configuring_api_key') {
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: layout.maxContentWidth, children: [_jsx(Static, { items: [
_jsx(Box, { flexDirection: "column", children: _jsx(Header, { terminalWidth: layout.width, version: version, nightly: false }) }, "header"),
], children: () => null }, staticKey), _jsx(ConnectionStatus, { status: connectionStatus, url: currentServerUrl, error: connectionError, retryCount: retryCount }), uiState === 'first_run_setup' && (_jsx(SettingsSetupDialog, { onSubmit: handleFirstRunSetup, onCancel: handleFirstRunCancel, connectionError: setupConnectionError })), uiState === 'configuring_server' && (_jsx(ServerConfigDialog, { currentUrl: currentServerUrl, onSubmit: handleServerConfigSubmit, onCancel: handleServerConfigCancel })), uiState === 'configuring_api_key' && (_jsx(APIKeyConfigDialog, { currentApiKey: appConfig.apiKey, authError: authenticationError, onSubmit: handleAPIKeyConfigSubmit, onCancel: handleAPIKeyConfigCancel }))] }));
}
if (uiState === 'selecting_type') {
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: layout.maxContentWidth, children: [_jsx(Static, { items: [
_jsx(Box, { flexDirection: "column", children: _jsx(Header, { terminalWidth: layout.width, version: version, nightly: false }) }, "header"),
], children: () => null }, staticKey), _jsx(TargetTypeDialog, { onSelect: handleTargetTypeSelect, availableTargets: availableTargets })] }));
}
if (uiState === 'selecting_target' && selectedTargetType) {
const targets = selectedTargetType === 'agent'
? availableTargets.agents
: selectedTargetType === 'team'
? availableTargets.teams
: availableTargets.workflows;
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: layout.maxContentWidth, children: [_jsx(Static, { items: [
_jsx(Box, { flexDirection: "column", children: _jsx(Header, { terminalWidth: layout.width, version: version, nightly: false }) }, "header"),
], children: () => null }, staticKey), _jsx(TargetSelectionDialog, { targetType: selectedTargetType, targets: targets, onSelect: handleTargetSelect, onBack: handleBackToTargetSelection })] }));
}
if (uiState === 'selecting_session' && selectedTarget) {
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: layout.maxContentWidth, children: [_jsx(Static, { items: [
_jsx(Box, { flexDirection: "column", children: _jsx(Header, { terminalWidth: layout.width, version: version, nightly: false }) }, "header"),
], children: () => null }, staticKey), _jsx(SessionSelectionDialog, { selectedTarget: selectedTarget, onSelect: handleSessionSelect, onBack: handleBackToTargetSelect })] }));
}
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: layout.maxContentWidth, children: [_jsx(Static, { items: [
_jsx(Box, { flexDirection: "column", children: _jsx(Header, { terminalWidth: layout.width, version: version, nightly: false }) }, "header"),
...history.map((h) => (_jsx(HistoryItemDisplay, { terminalWidth: mainAreaWidth, availableTerminalHeight: staticAreaMaxItemHeight, item: h, isPending: false }, h.id))),
], children: (item) => item }, staticKey), _jsxs(Box, { flexDirection: "column", ref: mainControlsRef, children: [_jsx(LoadingIndicator, { currentLoadingPhrase: currentLoadingPhrase, elapsedTime: elapsedTime, streamingState: streamingState }), ctrlCPressedOnce ? (_jsx(Text, { color: Colors.AccentYellow, children: "Press Ctrl+C again to exit." })) : ctrlDPressedOnce ? (_jsx(Text, { color: Colors.AccentYellow, children: "Press Ctrl+D again to exit." })) : null, initError && streamingState !== StreamingState.Responding && (_jsx(Box, { borderStyle: "round", borderColor: Colors.AccentRed, paddingX: 1, marginBottom: 1, children: _jsxs(Text, { color: Colors.AccentRed, children: ["Initialization Error: ", initError] }) })), isInputActive && (_jsx(InputPrompt, { buffer: buffer, onSubmit: handleFinalSubmit, userMessages: [], onClearScreen: handleClearScreen, inputWidth: inputWidth, suggestionsWidth: suggestionsWidth, shellModeActive: shellModeActive, setShellModeActive: setShellModeActive })), _jsx(Footer, { selectedTarget: selectedTarget || undefined, sessionId: currentSessionId, apiUrl: appConfig.apiBaseUrl, debugMode: appConfig.cliDebug, debugMessage: debugMessage })] })] }));
};
export default App;