diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 374a13c6b5..3527372983 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -310,22 +310,16 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { const hookIcon = activeHook?.eventName?.startsWith('After') ? '↩' : '↪'; return ( - - - - - - - + + + {showWit && uiState.currentWittyPhrase && ( - - - {uiState.currentWittyPhrase} :) - - + + {uiState.currentWittyPhrase} + )} ); @@ -353,7 +347,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { * Renders the minimal metadata row content shown when UI details are hidden. */ const renderMinimalMetaRowContent = () => ( - + {showMinimalInlineLoading && ( { /> )} {hasUserHooks && ( - - - - - - - + + + )} {showMinimalBleedThroughRow && ( - + {miniMode_ShowApprovalMode && modeContentObj && ( ● {modeContentObj.text} )} - {/* {zenMode_ShowToast && ( - - - - )} */} )} diff --git a/packages/cli/src/ui/components/HookStatusDisplay.test.tsx b/packages/cli/src/ui/components/HookStatusDisplay.test.tsx index e2f39c301c..4fd7dda210 100644 --- a/packages/cli/src/ui/components/HookStatusDisplay.test.tsx +++ b/packages/cli/src/ui/components/HookStatusDisplay.test.tsx @@ -78,4 +78,14 @@ describe('', () => { expect(lastFrame()).toContain('Working...'); unmount(); }); + + it('matches SVG snapshot for single hook', async () => { + const props = { + activeHooks: [{ name: 'test-hook', eventName: 'BeforeAgent', source: 'user' }], + }; + const renderResult = render(); + await renderResult.waitUntilReady(); + await expect(renderResult).toMatchSvgSnapshot(); + renderResult.unmount(); + }); }); diff --git a/packages/cli/src/ui/components/HookStatusDisplay.tsx b/packages/cli/src/ui/components/HookStatusDisplay.tsx index 8a464b9149..c049198e4a 100644 --- a/packages/cli/src/ui/components/HookStatusDisplay.tsx +++ b/packages/cli/src/ui/components/HookStatusDisplay.tsx @@ -8,6 +8,7 @@ import type React from 'react'; import { Text } from 'ink'; import { type ActiveHook } from '../types.js'; import { GENERIC_WORKING_LABEL } from '../textConstants.js'; +import { theme } from '../semantic-colors.js'; interface HookStatusDisplayProps { activeHooks: ActiveHook[]; @@ -38,9 +39,17 @@ export const HookStatusDisplay: React.FC = ({ }); const text = `${label}: ${displayNames.join(', ')}`; - return {text}; + return ( + + {text} + + ); } // If only system/extension hooks are running, show a generic message. - return {GENERIC_WORKING_LABEL}; + return ( + + {GENERIC_WORKING_LABEL} + + ); }; diff --git a/packages/cli/src/ui/components/__snapshots__/HookStatusDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/HookStatusDisplay.test.tsx.snap index 458728736e..5e04b96cb8 100644 --- a/packages/cli/src/ui/components/__snapshots__/HookStatusDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/HookStatusDisplay.test.tsx.snap @@ -1,5 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[` > matches SVG snapshot for single hook 1`] = `"Executing Hook: test-hook"`; + exports[` > should render a single executing hook 1`] = ` "Executing Hook: test-hook " diff --git a/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx b/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx index 4f5f5fb419..1b1f70affa 100644 --- a/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx +++ b/packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx @@ -211,4 +211,72 @@ describe('useLoadingIndicator', () => { expect(result.current.elapsedTime).toBe(0); expect(result.current.currentLoadingPhrase).toBeUndefined(); }); + + it('should reflect retry status in currentLoadingPhrase when provided', () => { + const retryStatus = { + model: 'gemini-pro', + attempt: 2, + maxAttempts: 3, + delayMs: 1000, + }; + const { result } = renderLoadingIndicatorHook( + StreamingState.Responding, + false, + retryStatus, + ); + + expect(result.current.currentLoadingPhrase).toContain('Trying to reach'); + expect(result.current.currentLoadingPhrase).toContain('Attempt 3/3'); + }); + + it('should hide low-verbosity retry status for early retry attempts', () => { + const retryStatus = { + model: 'gemini-pro', + attempt: 1, + maxAttempts: 5, + delayMs: 1000, + }; + const { result } = renderLoadingIndicatorHook( + StreamingState.Responding, + false, + retryStatus, + 'all', + 'low', + ); + + expect(result.current.currentLoadingPhrase).not.toBe( + "This is taking a bit longer, we're still on it.", + ); + }); + + it('should show a generic retry phrase in low error verbosity mode for later retries', () => { + const retryStatus = { + model: 'gemini-pro', + attempt: 2, + maxAttempts: 5, + delayMs: 1000, + }; + const { result } = renderLoadingIndicatorHook( + StreamingState.Responding, + false, + retryStatus, + 'all', + 'low', + ); + + expect(result.current.currentLoadingPhrase).toBe( + "This is taking a bit longer, we're still on it.", + ); + }); + + it('should show no phrases when loadingPhrasesMode is "off"', () => { + const { result } = renderLoadingIndicatorHook( + StreamingState.Responding, + false, + null, + 'off', + ); + + expect(result.current.currentLoadingPhrase).toBeUndefined(); + }); });