UNPKG

@vibe-kit/grok-cli

Version:

An open-source AI agent that brings the power of Grok directly into your terminal.

600 lines (590 loc) 25.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useInputHandler = void 0; const react_1 = require("react"); const ink_1 = require("ink"); const confirmation_service_1 = require("../utils/confirmation-service"); const command_suggestions_1 = require("../ui/components/command-suggestions"); const model_config_1 = require("../utils/model-config"); function useInputHandler({ agent, chatHistory, setChatHistory, setIsProcessing, setIsStreaming, setTokenCount, setProcessingTime, processingStartTime, isProcessing, isStreaming, isConfirmationActive = false, }) { const [input, setInput] = (0, react_1.useState)(""); const [showCommandSuggestions, setShowCommandSuggestions] = (0, react_1.useState)(false); const [selectedCommandIndex, setSelectedCommandIndex] = (0, react_1.useState)(0); const [showModelSelection, setShowModelSelection] = (0, react_1.useState)(false); const [selectedModelIndex, setSelectedModelIndex] = (0, react_1.useState)(0); const [autoEditEnabled, setAutoEditEnabled] = (0, react_1.useState)(() => { const confirmationService = confirmation_service_1.ConfirmationService.getInstance(); const sessionFlags = confirmationService.getSessionFlags(); return sessionFlags.allOperations; }); // Removed useApp().exit - using process.exit(0) instead for better terminal handling const commandSuggestions = [ { command: "/help", description: "Show help information" }, { command: "/clear", description: "Clear chat history" }, { command: "/models", description: "Switch Grok Model" }, { command: "/commit-and-push", description: "AI commit & push to remote" }, { command: "/exit", description: "Exit the application" }, ]; // Load models from configuration with fallback to defaults const availableModels = (0, react_1.useMemo)(() => { return (0, model_config_1.loadModelConfig)(); // Return directly, interface already matches }, []); const handleDirectCommand = async (input) => { const trimmedInput = input.trim(); if (trimmedInput === "/clear") { // Reset chat history setChatHistory([]); // Reset processing states setIsProcessing(false); setIsStreaming(false); setTokenCount(0); setProcessingTime(0); processingStartTime.current = 0; // Reset confirmation service session flags const confirmationService = confirmation_service_1.ConfirmationService.getInstance(); confirmationService.resetSession(); setInput(""); return true; } if (trimmedInput === "/help") { const helpEntry = { type: "assistant", content: `Grok CLI Help: Built-in Commands: /clear - Clear chat history /help - Show this help /models - Switch between available models /exit - Exit application exit, quit - Exit application Git Commands: /commit-and-push - AI-generated commit + push to remote Keyboard Shortcuts: Shift+Tab - Toggle auto-edit mode (bypass confirmations) Direct Commands (executed immediately): ls [path] - List directory contents pwd - Show current directory cd <path> - Change directory cat <file> - View file contents mkdir <dir> - Create directory touch <file>- Create empty file Model Configuration: Edit ~/.grok/models.json to add custom models (Claude, GPT, Gemini, etc.) For complex operations, just describe what you want in natural language. Examples: "edit package.json and add a new script" "create a new React component called Header" "show me all TypeScript files in this project"`, timestamp: new Date(), }; setChatHistory((prev) => [...prev, helpEntry]); setInput(""); return true; } if (trimmedInput === "/exit") { process.exit(0); return true; } if (trimmedInput === "/models") { setShowModelSelection(true); setSelectedModelIndex(0); setInput(""); return true; } if (trimmedInput.startsWith("/models ")) { const modelArg = trimmedInput.split(" ")[1]; const modelNames = availableModels.map((m) => m.model); if (modelNames.includes(modelArg)) { agent.setModel(modelArg); (0, model_config_1.updateCurrentModel)(modelArg); // Update project current model const confirmEntry = { type: "assistant", content: `✓ Switched to model: ${modelArg}`, timestamp: new Date(), }; setChatHistory((prev) => [...prev, confirmEntry]); } else { const errorEntry = { type: "assistant", content: `Invalid model: ${modelArg} Available models: ${modelNames.join(", ")}`, timestamp: new Date(), }; setChatHistory((prev) => [...prev, errorEntry]); } setInput(""); return true; } if (trimmedInput === "/commit-and-push") { const userEntry = { type: "user", content: "/commit-and-push", timestamp: new Date(), }; setChatHistory((prev) => [...prev, userEntry]); setIsProcessing(true); setIsStreaming(true); try { // First check if there are any changes at all const initialStatusResult = await agent.executeBashCommand("git status --porcelain"); if (!initialStatusResult.success || !initialStatusResult.output?.trim()) { const noChangesEntry = { type: "assistant", content: "No changes to commit. Working directory is clean.", timestamp: new Date(), }; setChatHistory((prev) => [...prev, noChangesEntry]); setIsProcessing(false); setIsStreaming(false); setInput(""); return true; } // Add all changes const addResult = await agent.executeBashCommand("git add ."); if (!addResult.success) { const addErrorEntry = { type: "assistant", content: `Failed to stage changes: ${addResult.error || "Unknown error"}`, timestamp: new Date(), }; setChatHistory((prev) => [...prev, addErrorEntry]); setIsProcessing(false); setIsStreaming(false); setInput(""); return true; } // Show that changes were staged const addEntry = { type: "tool_result", content: "Changes staged successfully", timestamp: new Date(), toolCall: { id: `git_add_${Date.now()}`, type: "function", function: { name: "bash", arguments: JSON.stringify({ command: "git add ." }), }, }, toolResult: addResult, }; setChatHistory((prev) => [...prev, addEntry]); // Get staged changes for commit message generation const diffResult = await agent.executeBashCommand("git diff --cached"); // Generate commit message using AI const commitPrompt = `Generate a concise, professional git commit message for these changes: Git Status: ${initialStatusResult.output} Git Diff (staged changes): ${diffResult.output || "No staged changes shown"} Follow conventional commit format (feat:, fix:, docs:, etc.) and keep it under 72 characters. Respond with ONLY the commit message, no additional text.`; let commitMessage = ""; let streamingEntry = null; for await (const chunk of agent.processUserMessageStream(commitPrompt)) { if (chunk.type === "content" && chunk.content) { if (!streamingEntry) { const newEntry = { type: "assistant", content: `Generating commit message...\n\n${chunk.content}`, timestamp: new Date(), isStreaming: true, }; setChatHistory((prev) => [...prev, newEntry]); streamingEntry = newEntry; commitMessage = chunk.content; } else { commitMessage += chunk.content; setChatHistory((prev) => prev.map((entry, idx) => idx === prev.length - 1 && entry.isStreaming ? { ...entry, content: `Generating commit message...\n\n${commitMessage}`, } : entry)); } } else if (chunk.type === "done") { if (streamingEntry) { setChatHistory((prev) => prev.map((entry) => entry.isStreaming ? { ...entry, content: `Generated commit message: "${commitMessage.trim()}"`, isStreaming: false, } : entry)); } break; } } // Execute the commit const cleanCommitMessage = commitMessage .trim() .replace(/^["']|["']$/g, ""); const commitCommand = `git commit -m "${cleanCommitMessage}"`; const commitResult = await agent.executeBashCommand(commitCommand); const commitEntry = { type: "tool_result", content: commitResult.success ? commitResult.output || "Commit successful" : commitResult.error || "Commit failed", timestamp: new Date(), toolCall: { id: `git_commit_${Date.now()}`, type: "function", function: { name: "bash", arguments: JSON.stringify({ command: commitCommand }), }, }, toolResult: commitResult, }; setChatHistory((prev) => [...prev, commitEntry]); // If commit was successful, push to remote if (commitResult.success) { // First try regular push, if it fails try with upstream setup let pushResult = await agent.executeBashCommand("git push"); let pushCommand = "git push"; if (!pushResult.success && pushResult.error?.includes("no upstream branch")) { pushCommand = "git push -u origin HEAD"; pushResult = await agent.executeBashCommand(pushCommand); } const pushEntry = { type: "tool_result", content: pushResult.success ? pushResult.output || "Push successful" : pushResult.error || "Push failed", timestamp: new Date(), toolCall: { id: `git_push_${Date.now()}`, type: "function", function: { name: "bash", arguments: JSON.stringify({ command: pushCommand }), }, }, toolResult: pushResult, }; setChatHistory((prev) => [...prev, pushEntry]); } } catch (error) { const errorEntry = { type: "assistant", content: `Error during commit and push: ${error.message}`, timestamp: new Date(), }; setChatHistory((prev) => [...prev, errorEntry]); } setIsProcessing(false); setIsStreaming(false); setInput(""); return true; } const directBashCommands = [ "ls", "pwd", "cd", "cat", "mkdir", "touch", "echo", "grep", "find", "cp", "mv", "rm", ]; const firstWord = trimmedInput.split(" ")[0]; if (directBashCommands.includes(firstWord)) { const userEntry = { type: "user", content: trimmedInput, timestamp: new Date(), }; setChatHistory((prev) => [...prev, userEntry]); try { const result = await agent.executeBashCommand(trimmedInput); const commandEntry = { type: "tool_result", content: result.success ? result.output || "Command completed" : result.error || "Command failed", timestamp: new Date(), toolCall: { id: `bash_${Date.now()}`, type: "function", function: { name: "bash", arguments: JSON.stringify({ command: trimmedInput }), }, }, toolResult: result, }; setChatHistory((prev) => [...prev, commandEntry]); } catch (error) { const errorEntry = { type: "assistant", content: `Error executing command: ${error.message}`, timestamp: new Date(), }; setChatHistory((prev) => [...prev, errorEntry]); } setInput(""); return true; } return false; }; const processUserMessage = async (userInput) => { 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) { 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 }; } // Update the existing tool_call entry with the result 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; }; (0, ink_1.useInput)(async (inputChar, key) => { // Don't handle input if confirmation dialog is active if (isConfirmationActive) { return; } if (key.ctrl && inputChar === "c") { process.exit(0); return; } // Handle shift+tab to toggle auto-edit mode if (key.shift && key.tab) { const newAutoEditState = !autoEditEnabled; setAutoEditEnabled(newAutoEditState); const confirmationService = confirmation_service_1.ConfirmationService.getInstance(); if (newAutoEditState) { // Enable auto-edit: set all operations to be accepted confirmationService.setSessionFlag("allOperations", true); } else { // Disable auto-edit: reset session flags confirmationService.resetSession(); } return; } if (key.escape) { if (showCommandSuggestions) { setShowCommandSuggestions(false); setSelectedCommandIndex(0); return; } if (showModelSelection) { setShowModelSelection(false); setSelectedModelIndex(0); return; } if (isProcessing || isStreaming) { agent.abortCurrentOperation(); setIsProcessing(false); setIsStreaming(false); setTokenCount(0); setProcessingTime(0); processingStartTime.current = 0; return; } } if (showCommandSuggestions) { const filteredSuggestions = (0, command_suggestions_1.filterCommandSuggestions)(commandSuggestions, input); if (filteredSuggestions.length === 0) { setShowCommandSuggestions(false); setSelectedCommandIndex(0); // Continue processing the input instead of returning } else { if (key.upArrow) { setSelectedCommandIndex((prev) => prev === 0 ? filteredSuggestions.length - 1 : prev - 1); return; } if (key.downArrow) { setSelectedCommandIndex((prev) => (prev + 1) % filteredSuggestions.length); return; } if (key.tab || key.return) { const safeIndex = Math.min(selectedCommandIndex, filteredSuggestions.length - 1); const selectedCommand = filteredSuggestions[safeIndex]; setInput(selectedCommand.command + " "); setShowCommandSuggestions(false); setSelectedCommandIndex(0); return; } } } if (showModelSelection) { if (key.upArrow) { setSelectedModelIndex((prev) => prev === 0 ? availableModels.length - 1 : prev - 1); return; } if (key.downArrow) { setSelectedModelIndex((prev) => (prev + 1) % availableModels.length); return; } if (key.tab || key.return) { const selectedModel = availableModels[selectedModelIndex]; agent.setModel(selectedModel.model); (0, model_config_1.updateCurrentModel)(selectedModel.model); // Update project current model const confirmEntry = { type: "assistant", content: `✓ Switched to model: ${selectedModel.model}`, timestamp: new Date(), }; setChatHistory((prev) => [...prev, confirmEntry]); setShowModelSelection(false); setSelectedModelIndex(0); return; } } if (key.return) { const userInput = input.trim(); if (userInput === "exit" || userInput === "quit") { process.exit(0); return; } if (userInput) { const directCommandResult = await handleDirectCommand(userInput); if (!directCommandResult) { await processUserMessage(userInput); } } return; } if (key.backspace || key.delete) { const newInput = input.slice(0, -1); setInput(newInput); if (!newInput.startsWith("/")) { setShowCommandSuggestions(false); setSelectedCommandIndex(0); } return; } if (inputChar && !key.ctrl && !key.meta) { const newInput = input + inputChar; setInput(newInput); if (newInput.startsWith("/")) { setShowCommandSuggestions(true); setSelectedCommandIndex(0); } else { setShowCommandSuggestions(false); setSelectedCommandIndex(0); } } }); return { input, showCommandSuggestions, selectedCommandIndex, showModelSelection, selectedModelIndex, commandSuggestions, availableModels, agent, autoEditEnabled, }; } exports.useInputHandler = useInputHandler; //# sourceMappingURL=use-input-handler.js.map