mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-16 06:17:21 -07:00
feat: Implement background shell commands (#14849)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { DefaultAppLayout } from './DefaultAppLayout.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { Text } from 'ink';
|
||||
import type { UIState } from '../contexts/UIStateContext.js';
|
||||
import type { BackgroundShell } from '../hooks/shellCommandProcessor.js';
|
||||
|
||||
// Mock dependencies
|
||||
const mockUIState = {
|
||||
rootUiRef: { current: null },
|
||||
terminalHeight: 24,
|
||||
terminalWidth: 80,
|
||||
mainAreaWidth: 80,
|
||||
backgroundShells: new Map<number, BackgroundShell>(),
|
||||
activeBackgroundShellPid: null as number | null,
|
||||
backgroundShellHeight: 10,
|
||||
embeddedShellFocused: false,
|
||||
dialogsVisible: false,
|
||||
streamingState: StreamingState.Idle,
|
||||
isBackgroundShellListOpen: false,
|
||||
mainControlsRef: { current: null },
|
||||
customDialog: null,
|
||||
historyManager: { addItem: vi.fn() },
|
||||
history: [],
|
||||
pendingHistoryItems: [],
|
||||
slashCommands: [],
|
||||
constrainHeight: false,
|
||||
availableTerminalHeight: 20,
|
||||
activePtyId: null,
|
||||
isBackgroundShellVisible: true,
|
||||
} as unknown as UIState;
|
||||
|
||||
vi.mock('../contexts/UIStateContext.js', () => ({
|
||||
useUIState: () => mockUIState,
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useFlickerDetector.js', () => ({
|
||||
useFlickerDetector: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useAlternateBuffer.js', () => ({
|
||||
useAlternateBuffer: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
vi.mock('../contexts/ConfigContext.js', () => ({
|
||||
useConfig: () => ({
|
||||
getAccessibility: vi.fn(() => ({
|
||||
enableLoadingPhrases: true,
|
||||
})),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock child components to simplify output
|
||||
vi.mock('../components/LoadingIndicator.js', () => ({
|
||||
LoadingIndicator: () => <Text>LoadingIndicator</Text>,
|
||||
}));
|
||||
vi.mock('../components/MainContent.js', () => ({
|
||||
MainContent: () => <Text>MainContent</Text>,
|
||||
}));
|
||||
vi.mock('../components/Notifications.js', () => ({
|
||||
Notifications: () => <Text>Notifications</Text>,
|
||||
}));
|
||||
vi.mock('../components/DialogManager.js', () => ({
|
||||
DialogManager: () => <Text>DialogManager</Text>,
|
||||
}));
|
||||
vi.mock('../components/Composer.js', () => ({
|
||||
Composer: () => <Text>Composer</Text>,
|
||||
}));
|
||||
vi.mock('../components/ExitWarning.js', () => ({
|
||||
ExitWarning: () => <Text>ExitWarning</Text>,
|
||||
}));
|
||||
vi.mock('../components/CopyModeWarning.js', () => ({
|
||||
CopyModeWarning: () => <Text>CopyModeWarning</Text>,
|
||||
}));
|
||||
vi.mock('../components/BackgroundShellDisplay.js', () => ({
|
||||
BackgroundShellDisplay: () => <Text>BackgroundShellDisplay</Text>,
|
||||
}));
|
||||
|
||||
const createMockShell = (pid: number): BackgroundShell => ({
|
||||
pid,
|
||||
command: 'test command',
|
||||
output: 'test output',
|
||||
isBinary: false,
|
||||
binaryBytesReceived: 0,
|
||||
status: 'running',
|
||||
});
|
||||
|
||||
describe('<DefaultAppLayout />', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset mock state defaults
|
||||
mockUIState.backgroundShells = new Map();
|
||||
mockUIState.activeBackgroundShellPid = null;
|
||||
mockUIState.streamingState = StreamingState.Idle;
|
||||
});
|
||||
|
||||
it('renders BackgroundShellDisplay when shells exist and active', () => {
|
||||
mockUIState.backgroundShells.set(123, createMockShell(123));
|
||||
mockUIState.activeBackgroundShellPid = 123;
|
||||
mockUIState.backgroundShellHeight = 5;
|
||||
|
||||
const { lastFrame } = render(<DefaultAppLayout />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('hides BackgroundShellDisplay when StreamingState is WaitingForConfirmation', () => {
|
||||
mockUIState.backgroundShells.set(123, createMockShell(123));
|
||||
mockUIState.activeBackgroundShellPid = 123;
|
||||
mockUIState.backgroundShellHeight = 5;
|
||||
mockUIState.streamingState = StreamingState.WaitingForConfirmation;
|
||||
|
||||
const { lastFrame } = render(<DefaultAppLayout />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows BackgroundShellDisplay when StreamingState is NOT WaitingForConfirmation', () => {
|
||||
mockUIState.backgroundShells.set(123, createMockShell(123));
|
||||
mockUIState.activeBackgroundShellPid = 123;
|
||||
mockUIState.backgroundShellHeight = 5;
|
||||
mockUIState.streamingState = StreamingState.Responding;
|
||||
|
||||
const { lastFrame } = render(<DefaultAppLayout />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,8 @@ import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useFlickerDetector } from '../hooks/useFlickerDetector.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { CopyModeWarning } from '../components/CopyModeWarning.js';
|
||||
import { BackgroundShellDisplay } from '../components/BackgroundShellDisplay.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
|
||||
export const DefaultAppLayout: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
@@ -37,6 +39,24 @@ export const DefaultAppLayout: React.FC = () => {
|
||||
>
|
||||
<MainContent />
|
||||
|
||||
{uiState.isBackgroundShellVisible &&
|
||||
uiState.backgroundShells.size > 0 &&
|
||||
uiState.activeBackgroundShellPid &&
|
||||
uiState.backgroundShellHeight > 0 &&
|
||||
uiState.streamingState !== StreamingState.WaitingForConfirmation && (
|
||||
<Box height={uiState.backgroundShellHeight} flexShrink={0}>
|
||||
<BackgroundShellDisplay
|
||||
shells={uiState.backgroundShells}
|
||||
activePid={uiState.activeBackgroundShellPid}
|
||||
width={uiState.terminalWidth}
|
||||
height={uiState.backgroundShellHeight}
|
||||
isFocused={
|
||||
uiState.embeddedShellFocused && !uiState.dialogsVisible
|
||||
}
|
||||
isListOpenProp={uiState.isBackgroundShellListOpen}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
flexDirection="column"
|
||||
ref={uiState.mainControlsRef}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<DefaultAppLayout /> > hides BackgroundShellDisplay when StreamingState is WaitingForConfirmation 1`] = `
|
||||
"MainContent
|
||||
Notifications
|
||||
CopyModeWarning
|
||||
Composer
|
||||
ExitWarning"
|
||||
`;
|
||||
|
||||
exports[`<DefaultAppLayout /> > renders BackgroundShellDisplay when shells exist and active 1`] = `
|
||||
"MainContent
|
||||
BackgroundShellDisplay
|
||||
|
||||
|
||||
|
||||
|
||||
Notifications
|
||||
CopyModeWarning
|
||||
Composer
|
||||
ExitWarning"
|
||||
`;
|
||||
|
||||
exports[`<DefaultAppLayout /> > shows BackgroundShellDisplay when StreamingState is NOT WaitingForConfirmation 1`] = `
|
||||
"MainContent
|
||||
BackgroundShellDisplay
|
||||
|
||||
|
||||
|
||||
|
||||
Notifications
|
||||
CopyModeWarning
|
||||
Composer
|
||||
ExitWarning"
|
||||
`;
|
||||
Reference in New Issue
Block a user