Files
grok-cli/src/hooks/use-input-handler.ts
T

481 lines
13 KiB
TypeScript

import { useState, useRef } from "react";
import { useInput, useApp } from "ink";
import { GrokAgent, ChatEntry } from "../agent/grok-agent";
import { ConfirmationService } from "../utils/confirmation-service";
interface UseInputHandlerProps {
agent: GrokAgent;
chatHistory: ChatEntry[];
setChatHistory: React.Dispatch<React.SetStateAction<ChatEntry[]>>;
setIsProcessing: (processing: boolean) => void;
setIsStreaming: (streaming: boolean) => void;
setTokenCount: (count: number) => void;
setProcessingTime: (time: number) => void;
processingStartTime: React.MutableRefObject<number>;
isProcessing: boolean;
isStreaming: boolean;
isConfirmationActive?: boolean;
}
interface CommandSuggestion {
command: string;
description: string;
}
interface ModelOption {
model: string;
description: string;
}
export function useInputHandler({
agent,
chatHistory,
setChatHistory,
setIsProcessing,
setIsStreaming,
setTokenCount,
setProcessingTime,
processingStartTime,
isProcessing,
isStreaming,
isConfirmationActive = false,
}: UseInputHandlerProps) {
const [input, setInput] = useState("");
const [showCommandSuggestions, setShowCommandSuggestions] = useState(false);
const [selectedCommandIndex, setSelectedCommandIndex] = useState(0);
const [showModelSelection, setShowModelSelection] = useState(false);
const [selectedModelIndex, setSelectedModelIndex] = useState(0);
const { exit } = useApp();
const commandSuggestions: CommandSuggestion[] = [
{ command: "/help", description: "Show help information" },
{ command: "/clear", description: "Clear chat history" },
{ command: "/models", description: "Switch Grok Model" },
{ command: "/exit", description: "Exit the application" },
];
const availableModels: ModelOption[] = [
{
model: "grok-4-latest",
description: "Latest Grok-4 model (most capable)",
},
{ model: "grok-3-latest", description: "Latest Grok-3 model" },
{ model: "grok-3-fast", description: "Fast Grok-3 variant" },
{ model: "grok-3-mini-fast", description: "Fastest Grok-3 variant" },
];
const handleDirectCommand = async (input: string): Promise<boolean> => {
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();
setInput("");
return true;
}
if (trimmedInput === "/help") {
const helpEntry: ChatEntry = {
type: "assistant",
content: `Grok CLI Help:
Built-in Commands:
/clear - Clear chat history
/help - Show this help
/models - Switch Grok models
/exit - Exit application
exit, quit - Exit application
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
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 === "/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);
const confirmEntry: ChatEntry = {
type: "assistant",
content: `✓ Switched to model: ${modelArg}`,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, confirmEntry]);
} else {
const errorEntry: ChatEntry = {
type: "assistant",
content: `Invalid model: ${modelArg}
Available models: ${modelNames.join(", ")}`,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, errorEntry]);
}
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: ChatEntry = {
type: "user",
content: trimmedInput,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, userEntry]);
try {
const result = await agent.executeBashCommand(trimmedInput);
const commandEntry: ChatEntry = {
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: any) {
const errorEntry: ChatEntry = {
type: "assistant",
content: `Error executing command: ${error.message}`,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, errorEntry]);
}
setInput("");
return true;
}
return false;
};
const processUserMessage = async (userInput: string) => {
const userEntry: ChatEntry = {
type: "user",
content: userInput,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, userEntry]);
setIsProcessing(true);
setInput("");
try {
setIsStreaming(true);
let streamingEntry: ChatEntry | null = null;
for await (const chunk of agent.processUserMessageStream(userInput)) {
switch (chunk.type) {
case "content":
if (chunk.content) {
if (!streamingEntry) {
const newStreamingEntry = {
type: "assistant" as const,
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: ChatEntry = {
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) =>
entry.isStreaming ? { ...entry, isStreaming: false } : entry
)
);
const toolResultEntry: ChatEntry = {
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: any) {
const errorEntry: ChatEntry = {
type: "assistant",
content: `Error: ${error.message}`,
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, errorEntry]);
setIsStreaming(false);
}
setIsProcessing(false);
processingStartTime.current = 0;
};
useInput(async (inputChar: string, key: any) => {
// Don't handle input if confirmation dialog is active
if (isConfirmationActive) {
return;
}
if (key.ctrl && inputChar === "c") {
exit();
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) {
if (key.upArrow) {
setSelectedCommandIndex((prev) =>
prev === 0 ? commandSuggestions.length - 1 : prev - 1
);
return;
}
if (key.downArrow) {
setSelectedCommandIndex(
(prev) => (prev + 1) % commandSuggestions.length
);
return;
}
if (key.tab || key.return) {
const selectedCommand = commandSuggestions[selectedCommandIndex];
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);
const confirmEntry: ChatEntry = {
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") {
exit();
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 === "/" ||
["ls", "pwd", "cd", "cat", "mkdir", "touch"].some((cmd) =>
cmd.startsWith(newInput)
)
) {
setShowCommandSuggestions(true);
setSelectedCommandIndex(0);
} else if (
!newInput.startsWith("/") &&
!["ls", "pwd", "cd", "cat", "mkdir", "touch"].some((cmd) =>
cmd.startsWith(newInput)
)
) {
setShowCommandSuggestions(false);
setSelectedCommandIndex(0);
}
}
});
return {
input,
showCommandSuggestions,
selectedCommandIndex,
showModelSelection,
selectedModelIndex,
commandSuggestions,
availableModels,
agent,
};
}