minor tweaks
This commit is contained in:
1 parent
d07d5f0691
commit
ff827e2eb4
10 files changed
+940
-247
No files matched your search
+25
-25
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user