summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
authorAllen Hutchison <[email protected]>2025-05-07 12:30:32 -0700
committerGitHub <[email protected]>2025-05-07 12:30:32 -0700
commit6b3ef9f93986a6c5f892b5f011b25b6826e522a1 (patch)
tree3af7fda7863a72a27185b48630dad2e3526337d9 /packages/cli/src
parent46490263123bf855c40da35ef8be62f2b9b134b2 (diff)
Refactor: Enhance @-command, Autocomplete, and Input Stability (#279)
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/ui/App.tsx29
-rw-r--r--packages/cli/src/ui/components/InputPrompt.tsx136
-rw-r--r--packages/cli/src/ui/hooks/atCommandProcessor.ts12
-rw-r--r--packages/cli/src/ui/hooks/useInputHistory.ts10
-rw-r--r--packages/cli/src/ui/utils/commandUtils.ts4
5 files changed, 103 insertions, 88 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index e14cea62..1c1ec424 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -96,17 +96,8 @@ export const App = ({ config, settings, cliVersion }: AppProps) => {
const isInputActive = streamingState === StreamingState.Idle && !initError;
- const {
- query,
- setQuery,
- handleSubmit: handleHistorySubmit,
- inputKey,
- setInputKey,
- } = useInputHistory({
- userMessages,
- onSubmit: handleFinalSubmit,
- isActive: isInputActive,
- });
+ // query and setQuery are now managed by useState here
+ const [query, setQuery] = useState('');
const completion = useCompletion(
query,
@@ -115,6 +106,22 @@ export const App = ({ config, settings, cliVersion }: AppProps) => {
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 ---
const { staticallyRenderedHistoryItems, updatableHistoryItems } =
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index a7f06bce..20d4bcdf 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -39,93 +39,83 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
}) => {
const { isFocused } = useFocus({ autoFocus: true });
- const handleAutocomplete = useCallback(() => {
- if (
- activeSuggestionIndex < 0 ||
- activeSuggestionIndex >= suggestions.length
- ) {
- return;
- }
- const selectedSuggestion = suggestions[activeSuggestionIndex];
- const trimmedQuery = query.trimStart();
+ const handleAutocomplete = useCallback(
+ (indexToUse: number) => {
+ if (indexToUse < 0 || indexToUse >= suggestions.length) {
+ return;
+ }
+ const selectedSuggestion = suggestions[indexToUse];
+ const trimmedQuery = query.trimStart();
- if (trimmedQuery.startsWith('/')) {
- // Handle / command completion
- const slashIndex = query.indexOf('/');
- const base = query.substring(0, slashIndex + 1);
- const newValue = base + selectedSuggestion.value;
- setQuery(newValue);
- } else {
- // Handle @ command completion
- const atIndex = query.lastIndexOf('@');
- if (atIndex === -1) return;
+ if (trimmedQuery.startsWith('/')) {
+ // Handle / command completion
+ const slashIndex = query.indexOf('/');
+ const base = query.substring(0, slashIndex + 1);
+ const newValue = base + selectedSuggestion.value;
+ setQuery(newValue);
+ } else {
+ // Handle @ command completion
+ const atIndex = query.lastIndexOf('@');
+ if (atIndex === -1) return;
- // Find the part of the query after the '@'
- const pathPart = query.substring(atIndex + 1);
- // Find the last slash within that part
- const lastSlashIndexInPath = pathPart.lastIndexOf('/');
+ // Find the part of the query after the '@'
+ const pathPart = query.substring(atIndex + 1);
+ // Find the last slash within that part
+ const lastSlashIndexInPath = pathPart.lastIndexOf('/');
- let base = '';
- if (lastSlashIndexInPath === -1) {
- // No slash after '@', replace everything after '@'
- base = query.substring(0, atIndex + 1);
- } else {
- // Slash found, keep everything up to and including the last slash
- base = query.substring(0, atIndex + 1 + lastSlashIndexInPath + 1);
- }
+ let base = '';
+ if (lastSlashIndexInPath === -1) {
+ // No slash after '@', replace everything after '@'
+ base = query.substring(0, atIndex + 1);
+ } else {
+ // Slash found, keep everything up to and including the last slash
+ base = query.substring(0, atIndex + 1 + lastSlashIndexInPath + 1);
+ }
- const newValue = base + selectedSuggestion.value;
- setQuery(newValue);
- }
+ const newValue = base + selectedSuggestion.value;
+ setQuery(newValue);
+ }
- resetCompletion(); // Hide suggestions after selection
- setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset
- }, [
- query,
- setQuery,
- suggestions,
- activeSuggestionIndex,
- resetCompletion,
- setInputKey,
- ]);
+ resetCompletion(); // Hide suggestions after selection
+ setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset
+ },
+ [query, setQuery, suggestions, resetCompletion, setInputKey],
+ );
useInput(
(input: string, key: Key) => {
- let handled = false;
+ if (!isFocused) {
+ return;
+ }
if (showSuggestions) {
if (key.upArrow) {
navigateUp();
- handled = true;
} else if (key.downArrow) {
navigateDown();
- handled = true;
- } else if ((key.tab || key.return) && activeSuggestionIndex >= 0) {
- handleAutocomplete();
- handled = true;
+ } else if (key.tab) {
+ if (suggestions.length > 0) {
+ const targetIndex =
+ activeSuggestionIndex === -1 ? 0 : activeSuggestionIndex;
+ if (targetIndex < suggestions.length) {
+ handleAutocomplete(targetIndex);
+ }
+ }
+ } else if (key.return) {
+ if (activeSuggestionIndex >= 0) {
+ handleAutocomplete(activeSuggestionIndex);
+ } else {
+ if (query.trim()) {
+ onSubmit(query);
+ }
+ }
} else if (key.escape) {
resetCompletion();
- handled = true;
}
}
-
- // Only submit on Enter if it wasn't handled above
- if (!handled && key.return) {
- if (query.trim()) {
- onSubmit(query);
- }
- handled = true;
- }
-
- if (
- 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 (
@@ -138,7 +128,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
onChange={setQuery}
placeholder="Enter your message or use tools (e.g., @src/file.txt)..."
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>
diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts
index 2d93d1cb..50e81589 100644
--- a/packages/cli/src/ui/hooks/atCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts
@@ -106,16 +106,25 @@ export async function handleAtCommand({
// Add the original user query to history first
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 '@'
+ // 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) {
addItem(
- { type: 'error', text: 'Error: No path specified after @.' },
+ { type: 'error', text: 'Error: No valid path specified after @ symbol.' },
userMessageTimestamp,
);
return { processedQuery: null, shouldProceed: false };
}
+ const contentLabel = pathPart;
const toolRegistry = config.getToolRegistry();
const readManyFilesTool = toolRegistry.getTool('read_many_files');
@@ -129,7 +138,6 @@ export async function handleAtCommand({
// Determine path spec (file or directory glob)
let pathSpec = pathPart;
- const contentLabel = pathPart;
try {
const absolutePath = path.resolve(config.getTargetDir(), pathPart);
const stats = await fs.stat(absolutePath);
diff --git a/packages/cli/src/ui/hooks/useInputHistory.ts b/packages/cli/src/ui/hooks/useInputHistory.ts
index 21d7b9bf..f8c873f1 100644
--- a/packages/cli/src/ui/hooks/useInputHistory.ts
+++ b/packages/cli/src/ui/hooks/useInputHistory.ts
@@ -4,13 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { useState, useCallback } from 'react';
+import { useCallback, useState } from 'react';
import { useInput } from 'ink';
interface UseInputHistoryProps {
userMessages: readonly string[];
onSubmit: (value: string) => void;
isActive: boolean;
+ query: string;
+ setQuery: React.Dispatch<React.SetStateAction<string>>;
}
interface UseInputHistoryReturn {
@@ -25,8 +27,9 @@ export function useInputHistory({
userMessages,
onSubmit,
isActive,
+ query,
+ setQuery,
}: UseInputHistoryProps): UseInputHistoryReturn {
- const [query, setQuery] = useState('');
const [historyIndex, setHistoryIndex] = useState<number>(-1);
const [originalQueryBeforeNav, setOriginalQueryBeforeNav] =
useState<string>('');
@@ -41,9 +44,8 @@ export function useInputHistory({
(value: string) => {
const trimmedValue = value.trim();
if (trimmedValue) {
- onSubmit(trimmedValue);
+ onSubmit(trimmedValue); // This will call handleFinalSubmit, which then calls setQuery('') from App.tsx
}
- setQuery('');
resetHistoryNav();
},
[onSubmit, resetHistoryNav],
diff --git a/packages/cli/src/ui/utils/commandUtils.ts b/packages/cli/src/ui/utils/commandUtils.ts
index bcae7b6c..b17b264d 100644
--- a/packages/cli/src/ui/utils/commandUtils.ts
+++ b/packages/cli/src/ui/utils/commandUtils.ts
@@ -13,8 +13,8 @@
* @returns True if the query looks like an '@' command, false otherwise.
*/
export const isAtCommand = (query: string): boolean =>
- // Check if starts with @ OR has a space, then @, then a non-space character.
- query.startsWith('@') || /\s@\S/.test(query);
+ // Check if starts with @ OR has a space, then @
+ query.startsWith('@') || /\s@/.test(query);
/**
* Checks if a query string potentially represents an '/' command.