buroventures-harald-code
Version:
Harald Code - AI-powered coding assistant CLI
377 lines • 17.8 kB
JavaScript
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