UNPKG

@vibe-kit/grok-cli

Version:

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

608 lines (598 loc) 26 kB
import { useState, useMemo, useEffect } from "react"; import { useInput } from "ink"; import { ConfirmationService } from "../utils/confirmation-service.js"; import { useEnhancedInput } from "./use-enhanced-input.js"; import { filterCommandSuggestions } from "../ui/components/command-suggestions.js"; import { loadModelConfig, updateCurrentModel } from "../utils/model-config.js"; export function useInputHandler({ agent, chatHistory, setChatHistory, setIsProcessing, setIsStreaming, setTokenCount, setProcessingTime, processingStartTime, isProcessing, isStreaming, isConfirmationActive = false, }) { const [showCommandSuggestions, setShowCommandSuggestions] = useState(false); const [selectedCommandIndex, setSelectedCommandIndex] = useState(0); const [showModelSelection, setShowModelSelection] = useState(false); const [selectedModelIndex, setSelectedModelIndex] = useState(0); const [autoEditEnabled, setAutoEditEnabled] = useState(() => { const confirmationService = ConfirmationService.getInstance(); const sessionFlags = confirmationService.getSessionFlags(); return sessionFlags.allOperations; }); const handleSpecialKey = (key) => { // Don't handle input if confirmation dialog is active if (isConfirmationActive) { return true; // Prevent default handling } // Handle shift+tab to toggle auto-edit mode if (key.shift && key.tab) { const newAutoEditState = !autoEditEnabled; setAutoEditEnabled(newAutoEditState); const confirmationService = 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 true; // Handled } // Handle escape key for closing menus if (key.escape) { if (showCommandSuggestions) { setShowCommandSuggestions(false); setSelectedCommandIndex(0); return true; } if (showModelSelection) { setShowModelSelection(false); setSelectedModelIndex(0); return true; } if (isProcessing || isStreaming) { agent.abortCurrentOperation(); setIsProcessing(false); setIsStreaming(false); setTokenCount(0); setProcessingTime(0); processingStartTime.current = 0; return true; } return false; // Let default escape handling work } // Handle command suggestions navigation if (showCommandSuggestions) { const filteredSuggestions = filterCommandSuggestions(commandSuggestions, input); if (filteredSuggestions.length === 0) { setShowCommandSuggestions(false); setSelectedCommandIndex(0); return false; // Continue processing } else { if (key.upArrow) { setSelectedCommandIndex((prev) => prev === 0 ? filteredSuggestions.length - 1 : prev - 1); return true; } if (key.downArrow) { setSelectedCommandIndex((prev) => (prev + 1) % filteredSuggestions.length); return true; } if (key.tab || key.return) { const safeIndex = Math.min(selectedCommandIndex, filteredSuggestions.length - 1); const selectedCommand = filteredSuggestions[safeIndex]; const newInput = selectedCommand.command + " "; setInput(newInput); setCursorPosition(newInput.length); setShowCommandSuggestions(false); setSelectedCommandIndex(0); return true; } } } // Handle model selection navigation if (showModelSelection) { if (key.upArrow) { setSelectedModelIndex((prev) => prev === 0 ? availableModels.length - 1 : prev - 1); return true; } if (key.downArrow) { setSelectedModelIndex((prev) => (prev + 1) % availableModels.length); return true; } if (key.tab || key.return) { const selectedModel = availableModels[selectedModelIndex]; agent.setModel(selectedModel.model); updateCurrentModel(selectedModel.model); const confirmEntry = { type: "assistant", content: `✓ Switched to model: ${selectedModel.model}`, timestamp: new Date(), }; setChatHistory((prev) => [...prev, confirmEntry]); setShowModelSelection(false); setSelectedModelIndex(0); return true; } } return false; // Let default handling proceed }; const handleInputSubmit = async (userInput) => { if (userInput === "exit" || userInput === "quit") { process.exit(0); return; } if (userInput.trim()) { const directCommandResult = await handleDirectCommand(userInput); if (!directCommandResult) { await processUserMessage(userInput); } } }; const handleInputChange = (newInput) => { // Update command suggestions based on input if (newInput.startsWith("/")) { setShowCommandSuggestions(true); setSelectedCommandIndex(0); } else { setShowCommandSuggestions(false); setSelectedCommandIndex(0); } }; const { input, cursorPosition, setInput, setCursorPosition, clearInput, resetHistory, handleInput, } = useEnhancedInput({ onSubmit: handleInputSubmit, onSpecialKey: handleSpecialKey, disabled: isConfirmationActive, }); // Hook up the actual input handling useInput((inputChar, key) => { handleInput(inputChar, key); }); // Update command suggestions when input changes useEffect(() => { handleInputChange(input); }, [input]); 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 = useMemo(() => { return 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 = ConfirmationService.getInstance(); confirmationService.resetSession(); clearInput(); resetHistory(); 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 Enhanced Input Features: ↑/↓ Arrow - Navigate command history Ctrl+C - Clear input (press twice to exit) Ctrl+←/→ - Move by word Ctrl+A/E - Move to line start/end Ctrl+W - Delete word before cursor Ctrl+K - Delete to end of line Ctrl+U - Delete to start of line 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]); clearInput(); return true; } if (trimmedInput === "/exit") { process.exit(0); return true; } if (trimmedInput === "/models") { setShowModelSelection(true); setSelectedModelIndex(0); clearInput(); 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); 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]); } clearInput(); 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); clearInput(); 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]); } clearInput(); return true; } return false; }; const processUserMessage = async (userInput) => { const userEntry = { type: "user", content: userInput, timestamp: new Date(), }; setChatHistory((prev) => [...prev, userEntry]); setIsProcessing(true); clearInput(); 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; }; return { input, cursorPosition, showCommandSuggestions, selectedCommandIndex, showModelSelection, selectedModelIndex, commandSuggestions, availableModels, agent, autoEditEnabled, }; } //# sourceMappingURL=use-input-handler.js.map