mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-28 18:51:01 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80ff4bd39c |
@@ -534,6 +534,7 @@ export const mockAppState: AppState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockUIActions: UIActions = {
|
const mockUIActions: UIActions = {
|
||||||
|
toggleAlternateBuffer: vi.fn(),
|
||||||
handleThemeSelect: vi.fn(),
|
handleThemeSelect: vi.fn(),
|
||||||
closeThemeDialog: vi.fn(),
|
closeThemeDialog: vi.fn(),
|
||||||
handleThemeHighlight: vi.fn(),
|
handleThemeHighlight: vi.fn(),
|
||||||
|
|||||||
@@ -68,8 +68,10 @@ import {
|
|||||||
writeToStdout,
|
writeToStdout,
|
||||||
disableMouseEvents,
|
disableMouseEvents,
|
||||||
enterAlternateScreen,
|
enterAlternateScreen,
|
||||||
|
exitAlternateScreen,
|
||||||
enableMouseEvents,
|
enableMouseEvents,
|
||||||
disableLineWrapping,
|
disableLineWrapping,
|
||||||
|
enableLineWrapping,
|
||||||
shouldEnterAlternateScreen,
|
shouldEnterAlternateScreen,
|
||||||
startupProfiler,
|
startupProfiler,
|
||||||
SessionStartSource,
|
SessionStartSource,
|
||||||
@@ -213,7 +215,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useMemoryMonitor(historyManager);
|
useMemoryMonitor(historyManager);
|
||||||
const isAlternateBuffer = config.getUseAlternateBuffer();
|
const [isAlternateBuffer, setIsAlternateBuffer] = useState(config.getUseAlternateBuffer());
|
||||||
const [corgiMode, setCorgiMode] = useState(false);
|
const [corgiMode, setCorgiMode] = useState(false);
|
||||||
const [forceRerenderKey, setForceRerenderKey] = useState(0);
|
const [forceRerenderKey, setForceRerenderKey] = useState(0);
|
||||||
const [debugMessage, setDebugMessage] = useState<string>('');
|
const [debugMessage, setDebugMessage] = useState<string>('');
|
||||||
@@ -1550,6 +1552,23 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
type: TransientMessageType;
|
type: TransientMessageType;
|
||||||
}>(WARNING_PROMPT_DURATION_MS);
|
}>(WARNING_PROMPT_DURATION_MS);
|
||||||
|
|
||||||
|
const [shownBufferToggleHint, setShownBufferToggleHint] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAlternateBuffer) return;
|
||||||
|
|
||||||
|
const isLongHistory = historyManager.history.length > 15;
|
||||||
|
const isComplexPrompt = buffer.text.length > 200 || buffer.text.includes('\n');
|
||||||
|
|
||||||
|
if ((isLongHistory || isComplexPrompt) && !shownBufferToggleHint) {
|
||||||
|
showTransientMessage({
|
||||||
|
text: 'Tip: Press Alt+T to toggle full-screen mode for better scrolling/editing',
|
||||||
|
type: TransientMessageType.Hint
|
||||||
|
});
|
||||||
|
setShownBufferToggleHint(true);
|
||||||
|
}
|
||||||
|
}, [historyManager.history.length, buffer.text, isAlternateBuffer, shownBufferToggleHint, showTransientMessage]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isFolderTrustDialogOpen,
|
isFolderTrustDialogOpen,
|
||||||
discoveryResults: folderDiscoveryResults,
|
discoveryResults: folderDiscoveryResults,
|
||||||
@@ -1700,6 +1719,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyMatchers[Command.TOGGLE_BUFFER_MODE](key)) {
|
||||||
|
toggleAlternateBuffer();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (keyMatchers[Command.QUIT](key)) {
|
if (keyMatchers[Command.QUIT](key)) {
|
||||||
// If the user presses Ctrl+C, we want to cancel any ongoing requests.
|
// If the user presses Ctrl+C, we want to cancel any ongoing requests.
|
||||||
// This should happen regardless of the count.
|
// This should happen regardless of the count.
|
||||||
@@ -2204,8 +2228,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
};
|
};
|
||||||
}, [config, refreshStatic]);
|
}, [config, refreshStatic]);
|
||||||
|
|
||||||
|
const showIsAlternateBufferHint = (historyManager.history.length > 15 || buffer.text.length > 200 || buffer.text.includes('\n')) && !isAlternateBuffer;
|
||||||
|
|
||||||
const uiState: UIState = useMemo(
|
const uiState: UIState = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
isAlternateBuffer,
|
||||||
history: historyManager.history,
|
history: historyManager.history,
|
||||||
historyManager,
|
historyManager,
|
||||||
isThemeDialogOpen,
|
isThemeDialogOpen,
|
||||||
@@ -2331,6 +2358,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
adminSettingsChanged,
|
adminSettingsChanged,
|
||||||
newAgents,
|
newAgents,
|
||||||
showIsExpandableHint,
|
showIsExpandableHint,
|
||||||
|
showIsAlternateBufferHint,
|
||||||
hintMode:
|
hintMode:
|
||||||
config.isModelSteeringEnabled() && isToolExecuting(pendingHistoryItems),
|
config.isModelSteeringEnabled() && isToolExecuting(pendingHistoryItems),
|
||||||
hintBuffer: '',
|
hintBuffer: '',
|
||||||
@@ -2457,6 +2485,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
adminSettingsChanged,
|
adminSettingsChanged,
|
||||||
newAgents,
|
newAgents,
|
||||||
showIsExpandableHint,
|
showIsExpandableHint,
|
||||||
|
showIsAlternateBufferHint,
|
||||||
|
isAlternateBuffer,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2465,6 +2495,31 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
[setShowPrivacyNotice],
|
[setShowPrivacyNotice],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toggleAlternateBuffer = useCallback(() => {
|
||||||
|
setIsAlternateBuffer(prev => {
|
||||||
|
const next = !prev;
|
||||||
|
if (next) {
|
||||||
|
enterAlternateScreen();
|
||||||
|
enableMouseEvents();
|
||||||
|
disableLineWrapping();
|
||||||
|
} else {
|
||||||
|
exitAlternateScreen();
|
||||||
|
disableMouseEvents();
|
||||||
|
enableLineWrapping();
|
||||||
|
writeToStdout('\x1b[2J\x1b[H');
|
||||||
|
}
|
||||||
|
process.stdout.emit('resize');
|
||||||
|
|
||||||
|
// Give a tick for resize to process, then trigger remount to force full redraw
|
||||||
|
setImmediate(() => {
|
||||||
|
refreshStatic();
|
||||||
|
setForceRerenderKey((prev) => prev + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [setIsAlternateBuffer, refreshStatic, setForceRerenderKey]);
|
||||||
|
|
||||||
const uiActions: UIActions = useMemo(
|
const uiActions: UIActions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
handleThemeSelect,
|
handleThemeSelect,
|
||||||
@@ -2476,6 +2531,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
handleEditorSelect,
|
handleEditorSelect,
|
||||||
exitEditorDialog,
|
exitEditorDialog,
|
||||||
exitPrivacyNotice,
|
exitPrivacyNotice,
|
||||||
|
toggleAlternateBuffer,
|
||||||
closeSettingsDialog,
|
closeSettingsDialog,
|
||||||
closeModelDialog,
|
closeModelDialog,
|
||||||
openAgentConfigDialog,
|
openAgentConfigDialog,
|
||||||
@@ -2614,6 +2670,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
config,
|
config,
|
||||||
historyManager,
|
historyManager,
|
||||||
getPreferredEditor,
|
getPreferredEditor,
|
||||||
|
toggleAlternateBuffer,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -142,4 +142,38 @@ describe('<StatusRow />', () => {
|
|||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
expect(lastFrame()).toContain('Tip: Test Tip');
|
expect(lastFrame()).toContain('Tip: Test Tip');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders buffer toggle hint when showIsAlternateBufferHint is true', async () => {
|
||||||
|
(useComposerStatus as Mock).mockReturnValue({
|
||||||
|
isInteractiveShellWaiting: false,
|
||||||
|
showLoadingIndicator: false,
|
||||||
|
showTips: false,
|
||||||
|
showWit: false,
|
||||||
|
modeContentObj: null,
|
||||||
|
showMinimalContext: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uiState: Partial<UIState> = {
|
||||||
|
...defaultUiState,
|
||||||
|
showIsAlternateBufferHint: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||||
|
<StatusRow
|
||||||
|
showUiDetails={false}
|
||||||
|
isNarrow={false}
|
||||||
|
terminalWidth={100}
|
||||||
|
hideContextSummary={false}
|
||||||
|
hideUiDetailsForSuggestions={false}
|
||||||
|
hasPendingActionRequired={false}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
width: 100,
|
||||||
|
uiState,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitUntilReady();
|
||||||
|
expect(lastFrame()).toContain('[Alt+T] Switch to Full Screen');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -206,7 +206,12 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
|||||||
return uiState.currentTip;
|
return uiState.currentTip;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Shortcut Hint (Fallback)
|
// 2. Buffer Toggle Hint
|
||||||
|
if (uiState.showIsAlternateBufferHint) {
|
||||||
|
return '[Alt+T] Switch to Full Screen';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Shortcut Hint (Fallback)
|
||||||
if (
|
if (
|
||||||
settings.merged.ui.showShortcutsHint &&
|
settings.merged.ui.showShortcutsHint &&
|
||||||
!hideUiDetailsForSuggestions &&
|
!hideUiDetailsForSuggestions &&
|
||||||
|
|||||||
@@ -205,6 +205,8 @@ const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
|
|||||||
'\u03A9': 'z', // "Ω" Option+z
|
'\u03A9': 'z', // "Ω" Option+z
|
||||||
'\u00B8': 'Z', // "¸" Option+Shift+z
|
'\u00B8': 'Z', // "¸" Option+Shift+z
|
||||||
'\u2202': 'd', // "∂" delete word forward
|
'\u2202': 'd', // "∂" delete word forward
|
||||||
|
'\u2020': 't', // "†" toggle full screen buffer
|
||||||
|
'\u00E5': 'a', // "å" Option+a for alternate buffer
|
||||||
};
|
};
|
||||||
|
|
||||||
function nonKeyboardEventFilter(
|
function nonKeyboardEventFilter(
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export interface UIActions {
|
|||||||
setShortcutsHelpVisible: (visible: boolean) => void;
|
setShortcutsHelpVisible: (visible: boolean) => void;
|
||||||
setCleanUiDetailsVisible: (visible: boolean) => void;
|
setCleanUiDetailsVisible: (visible: boolean) => void;
|
||||||
toggleCleanUiDetailsVisible: () => void;
|
toggleCleanUiDetailsVisible: () => void;
|
||||||
|
toggleAlternateBuffer: () => void;
|
||||||
revealCleanUiDetailsTemporarily: (durationMs?: number) => void;
|
revealCleanUiDetailsTemporarily: (durationMs?: number) => void;
|
||||||
handleWarning: (message: string) => void;
|
handleWarning: (message: string) => void;
|
||||||
setEmbeddedShellFocused: (value: boolean) => void;
|
setEmbeddedShellFocused: (value: boolean) => void;
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ export interface UIState {
|
|||||||
isEditorDialogOpen: boolean;
|
isEditorDialogOpen: boolean;
|
||||||
showPrivacyNotice: boolean;
|
showPrivacyNotice: boolean;
|
||||||
corgiMode: boolean;
|
corgiMode: boolean;
|
||||||
|
isAlternateBuffer: boolean;
|
||||||
|
showIsAlternateBufferHint: boolean;
|
||||||
debugMessage: string;
|
debugMessage: string;
|
||||||
quittingMessages: HistoryItem[] | null;
|
quittingMessages: HistoryItem[] | null;
|
||||||
isSettingsDialogOpen: boolean;
|
isSettingsDialogOpen: boolean;
|
||||||
|
|||||||
@@ -11,49 +11,47 @@ import {
|
|||||||
isAlternateBufferEnabled,
|
isAlternateBufferEnabled,
|
||||||
} from './useAlternateBuffer.js';
|
} from './useAlternateBuffer.js';
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
|
|
||||||
vi.mock('../contexts/ConfigContext.js', () => ({
|
vi.mock('../contexts/UIStateContext.js');
|
||||||
useConfig: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockUseConfig = vi.mocked(
|
const mockUseUIState = vi.mocked(useUIState);
|
||||||
await import('../contexts/ConfigContext.js').then((m) => m.useConfig),
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('useAlternateBuffer', () => {
|
describe('useAlternateBuffer', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when config.getUseAlternateBuffer returns false', async () => {
|
it('should return false when uiState.isAlternateBuffer is false', async () => {
|
||||||
mockUseConfig.mockReturnValue({
|
mockUseUIState.mockReturnValue({
|
||||||
getUseAlternateBuffer: () => false,
|
isAlternateBuffer: false,
|
||||||
} as unknown as ReturnType<typeof mockUseConfig>);
|
} as unknown as ReturnType<typeof mockUseUIState>);
|
||||||
|
|
||||||
const { result } = await renderHook(() => useAlternateBuffer());
|
const { result } = await renderHook(() => useAlternateBuffer());
|
||||||
expect(result.current).toBe(false);
|
expect(result.current).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when config.getUseAlternateBuffer returns true', async () => {
|
it('should return true when uiState.isAlternateBuffer is true', async () => {
|
||||||
mockUseConfig.mockReturnValue({
|
mockUseUIState.mockReturnValue({
|
||||||
getUseAlternateBuffer: () => true,
|
isAlternateBuffer: true,
|
||||||
} as unknown as ReturnType<typeof mockUseConfig>);
|
} as unknown as ReturnType<typeof mockUseUIState>);
|
||||||
|
|
||||||
const { result } = await renderHook(() => useAlternateBuffer());
|
const { result } = await renderHook(() => useAlternateBuffer());
|
||||||
expect(result.current).toBe(true);
|
expect(result.current).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the immutable config value, not react to settings changes', async () => {
|
it('should react to state changes', async () => {
|
||||||
const mockConfig = {
|
mockUseUIState.mockReturnValue({
|
||||||
getUseAlternateBuffer: () => true,
|
isAlternateBuffer: false,
|
||||||
} as unknown as ReturnType<typeof mockUseConfig>;
|
} as unknown as ReturnType<typeof mockUseUIState>);
|
||||||
|
|
||||||
mockUseConfig.mockReturnValue(mockConfig);
|
|
||||||
|
|
||||||
const { result, rerender } = await renderHook(() => useAlternateBuffer());
|
const { result, rerender } = await renderHook(() => useAlternateBuffer());
|
||||||
|
|
||||||
// Value should remain true even after rerender
|
expect(result.current).toBe(false);
|
||||||
expect(result.current).toBe(true);
|
|
||||||
|
mockUseUIState.mockReturnValue({
|
||||||
|
isAlternateBuffer: true,
|
||||||
|
} as unknown as ReturnType<typeof mockUseUIState>);
|
||||||
|
|
||||||
rerender();
|
rerender();
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useConfig } from '../contexts/ConfigContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
export const isAlternateBufferEnabled = (config: Config): boolean =>
|
export const isAlternateBufferEnabled = (config: Config): boolean =>
|
||||||
config.getUseAlternateBuffer();
|
config.getUseAlternateBuffer();
|
||||||
|
|
||||||
// This is read from Config so that the UI reads the same value per application session
|
// This is read from UIState so that the UI can toggle dynamically
|
||||||
export const useAlternateBuffer = (): boolean => {
|
export const useAlternateBuffer = (): boolean => {
|
||||||
const config = useConfig();
|
const uiState = useUIState();
|
||||||
return isAlternateBufferEnabled(config);
|
return uiState.isAlternateBuffer;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export enum Command {
|
|||||||
RESTART_APP = 'app.restart',
|
RESTART_APP = 'app.restart',
|
||||||
SUSPEND_APP = 'app.suspend',
|
SUSPEND_APP = 'app.suspend',
|
||||||
SHOW_SHELL_INPUT_UNFOCUS_WARNING = 'app.showShellUnfocusWarning',
|
SHOW_SHELL_INPUT_UNFOCUS_WARNING = 'app.showShellUnfocusWarning',
|
||||||
|
TOGGLE_BUFFER_MODE = 'app.toggleBufferMode',
|
||||||
|
|
||||||
// Background Shell Controls
|
// Background Shell Controls
|
||||||
BACKGROUND_SHELL_ESCAPE = 'background.escape',
|
BACKGROUND_SHELL_ESCAPE = 'background.escape',
|
||||||
@@ -392,6 +393,7 @@ export const defaultKeyBindingConfig: KeyBindingConfig = new Map([
|
|||||||
[Command.RESTART_APP, [new KeyBinding('r'), new KeyBinding('shift+r')]],
|
[Command.RESTART_APP, [new KeyBinding('r'), new KeyBinding('shift+r')]],
|
||||||
[Command.SUSPEND_APP, [new KeyBinding('ctrl+z')]],
|
[Command.SUSPEND_APP, [new KeyBinding('ctrl+z')]],
|
||||||
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING, [new KeyBinding('tab')]],
|
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING, [new KeyBinding('tab')]],
|
||||||
|
[Command.TOGGLE_BUFFER_MODE, [new KeyBinding('alt+a')]],
|
||||||
|
|
||||||
// Background Shell Controls
|
// Background Shell Controls
|
||||||
[Command.BACKGROUND_SHELL_ESCAPE, [new KeyBinding('escape')]],
|
[Command.BACKGROUND_SHELL_ESCAPE, [new KeyBinding('escape')]],
|
||||||
@@ -609,6 +611,7 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
|||||||
[Command.SHOW_IDE_CONTEXT_DETAIL]: 'Show IDE context details.',
|
[Command.SHOW_IDE_CONTEXT_DETAIL]: 'Show IDE context details.',
|
||||||
[Command.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.',
|
[Command.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.',
|
||||||
[Command.TOGGLE_COPY_MODE]: 'Toggle copy mode when in alternate buffer mode.',
|
[Command.TOGGLE_COPY_MODE]: 'Toggle copy mode when in alternate buffer mode.',
|
||||||
|
[Command.TOGGLE_BUFFER_MODE]: 'Toggle between regular and full screen (alternate buffer) mode.',
|
||||||
[Command.TOGGLE_YOLO]: 'Toggle YOLO (auto-approval) mode for tool calls.',
|
[Command.TOGGLE_YOLO]: 'Toggle YOLO (auto-approval) mode for tool calls.',
|
||||||
[Command.CYCLE_APPROVAL_MODE]:
|
[Command.CYCLE_APPROVAL_MODE]:
|
||||||
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy.',
|
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy.',
|
||||||
|
|||||||
Reference in New Issue
Block a user