UNPKG

automagik-cli

Version:

Automagik CLI - A powerful command-line interface for interacting with Automagik Hive multi-agent AI systems

198 lines (197 loc) 12.3 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Gemini-style App that uses the visual structure from gemini-cli * but connects to automagik's backend (agents, teams, workflows) */ import { useCallback, useEffect, useState, useRef } from 'react'; import { Box, Static, Text, useStdin, useStdout, useInput, } from 'ink'; import { StreamingState, MessageType } from './types.js'; import { useTerminalSize } from './hooks/useTerminalSize.js'; import { useLocalAPIStream } from './hooks/useLocalAPIStream.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; import { Colors } from './colors.js'; import { Header } from './components/Header.js'; import { SessionProvider, useSession } from './contexts/SessionContext.js'; import { StreamingProvider } from './contexts/StreamingContext.js'; import { appConfig } from '../config/settings.js'; import { localAPIClient } from '../config/localClient.js'; import { useTextBuffer } from './components/shared/text-buffer.js'; // Simplified components that will work with our backend import { TargetTypeDialog } from './components/TargetTypeDialog.js'; import { TargetSelectionDialog } from './components/TargetSelectionDialog.js'; import { SessionSelectionDialog } from './components/SessionSelectionDialog.js'; import { GeminiInputPrompt } from './components/GeminiInputPrompt.js'; const CTRL_EXIT_PROMPT_DURATION_MS = 1000; export const GeminiAppWrapper = (props) => (_jsx(SessionProvider, { children: _jsx(StreamingProvider, { children: _jsx(GeminiApp, { ...props }) }) })); const GeminiApp = ({ version }) => { const { stdout } = useStdout(); const { stdin, setRawMode } = useStdin(); const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize(); // Session and history management const { history, addMessage, clearHistory, currentSessionId, } = useSession(); // UI state for automagik's flow const [connectionStatus, setConnectionStatus] = useState('connecting'); const [selectedTarget, setSelectedTarget] = useState(null); const [availableTargets, setAvailableTargets] = useState({ agents: [], teams: [], workflows: [] }); const [uiState, setUiState] = useState('selecting_type'); const [selectedTargetType, setSelectedTargetType] = useState(null); // Gemini-style state const [debugMessage, setDebugMessage] = useState(''); const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false); const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false); const ctrlCTimerRef = useRef(null); const ctrlDTimerRef = useRef(null); // Local API streaming const { streamingState, submitQuery, cancelStream, initError, pendingMessage, } = useLocalAPIStream(addMessage, selectedTarget, currentSessionId, setDebugMessage); const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(streamingState); // Text buffer for gemini-style input const widthFraction = 0.9; const inputWidth = Math.max(20, Math.floor(terminalWidth * widthFraction) - 3); const buffer = useTextBuffer({ initialText: '', viewport: { height: 10, width: inputWidth }, stdin, setRawMode, isValidPath: () => false, // Simplified for now shellModeActive: false, }); // Initialize API connection useEffect(() => { const initializeAPI = async () => { try { // Add a small delay to ensure banner is visible await new Promise(resolve => setTimeout(resolve, 1000)); const healthResponse = await localAPIClient.healthCheck(); if (healthResponse.error) { throw new Error(healthResponse.error); } 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'); // Auto-select first agent for direct gemini-style interface if (agentsResponse.data && agentsResponse.data.length > 0) { const firstAgent = agentsResponse.data[0]; setSelectedTarget({ type: 'agent', id: firstAgent.agent_id, name: firstAgent.name }); setUiState('chatting'); } else { setUiState('selecting_type'); } } catch (error) { setConnectionStatus('error'); addMessage({ type: MessageType.ERROR, text: `Failed to connect to API: ${error instanceof Error ? error.message : 'Unknown error'}`, timestamp: Date.now(), }); } }; initializeAPI(); }, [addMessage]); 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 && connectionStatus === 'connected' && uiState === 'chatting'; useInput((input, key) => { if (key.ctrl && (input === 'c' || input === 'C')) { handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef); } else if (key.ctrl && (input === 'd' || input === 'D')) { handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef); } else if (key.ctrl && input === 'l') { clearHistory(); stdout.write('\\x1B[2J\\x1B[3J\\x1B[H'); // Clear screen } else if (key.escape) { if (streamingState !== StreamingState.Idle) { cancelStream(); } } }, { isActive: !isInputActive }); const handleFinalSubmit = useCallback((submittedValue) => { const trimmedValue = submittedValue.trim(); if (trimmedValue.length > 0 && selectedTarget) { submitQuery(trimmedValue); } }, [submitQuery, selectedTarget]); // Flow handlers const handleTargetTypeSelect = useCallback((targetType) => { setSelectedTargetType(targetType); setUiState('selecting_target'); }, []); const handleTargetSelect = useCallback((target) => { setSelectedTarget(target); setUiState('selecting_session'); }, []); const handleSessionSelect = useCallback((sessionAction) => { if (sessionAction === 'new') { setUiState('chatting'); } }, []); const handleBackToTargetType = useCallback(() => { setSelectedTargetType(null); setUiState('selecting_type'); }, []); const handleBackToTargetSelection = useCallback(() => { setSelectedTarget(null); setUiState('selecting_target'); }, []); const handleClearScreen = useCallback(() => { clearHistory(); stdout.write('\\x1B[2J\\x1B[3J\\x1B[H'); }, [clearHistory, stdout]); const mainAreaWidth = Math.floor(terminalWidth * 0.9); // Show connection error if (connectionStatus === 'error') { return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: "90%", children: [_jsx(Header, { terminalWidth: terminalWidth, version: version, nightly: false }), _jsx(Box, { marginTop: 2, children: _jsx(Box, { borderStyle: "round", borderColor: Colors.AccentRed, paddingX: 1, marginY: 1, children: _jsxs(Text, { color: Colors.AccentRed, children: ["Failed to connect to API at ", appConfig.apiBaseUrl] }) }) })] })); } // Show loading while connecting if (connectionStatus === 'connecting') { return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: "90%", children: [_jsx(Header, { terminalWidth: terminalWidth, version: version, nightly: false }), _jsx(Box, { marginTop: 2, children: _jsxs(Text, { children: ["\uD83D\uDD17 Connecting to ", appConfig.apiBaseUrl, "..."] }) })] })); } // Interactive setup flow if (uiState === 'selecting_type') { return (_jsx(Box, { flexDirection: "column", marginBottom: 1, width: "90%", children: _jsx(TargetTypeDialog, { onSelect: handleTargetTypeSelect, availableTargets: availableTargets }) })); } if (uiState === 'selecting_target' && selectedTargetType) { const targets = selectedTargetType === 'agent' ? availableTargets.agents : selectedTargetType === 'team' ? availableTargets.teams : availableTargets.workflows; return (_jsx(Box, { flexDirection: "column", marginBottom: 1, width: "90%", children: _jsx(TargetSelectionDialog, { targetType: selectedTargetType, targets: targets, onSelect: handleTargetSelect, onBack: handleBackToTargetType }) })); } if (uiState === 'selecting_session' && selectedTarget) { return (_jsx(Box, { flexDirection: "column", marginBottom: 1, width: "90%", children: _jsx(SessionSelectionDialog, { selectedTarget: selectedTarget, onSelect: handleSessionSelect, onBack: handleBackToTargetSelection }) })); } // Main chat interface with gemini styling return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: "90%", children: [_jsxs(Box, { borderStyle: "round", borderColor: Colors.AccentPurple, paddingX: 1, marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "\uD83C\uDFAF Automagik Local CLI" }), _jsxs(Text, { color: "gray", children: [" v", version] })] }), _jsx(Text, { color: Colors.AccentCyan, children: "\u25CF Connected" })] }), _jsxs(Box, { borderStyle: "round", borderColor: "magenta", paddingX: 1, marginBottom: 1, children: [_jsxs(Text, { color: "magenta", children: ["\uD83D\uDCAC Chatting with: ", selectedTarget?.name] }), _jsxs(Text, { color: "gray", children: [" (", selectedTarget?.type, ")"] })] }), _jsx(Static, { items: history.map((h, index) => (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: h.type === MessageType.USER ? Colors.AccentPurple : "white", children: [h.type === MessageType.USER ? '> ' : '< ', h.text] }) }, `history-${h.id}-${index}`))), children: (item) => item }), pendingMessage && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "white", children: ['< ', pendingMessage.text] }) })), streamingState !== StreamingState.Idle && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: [currentLoadingPhrase, " (", elapsedTime > 0 ? `${Math.floor(elapsedTime / 1000)}s` : '', ")"] }) })), ctrlCPressedOnce ? (_jsx(Text, { color: "yellow", children: "Press Ctrl+C again to exit." })) : ctrlDPressedOnce ? (_jsx(Text, { color: "yellow", children: "Press Ctrl+D again to exit." })) : null, isInputActive && (_jsx(GeminiInputPrompt, { buffer: buffer, onSubmit: handleFinalSubmit, onClearScreen: handleClearScreen, placeholder: " Type your message...", focus: true, inputWidth: inputWidth, shellModeActive: false, isActive: isInputActive })), _jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, justifyContent: "space-between", children: [_jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["Session: ", currentSessionId.slice(-8)] }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["API: ", appConfig.apiBaseUrl] }) })] }), appConfig.cliDebug && debugMessage && (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: _jsxs(Text, { color: "yellow", children: ["Debug: ", debugMessage] }) }))] })); };