@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
JavaScript
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