@vibe-kit/grok-cli
Version:
An open-source AI agent that brings the power of Grok directly into your terminal.
343 lines • 16.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importStar(require("react"));
const ink_1 = require("ink");
const DiffRenderer_1 = require("./components/DiffRenderer");
const cfonts_1 = __importDefault(require("cfonts"));
function ChatInterface({ agent }) {
const [input, setInput] = (0, react_1.useState)("");
const [chatHistory, setChatHistory] = (0, react_1.useState)([]);
const [isProcessing, setIsProcessing] = (0, react_1.useState)(false);
const [spinnerFrame, setSpinnerFrame] = (0, react_1.useState)(0);
const [processingTime, setProcessingTime] = (0, react_1.useState)(0);
const [tokenCount, setTokenCount] = (0, react_1.useState)(0);
const [loadingTextIndex, setLoadingTextIndex] = (0, react_1.useState)(0);
const [isStreaming, setIsStreaming] = (0, react_1.useState)(false);
const { exit } = (0, ink_1.useApp)();
const scrollRef = (0, react_1.useRef)();
const processingStartTime = (0, react_1.useRef)(0);
const loadingTexts = [
"Thinking...",
"Computing...",
"Analyzing...",
"Processing...",
"Calculating...",
"Interfacing...",
"Optimizing...",
"Synthesizing...",
"Decrypting...",
"Calibrating...",
"Bootstrapping...",
"Synchronizing...",
"Compiling...",
"Downloading...",
];
(0, react_1.useEffect)(() => {
// Display ASCII art banner
console.clear();
cfonts_1.default.say("GROK", {
font: "3d",
align: "left",
colors: ["magenta", "gray"],
space: true,
maxLength: "0",
gradient: ["magenta", "cyan"],
independentGradient: false,
transitionGradient: true,
env: "node",
});
console.log("Tips for getting started:");
console.log("1. Ask questions, edit files, or run commands.");
console.log("2. Be specific for the best results.");
console.log("3. Create GROK.md files to customize your interactions with Grok.");
console.log("4. /help for more information.");
console.log("");
setChatHistory([]);
}, []);
// Spinner animation effect
(0, react_1.useEffect)(() => {
if (!isProcessing && !isStreaming)
return;
const spinnerFrames = ["/", "-", "\\", "|"];
const interval = setInterval(() => {
setSpinnerFrame((prev) => (prev + 1) % spinnerFrames.length);
}, 250);
return () => clearInterval(interval);
}, [isProcessing, isStreaming]);
// Loading text rotation effect
(0, react_1.useEffect)(() => {
if (!isProcessing && !isStreaming) {
return;
}
// Set random initial text
setLoadingTextIndex(Math.floor(Math.random() * loadingTexts.length));
const interval = setInterval(() => {
setLoadingTextIndex(Math.floor(Math.random() * loadingTexts.length));
}, 2000);
return () => clearInterval(interval);
}, [isProcessing, isStreaming, loadingTexts.length]);
// Timer effect
(0, react_1.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]);
(0, ink_1.useInput)(async (inputChar, key) => {
if (key.ctrl && inputChar === "c") {
exit();
return;
}
if (key.escape && (isProcessing || isStreaming)) {
setIsProcessing(false);
setIsStreaming(false);
setTokenCount(0);
setProcessingTime(0);
processingStartTime.current = 0;
return;
}
if (key.return) {
const userInput = input.trim();
if (userInput === "exit" || userInput === "quit") {
exit();
return;
}
if (userInput) {
// Add user message immediately for immediate display
const userEntry = {
type: "user",
content: userInput,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, userEntry]);
setIsProcessing(true);
setInput("");
try {
setIsStreaming(true);
let streamingEntry = null;
for await (const chunk of agent.processUserMessageStream(userInput)) {
switch (chunk.type) {
case 'content':
if (chunk.content) {
// Update or create streaming entry
if (!streamingEntry) {
// Create new streaming entry
const newStreamingEntry = {
type: 'assistant',
content: chunk.content,
timestamp: new Date(),
isStreaming: true
};
setChatHistory(prev => [...prev, newStreamingEntry]);
streamingEntry = newStreamingEntry;
}
else {
// Update existing streaming entry
setChatHistory(prev => prev.map((entry, idx) => idx === prev.length - 1 && entry.isStreaming
? { ...entry, content: entry.content + chunk.content }
: entry));
}
}
break;
case 'tool_calls':
// Don't handle tool calls in streaming - they're already in the assistant message
break;
case 'tool_result':
if (chunk.toolCall && chunk.toolResult) {
// Mark any streaming entries as complete
setChatHistory(prev => prev.map(entry => entry.isStreaming ? { ...entry, isStreaming: false } : entry));
const toolResultEntry = {
type: 'tool_result',
content: chunk.toolResult.success
? chunk.toolResult.output || 'Success'
: chunk.toolResult.error || 'Error occurred',
timestamp: new Date(),
toolCall: chunk.toolCall,
toolResult: chunk.toolResult
};
setChatHistory(prev => [...prev, toolResultEntry]);
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;
}
return;
}
if (key.backspace || key.delete) {
setInput((prev) => prev.slice(0, -1));
return;
}
if (inputChar && !key.ctrl && !key.meta) {
setInput((prev) => prev + inputChar);
}
});
const renderDiff = (diffContent, filename) => {
return (react_1.default.createElement(DiffRenderer_1.DiffRenderer, { diffContent: diffContent, filename: filename, terminalWidth: 80 }));
};
const renderChatEntry = (entry, index) => {
switch (entry.type) {
case "user":
return (react_1.default.createElement(ink_1.Box, { key: index, flexDirection: "column", marginTop: 1 },
react_1.default.createElement(ink_1.Box, null,
react_1.default.createElement(ink_1.Text, { color: "gray" },
">",
" ",
entry.content))));
case "assistant":
return (react_1.default.createElement(ink_1.Box, { key: index, flexDirection: "column", marginTop: 1 },
react_1.default.createElement(ink_1.Box, null,
react_1.default.createElement(ink_1.Text, { color: "white" },
"\u23FA ",
entry.content,
entry.isStreaming && react_1.default.createElement(ink_1.Text, { color: "cyan" }, "\u2588"))),
entry.toolCalls && entry.toolCalls.map((toolCall, tcIndex) => (react_1.default.createElement(ink_1.Box, { key: tcIndex, marginLeft: 2 },
react_1.default.createElement(ink_1.Text, { color: "magenta" },
"\uD83D\uDD27 ",
toolCall.function.name))))));
case "tool_result":
const getToolActionName = (toolName) => {
switch (toolName) {
case "view_file":
return "Read";
case "str_replace_editor":
return "Update";
case "create_file":
return "Create";
case "bash":
return "Execute";
default:
return "Tool";
}
};
const getToolFilePath = (toolCall) => {
if (toolCall?.function?.arguments) {
try {
const args = JSON.parse(toolCall.function.arguments);
return args.path || args.file_path || args.command || "unknown";
}
catch {
return "unknown";
}
}
return "unknown";
};
const toolName = entry.toolCall?.function?.name || "unknown";
const actionName = getToolActionName(toolName);
const filePath = getToolFilePath(entry.toolCall);
// Show diff only for write/update tools
const shouldShowDiff = toolName === "str_replace_editor" || toolName === "create_file";
const getToolSummary = () => {
if (!entry.toolResult?.success)
return "Failed";
switch (toolName) {
case "view_file":
const lineCount = entry.content.split("\n").length;
return `Read ${lineCount} lines`;
case "bash":
return "Command executed";
case "str_replace_editor":
// Extract summary from first line of diff
const firstLine = entry.content.split('\n')[0];
return firstLine.startsWith('Updated ') ? firstLine.substring(8) : "Updated with changes";
case "create_file":
// Extract summary from first line of output
const createFirstLine = entry.content.split('\n')[0];
return createFirstLine.startsWith('Created ') ? createFirstLine.substring(8) : "File created";
default:
return `${actionName} completed`;
}
};
return (react_1.default.createElement(ink_1.Box, { key: index, flexDirection: "column" },
react_1.default.createElement(ink_1.Box, null,
react_1.default.createElement(ink_1.Text, { color: "magenta" }, "\u23FA"),
react_1.default.createElement(ink_1.Text, { color: "white" },
" ",
actionName,
"(",
filePath,
")")),
react_1.default.createElement(ink_1.Box, { marginLeft: 2 },
react_1.default.createElement(ink_1.Text, { color: "gray" },
"\u23BF ",
getToolSummary())),
shouldShowDiff && (react_1.default.createElement(ink_1.Box, { marginLeft: 6, flexDirection: "column" }, renderDiff(entry.content, filePath)))));
default:
return null;
}
};
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
react_1.default.createElement(ink_1.Box, { flexDirection: "column", marginBottom: 1 },
react_1.default.createElement(ink_1.Text, { dimColor: true }, "Type your request in natural language. Type 'exit' or Ctrl+C to quit.")),
react_1.default.createElement(ink_1.Box, { flexDirection: "column", ref: scrollRef }, chatHistory.slice(-20).map(renderChatEntry)),
(isProcessing || isStreaming) && (react_1.default.createElement(ink_1.Box, { marginTop: 1 },
react_1.default.createElement(ink_1.Text, { color: "cyan" },
["/", "-", "\\", "|"][spinnerFrame],
" ",
loadingTexts[loadingTextIndex],
" "),
react_1.default.createElement(ink_1.Text, { color: "gray" },
"(",
processingTime,
"s \u00B7 \u2191 ",
tokenCount,
" tokens \u00B7 esc to interrupt)"))),
react_1.default.createElement(ink_1.Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1 },
react_1.default.createElement(ink_1.Text, { color: "gray" }, "\u276F "),
react_1.default.createElement(ink_1.Text, null,
input,
!isProcessing && !isStreaming && react_1.default.createElement(ink_1.Text, { color: "white" }, "\u2588")))));
}
exports.default = ChatInterface;
//# sourceMappingURL=chat-interface.js.map