diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 3cde63a6e8..3b1b2afa1f 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1108,6 +1108,7 @@ Logging in with Google... Restarting Gemini CLI to continue. toggleBackgroundShell, backgroundCurrentShell, backgroundShells, + isCursorHidden, dismissBackgroundShell, retryStatus, } = useGeminiStream( @@ -1177,6 +1178,7 @@ Logging in with Google... Restarting Gemini CLI to continue. pendingToolCalls, embeddedShellFocused, isInteractiveShellEnabled: config.isInteractiveShellEnabled(), + isCursorHidden, }); const shouldShowActionRequiredTitle = inactivityStatus === 'action_required'; @@ -2326,6 +2328,7 @@ Logging in with Google... Restarting Gemini CLI to continue. settingsNonce, backgroundShells, activeBackgroundShellPid, + isCursorHidden, backgroundShellHeight, isBackgroundShellListOpen, adminSettingsChanged, @@ -2454,6 +2457,7 @@ Logging in with Google... Restarting Gemini CLI to continue. isBackgroundShellListOpen, activeBackgroundShellPid, backgroundShells, + isCursorHidden, adminSettingsChanged, newAgents, showIsExpandableHint, diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 8447247e53..ce2723b3d7 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -217,6 +217,7 @@ export interface UIState { settingsNonce: number; backgroundShells: Map; activeBackgroundShellPid: number | null; + isCursorHidden?: boolean; backgroundShellHeight: number; isBackgroundShellListOpen: boolean; adminSettingsChanged: boolean; diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts index 3e67ad84b7..60850d5639 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts @@ -242,7 +242,12 @@ export const useShellCommandProcessor = ( // Subscribe to future updates (data only) const dataUnsubscribe = ShellExecutionService.subscribe(pid, (event) => { if (event.type === 'data') { - dispatch({ type: 'APPEND_SHELL_OUTPUT', pid, chunk: event.chunk }); + dispatch({ + type: 'APPEND_SHELL_OUTPUT', + pid, + chunk: event.chunk, + isCursorHidden: event.isCursorHidden, + }); } else if (event.type === 'binary_detected') { dispatch({ type: 'UPDATE_SHELL', pid, update: { isBinary: true } }); } else if (event.type === 'binary_progress') { @@ -381,6 +386,8 @@ export const useShellCommandProcessor = ( pid: executionPid, chunk: event.type === 'data' ? event.chunk : cumulativeStdout, + isCursorHidden: + event.type === 'data' ? event.isCursorHidden : undefined, }); return; } @@ -396,7 +403,12 @@ export const useShellCommandProcessor = ( } if (shouldUpdate) { - dispatch({ type: 'SET_OUTPUT_TIME', time: Date.now() }); + dispatch({ + type: 'SET_OUTPUT_TIME', + time: Date.now(), + isCursorHidden: + event.type === 'data' ? event.isCursorHidden : undefined, + }); setPendingHistoryItem((prevItem) => { if (prevItem?.type === 'tool_group') { return { @@ -550,5 +562,6 @@ export const useShellCommandProcessor = ( registerBackgroundShell, dismissBackgroundShell, backgroundShells: state.backgroundShells, + isCursorHidden: state.isCursorHidden, }; }; diff --git a/packages/cli/src/ui/hooks/shellReducer.ts b/packages/cli/src/ui/hooks/shellReducer.ts index 7d3917c681..a673dc1bc4 100644 --- a/packages/cli/src/ui/hooks/shellReducer.ts +++ b/packages/cli/src/ui/hooks/shellReducer.ts @@ -14,6 +14,7 @@ export interface BackgroundShell { binaryBytesReceived: number; status: 'running' | 'exited'; exitCode?: number; + isCursorHidden?: boolean; } export interface ShellState { @@ -21,11 +22,12 @@ export interface ShellState { lastShellOutputTime: number; backgroundShells: Map; isBackgroundShellVisible: boolean; + isCursorHidden?: boolean; } export type ShellAction = | { type: 'SET_ACTIVE_PTY'; pid: number | null } - | { type: 'SET_OUTPUT_TIME'; time: number } + | { type: 'SET_OUTPUT_TIME'; time: number; isCursorHidden?: boolean } | { type: 'SET_VISIBILITY'; visible: boolean } | { type: 'TOGGLE_VISIBILITY' } | { @@ -35,7 +37,12 @@ export type ShellAction = initialOutput: string | AnsiOutput; } | { type: 'UPDATE_SHELL'; pid: number; update: Partial } - | { type: 'APPEND_SHELL_OUTPUT'; pid: number; chunk: string | AnsiOutput } + | { + type: 'APPEND_SHELL_OUTPUT'; + pid: number; + chunk: string | AnsiOutput; + isCursorHidden?: boolean; + } | { type: 'SYNC_BACKGROUND_SHELLS' } | { type: 'DISMISS_SHELL'; pid: number }; @@ -54,7 +61,11 @@ export function shellReducer( case 'SET_ACTIVE_PTY': return { ...state, activeShellPtyId: action.pid }; case 'SET_OUTPUT_TIME': - return { ...state, lastShellOutputTime: action.time }; + return { + ...state, + lastShellOutputTime: action.time, + isCursorHidden: action.isCursorHidden, + }; case 'SET_VISIBILITY': return { ...state, isBackgroundShellVisible: action.visible }; case 'TOGGLE_VISIBILITY': @@ -103,6 +114,9 @@ export function shellReducer( newOutput = action.chunk; } shell.output = newOutput; + if (action.isCursorHidden !== undefined) { + shell.isCursorHidden = action.isCursorHidden; + } const nextState = { ...state, lastShellOutputTime: Date.now() }; diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 757c24f2c3..5b4729ee7e 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -371,6 +371,7 @@ export const useGeminiStream = ( registerBackgroundShell, dismissBackgroundShell, backgroundShells, + isCursorHidden, } = useShellCommandProcessor( addItem, setPendingHistoryItem, @@ -2028,6 +2029,7 @@ export const useGeminiStream = ( toggleBackgroundShell, backgroundCurrentShell, backgroundShells, + isCursorHidden, dismissBackgroundShell, retryStatus, }; diff --git a/packages/cli/src/ui/hooks/useShellInactivityStatus.test.ts b/packages/cli/src/ui/hooks/useShellInactivityStatus.test.ts index 74dc8e5ed1..8b57b7bc62 100644 --- a/packages/cli/src/ui/hooks/useShellInactivityStatus.test.ts +++ b/packages/cli/src/ui/hooks/useShellInactivityStatus.test.ts @@ -106,4 +106,17 @@ describe('useShellInactivityStatus', () => { }); expect(result.current.shouldShowFocusHint).toBe(false); }); + + it('should suppress all inactivity indicators when cursor is hidden', async () => { + const { result } = await renderHook(() => + useShellInactivityStatus({ ...defaultProps, isCursorHidden: true }), + ); + + // After 30s, status should still be 'none' and focus hint false + await act(async () => { + await vi.advanceTimersByTimeAsync(30000); + }); + expect(result.current.inactivityStatus).toBe('none'); + expect(result.current.shouldShowFocusHint).toBe(false); + }); }); diff --git a/packages/cli/src/ui/hooks/useShellInactivityStatus.ts b/packages/cli/src/ui/hooks/useShellInactivityStatus.ts index 092e58baae..5d61703bd8 100644 --- a/packages/cli/src/ui/hooks/useShellInactivityStatus.ts +++ b/packages/cli/src/ui/hooks/useShellInactivityStatus.ts @@ -21,6 +21,7 @@ interface ShellInactivityStatusProps { pendingToolCalls: TrackedToolCall[]; embeddedShellFocused: boolean; isInteractiveShellEnabled: boolean; + isCursorHidden?: boolean; } export type InactivityStatus = 'none' | 'action_required' | 'silent_working'; @@ -41,6 +42,7 @@ export const useShellInactivityStatus = ({ pendingToolCalls, embeddedShellFocused, isInteractiveShellEnabled, + isCursorHidden, }: ShellInactivityStatusProps): ShellInactivityStatus => { const { operationStartTime, isRedirectionActive } = useTurnActivityMonitor( streamingState, @@ -49,7 +51,10 @@ export const useShellInactivityStatus = ({ ); const isAwaitingFocus = - !!activePtyId && !embeddedShellFocused && isInteractiveShellEnabled; + !!activePtyId && + !embeddedShellFocused && + isInteractiveShellEnabled && + !isCursorHidden; // Derive whether output was produced by comparing the last output time to when the operation started. const hasProducedOutput = lastOutputTime > operationStartTime;