diff options
Diffstat (limited to 'packages/cli/src/ui/hooks')
| -rw-r--r-- | packages/cli/src/ui/hooks/atCommandProcessor.ts | 32 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useCompletion.ts | 86 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useInputHistory.ts | 63 |
3 files changed, 85 insertions, 96 deletions
diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts index a075157d..4b583d6c 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts @@ -12,7 +12,6 @@ import { ToolCallStatus, } from '../types.js'; -// Helper function to add history items const addHistoryItem = ( setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>, itemData: Omit<HistoryItem, 'id'>, @@ -25,7 +24,7 @@ const addHistoryItem = ( }; interface HandleAtCommandParams { - query: string; // Raw user input, potentially containing '@' + query: string; config: Config; setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>; setDebugMessage: React.Dispatch<React.SetStateAction<string>>; @@ -34,8 +33,8 @@ interface HandleAtCommandParams { } interface HandleAtCommandResult { - processedQuery: PartListUnion | null; // Query for Gemini (null on error/no-proceed) - shouldProceed: boolean; // Whether the main hook should continue processing + processedQuery: PartListUnion | null; + shouldProceed: boolean; } /** @@ -57,9 +56,7 @@ export async function handleAtCommand({ }: HandleAtCommandParams): Promise<HandleAtCommandResult> { const trimmedQuery = query.trim(); - // Regex to find the first occurrence of @ followed by non-whitespace chars - // It captures the text before, the @path itself (including @), and the text after. - const atCommandRegex = /^(.*?)(@\S+)(.*)$/s; // s flag for dot to match newline + const atCommandRegex = /^(.*?)(@\S+)(.*)$/s; const match = trimmedQuery.match(atCommandRegex); if (!match) { @@ -75,20 +72,18 @@ export async function handleAtCommand({ } const textBefore = match[1].trim(); - const atPath = match[2]; // Includes the '@' + const atPath = match[2]; const textAfter = match[3].trim(); - const pathPart = atPath.substring(1); // Remove the leading '@' + const pathPart = atPath.substring(1); - // Add user message for the full original @ command addHistoryItem( setHistory, - { type: 'user', text: query }, // Use original full query for history + { type: 'user', text: query }, userMessageTimestamp, ); if (!pathPart) { - // Handle case where it's just "@" or "@ " - treat as error/don't proceed const errorTimestamp = getNextMessageId(userMessageTimestamp); addHistoryItem( setHistory, @@ -108,18 +103,18 @@ export async function handleAtCommand({ { type: 'error', text: 'Error: read_many_files tool not found.' }, errorTimestamp, ); - return { processedQuery: null, shouldProceed: false }; // Don't proceed if tool is missing + return { processedQuery: null, shouldProceed: false }; } // --- Path Handling for @ command --- - let pathSpec = pathPart; // Use the extracted path part + let pathSpec = pathPart; // Basic check: If no extension or ends with '/', assume directory and add globstar. if (!pathPart.includes('.') || pathPart.endsWith('/')) { pathSpec = pathPart.endsWith('/') ? `${pathPart}**` : `${pathPart}/**`; } const toolArgs = { paths: [pathSpec] }; const contentLabel = - pathSpec === pathPart ? pathPart : `directory ${pathPart}`; // Adjust label + pathSpec === pathPart ? pathPart : `directory ${pathPart}`; // --- End Path Handling --- let toolCallDisplay: IndividualToolCallDisplay; @@ -129,7 +124,6 @@ export async function handleAtCommand({ const result = await readManyFilesTool.execute(toolArgs); const fileContent = result.llmContent || ''; - // Construct success UI toolCallDisplay = { callId: `client-read-${userMessageTimestamp}`, name: readManyFilesTool.displayName, @@ -153,7 +147,6 @@ export async function handleAtCommand({ const processedQuery: PartListUnion = processedQueryParts; - // Add the tool group UI const toolGroupId = getNextMessageId(userMessageTimestamp); addHistoryItem( setHistory, @@ -164,7 +157,7 @@ export async function handleAtCommand({ toolGroupId, ); - return { processedQuery, shouldProceed: true }; // Proceed to Gemini + return { processedQuery, shouldProceed: true }; } catch (error) { // Construct error UI toolCallDisplay = { @@ -176,7 +169,6 @@ export async function handleAtCommand({ confirmationDetails: undefined, }; - // Add the tool group UI and signal not to proceed const toolGroupId = getNextMessageId(userMessageTimestamp); addHistoryItem( setHistory, @@ -187,6 +179,6 @@ export async function handleAtCommand({ toolGroupId, ); - return { processedQuery: null, shouldProceed: false }; // Don't proceed on error + return { processedQuery: null, shouldProceed: false }; } } diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts index 92841028..36aed0d1 100644 --- a/packages/cli/src/ui/hooks/useCompletion.ts +++ b/packages/cli/src/ui/hooks/useCompletion.ts @@ -8,8 +8,7 @@ import { useState, useEffect, useCallback } from 'react'; import * as fs from 'fs/promises'; import * as path from 'path'; import { isNodeError } from '@gemini-code/server'; - -const MAX_SUGGESTIONS_TO_SHOW = 8; +import { MAX_SUGGESTIONS_TO_SHOW } from '../components/SuggestionsDisplay.js'; export interface UseCompletionReturn { suggestions: string[]; @@ -45,51 +44,64 @@ export function useCompletion( setIsLoadingSuggestions(false); }, []); - // --- Navigation Logic --- const navigateUp = useCallback(() => { if (suggestions.length === 0) return; - setActiveSuggestionIndex((prevIndex) => { - const newIndex = prevIndex <= 0 ? suggestions.length - 1 : prevIndex - 1; + setActiveSuggestionIndex((prevActiveIndex) => { + // Calculate new active index, handling wrap-around + const newActiveIndex = + prevActiveIndex <= 0 ? suggestions.length - 1 : prevActiveIndex - 1; - // Adjust visible window if needed (scrolling up) - if (newIndex < visibleStartIndex) { - setVisibleStartIndex(newIndex); - } else if ( - newIndex === suggestions.length - 1 && - suggestions.length > MAX_SUGGESTIONS_TO_SHOW - ) { - // Handle wrapping from first to last item - setVisibleStartIndex( - Math.max(0, suggestions.length - MAX_SUGGESTIONS_TO_SHOW), - ); - } + // Adjust scroll position based on the new active index + setVisibleStartIndex((prevVisibleStart) => { + // Case 1: Wrapped around to the last item + if ( + newActiveIndex === suggestions.length - 1 && + suggestions.length > MAX_SUGGESTIONS_TO_SHOW + ) { + return Math.max(0, suggestions.length - MAX_SUGGESTIONS_TO_SHOW); + } + // Case 2: Scrolled above the current visible window + if (newActiveIndex < prevVisibleStart) { + return newActiveIndex; + } + // Otherwise, keep the current scroll position + return prevVisibleStart; + }); - return newIndex; + return newActiveIndex; }); - }, [suggestions.length, visibleStartIndex]); + }, [suggestions.length]); const navigateDown = useCallback(() => { if (suggestions.length === 0) return; - setActiveSuggestionIndex((prevIndex) => { - const newIndex = prevIndex >= suggestions.length - 1 ? 0 : prevIndex + 1; + setActiveSuggestionIndex((prevActiveIndex) => { + // Calculate new active index, handling wrap-around + const newActiveIndex = + prevActiveIndex >= suggestions.length - 1 ? 0 : prevActiveIndex + 1; - // Adjust visible window if needed (scrolling down) - if (newIndex >= visibleStartIndex + MAX_SUGGESTIONS_TO_SHOW) { - setVisibleStartIndex(visibleStartIndex + 1); - } else if ( - newIndex === 0 && - suggestions.length > MAX_SUGGESTIONS_TO_SHOW - ) { - // Handle wrapping from last to first item - setVisibleStartIndex(0); - } + // Adjust scroll position based on the new active index + setVisibleStartIndex((prevVisibleStart) => { + // Case 1: Wrapped around to the first item + if ( + newActiveIndex === 0 && + suggestions.length > MAX_SUGGESTIONS_TO_SHOW + ) { + return 0; + } + // Case 2: Scrolled below the current visible window + const visibleEndIndex = prevVisibleStart + MAX_SUGGESTIONS_TO_SHOW; + if (newActiveIndex >= visibleEndIndex) { + return newActiveIndex - MAX_SUGGESTIONS_TO_SHOW + 1; + } + // Otherwise, keep the current scroll position + return prevVisibleStart; + }); - return newIndex; + return newActiveIndex; }); - }, [suggestions.length, visibleStartIndex]); - // --- End Navigation Logic --- + }, [suggestions.length]); useEffect(() => { if (!isActive) { @@ -137,8 +149,8 @@ export function useCompletion( if (isMounted) { setSuggestions(filteredSuggestions); setShowSuggestions(filteredSuggestions.length > 0); - setActiveSuggestionIndex(-1); // Reset selection on new suggestions - setVisibleStartIndex(0); // Reset scroll on new suggestions + setActiveSuggestionIndex(-1); + setVisibleStartIndex(0); } } catch (error) { if (isNodeError(error) && error.code === 'ENOENT') { @@ -162,13 +174,11 @@ export function useCompletion( } }; - // Debounce the fetch slightly const debounceTimeout = setTimeout(fetchSuggestions, 100); return () => { isMounted = false; clearTimeout(debounceTimeout); - // Don't reset loading state here, let the next effect handle it or resetCompletionState }; }, [query, cwd, isActive, resetCompletionState]); diff --git a/packages/cli/src/ui/hooks/useInputHistory.ts b/packages/cli/src/ui/hooks/useInputHistory.ts index 9a6aaacb..21d7b9bf 100644 --- a/packages/cli/src/ui/hooks/useInputHistory.ts +++ b/packages/cli/src/ui/hooks/useInputHistory.ts @@ -7,19 +7,18 @@ import { useState, useCallback } from 'react'; import { useInput } from 'ink'; -// Props for the hook interface UseInputHistoryProps { - userMessages: readonly string[]; // History of user messages - onSubmit: (value: string) => void; // Original submit function from App - isActive: boolean; // To enable/disable the useInput hook + userMessages: readonly string[]; + onSubmit: (value: string) => void; + isActive: boolean; } -// Return type of the hook interface UseInputHistoryReturn { - query: string; // The current input query managed by the hook - setQuery: React.Dispatch<React.SetStateAction<string>>; // Setter for the query - handleSubmit: (value: string) => void; // Wrapped submit handler - inputKey: number; // Key to force input reset + query: string; + setQuery: React.Dispatch<React.SetStateAction<string>>; + handleSubmit: (value: string) => void; + inputKey: number; + setInputKey: React.Dispatch<React.SetStateAction<number>>; } export function useInputHistory({ @@ -27,36 +26,31 @@ export function useInputHistory({ onSubmit, isActive, }: UseInputHistoryProps): UseInputHistoryReturn { - const [query, setQuery] = useState(''); // Hook manages its own query state - const [historyIndex, setHistoryIndex] = useState<number>(-1); // -1 means current query + const [query, setQuery] = useState(''); + const [historyIndex, setHistoryIndex] = useState<number>(-1); const [originalQueryBeforeNav, setOriginalQueryBeforeNav] = useState<string>(''); - const [inputKey, setInputKey] = useState<number>(0); // Key for forcing input reset + const [inputKey, setInputKey] = useState<number>(0); - // Function to reset navigation state, called on submit or manual reset const resetHistoryNav = useCallback(() => { setHistoryIndex(-1); setOriginalQueryBeforeNav(''); }, []); - // Wrapper for the onSubmit prop to include resetting history navigation const handleSubmit = useCallback( (value: string) => { const trimmedValue = value.trim(); if (trimmedValue) { - // Only submit non-empty values - onSubmit(trimmedValue); // Call the original submit function + onSubmit(trimmedValue); } - setQuery(''); // Clear the input field managed by this hook - resetHistoryNav(); // Reset history state - // Don't increment inputKey here, only on nav changes + setQuery(''); + resetHistoryNav(); }, [onSubmit, resetHistoryNav], ); useInput( (input, key) => { - // Do nothing if the hook is not active if (!isActive) { return; } @@ -68,58 +62,51 @@ export function useInputHistory({ let nextIndex = historyIndex; if (historyIndex === -1) { - // Starting navigation UP, save current input setOriginalQueryBeforeNav(query); - nextIndex = 0; // Go to the most recent item (index 0 in reversed view) + nextIndex = 0; } else if (historyIndex < userMessages.length - 1) { - // Continue navigating UP (towards older items) nextIndex = historyIndex + 1; } else { - return; // Already at the oldest item + return; } if (nextIndex !== historyIndex) { setHistoryIndex(nextIndex); - // History is ordered newest to oldest, so access from the end const newValue = userMessages[userMessages.length - 1 - nextIndex]; setQuery(newValue); - setInputKey((k) => k + 1); // Increment key on navigation change + setInputKey((k) => k + 1); didNavigate = true; } } else if (key.downArrow) { - if (historyIndex === -1) return; // Already at the bottom (current input) + if (historyIndex === -1) return; - const nextIndex = historyIndex - 1; // Move towards more recent items / current input + const nextIndex = historyIndex - 1; setHistoryIndex(nextIndex); if (nextIndex === -1) { - // Restore original query setQuery(originalQueryBeforeNav); } else { - // Set query based on reversed index const newValue = userMessages[userMessages.length - 1 - nextIndex]; setQuery(newValue); } - setInputKey((k) => k + 1); // Increment key on navigation change + setInputKey((k) => k + 1); didNavigate = true; } else { - // If user types anything other than arrows while navigating, reset history navigation state if (historyIndex !== -1 && !didNavigate) { - // Check if it's a key that modifies input content if (input || key.backspace || key.delete) { resetHistoryNav(); - // The actual query state update for typing is handled by the component's onChange calling setQuery } } } }, - { isActive }, // Pass isActive to useInput + { isActive }, ); return { query, - setQuery, // Return the hook's setQuery - handleSubmit, // Return the wrapped submit handler - inputKey, // Return the key + setQuery, + handleSubmit, + inputKey, + setInputKey, }; } |
