mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 21:21:09 -07:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc814bdfe6 | |||
| bc17679db7 | |||
| 5a39e69164 | |||
| 317c13f846 | |||
| a512438f37 | |||
| 4829e80596 |
@@ -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,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { type TextBuffer } from '../components/shared/text-buffer.js';
|
||||
import { type SessionStatsState } from '../contexts/SessionContext.js';
|
||||
import { type ThoughtSummary } from '../types.js';
|
||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
|
||||
|
||||
vi.mock('../hooks/useComposerStatus.js', () => ({
|
||||
useComposerStatus: vi.fn(),
|
||||
@@ -106,7 +107,7 @@ describe('<StatusRow />', () => {
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('! Shell awaiting input (Tab to focus)');
|
||||
expect(lastFrame()).toContain(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
});
|
||||
|
||||
it('renders tip with absolute positioning when it fits but might collide (verification of container logic)', async () => {
|
||||
|
||||
@@ -313,8 +313,8 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
||||
</Box>
|
||||
) : isInteractiveShellWaiting ? (
|
||||
<Box width="100%" marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
|
||||
<Text color={theme.status.warning}>
|
||||
! Shell awaiting input (Tab to focus)
|
||||
<Text color={theme.ui.active}>
|
||||
{INTERACTIVE_SHELL_WAITING_PHRASE}
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
export const PHRASE_CHANGE_INTERVAL_MS = 10000;
|
||||
export const WITTY_PHRASE_CHANGE_INTERVAL_MS = 5000;
|
||||
export const INTERACTIVE_SHELL_WAITING_PHRASE =
|
||||
'! Shell awaiting input (Tab to focus)';
|
||||
'Shell awaiting input (Tab to focus)';
|
||||
|
||||
/**
|
||||
* Custom hook to manage cycling through loading phrases.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -36,6 +36,7 @@ export type ExecutionOutputEvent =
|
||||
| {
|
||||
type: 'data';
|
||||
chunk: string | AnsiOutput;
|
||||
isCursorHidden?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'binary_detected';
|
||||
|
||||
@@ -299,10 +299,12 @@ describe('ShellExecutionService', () => {
|
||||
expect(result.output.trim()).toBe('file1.txt');
|
||||
expect(handle.pid).toBe(12345);
|
||||
|
||||
expect(onOutputEventMock).toHaveBeenCalledWith({
|
||||
type: 'data',
|
||||
chunk: createExpectedAnsiOutput('file1.txt'),
|
||||
});
|
||||
expect(onOutputEventMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'data',
|
||||
chunk: createExpectedAnsiOutput('file1.txt'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should strip ANSI color codes from output', async () => {
|
||||
|
||||
@@ -46,6 +46,19 @@ import {
|
||||
} from './executionLifecycleService.js';
|
||||
const { Terminal } = pkg;
|
||||
|
||||
/**
|
||||
* Internal interfaces to access non-public xterm properties.
|
||||
*/
|
||||
interface XTermCore {
|
||||
coreService: {
|
||||
isCursorHidden: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface XTermInternal {
|
||||
_core?: XTermCore;
|
||||
}
|
||||
|
||||
const MAX_CHILD_PROCESS_BUFFER_SIZE = 16 * 1024 * 1024; // 16MB
|
||||
|
||||
/**
|
||||
@@ -952,9 +965,14 @@ export class ShellExecutionService {
|
||||
|
||||
if (output !== finalOutput) {
|
||||
output = finalOutput;
|
||||
const isCursorHidden =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(headlessTerminal as unknown as XTermInternal)._core?.coreService
|
||||
?.isCursorHidden;
|
||||
const event: ShellOutputEvent = {
|
||||
type: 'data',
|
||||
chunk: finalOutput,
|
||||
isCursorHidden,
|
||||
};
|
||||
onOutputEvent(event);
|
||||
ExecutionLifecycleService.emitEvent(ptyPid, event);
|
||||
@@ -1303,7 +1321,15 @@ export class ShellExecutionService {
|
||||
startLine,
|
||||
endLine,
|
||||
);
|
||||
const event: ShellOutputEvent = { type: 'data', chunk: bufferData };
|
||||
const isCursorHidden =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(activePty.headlessTerminal as unknown as XTermInternal)._core
|
||||
?.coreService?.isCursorHidden;
|
||||
const event: ShellOutputEvent = {
|
||||
type: 'data',
|
||||
chunk: bufferData,
|
||||
isCursorHidden,
|
||||
};
|
||||
ExecutionLifecycleService.emitEvent(pid, event);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user