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
JavaScript
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] }) }))] }));
};