@vibe-kit/grok-cli
Version:
An open-source AI agent that brings the power of Grok directly into your terminal.
265 lines • 14.3 kB
JavaScript
import React, { useState, useEffect, useRef } from "react";
import { Box, Text } from "ink";
import { useInputHandler } from "../../hooks/use-input-handler.js";
import { LoadingSpinner } from "./loading-spinner.js";
import { CommandSuggestions } from "./command-suggestions.js";
import { ModelSelection } from "./model-selection.js";
import { ChatHistory } from "./chat-history.js";
import { ChatInput } from "./chat-input.js";
import { MCPStatus } from "./mcp-status.js";
import ConfirmationDialog from "./confirmation-dialog.js";
import { ConfirmationService, } from "../../utils/confirmation-service.js";
import ApiKeyInput from "./api-key-input.js";
import cfonts from "cfonts";
// Main chat component that handles input when agent is available
function ChatInterfaceWithAgent({ agent, initialMessage, }) {
const [chatHistory, setChatHistory] = useState([]);
const [isProcessing, setIsProcessing] = useState(false);
const [processingTime, setProcessingTime] = useState(0);
const [tokenCount, setTokenCount] = useState(0);
const [isStreaming, setIsStreaming] = useState(false);
const [confirmationOptions, setConfirmationOptions] = useState(null);
const scrollRef = useRef();
const processingStartTime = useRef(0);
const confirmationService = ConfirmationService.getInstance();
const { input, cursorPosition, showCommandSuggestions, selectedCommandIndex, showModelSelection, selectedModelIndex, commandSuggestions, availableModels, autoEditEnabled, } = useInputHandler({
agent,
chatHistory,
setChatHistory,
setIsProcessing,
setIsStreaming,
setTokenCount,
setProcessingTime,
processingStartTime,
isProcessing,
isStreaming,
isConfirmationActive: !!confirmationOptions,
});
useEffect(() => {
// Only clear console on non-Windows platforms or if not PowerShell
// Windows PowerShell can have issues with console.clear() causing flickering
const isWindows = process.platform === "win32";
const isPowerShell = process.env.ComSpec?.toLowerCase().includes("powershell") ||
process.env.PSModulePath !== undefined;
if (!isWindows || !isPowerShell) {
console.clear();
}
// Add top padding
console.log(" ");
// Generate logo with margin to match Ink paddingX={2}
const logoOutput = cfonts.render("GROK", {
font: "3d",
align: "left",
colors: ["magenta", "gray"],
space: true,
maxLength: "0",
gradient: ["magenta", "cyan"],
independentGradient: false,
transitionGradient: true,
env: "node",
});
// Add horizontal margin (2 spaces) to match Ink paddingX={2}
const logoLines = logoOutput.string.split("\n");
logoLines.forEach((line) => {
if (line.trim()) {
console.log(" " + line); // Add 2 spaces for horizontal margin
}
else {
console.log(line); // Keep empty lines as-is
}
});
console.log(" "); // Spacing after logo
setChatHistory([]);
}, []);
// Process initial message if provided (streaming for faster feedback)
useEffect(() => {
if (initialMessage && agent) {
const userEntry = {
type: "user",
content: initialMessage,
timestamp: new Date(),
};
setChatHistory([userEntry]);
const processInitialMessage = async () => {
setIsProcessing(true);
setIsStreaming(true);
try {
let streamingEntry = null;
for await (const chunk of agent.processUserMessageStream(initialMessage)) {
switch (chunk.type) {
case "content":
if (chunk.content) {
if (!streamingEntry) {
const newStreamingEntry = {
type: "assistant",
content: chunk.content,
timestamp: new Date(),
isStreaming: true,
};
setChatHistory((prev) => [...prev, newStreamingEntry]);
streamingEntry = newStreamingEntry;
}
else {
setChatHistory((prev) => prev.map((entry, idx) => idx === prev.length - 1 && entry.isStreaming
? { ...entry, content: entry.content + chunk.content }
: entry));
}
}
break;
case "token_count":
if (chunk.tokenCount !== undefined) {
setTokenCount(chunk.tokenCount);
}
break;
case "tool_calls":
if (chunk.toolCalls) {
// Stop streaming for the current assistant message
setChatHistory((prev) => prev.map((entry) => entry.isStreaming
? {
...entry,
isStreaming: false,
toolCalls: chunk.toolCalls,
}
: entry));
streamingEntry = null;
// Add individual tool call entries to show tools are being executed
chunk.toolCalls.forEach((toolCall) => {
const toolCallEntry = {
type: "tool_call",
content: "Executing...",
timestamp: new Date(),
toolCall: toolCall,
};
setChatHistory((prev) => [...prev, toolCallEntry]);
});
}
break;
case "tool_result":
if (chunk.toolCall && chunk.toolResult) {
setChatHistory((prev) => prev.map((entry) => {
if (entry.isStreaming) {
return { ...entry, isStreaming: false };
}
if (entry.type === "tool_call" &&
entry.toolCall?.id === chunk.toolCall?.id) {
return {
...entry,
type: "tool_result",
content: chunk.toolResult.success
? chunk.toolResult.output || "Success"
: chunk.toolResult.error || "Error occurred",
toolResult: chunk.toolResult,
};
}
return entry;
}));
streamingEntry = null;
}
break;
case "done":
if (streamingEntry) {
setChatHistory((prev) => prev.map((entry) => entry.isStreaming ? { ...entry, isStreaming: false } : entry));
}
setIsStreaming(false);
break;
}
}
}
catch (error) {
const errorEntry = {
type: "assistant",
content: `Error: ${error.message}`,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, errorEntry]);
setIsStreaming(false);
}
setIsProcessing(false);
processingStartTime.current = 0;
};
processInitialMessage();
}
}, [initialMessage, agent]);
useEffect(() => {
const handleConfirmationRequest = (options) => {
setConfirmationOptions(options);
};
confirmationService.on("confirmation-requested", handleConfirmationRequest);
return () => {
confirmationService.off("confirmation-requested", handleConfirmationRequest);
};
}, [confirmationService]);
useEffect(() => {
if (!isProcessing && !isStreaming) {
setProcessingTime(0);
return;
}
if (processingStartTime.current === 0) {
processingStartTime.current = Date.now();
}
const interval = setInterval(() => {
setProcessingTime(Math.floor((Date.now() - processingStartTime.current) / 1000));
}, 1000);
return () => clearInterval(interval);
}, [isProcessing, isStreaming]);
const handleConfirmation = (dontAskAgain) => {
confirmationService.confirmOperation(true, dontAskAgain);
setConfirmationOptions(null);
};
const handleRejection = (feedback) => {
confirmationService.rejectOperation(feedback);
setConfirmationOptions(null);
// Reset processing states when operation is cancelled
setIsProcessing(false);
setIsStreaming(false);
setTokenCount(0);
setProcessingTime(0);
processingStartTime.current = 0;
};
return (React.createElement(Box, { flexDirection: "column", paddingX: 2 },
chatHistory.length === 0 && !confirmationOptions && (React.createElement(Box, { flexDirection: "column", marginBottom: 2 },
React.createElement(Text, { color: "cyan", bold: true }, "Tips for getting started:"),
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
React.createElement(Text, { color: "gray" }, "1. Ask questions, edit files, or run commands."),
React.createElement(Text, { color: "gray" }, "2. Be specific for the best results."),
React.createElement(Text, { color: "gray" }, "3. Create GROK.md files to customize your interactions with Grok."),
React.createElement(Text, { color: "gray" }, "4. Press Shift+Tab to toggle auto-edit mode."),
React.createElement(Text, { color: "gray" }, "5. /help for more information.")))),
React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
React.createElement(Text, { color: "gray" }, "Type your request in natural language. Ctrl+C to clear, 'exit' to quit.")),
React.createElement(Box, { flexDirection: "column", ref: scrollRef },
React.createElement(ChatHistory, { entries: chatHistory, isConfirmationActive: !!confirmationOptions })),
confirmationOptions && (React.createElement(ConfirmationDialog, { operation: confirmationOptions.operation, filename: confirmationOptions.filename, showVSCodeOpen: confirmationOptions.showVSCodeOpen, content: confirmationOptions.content, onConfirm: handleConfirmation, onReject: handleRejection })),
!confirmationOptions && (React.createElement(React.Fragment, null,
React.createElement(LoadingSpinner, { isActive: isProcessing || isStreaming, processingTime: processingTime, tokenCount: tokenCount }),
React.createElement(ChatInput, { input: input, cursorPosition: cursorPosition, isProcessing: isProcessing, isStreaming: isStreaming }),
React.createElement(Box, { flexDirection: "row", marginTop: 1 },
React.createElement(Box, { marginRight: 2 },
React.createElement(Text, { color: "cyan" },
autoEditEnabled ? "▶" : "⏸",
" auto-edit:",
" ",
autoEditEnabled ? "on" : "off"),
React.createElement(Text, { color: "gray", dimColor: true },
" ",
"(shift + tab)")),
React.createElement(Box, { marginRight: 2 },
React.createElement(Text, { color: "yellow" },
"\u224B ",
agent.getCurrentModel())),
React.createElement(MCPStatus, null)),
React.createElement(CommandSuggestions, { suggestions: commandSuggestions, input: input, selectedIndex: selectedCommandIndex, isVisible: showCommandSuggestions }),
React.createElement(ModelSelection, { models: availableModels, selectedIndex: selectedModelIndex, isVisible: showModelSelection, currentModel: agent.getCurrentModel() })))));
}
// Main component that handles API key input or chat interface
export default function ChatInterface({ agent, initialMessage, }) {
const [currentAgent, setCurrentAgent] = useState(agent || null);
const handleApiKeySet = (newAgent) => {
setCurrentAgent(newAgent);
};
if (!currentAgent) {
return React.createElement(ApiKeyInput, { onApiKeySet: handleApiKeySet });
}
return (React.createElement(ChatInterfaceWithAgent, { agent: currentAgent, initialMessage: initialMessage }));
}
//# sourceMappingURL=chat-interface.js.map