UNPKG

buroventures-harald-code

Version:

Harald Code - AI-powered coding assistant CLI

377 lines 17.8 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; /** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { useCallback, useEffect, useState } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { SuggestionsDisplay } from './SuggestionsDisplay.js'; import { useInputHistory } from '../hooks/useInputHistory.js'; import { logicalPosToOffset } from './shared/text-buffer.js'; import { cpSlice, cpLen } from '../utils/textUtils.js'; import chalk from 'chalk'; import stringWidth from 'string-width'; import { useShellHistory } from '../hooks/useShellHistory.js'; import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js'; import { useCommandCompletion } from '../hooks/useCommandCompletion.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { clipboardHasImage, saveClipboardImage, cleanupOldClipboardImages, } from '../utils/clipboardUtils.js'; import * as path from 'path'; export const InputPrompt = ({ buffer, onSubmit, userMessages, onClearScreen, config, slashCommands, commandContext, placeholder = ' Type your message or @path/to/file', focus = true, inputWidth, suggestionsWidth, shellModeActive, setShellModeActive, vimHandleInput, }) => { const [justNavigatedHistory, setJustNavigatedHistory] = useState(false); const [dirs, setDirs] = useState(config.getWorkspaceContext().getDirectories()); const dirsChanged = config.getWorkspaceContext().getDirectories(); useEffect(() => { if (dirs.length !== dirsChanged.length) { setDirs(dirsChanged); } }, [dirs.length, dirsChanged]); const [reverseSearchActive, setReverseSearchActive] = useState(false); const [textBeforeReverseSearch, setTextBeforeReverseSearch] = useState(''); const [cursorPosition, setCursorPosition] = useState([ 0, 0, ]); const shellHistory = useShellHistory(config.getProjectRoot()); const historyData = shellHistory.history; const completion = useCommandCompletion(buffer, dirs, config.getTargetDir(), slashCommands, commandContext, reverseSearchActive, config); const reverseSearchCompletion = useReverseSearchCompletion(buffer, historyData, reverseSearchActive); const resetCompletionState = completion.resetCompletionState; const resetReverseSearchCompletionState = reverseSearchCompletion.resetCompletionState; const handleSubmitAndClear = useCallback((submittedValue) => { if (shellModeActive) { shellHistory.addCommandToHistory(submittedValue); } // Clear the buffer *before* calling onSubmit to prevent potential re-submission // if onSubmit triggers a re-render while the buffer still holds the old value. buffer.setText(''); onSubmit(submittedValue); resetCompletionState(); resetReverseSearchCompletionState(); }, [ onSubmit, buffer, resetCompletionState, shellModeActive, shellHistory, resetReverseSearchCompletionState, ]); const customSetTextAndResetCompletionSignal = useCallback((newText) => { buffer.setText(newText); setJustNavigatedHistory(true); }, [buffer, setJustNavigatedHistory]); const inputHistory = useInputHistory({ userMessages, onSubmit: handleSubmitAndClear, isActive: (!completion.showSuggestions || completion.suggestions.length === 1) && !shellModeActive, currentQuery: buffer.text, onChange: customSetTextAndResetCompletionSignal, }); // Effect to reset completion if history navigation just occurred and set the text useEffect(() => { if (justNavigatedHistory) { resetCompletionState(); resetReverseSearchCompletionState(); setJustNavigatedHistory(false); } }, [ justNavigatedHistory, buffer.text, resetCompletionState, setJustNavigatedHistory, resetReverseSearchCompletionState, ]); // Handle clipboard image pasting with Ctrl+V const handleClipboardImage = useCallback(async () => { try { if (await clipboardHasImage()) { const imagePath = await saveClipboardImage(config.getTargetDir()); if (imagePath) { // Clean up old images cleanupOldClipboardImages(config.getTargetDir()).catch(() => { // Ignore cleanup errors }); // Get relative path from current directory const relativePath = path.relative(config.getTargetDir(), imagePath); // Insert @path reference at cursor position const insertText = `@${relativePath}`; const currentText = buffer.text; const [row, col] = buffer.cursor; // Calculate offset from row/col let offset = 0; for (let i = 0; i < row; i++) { offset += buffer.lines[i].length + 1; // +1 for newline } offset += col; // Add spaces around the path if needed let textToInsert = insertText; const charBefore = offset > 0 ? currentText[offset - 1] : ''; const charAfter = offset < currentText.length ? currentText[offset] : ''; if (charBefore && charBefore !== ' ' && charBefore !== '\n') { textToInsert = ' ' + textToInsert; } if (!charAfter || (charAfter !== ' ' && charAfter !== '\n')) { textToInsert = textToInsert + ' '; } // Insert at cursor position buffer.replaceRangeByOffset(offset, offset, textToInsert); } } } catch (error) { console.error('Error handling clipboard image:', error); } }, [buffer, config]); const handleInput = useCallback((key) => { /// We want to handle paste even when not focused to support drag and drop. if (!focus && !key.paste) { return; } if (vimHandleInput && vimHandleInput(key)) { return; } if (key.sequence === '!' && buffer.text === '' && !completion.showSuggestions) { setShellModeActive(!shellModeActive); buffer.setText(''); // Clear the '!' from input return; } if (key.name === 'escape') { if (reverseSearchActive) { setReverseSearchActive(false); reverseSearchCompletion.resetCompletionState(); buffer.setText(textBeforeReverseSearch); const offset = logicalPosToOffset(buffer.lines, cursorPosition[0], cursorPosition[1]); buffer.moveToOffset(offset); return; } if (shellModeActive) { setShellModeActive(false); return; } if (completion.showSuggestions) { completion.resetCompletionState(); return; } } if (shellModeActive && key.ctrl && key.name === 'r') { setReverseSearchActive(true); setTextBeforeReverseSearch(buffer.text); setCursorPosition(buffer.cursor); return; } if (key.ctrl && key.name === 'l') { onClearScreen(); return; } if (reverseSearchActive) { const { activeSuggestionIndex, navigateUp, navigateDown, showSuggestions, suggestions, } = reverseSearchCompletion; if (showSuggestions) { if (key.name === 'up') { navigateUp(); return; } if (key.name === 'down') { navigateDown(); return; } if (key.name === 'tab') { reverseSearchCompletion.handleAutocomplete(activeSuggestionIndex); reverseSearchCompletion.resetCompletionState(); setReverseSearchActive(false); return; } } if (key.name === 'return' && !key.ctrl) { const textToSubmit = showSuggestions && activeSuggestionIndex > -1 ? suggestions[activeSuggestionIndex].value : buffer.text; handleSubmitAndClear(textToSubmit); reverseSearchCompletion.resetCompletionState(); setReverseSearchActive(false); return; } // Prevent up/down from falling through to regular history navigation if (key.name === 'up' || key.name === 'down') { return; } } // If the command is a perfect match, pressing enter should execute it. if (completion.isPerfectMatch && key.name === 'return') { handleSubmitAndClear(buffer.text); return; } if (completion.showSuggestions) { if (completion.suggestions.length > 1) { if (key.name === 'up' || (key.ctrl && key.name === 'p')) { completion.navigateUp(); return; } if (key.name === 'down' || (key.ctrl && key.name === 'n')) { completion.navigateDown(); return; } } if (key.name === 'tab' || (key.name === 'return' && !key.ctrl)) { if (completion.suggestions.length > 0) { const targetIndex = completion.activeSuggestionIndex === -1 ? 0 // Default to the first if none is active : completion.activeSuggestionIndex; if (targetIndex < completion.suggestions.length) { completion.handleAutocomplete(targetIndex); } } return; } } if (!shellModeActive) { if (key.ctrl && key.name === 'p') { inputHistory.navigateUp(); return; } if (key.ctrl && key.name === 'n') { inputHistory.navigateDown(); return; } // Handle arrow-up/down for history on single-line or at edges if (key.name === 'up' && (buffer.allVisualLines.length === 1 || (buffer.visualCursor[0] === 0 && buffer.visualScrollRow === 0))) { inputHistory.navigateUp(); return; } if (key.name === 'down' && (buffer.allVisualLines.length === 1 || buffer.visualCursor[0] === buffer.allVisualLines.length - 1)) { inputHistory.navigateDown(); return; } } else { if (key.name === 'up') { const prevCommand = shellHistory.getPreviousCommand(); if (prevCommand !== null) buffer.setText(prevCommand); return; } if (key.name === 'down') { const nextCommand = shellHistory.getNextCommand(); if (nextCommand !== null) buffer.setText(nextCommand); return; } } if (key.name === 'return' && !key.ctrl && !key.meta && !key.paste) { if (buffer.text.trim()) { const [row, col] = buffer.cursor; const line = buffer.lines[row]; const charBefore = col > 0 ? cpSlice(line, col - 1, col) : ''; if (charBefore === '\\') { buffer.backspace(); buffer.newline(); } else { handleSubmitAndClear(buffer.text); } } return; } // Newline insertion if (key.name === 'return' && (key.ctrl || key.meta || key.paste)) { buffer.newline(); return; } // Ctrl+A (Home) / Ctrl+E (End) if (key.ctrl && key.name === 'a') { buffer.move('home'); return; } if (key.ctrl && key.name === 'e') { buffer.move('end'); buffer.moveToOffset(cpLen(buffer.text)); return; } // Ctrl+C (Clear input) if (key.ctrl && key.name === 'c') { if (buffer.text.length > 0) { buffer.setText(''); resetCompletionState(); return; } return; } // Kill line commands if (key.ctrl && key.name === 'k') { buffer.killLineRight(); return; } if (key.ctrl && key.name === 'u') { buffer.killLineLeft(); return; } // External editor const isCtrlX = key.ctrl && (key.name === 'x' || key.sequence === '\x18'); if (isCtrlX) { buffer.openInExternalEditor(); return; } // Ctrl+V for clipboard image paste if (key.ctrl && key.name === 'v') { handleClipboardImage(); return; } // Fall back to the text buffer's default input handling for all other keys buffer.handleInput(key); }, [ focus, buffer, completion, shellModeActive, setShellModeActive, onClearScreen, inputHistory, handleSubmitAndClear, shellHistory, reverseSearchCompletion, handleClipboardImage, resetCompletionState, vimHandleInput, reverseSearchActive, textBeforeReverseSearch, cursorPosition, ]); useKeypress(handleInput, { isActive: true }); const linesToRender = buffer.viewportVisualLines; const [cursorVisualRowAbsolute, cursorVisualColAbsolute] = buffer.visualCursor; const scrollVisualRow = buffer.visualScrollRow; return (_jsxs(_Fragment, { children: [_jsxs(Box, { borderStyle: "round", borderColor: shellModeActive ? Colors.AccentYellow : Colors.AccentBlue, paddingX: 1, children: [_jsx(Text, { color: shellModeActive ? Colors.AccentYellow : Colors.AccentPurple, children: shellModeActive ? (reverseSearchActive ? (_jsx(Text, { color: Colors.AccentCyan, children: "(r:) " })) : ('! ')) : ('> ') }), _jsx(Box, { flexGrow: 1, flexDirection: "column", children: buffer.text.length === 0 && placeholder ? (focus ? (_jsxs(Text, { children: [chalk.inverse(placeholder.slice(0, 1)), _jsx(Text, { color: Colors.Gray, children: placeholder.slice(1) })] })) : (_jsx(Text, { color: Colors.Gray, children: placeholder }))) : (linesToRender.map((lineText, visualIdxInRenderedSet) => { const cursorVisualRow = cursorVisualRowAbsolute - scrollVisualRow; let display = cpSlice(lineText, 0, inputWidth); const currentVisualWidth = stringWidth(display); if (currentVisualWidth < inputWidth) { display = display + ' '.repeat(inputWidth - currentVisualWidth); } if (focus && visualIdxInRenderedSet === cursorVisualRow) { const relativeVisualColForHighlight = cursorVisualColAbsolute; if (relativeVisualColForHighlight >= 0) { if (relativeVisualColForHighlight < cpLen(display)) { const charToHighlight = cpSlice(display, relativeVisualColForHighlight, relativeVisualColForHighlight + 1) || ' '; const highlighted = chalk.inverse(charToHighlight); display = cpSlice(display, 0, relativeVisualColForHighlight) + highlighted + cpSlice(display, relativeVisualColForHighlight + 1); } else if (relativeVisualColForHighlight === cpLen(display) && cpLen(display) === inputWidth) { display = display + chalk.inverse(' '); } } } return (_jsx(Text, { children: display }, `line-${visualIdxInRenderedSet}`)); })) })] }), completion.showSuggestions && (_jsx(Box, { children: _jsx(SuggestionsDisplay, { suggestions: completion.suggestions, activeIndex: completion.activeSuggestionIndex, isLoading: completion.isLoadingSuggestions, width: suggestionsWidth, scrollOffset: completion.visibleStartIndex, userInput: buffer.text }) })), reverseSearchActive && (_jsx(Box, { children: _jsx(SuggestionsDisplay, { suggestions: reverseSearchCompletion.suggestions, activeIndex: reverseSearchCompletion.activeSuggestionIndex, isLoading: reverseSearchCompletion.isLoadingSuggestions, width: suggestionsWidth, scrollOffset: reverseSearchCompletion.visibleStartIndex, userInput: buffer.text }) }))] })); }; //# sourceMappingURL=InputPrompt.js.map