feat(cli): suppress shell inactivity indicators when cursor is hidden

This commit is contained in:
Keith Guerin
2026-03-27 20:59:00 -07:00
parent 5a39e69164
commit bc17679db7
7 changed files with 58 additions and 6 deletions
+4
View File
@@ -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,
@@ -217,6 +217,7 @@ export interface UIState {
settingsNonce: number;
backgroundShells: Map<number, BackgroundShell>;
activeBackgroundShellPid: number | null;
isCursorHidden?: boolean;
backgroundShellHeight: number;
isBackgroundShellListOpen: boolean;
adminSettingsChanged: boolean;
@@ -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,
};
};
+17 -3
View File
@@ -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<number, BackgroundShell>;
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<BackgroundShell> }
| { 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() };
@@ -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,
};
@@ -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);
});
});
@@ -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;