Refactor: Enhance @-command, Autocomplete, and Input Stability (#279)
This commit is contained in:
parent
4649026312
commit
6b3ef9f939
|
@ -96,17 +96,8 @@ export const App = ({ config, settings, cliVersion }: AppProps) => {
|
||||||
|
|
||||||
const isInputActive = streamingState === StreamingState.Idle && !initError;
|
const isInputActive = streamingState === StreamingState.Idle && !initError;
|
||||||
|
|
||||||
const {
|
// query and setQuery are now managed by useState here
|
||||||
query,
|
const [query, setQuery] = useState('');
|
||||||
setQuery,
|
|
||||||
handleSubmit: handleHistorySubmit,
|
|
||||||
inputKey,
|
|
||||||
setInputKey,
|
|
||||||
} = useInputHistory({
|
|
||||||
userMessages,
|
|
||||||
onSubmit: handleFinalSubmit,
|
|
||||||
isActive: isInputActive,
|
|
||||||
});
|
|
||||||
|
|
||||||
const completion = useCompletion(
|
const completion = useCompletion(
|
||||||
query,
|
query,
|
||||||
|
@ -115,6 +106,22 @@ export const App = ({ config, settings, cliVersion }: AppProps) => {
|
||||||
slashCommands,
|
slashCommands,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit: handleHistorySubmit,
|
||||||
|
inputKey,
|
||||||
|
setInputKey,
|
||||||
|
} = useInputHistory({
|
||||||
|
userMessages,
|
||||||
|
onSubmit: (value) => {
|
||||||
|
// Adapt onSubmit to use the lifted setQuery
|
||||||
|
handleFinalSubmit(value);
|
||||||
|
setQuery(''); // Clear query from the App's state
|
||||||
|
},
|
||||||
|
isActive: isInputActive && !completion.showSuggestions,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
});
|
||||||
|
|
||||||
// --- Render Logic ---
|
// --- Render Logic ---
|
||||||
|
|
||||||
const { staticallyRenderedHistoryItems, updatableHistoryItems } =
|
const { staticallyRenderedHistoryItems, updatableHistoryItems } =
|
||||||
|
|
|
@ -39,14 +39,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { isFocused } = useFocus({ autoFocus: true });
|
const { isFocused } = useFocus({ autoFocus: true });
|
||||||
|
|
||||||
const handleAutocomplete = useCallback(() => {
|
const handleAutocomplete = useCallback(
|
||||||
if (
|
(indexToUse: number) => {
|
||||||
activeSuggestionIndex < 0 ||
|
if (indexToUse < 0 || indexToUse >= suggestions.length) {
|
||||||
activeSuggestionIndex >= suggestions.length
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedSuggestion = suggestions[activeSuggestionIndex];
|
const selectedSuggestion = suggestions[indexToUse];
|
||||||
const trimmedQuery = query.trimStart();
|
const trimmedQuery = query.trimStart();
|
||||||
|
|
||||||
if (trimmedQuery.startsWith('/')) {
|
if (trimmedQuery.startsWith('/')) {
|
||||||
|
@ -80,52 +78,44 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
|
|
||||||
resetCompletion(); // Hide suggestions after selection
|
resetCompletion(); // Hide suggestions after selection
|
||||||
setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset
|
setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset
|
||||||
}, [
|
},
|
||||||
query,
|
[query, setQuery, suggestions, resetCompletion, setInputKey],
|
||||||
setQuery,
|
);
|
||||||
suggestions,
|
|
||||||
activeSuggestionIndex,
|
|
||||||
resetCompletion,
|
|
||||||
setInputKey,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useInput(
|
useInput(
|
||||||
(input: string, key: Key) => {
|
(input: string, key: Key) => {
|
||||||
let handled = false;
|
if (!isFocused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (showSuggestions) {
|
if (showSuggestions) {
|
||||||
if (key.upArrow) {
|
if (key.upArrow) {
|
||||||
navigateUp();
|
navigateUp();
|
||||||
handled = true;
|
|
||||||
} else if (key.downArrow) {
|
} else if (key.downArrow) {
|
||||||
navigateDown();
|
navigateDown();
|
||||||
handled = true;
|
} else if (key.tab) {
|
||||||
} else if ((key.tab || key.return) && activeSuggestionIndex >= 0) {
|
if (suggestions.length > 0) {
|
||||||
handleAutocomplete();
|
const targetIndex =
|
||||||
handled = true;
|
activeSuggestionIndex === -1 ? 0 : activeSuggestionIndex;
|
||||||
} else if (key.escape) {
|
if (targetIndex < suggestions.length) {
|
||||||
resetCompletion();
|
handleAutocomplete(targetIndex);
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (key.return) {
|
||||||
// Only submit on Enter if it wasn't handled above
|
if (activeSuggestionIndex >= 0) {
|
||||||
if (!handled && key.return) {
|
handleAutocomplete(activeSuggestionIndex);
|
||||||
|
} else {
|
||||||
if (query.trim()) {
|
if (query.trim()) {
|
||||||
onSubmit(query);
|
onSubmit(query);
|
||||||
}
|
}
|
||||||
handled = true;
|
|
||||||
}
|
}
|
||||||
|
} else if (key.escape) {
|
||||||
if (
|
resetCompletion();
|
||||||
handled &&
|
|
||||||
showSuggestions &&
|
|
||||||
(key.upArrow || key.downArrow || key.tab || key.escape || key.return)
|
|
||||||
) {
|
|
||||||
// No explicit preventDefault needed, handled flag stops further processing
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Enter key when suggestions are NOT showing is handled by TextInput's onSubmit prop below
|
||||||
},
|
},
|
||||||
{ isActive: isFocused },
|
{ isActive: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -138,7 +128,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
onChange={setQuery}
|
onChange={setQuery}
|
||||||
placeholder="Enter your message or use tools (e.g., @src/file.txt)..."
|
placeholder="Enter your message or use tools (e.g., @src/file.txt)..."
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
/* onSubmit is handled by useInput hook above */
|
// This onSubmit is for the TextInput component itself.
|
||||||
|
// It should only fire if suggestions are NOT showing,
|
||||||
|
// as useInput handles Enter when suggestions are visible.
|
||||||
|
const trimmedQuery = query.trim();
|
||||||
|
if (!showSuggestions && trimmedQuery) {
|
||||||
|
onSubmit(trimmedQuery);
|
||||||
|
}
|
||||||
|
// If suggestions ARE showing, useInput's Enter handler
|
||||||
|
// would have already dealt with it (either completing or submitting).
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -106,16 +106,25 @@ export async function handleAtCommand({
|
||||||
// Add the original user query to history first
|
// Add the original user query to history first
|
||||||
addItem({ type: 'user', text: query }, userMessageTimestamp);
|
addItem({ type: 'user', text: query }, userMessageTimestamp);
|
||||||
|
|
||||||
|
// If the atPath is just "@", pass the original query to the LLM
|
||||||
|
if (atPath === '@') {
|
||||||
|
setDebugMessage('Lone @ detected, passing directly to LLM.');
|
||||||
|
return { processedQuery: [{ text: query }], shouldProceed: true };
|
||||||
|
}
|
||||||
|
|
||||||
const pathPart = atPath.substring(1); // Remove leading '@'
|
const pathPart = atPath.substring(1); // Remove leading '@'
|
||||||
|
|
||||||
|
// This error condition is for cases where pathPart becomes empty *after* the initial "@" check,
|
||||||
|
// which is unlikely with the current parser but good for robustness.
|
||||||
if (!pathPart) {
|
if (!pathPart) {
|
||||||
addItem(
|
addItem(
|
||||||
{ type: 'error', text: 'Error: No path specified after @.' },
|
{ type: 'error', text: 'Error: No valid path specified after @ symbol.' },
|
||||||
userMessageTimestamp,
|
userMessageTimestamp,
|
||||||
);
|
);
|
||||||
return { processedQuery: null, shouldProceed: false };
|
return { processedQuery: null, shouldProceed: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentLabel = pathPart;
|
||||||
const toolRegistry = config.getToolRegistry();
|
const toolRegistry = config.getToolRegistry();
|
||||||
const readManyFilesTool = toolRegistry.getTool('read_many_files');
|
const readManyFilesTool = toolRegistry.getTool('read_many_files');
|
||||||
|
|
||||||
|
@ -129,7 +138,6 @@ export async function handleAtCommand({
|
||||||
|
|
||||||
// Determine path spec (file or directory glob)
|
// Determine path spec (file or directory glob)
|
||||||
let pathSpec = pathPart;
|
let pathSpec = pathPart;
|
||||||
const contentLabel = pathPart;
|
|
||||||
try {
|
try {
|
||||||
const absolutePath = path.resolve(config.getTargetDir(), pathPart);
|
const absolutePath = path.resolve(config.getTargetDir(), pathPart);
|
||||||
const stats = await fs.stat(absolutePath);
|
const stats = await fs.stat(absolutePath);
|
||||||
|
|
|
@ -4,13 +4,15 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useInput } from 'ink';
|
import { useInput } from 'ink';
|
||||||
|
|
||||||
interface UseInputHistoryProps {
|
interface UseInputHistoryProps {
|
||||||
userMessages: readonly string[];
|
userMessages: readonly string[];
|
||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
query: string;
|
||||||
|
setQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseInputHistoryReturn {
|
interface UseInputHistoryReturn {
|
||||||
|
@ -25,8 +27,9 @@ export function useInputHistory({
|
||||||
userMessages,
|
userMessages,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isActive,
|
isActive,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
}: UseInputHistoryProps): UseInputHistoryReturn {
|
}: UseInputHistoryProps): UseInputHistoryReturn {
|
||||||
const [query, setQuery] = useState('');
|
|
||||||
const [historyIndex, setHistoryIndex] = useState<number>(-1);
|
const [historyIndex, setHistoryIndex] = useState<number>(-1);
|
||||||
const [originalQueryBeforeNav, setOriginalQueryBeforeNav] =
|
const [originalQueryBeforeNav, setOriginalQueryBeforeNav] =
|
||||||
useState<string>('');
|
useState<string>('');
|
||||||
|
@ -41,9 +44,8 @@ export function useInputHistory({
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
const trimmedValue = value.trim();
|
const trimmedValue = value.trim();
|
||||||
if (trimmedValue) {
|
if (trimmedValue) {
|
||||||
onSubmit(trimmedValue);
|
onSubmit(trimmedValue); // This will call handleFinalSubmit, which then calls setQuery('') from App.tsx
|
||||||
}
|
}
|
||||||
setQuery('');
|
|
||||||
resetHistoryNav();
|
resetHistoryNav();
|
||||||
},
|
},
|
||||||
[onSubmit, resetHistoryNav],
|
[onSubmit, resetHistoryNav],
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
* @returns True if the query looks like an '@' command, false otherwise.
|
* @returns True if the query looks like an '@' command, false otherwise.
|
||||||
*/
|
*/
|
||||||
export const isAtCommand = (query: string): boolean =>
|
export const isAtCommand = (query: string): boolean =>
|
||||||
// Check if starts with @ OR has a space, then @, then a non-space character.
|
// Check if starts with @ OR has a space, then @
|
||||||
query.startsWith('@') || /\s@\S/.test(query);
|
query.startsWith('@') || /\s@/.test(query);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a query string potentially represents an '/' command.
|
* Checks if a query string potentially represents an '/' command.
|
||||||
|
|
Loading…
Reference in New Issue