diff --git a/package-lock.json b/package-lock.json index f3bf8fa616..8ea54f5920 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17551,7 +17551,7 @@ "fzf": "^0.5.2", "glob": "^12.0.0", "highlight.js": "^11.11.1", - "ink": "npm:@jrichman/ink@6.5.0", + "ink": "npm:@jrichman/ink@6.6.2", "ink-gradient": "^3.0.0", "ink-spinner": "^5.0.0", "latest-version": "^9.0.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 072f2b8a72..04e55e783c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,7 +49,7 @@ "fzf": "^0.5.2", "glob": "^12.0.0", "highlight.js": "^11.11.1", - "ink": "npm:@jrichman/ink@6.5.0", + "ink": "npm:@jrichman/ink@6.6.2", "ink-gradient": "^3.0.0", "ink-spinner": "^5.0.0", "latest-version": "^9.0.0", diff --git a/packages/cli/src/test-utils/AppRig.tsx b/packages/cli/src/test-utils/AppRig.tsx index 65ce1aea3f..4a8334450e 100644 --- a/packages/cli/src/test-utils/AppRig.tsx +++ b/packages/cli/src/test-utils/AppRig.tsx @@ -732,8 +732,16 @@ export class AppRig { // eslint-disable-next-line @typescript-eslint/no-explicit-any const find = (node: any): boolean => { - if (node.internal_componentName === componentName) return true; - if (node.internal_testId === componentName) return true; + if ( + node.internal_componentName === componentName || + node.internal_testId === componentName || + node.attributes?.internal_testId === componentName || + node.attributes?.internal_componentName === componentName || + node.style?.internal_testId === componentName || + node.style?.internal_componentName === componentName + ) { + return true; + } if (node.childNodes) { for (const child of node.childNodes) { if (child.nodeName !== '#text' && find(child)) return true; @@ -746,7 +754,7 @@ export class AppRig { }, { timeout, - message: `Timed out waiting for component: ${componentName}`, + message: `Timed out waiting for component: ${componentName}\nLast frame:\n${this.lastFrame}`, }, ); } diff --git a/packages/cli/src/test-utils/customMatchers.ts b/packages/cli/src/test-utils/customMatchers.ts index 1bf240bdac..a918989297 100644 --- a/packages/cli/src/test-utils/customMatchers.ts +++ b/packages/cli/src/test-utils/customMatchers.ts @@ -34,7 +34,7 @@ function findInTree( ): DOMNode | undefined { if (predicate(node)) return node; if (node.nodeName !== '#text') { - for (const child of (node as _DOMElement).childNodes) { + for (const child of (node).childNodes) { const found = findInTree(child, predicate); if (found) return found; } @@ -66,7 +66,11 @@ export function toVisuallyContain( const el = node as any; const match = el.internal_componentName === componentName || - el.internal_testId === componentName; + el.internal_testId === componentName || + el.attributes?.internal_componentName === componentName || + el.attributes?.internal_testId === componentName || + el.style?.internal_componentName === componentName || + el.style?.internal_testId === componentName; return match; }) : false; @@ -107,11 +111,17 @@ export async function toMatchSvgSnapshot( let textContent: string; if (renderInstance.lastFrameRaw) { - textContent = renderInstance.lastFrameRaw({ - allowEmpty: options?.allowEmpty, - }); + textContent = + typeof renderInstance.lastFrameRaw === 'function' + ? renderInstance.lastFrameRaw({ + allowEmpty: options?.allowEmpty, + }) + : renderInstance.lastFrameRaw; } else if (renderInstance.lastFrame) { - textContent = renderInstance.lastFrame({ allowEmpty: options?.allowEmpty }); + textContent = + typeof renderInstance.lastFrame === 'function' + ? renderInstance.lastFrame({ allowEmpty: options?.allowEmpty }) + : renderInstance.lastFrame; } else { throw new Error( 'toMatchSvgSnapshot requires a renderInstance with either lastFrameRaw or lastFrame', diff --git a/packages/cli/src/test-utils/svg.ts b/packages/cli/src/test-utils/svg.ts index 49871a3e88..a37f520bf0 100644 --- a/packages/cli/src/test-utils/svg.ts +++ b/packages/cli/src/test-utils/svg.ts @@ -221,6 +221,14 @@ export const generateSvgForTerminal = ( if (node.internal_componentName) components.add(node.internal_componentName); if (node.internal_testId) components.add(node.internal_testId); + if (node.attributes?.internal_componentName) + components.add(node.attributes.internal_componentName); + if (node.attributes?.internal_testId) + components.add(node.attributes.internal_testId); + if (node.style?.internal_componentName) + components.add(node.style.internal_componentName); + if (node.style?.internal_testId) + components.add(node.style.internal_testId); if (node.childNodes) { for (const child of node.childNodes) collect(child); } diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.tsx index c17341faae..ffbb4eecb7 100644 --- a/packages/cli/src/ui/components/SuggestionsDisplay.tsx +++ b/packages/cli/src/ui/components/SuggestionsDisplay.tsx @@ -80,7 +80,13 @@ export function SuggestionsDisplay({ mode === 'slash' ? Math.min(maxLabelLength, Math.floor(width * 0.5)) : 0; return ( - + {scrollOffset > 0 && } {visibleSuggestions.map((suggestion, index) => { diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.ux.test.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.ux.test.tsx index bf0d8928d2..fed9d8c933 100644 --- a/packages/cli/src/ui/components/SuggestionsDisplay.ux.test.tsx +++ b/packages/cli/src/ui/components/SuggestionsDisplay.ux.test.tsx @@ -26,8 +26,10 @@ describe('SuggestionsDisplay UX Journey', () => { ); rig = new AppRig({ fakeResponsesPath }); await rig.initialize(); - rig.render(); + await rig.render(); await rig.waitForIdle(); + // Allow async command loading to settle + await new Promise((resolve) => setTimeout(resolve, 500)); }); afterEach(async () => { diff --git a/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay-ux-suggestions-opened.snap.svg b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay-ux-suggestions-opened.snap.svg index a23e362b46..332fc99eb9 100644 --- a/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay-ux-suggestions-opened.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay-ux-suggestions-opened.snap.svg @@ -4,38 +4,38 @@ - - - - Gemini CLI vtest-version - - - - - - - Authenticated with gemini-api-key /auth - - + + + + Gemini CLI vtest-version + + + + + + + Authenticated with gemini-api-key /auth + + Tips for getting started: 1. Create GEMINI.md files to customize your interactions 2. /help for more information 3. Ask coding questions, edit code or run commands 4. Be specific for the best results - ──────────────────────────────────────────────────────────── - Shift+Tab to accept edits + ──────────────────────────────────────────────────────────── + Shift+Tab to accept edits ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ > / ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - about Show version info - agents Manage agents - auth Manage authentication - bug Submit a bug report - chat Browse auto-saved conversations and ma… - clear Clear the screen and conversation hist… - commands Manage custom slash commands. Usage: /… - compress Compresses the context by replacing it… + compress Compresses the context by replacing it… + directory Manage workspace directories + footer Configure which items appear in the fo… + quit Exit the cli + stats Check session stats. Usage: /stats [se… + tasks Toggle background tasks view + about Show version info + agents Manage agents (1/38) workspace (/directory) sandbox /model diff --git a/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.ux.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.ux.test.tsx.snap index fa3827b4f5..d9b18b256a 100644 --- a/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.ux.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.ux.test.tsx.snap @@ -2,10 +2,10 @@ exports[`SuggestionsDisplay UX Journey > should visually show the suggestions display when / is typed 1`] = ` " - ▝▜▄ Gemini CLI vtest-version - ▝▜▄ - ▗▟▀ Authenticated with gemini-api-key /auth - ▝▀ + ▝▜▄ Gemini CLI vtest-version + ▝▜▄ + ▗▟▀ Authenticated with gemini-api-key /auth + ▝▀ Tips for getting started: @@ -15,21 +15,21 @@ Tips for getting started: 4. Be specific for the best results + ──────────────────────────────────────────────────────────── Shift+Tab to accept edits - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ > / ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + compress Compresses the context by replacing it… + directory Manage workspace directories + footer Configure which items appear in the fo… + quit Exit the cli + stats Check session stats. Usage: /stats [se… + tasks Toggle background tasks view about Show version info agents Manage agents - auth Manage authentication - bug Submit a bug report - chat Browse auto-saved conversations and ma… - clear Clear the screen and conversation hist… - commands Manage custom slash commands. Usage: /… - compress Compresses the context by replacing it… ▼ (1/38) workspace (/directory) sandbox /model diff --git a/packages/cli/src/ui/hooks/useReverseSearchCompletion.tsx b/packages/cli/src/ui/hooks/useReverseSearchCompletion.tsx index 289e51588c..1d20dc8ee8 100644 --- a/packages/cli/src/ui/hooks/useReverseSearchCompletion.tsx +++ b/packages/cli/src/ui/hooks/useReverseSearchCompletion.tsx @@ -9,13 +9,14 @@ import { useCompletion } from './useCompletion.js'; import type { TextBuffer } from '../components/shared/text-buffer.js'; import type { Suggestion } from '../components/SuggestionsDisplay.js'; -function useDebouncedValue(value: T, delay = 200): T { +function useDebouncedValue(value: T, delay = 200, enabled = true): T { const [debounced, setDebounced] = useState(value); useEffect(() => { + if (!enabled) return; const handle = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(handle); - }, [value, delay]); - return debounced; + }, [value, delay, enabled]); + return enabled ? debounced : value; } export interface UseReverseSearchCompletionReturn { @@ -48,7 +49,11 @@ export function useReverseSearchCompletion( setVisibleStartIndex, } = useCompletion(); - const debouncedQuery = useDebouncedValue(buffer.text, 100); + const debouncedQuery = useDebouncedValue( + buffer.text, + 100, + reverseSearchActive, + ); // incremental search const prevQueryRef = useRef(''); diff --git a/packages/cli/test-setup.ts b/packages/cli/test-setup.ts index 1f95bfbfeb..e73e8945b7 100644 --- a/packages/cli/test-setup.ts +++ b/packages/cli/test-setup.ts @@ -72,7 +72,9 @@ beforeEach(() => { if ( relevantStack.includes('OverflowContext.tsx') || - relevantStack.includes('useTimedMessage.ts') + relevantStack.includes('useTimedMessage.ts') || + relevantStack.includes('useSlashCompletion.ts') || + relevantStack.includes('slashCommandProcessor.ts') ) { return; }