minor tweaks

This commit is contained in:
Ismail Pelaseyed committed 2025-08-04 22:11:55 +02:00
1 parent d07d5f0691
commit ff827e2eb4
10 files changed
+940 -247

No files matched your search

+25 -25
View File
@@ -55,7 +55,12 @@ export class GrokAgent extends EventEmitter {
private mcpInitialized: boolean = false;
private maxToolRounds: number;
constructor(apiKey: string, baseURL?: string, model?: string, maxToolRounds?: number) {
constructor(
apiKey: string,
baseURL?: string,
model?: string,
maxToolRounds?: number
) {
super();
const manager = getSettingsManager();
const savedModel = manager.getCurrentModel();
@@ -87,7 +92,11 @@ export class GrokAgent extends EventEmitter {
You have access to these tools:
- view_file: View file contents or directory listings
- create_file: Create new files with content (ONLY use this for files that don't exist yet)
- str_replace_editor: Replace text in existing files (ALWAYS use this to edit or update existing files)${this.morphEditor ? '\n- edit_file: High-speed file editing with Morph Fast Apply (4,500+ tokens/sec with 98% accuracy)' : ''}
- str_replace_editor: Replace text in existing files (ALWAYS use this to edit or update existing files)${
this.morphEditor
? "\n- edit_file: High-speed file editing with Morph Fast Apply (4,500+ tokens/sec with 98% accuracy)"
: ""
}
- bash: Execute bash commands (use for searching, file discovery, navigation, and system operations)
- search: Unified search tool for finding text content or files (similar to Cursor's search functionality)
- create_todo_list: Create a visual todo list for planning and tracking tasks
@@ -143,26 +152,19 @@ Current working directory: ${process.cwd()}`,
}
private async initializeMCP(): Promise<void> {
try {
const config = loadMCPConfig();
if (config.servers.length > 0) {
console.log(
`Found ${config.servers.length} MCP server(s) - connecting now...`
);
await initializeMCPServers();
console.log(`Successfully connected to MCP servers`);
// Initialize MCP in the background without blocking
Promise.resolve().then(async () => {
try {
const config = loadMCPConfig();
if (config.servers.length > 0) {
await initializeMCPServers();
}
} catch (error) {
console.warn('MCP initialization failed:', error);
} finally {
this.mcpInitialized = true;
}
this.mcpInitialized = true;
} catch (error) {
console.warn("Failed to initialize MCP servers:", error);
this.mcpInitialized = true; // Don't block if MCP fails
}
}
private async waitForMCPInitialization(): Promise<void> {
while (!this.mcpInitialized) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
});
}
private isGrokModel(): boolean {
@@ -171,9 +173,6 @@ Current working directory: ${process.cwd()}`,
}
async processUserMessage(message: string): Promise<ChatEntry[]> {
// Wait for MCP initialization before processing
await this.waitForMCPInitialization();
// Add user message to conversation
const userEntry: ChatEntry = {
type: "user",
@@ -625,7 +624,8 @@ Current working directory: ${process.cwd()}`,
if (!this.morphEditor) {
return {
success: false,
error: "Morph Fast Apply not available. Please set MORPH_API_KEY environment variable to use this feature.",
error:
"Morph Fast Apply not available. Please set MORPH_API_KEY environment variable to use this feature.",
};
}
return await this.morphEditor.editFile(
+4 -1
View File
@@ -367,6 +367,9 @@ export function addMCPToolsToGrokTools(baseTools: GrokTool[]): GrokTool[] {
export async function getAllGrokTools(): Promise<GrokTool[]> {
const manager = getMCPManager();
await manager.ensureServersInitialized();
// Try to initialize servers if not already done, but don't block
manager.ensureServersInitialized().catch(() => {
// Ignore initialization errors to avoid blocking
});
return addMCPToolsToGrokTools(GROK_TOOLS);
}
+298
View File
@@ -0,0 +1,298 @@
import { useState, useCallback, useRef } from "react";
import {
deleteCharBefore,
deleteCharAfter,
deleteWordBefore,
deleteWordAfter,
insertText,
moveToLineStart,
moveToLineEnd,
moveToPreviousWord,
moveToNextWord,
} from "../utils/text-utils";
import { useInputHistory } from "./use-input-history";
export interface Key {
name?: string;
ctrl?: boolean;
meta?: boolean;
shift?: boolean;
paste?: boolean;
sequence?: string;
upArrow?: boolean;
downArrow?: boolean;
leftArrow?: boolean;
rightArrow?: boolean;
return?: boolean;
escape?: boolean;
tab?: boolean;
backspace?: boolean;
delete?: boolean;
}
export interface EnhancedInputHook {
input: string;
cursorPosition: number;
isMultiline: boolean;
setInput: (text: string) => void;
setCursorPosition: (position: number) => void;
clearInput: () => void;
insertAtCursor: (text: string) => void;
resetHistory: () => void;
handleInput: (inputChar: string, key: Key) => void;
}
interface UseEnhancedInputProps {
onSubmit?: (text: string) => void;
onEscape?: () => void;
onSpecialKey?: (key: Key) => boolean; // Return true to prevent default handling
disabled?: boolean;
multiline?: boolean;
}
export function useEnhancedInput({
onSubmit,
onEscape,
onSpecialKey,
disabled = false,
multiline = false,
}: UseEnhancedInputProps = {}): EnhancedInputHook {
const [input, setInputState] = useState("");
const [cursorPosition, setCursorPositionState] = useState(0);
const isMultilineRef = useRef(multiline);
const {
addToHistory,
navigateHistory,
resetHistory,
setOriginalInput,
isNavigatingHistory,
} = useInputHistory();
const setInput = useCallback((text: string) => {
setInputState(text);
setCursorPositionState(Math.min(text.length, cursorPosition));
if (!isNavigatingHistory()) {
setOriginalInput(text);
}
}, [cursorPosition, isNavigatingHistory, setOriginalInput]);
const setCursorPosition = useCallback((position: number) => {
setCursorPositionState(Math.max(0, Math.min(input.length, position)));
}, [input.length]);
const clearInput = useCallback(() => {
setInputState("");
setCursorPositionState(0);
setOriginalInput("");
}, [setOriginalInput]);
const insertAtCursor = useCallback((text: string) => {
const result = insertText(input, cursorPosition, text);
setInputState(result.text);
setCursorPositionState(result.position);
setOriginalInput(result.text);
}, [input, cursorPosition, setOriginalInput]);
const handleSubmit = useCallback(() => {
if (input.trim()) {
addToHistory(input);
onSubmit?.(input);
clearInput();
}
}, [input, addToHistory, onSubmit, clearInput]);
const handleInput = useCallback((inputChar: string, key: Key) => {
if (disabled) return;
// Handle Ctrl+C - check multiple ways it could be detected
if ((key.ctrl && inputChar === "c") || inputChar === "\x03") {
setInputState("");
setCursorPositionState(0);
setOriginalInput("");
return;
}
// Allow special key handler to override default behavior
if (onSpecialKey?.(key)) {
return;
}
// Handle Escape
if (key.escape) {
onEscape?.();
return;
}
// Handle Enter/Return
if (key.return) {
if (multiline && key.shift) {
// Shift+Enter in multiline mode inserts newline
const result = insertText(input, cursorPosition, "\n");
setInputState(result.text);
setCursorPositionState(result.position);
setOriginalInput(result.text);
} else {
handleSubmit();
}
return;
}
// Handle history navigation
if ((key.upArrow || key.name === 'up') && !key.ctrl && !key.meta) {
const historyInput = navigateHistory("up");
if (historyInput !== null) {
setInputState(historyInput);
setCursorPositionState(historyInput.length);
}
return;
}
if ((key.downArrow || key.name === 'down') && !key.ctrl && !key.meta) {
const historyInput = navigateHistory("down");
if (historyInput !== null) {
setInputState(historyInput);
setCursorPositionState(historyInput.length);
}
return;
}
// Handle cursor movement - ignore meta flag for arrows as it's unreliable in terminals
// Only do word movement if ctrl is pressed AND no arrow escape sequence is in inputChar
if ((key.leftArrow || key.name === 'left') && key.ctrl && !inputChar.includes('[')) {
const newPos = moveToPreviousWord(input, cursorPosition);
setCursorPositionState(newPos);
return;
}
if ((key.rightArrow || key.name === 'right') && key.ctrl && !inputChar.includes('[')) {
const newPos = moveToNextWord(input, cursorPosition);
setCursorPositionState(newPos);
return;
}
// Handle regular cursor movement - single character (ignore meta flag)
if (key.leftArrow || key.name === 'left') {
const newPos = Math.max(0, cursorPosition - 1);
setCursorPositionState(newPos);
return;
}
if (key.rightArrow || key.name === 'right') {
const newPos = Math.min(input.length, cursorPosition + 1);
setCursorPositionState(newPos);
return;
}
// Handle Home/End keys or Ctrl+A/E
if ((key.ctrl && inputChar === "a") || key.name === "home") {
setCursorPositionState(0); // Simple start of input
return;
}
if ((key.ctrl && inputChar === "e") || key.name === "end") {
setCursorPositionState(input.length); // Simple end of input
return;
}
// Handle deletion - check multiple ways backspace might be detected
// Backspace can be detected in different ways depending on terminal
// In some terminals, backspace shows up as delete:true with empty inputChar
const isBackspace = key.backspace ||
key.name === 'backspace' ||
inputChar === '\b' ||
inputChar === '\x7f' ||
(key.delete && inputChar === '' && !key.shift);
if (isBackspace) {
if (key.ctrl || key.meta) {
// Ctrl/Cmd + Backspace: Delete word before cursor
const result = deleteWordBefore(input, cursorPosition);
setInputState(result.text);
setCursorPositionState(result.position);
setOriginalInput(result.text);
} else {
// Regular backspace
const result = deleteCharBefore(input, cursorPosition);
setInputState(result.text);
setCursorPositionState(result.position);
setOriginalInput(result.text);
}
return;
}
// Handle forward delete (Del key) - but not if it was already handled as backspace above
if ((key.delete && inputChar !== '') || (key.ctrl && inputChar === "d")) {
if (key.ctrl || key.meta) {
// Ctrl/Cmd + Delete: Delete word after cursor
const result = deleteWordAfter(input, cursorPosition);
setInputState(result.text);
setCursorPositionState(result.position);
setOriginalInput(result.text);
} else {
// Regular delete
const result = deleteCharAfter(input, cursorPosition);
setInputState(result.text);
setCursorPositionState(result.position);
setOriginalInput(result.text);
}
return;
}
// Handle Ctrl+K: Delete from cursor to end of line
if (key.ctrl && inputChar === "k") {
const lineEnd = moveToLineEnd(input, cursorPosition);
const newText = input.slice(0, cursorPosition) + input.slice(lineEnd);
setInputState(newText);
setOriginalInput(newText);
return;
}
// Handle Ctrl+U: Delete from cursor to start of line
if (key.ctrl && inputChar === "u") {
const lineStart = moveToLineStart(input, cursorPosition);
const newText = input.slice(0, lineStart) + input.slice(cursorPosition);
setInputState(newText);
setCursorPositionState(lineStart);
setOriginalInput(newText);
return;
}
// Handle Ctrl+W: Delete word before cursor
if (key.ctrl && inputChar === "w") {
const result = deleteWordBefore(input, cursorPosition);
setInputState(result.text);
setCursorPositionState(result.position);
setOriginalInput(result.text);
return;
}
// Handle Ctrl+X: Clear entire input
if (key.ctrl && inputChar === "x") {
setInputState("");
setCursorPositionState(0);
setOriginalInput("");
return;
}
// Handle regular character input
if (inputChar && !key.ctrl && !key.meta) {
const result = insertText(input, cursorPosition, inputChar);
setInputState(result.text);
setCursorPositionState(result.position);
setOriginalInput(result.text);
}
}, [disabled, onSpecialKey, input, cursorPosition, multiline, handleSubmit, navigateHistory, setOriginalInput]);
return {
input,
cursorPosition,
isMultiline: isMultilineRef.current,
setInput,
setCursorPosition,
clearInput,
insertAtCursor,
resetHistory,
handleInput,
};
}
+184 -190
View File
@@ -1,7 +1,8 @@
import { useState, useMemo } from "react";
import { useState, useMemo, useEffect } from "react";
import { useInput } from "ink";
import { GrokAgent, ChatEntry } from "../agent/grok-agent";
import { ConfirmationService } from "../utils/confirmation-service";
import { useEnhancedInput, Key } from "./use-enhanced-input";
import { filterCommandSuggestions } from "../ui/components/command-suggestions";
import { loadModelConfig, updateCurrentModel } from "../utils/model-config";
@@ -42,8 +43,6 @@ export function useInputHandler({
isStreaming,
isConfirmationActive = false,
}: UseInputHandlerProps) {
const [input, setInput] = useState("");
const [cursorPosition, setCursorPosition] = useState(0);
const [showCommandSuggestions, setShowCommandSuggestions] = useState(false);
const [selectedCommandIndex, setSelectedCommandIndex] = useState(0);
const [showModelSelection, setShowModelSelection] = useState(false);
@@ -53,7 +52,172 @@ export function useInputHandler({
const sessionFlags = confirmationService.getSessionFlags();
return sessionFlags.allOperations;
});
// Removed useApp().exit - using process.exit(0) instead for better terminal handling
const handleSpecialKey = (key: Key): boolean => {
// 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: ChatEntry = {
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: string) => {
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: string) => {
// 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: string, key: Key) => {
handleInput(inputChar, key);
});
// Update command suggestions when input changes
useEffect(() => {
handleInputChange(input);
}, [input]);
const commandSuggestions: CommandSuggestion[] = [
{ command: "/help", description: "Show help information" },
@@ -86,8 +250,8 @@ export function useInputHandler({
const confirmationService = ConfirmationService.getInstance();
confirmationService.resetSession();
setInput("");
setCursorPosition(0);
clearInput();
resetHistory();
return true;
}
@@ -106,7 +270,14 @@ Built-in Commands:
Git Commands:
/commit-and-push - AI-generated commit + push to remote
Keyboard Shortcuts:
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):
@@ -128,8 +299,7 @@ Examples:
timestamp: new Date(),
};
setChatHistory((prev) => [...prev, helpEntry]);
setInput("");
setCursorPosition(0);
clearInput();
return true;
}
@@ -141,8 +311,7 @@ Examples:
if (trimmedInput === "/models") {
setShowModelSelection(true);
setSelectedModelIndex(0);
setInput("");
setCursorPosition(0);
clearInput();
return true;
}
@@ -170,8 +339,7 @@ Available models: ${modelNames.join(", ")}`,
setChatHistory((prev) => [...prev, errorEntry]);
}
setInput("");
setCursorPosition(0);
clearInput();
return true;
}
@@ -375,8 +543,7 @@ Respond with ONLY the commit message, no additional text.`;
setIsProcessing(false);
setIsStreaming(false);
setInput("");
setCursorPosition(0);
clearInput();
return true;
}
@@ -433,8 +600,7 @@ Respond with ONLY the commit message, no additional text.`;
setChatHistory((prev) => [...prev, errorEntry]);
}
setInput("");
setCursorPosition(0);
clearInput();
return true;
}
@@ -450,8 +616,7 @@ Respond with ONLY the commit message, no additional text.`;
setChatHistory((prev) => [...prev, userEntry]);
setIsProcessing(true);
setInput("");
setCursorPosition(0);
clearInput();
try {
setIsStreaming(true);
@@ -571,177 +736,6 @@ Respond with ONLY the commit message, no additional text.`;
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") {
process.exit(0);
return;
}
// 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;
}
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 = 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];
const newInput = selectedCommand.command + " ";
setInput(newInput);
setCursorPosition(newInput.length);
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);
updateCurrentModel(selectedModel.model); // Update project current 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") {
process.exit(0);
return;
}
if (userInput) {
const directCommandResult = await handleDirectCommand(userInput);
if (!directCommandResult) {
await processUserMessage(userInput);
}
}
return;
}
// Handle left and right arrow keys for cursor movement
if (key.leftArrow && !showCommandSuggestions && !showModelSelection) {
setCursorPosition(prev => Math.max(0, prev - 1));
return;
}
if (key.rightArrow && !showCommandSuggestions && !showModelSelection) {
setCursorPosition(prev => Math.min(input.length, prev + 1));
return;
}
if (key.backspace || key.delete) {
if (cursorPosition > 0) {
const newInput = input.slice(0, cursorPosition - 1) + input.slice(cursorPosition);
setInput(newInput);
setCursorPosition(prev => prev - 1);
if (!newInput.startsWith("/")) {
setShowCommandSuggestions(false);
setSelectedCommandIndex(0);
}
}
return;
}
if (inputChar && !key.ctrl && !key.meta) {
const newInput = input.slice(0, cursorPosition) + inputChar + input.slice(cursorPosition);
setInput(newInput);
setCursorPosition(prev => prev + 1);
if (newInput.startsWith("/")) {
setShowCommandSuggestions(true);
setSelectedCommandIndex(0);
} else {
setShowCommandSuggestions(false);
setSelectedCommandIndex(0);
}
}
});
return {
input,
+75
View File
@@ -0,0 +1,75 @@
import { useState, useCallback } from "react";
export interface InputHistoryHook {
addToHistory: (input: string) => void;
navigateHistory: (direction: "up" | "down") => string | null;
getCurrentHistoryIndex: () => number;
resetHistory: () => void;
isNavigatingHistory: () => boolean;
setOriginalInput: (input: string) => void;
}
export function useInputHistory(): InputHistoryHook {
const [history, setHistory] = useState<string[]>([]);
const [currentIndex, setCurrentIndex] = useState(-1);
const [originalInput, setOriginalInput] = useState("");
const addToHistory = useCallback((input: string) => {
if (input.trim() && !history.includes(input.trim())) {
setHistory(prev => [...prev, input.trim()]);
}
setCurrentIndex(-1);
setOriginalInput("");
}, [history]);
const navigateHistory = useCallback((direction: "up" | "down"): string | null => {
if (history.length === 0) return null;
let newIndex: number;
if (direction === "up") {
if (currentIndex === -1) {
newIndex = history.length - 1;
} else {
newIndex = Math.max(0, currentIndex - 1);
}
} else {
if (currentIndex === -1) {
return null;
} else if (currentIndex === history.length - 1) {
newIndex = -1;
return originalInput;
} else {
newIndex = Math.min(history.length - 1, currentIndex + 1);
}
}
setCurrentIndex(newIndex);
return newIndex === -1 ? originalInput : history[newIndex];
}, [history, currentIndex, originalInput]);
const getCurrentHistoryIndex = useCallback(() => currentIndex, [currentIndex]);
const resetHistory = useCallback(() => {
setHistory([]);
setCurrentIndex(-1);
setOriginalInput("");
}, []);
const isNavigatingHistory = useCallback(() => currentIndex !== -1, [currentIndex]);
const setOriginalInputCallback = useCallback((input: string) => {
if (currentIndex === -1) {
setOriginalInput(input);
}
}, [currentIndex]);
return {
addToHistory,
navigateHistory,
getCurrentHistoryIndex,
resetHistory,
isNavigatingHistory,
setOriginalInput: setOriginalInputCallback,
};
}
+8 -11
View File
@@ -14,20 +14,17 @@ import type { ChatCompletionMessageParam } from "openai/resources/chat";
// Load environment variables
dotenv.config();
// Add proper signal handling for terminal cleanup
process.on("SIGINT", () => {
// Restore terminal to normal mode before exit
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
console.log("\nGracefully shutting down...");
process.exit(0);
});
// Disable default SIGINT handling to let Ink handle Ctrl+C
// We'll handle exit through the input system instead
process.on("SIGTERM", () => {
// Restore terminal to normal mode before exit
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
if (process.stdin.isTTY && process.stdin.setRawMode) {
try {
process.stdin.setRawMode(false);
} catch (e) {
// Ignore errors when setting raw mode
}
}
console.log("\nGracefully shutting down...");
process.exit(0);
+5 -2
View File
@@ -156,12 +156,15 @@ export class MCPManager extends EventEmitter {
const { loadMCPConfig } = await import('../mcp/config');
const config = loadMCPConfig();
for (const serverConfig of config.servers) {
// Initialize servers in parallel to avoid blocking
const initPromises = config.servers.map(async (serverConfig) => {
try {
await this.addServer(serverConfig);
} catch (error) {
console.warn(`Failed to initialize MCP server ${serverConfig.name}:`, error);
}
}
});
await Promise.all(initPromises);
}
}
+110 -8
View File
@@ -16,15 +16,117 @@ export function ChatInput({
}: ChatInputProps) {
const beforeCursor = input.slice(0, cursorPosition);
const afterCursor = input.slice(cursorPosition);
// Handle multiline input display
const lines = input.split("\n");
const isMultiline = lines.length > 1;
// Calculate cursor position across lines
let currentLineIndex = 0;
let currentCharIndex = 0;
let totalChars = 0;
for (let i = 0; i < lines.length; i++) {
if (totalChars + lines[i].length >= cursorPosition) {
currentLineIndex = i;
currentCharIndex = cursorPosition - totalChars;
break;
}
totalChars += lines[i].length + 1; // +1 for newline
}
const showCursor = !isProcessing && !isStreaming;
const borderColor = isProcessing || isStreaming ? "yellow" : "blue";
const promptColor = "cyan";
// Display placeholder when input is empty
const placeholderText = "Ask me anything...";
const isPlaceholder = !input;
if (isMultiline) {
return (
<Box
borderStyle="round"
borderColor={borderColor}
paddingY={0}
marginTop={1}
>
{lines.map((line, index) => {
const isCurrentLine = index === currentLineIndex;
const promptChar = index === 0 ? "❯" : "│";
if (isCurrentLine) {
const beforeCursorInLine = line.slice(0, currentCharIndex);
const cursorChar =
line.slice(currentCharIndex, currentCharIndex + 1) || " ";
const afterCursorInLine = line.slice(currentCharIndex + 1);
return (
<Box key={index}>
<Text color={promptColor}>{promptChar} </Text>
<Text>
{beforeCursorInLine}
{showCursor && (
<Text backgroundColor="white" color="black">
{cursorChar}
</Text>
)}
{!showCursor && cursorChar !== " " && cursorChar}
{afterCursorInLine}
</Text>
</Box>
);
} else {
return (
<Box key={index}>
<Text color={promptColor}>{promptChar} </Text>
<Text>{line}</Text>
</Box>
);
}
})}
</Box>
);
}
// Single line input box
const cursorChar = input.slice(cursorPosition, cursorPosition + 1) || " ";
const afterCursorText = input.slice(cursorPosition + 1);
return (
<Box borderStyle="round" borderColor="gray" paddingX={1} marginTop={1}>
<Text color="gray">❯ </Text>
<Text>
{beforeCursor}
{!isProcessing && !isStreaming && <Text color="white">█</Text>}
{afterCursor}
</Text>
<Box
borderStyle="round"
borderColor={borderColor}
paddingX={1}
paddingY={0}
marginTop={1}
>
<Box>
<Text color={promptColor}>❯ </Text>
{isPlaceholder ? (
<>
<Text color="gray" dimColor>
{placeholderText}
</Text>
{showCursor && (
<Text backgroundColor="white" color="black">
{" "}
</Text>
)}
</>
) : (
<Text>
{beforeCursor}
{showCursor && (
<Text backgroundColor="white" color="black">
{cursorChar}
</Text>
)}
{!showCursor && cursorChar !== " " && cursorChar}
{afterCursorText}
</Text>
)}
</Box>
</Box>
);
}
+10 -10
View File
@@ -177,8 +177,8 @@ function ChatInterfaceWithAgent({ agent }: { agent: GrokAgent }) {
)}
<Box flexDirection="column" marginBottom={1}>
<Text dimColor>
Type your request in natural language. Type 'exit' or Ctrl+C to quit.
<Text color="gray">
Type your request in natural language. Ctrl+C to clear, 'exit' to quit.
</Text>
</Box>
@@ -216,15 +216,15 @@ function ChatInterfaceWithAgent({ agent }: { agent: GrokAgent }) {
isStreaming={isStreaming}
/>
<Box flexDirection="row">
<Text color="cyan">
{autoEditEnabled ? "▶" : "⏸"} auto-edit:{" "}
{autoEditEnabled ? "on" : "off"}
</Text>
<Box marginLeft={1}>
<Text dimColor>(shift + tab)</Text>
<Box flexDirection="row" marginTop={1}>
<Box marginRight={2}>
<Text color="cyan">
{autoEditEnabled ? "▶" : "⏸"} auto-edit:{" "}
{autoEditEnabled ? "on" : "off"}
</Text>
<Text color="gray" dimColor> (shift + tab)</Text>
</Box>
<Box marginLeft={2}>
<Box marginRight={2}>
<Text color="yellow">≋ {agent.getCurrentModel()}</Text>
</Box>
<MCPStatus />
+221
View File
@@ -0,0 +1,221 @@
/**
* Text manipulation utilities for terminal input handling
* Inspired by Gemini CLI's text processing capabilities
*/
export interface TextPosition {
index: number;
line: number;
column: number;
}
export interface TextSelection {
start: number;
end: number;
}
/**
* Check if a character is a word boundary
*/
export function isWordBoundary(char: string | undefined): boolean {
if (!char) return true;
return /\s/.test(char) || /[^\w]/.test(char);
}
/**
* Find the start of the current word at the given position
*/
export function findWordStart(text: string, position: number): number {
if (position <= 0) return 0;
let pos = position - 1;
while (pos > 0 && !isWordBoundary(text[pos])) {
pos--;
}
// If we stopped at a word boundary, move forward to the actual word start
if (pos > 0 && isWordBoundary(text[pos])) {
pos++;
}
return pos;
}
/**
* Find the end of the current word at the given position
*/
export function findWordEnd(text: string, position: number): number {
if (position >= text.length) return text.length;
let pos = position;
while (pos < text.length && !isWordBoundary(text[pos])) {
pos++;
}
return pos;
}
/**
* Move cursor to the previous word boundary
*/
export function moveToPreviousWord(text: string, position: number): number {
if (position <= 0) return 0;
let pos = position - 1;
// Skip whitespace
while (pos > 0 && isWordBoundary(text[pos])) {
pos--;
}
// Find start of the word
while (pos > 0 && !isWordBoundary(text[pos - 1])) {
pos--;
}
return pos;
}
/**
* Move cursor to the next word boundary
*/
export function moveToNextWord(text: string, position: number): number {
if (position >= text.length) return text.length;
let pos = position;
// Skip current word
while (pos < text.length && !isWordBoundary(text[pos])) {
pos++;
}
// Skip whitespace
while (pos < text.length && isWordBoundary(text[pos])) {
pos++;
}
return pos;
}
/**
* Delete the word before the cursor
*/
export function deleteWordBefore(text: string, position: number): { text: string; position: number } {
const wordStart = moveToPreviousWord(text, position);
const newText = text.slice(0, wordStart) + text.slice(position);
return {
text: newText,
position: wordStart,
};
}
/**
* Delete the word after the cursor
*/
export function deleteWordAfter(text: string, position: number): { text: string; position: number } {
const wordEnd = moveToNextWord(text, position);
const newText = text.slice(0, position) + text.slice(wordEnd);
return {
text: newText,
position,
};
}
/**
* Get the current line and column from text position
*/
export function getTextPosition(text: string, index: number): TextPosition {
const lines = text.slice(0, index).split('\n');
return {
index,
line: lines.length - 1,
column: lines[lines.length - 1].length,
};
}
/**
* Move to the beginning of the current line
*/
export function moveToLineStart(text: string, position: number): number {
const beforeCursor = text.slice(0, position);
const lastNewlineIndex = beforeCursor.lastIndexOf('\n');
return lastNewlineIndex === -1 ? 0 : lastNewlineIndex + 1;
}
/**
* Move to the end of the current line
*/
export function moveToLineEnd(text: string, position: number): number {
const afterCursor = text.slice(position);
const nextNewlineIndex = afterCursor.indexOf('\n');
return nextNewlineIndex === -1 ? text.length : position + nextNewlineIndex;
}
/**
* Handle proper Unicode-aware character deletion
*/
export function deleteCharBefore(text: string, position: number): { text: string; position: number } {
if (position <= 0) {
return { text, position };
}
// Handle surrogate pairs and combining characters
let deleteCount = 1;
const charBefore = text.charAt(position - 1);
// Check for high surrogate (first part of surrogate pair)
if (position >= 2) {
const charBeforeBefore = text.charAt(position - 2);
if (charBeforeBefore >= '\uD800' && charBeforeBefore <= '\uDBFF' &&
charBefore >= '\uDC00' && charBefore <= '\uDFFF') {
deleteCount = 2;
}
}
const newText = text.slice(0, position - deleteCount) + text.slice(position);
return {
text: newText,
position: position - deleteCount,
};
}
/**
* Handle proper Unicode-aware character deletion forward
*/
export function deleteCharAfter(text: string, position: number): { text: string; position: number } {
if (position >= text.length) {
return { text, position };
}
// Handle surrogate pairs and combining characters
let deleteCount = 1;
const charAfter = text.charAt(position);
// Check for high surrogate (first part of surrogate pair)
if (position + 1 < text.length) {
const charAfterAfter = text.charAt(position + 1);
if (charAfter >= '\uD800' && charAfter <= '\uDBFF' &&
charAfterAfter >= '\uDC00' && charAfterAfter <= '\uDFFF') {
deleteCount = 2;
}
}
const newText = text.slice(0, position) + text.slice(position + deleteCount);
return {
text: newText,
position,
};
}
/**
* Insert text at the given position with proper Unicode handling
*/
export function insertText(text: string, position: number, insert: string): { text: string; position: number } {
const newText = text.slice(0, position) + insert + text.slice(position);
return {
text: newText,
position: position + insert.length,
};
}