feat(cli) - enhance input UX with double ESC clear (#4453)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
fuyou
2025-08-10 06:26:43 +08:00
committed by GitHub
parent 34434cd4aa
commit 0dea7233b6
3 changed files with 174 additions and 2 deletions
+65 -2
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { SuggestionsDisplay } from './SuggestionsDisplay.js';
@@ -41,6 +41,7 @@ export interface InputPromptProps {
suggestionsWidth: number;
shellModeActive: boolean;
setShellModeActive: (value: boolean) => void;
onEscapePromptChange?: (showPrompt: boolean) => void;
vimHandleInput?: (key: Key) => boolean;
}
@@ -58,9 +59,13 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
suggestionsWidth,
shellModeActive,
setShellModeActive,
onEscapePromptChange,
vimHandleInput,
}) => {
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
const [escPressCount, setEscPressCount] = useState(0);
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
const [dirs, setDirs] = useState<readonly string[]>(
config.getWorkspaceContext().getDirectories(),
@@ -98,6 +103,32 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const resetReverseSearchCompletionState =
reverseSearchCompletion.resetCompletionState;
const resetEscapeState = useCallback(() => {
if (escapeTimerRef.current) {
clearTimeout(escapeTimerRef.current);
escapeTimerRef.current = null;
}
setEscPressCount(0);
setShowEscapePrompt(false);
}, []);
// Notify parent component about escape prompt state changes
useEffect(() => {
if (onEscapePromptChange) {
onEscapePromptChange(showEscapePrompt);
}
}, [showEscapePrompt, onEscapePromptChange]);
// Clear escape prompt timer on unmount
useEffect(
() => () => {
if (escapeTimerRef.current) {
clearTimeout(escapeTimerRef.current);
}
},
[],
);
const handleSubmitAndClear = useCallback(
(submittedValue: string) => {
if (shellModeActive) {
@@ -212,6 +243,13 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return;
}
// Reset ESC count and hide prompt on any non-ESC key
if (key.name !== 'escape') {
if (escPressCount > 0 || showEscapePrompt) {
resetEscapeState();
}
}
if (
key.sequence === '!' &&
buffer.text === '' &&
@@ -237,13 +275,36 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
}
if (shellModeActive) {
setShellModeActive(false);
resetEscapeState();
return;
}
if (completion.showSuggestions) {
completion.resetCompletionState();
resetEscapeState();
return;
}
// Handle double ESC for clearing input
if (escPressCount === 0) {
if (buffer.text === '') {
return;
}
setEscPressCount(1);
setShowEscapePrompt(true);
if (escapeTimerRef.current) {
clearTimeout(escapeTimerRef.current);
}
escapeTimerRef.current = setTimeout(() => {
resetEscapeState();
}, 500);
} else {
// clear input and immediately reset state
buffer.setText('');
resetCompletionState();
resetEscapeState();
}
return;
}
if (shellModeActive && keyMatchers[Command.REVERSE_SEARCH](key)) {
@@ -418,7 +479,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
if (buffer.text.length > 0) {
buffer.setText('');
resetCompletionState();
return;
}
return;
}
@@ -461,6 +521,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
reverseSearchCompletion,
handleClipboardImage,
resetCompletionState,
escPressCount,
showEscapePrompt,
resetEscapeState,
vimHandleInput,
reverseSearchActive,
textBeforeReverseSearch,