Compare commits

..

1 Commits

Author SHA1 Message Date
Coco Sheng 80ff4bd39c feat: implement alternate buffer toggling and unit tests 2026-03-30 11:40:52 -04:00
36 changed files with 680 additions and 510 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ they appear in the UI.
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Escape Pasted @ Symbols | `ui.escapePastedAtSymbols` | When enabled, @ symbols in pasted text are escaped to prevent unintended @path expansion. | `false` |
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
+2 -2
View File
@@ -246,7 +246,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`ui.hideTips`** (boolean):
- **Description:** Hide helpful tips in the UI
- **Default:** `true`
- **Default:** `false`
- **`ui.escapePastedAtSymbols`** (boolean):
- **Description:** When enabled, @ symbols in pasted text are escaped to
@@ -1933,7 +1933,7 @@ of v0.3.0:
"ui": {
"theme": "GitHub",
"hideBanner": true,
"hideTips": true,
"hideTips": false,
"customWittyPhrases": [
"You forget a thousand things every day. Make sure this is one of em",
"Connecting to AGI"
+1 -1
View File
@@ -538,7 +538,7 @@ const SETTINGS_SCHEMA = {
label: 'Hide Tips',
category: 'UI',
requiresRestart: false,
default: true,
default: false,
description: 'Hide helpful tips in the UI',
showInDialog: true,
},
-2
View File
@@ -93,7 +93,6 @@ import { setupTerminalAndTheme } from './utils/terminalTheme.js';
import { runDeferredCommand } from './deferred.js';
import { cleanupBackgroundLogs } from './utils/logCleanup.js';
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
import { initializeConsoleStore } from './ui/hooks/useConsoleMessages.js';
export function validateDnsResolutionOrder(
order: string | undefined,
@@ -295,7 +294,6 @@ export async function main() {
process.exit(ExitCodes.FATAL_INPUT_ERROR);
}
initializeConsoleStore();
const isDebugMode = cliConfig.isDebugMode(argv);
const consolePatcher = new ConsolePatcher({
stderr: true,
+1
View File
@@ -534,6 +534,7 @@ export const mockAppState: AppState = {
};
const mockUIActions: UIActions = {
toggleAlternateBuffer: vi.fn(),
handleThemeSelect: vi.fn(),
closeThemeDialog: vi.fn(),
handleThemeHighlight: vi.fn(),
+11 -33
View File
@@ -91,37 +91,15 @@ describe('App', () => {
backgroundTasks: new Map(),
};
it('should render main content and composer without tips by default', async () => {
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
settings: createMockSettings({
ui: { useAlternateBuffer: false },
}),
},
);
await waitUntilReady();
expect(lastFrame()).not.toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Composer');
unmount();
});
it('should render tips when hideTips is explicitly set to false', async () => {
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
<App />,
{
uiState: mockUIState,
settings: createMockSettings({
ui: { useAlternateBuffer: false, hideTips: false },
}),
},
);
await waitUntilReady();
it('should render main content and composer when not quitting', async () => {
const { lastFrame, unmount } = await renderWithProviders(<App />, {
uiState: mockUIState,
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
});
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Composer');
unmount();
});
@@ -169,7 +147,7 @@ describe('App', () => {
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
});
expect(lastFrame()).not.toContain('Tips for getting started');
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('DialogManager');
unmount();
@@ -207,7 +185,7 @@ describe('App', () => {
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Footer');
expect(lastFrame()).not.toContain('Tips for getting started');
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Composer');
unmount();
});
@@ -220,7 +198,7 @@ describe('App', () => {
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
});
expect(lastFrame()).not.toContain('Tips for getting started');
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Composer');
unmount();
@@ -272,7 +250,7 @@ describe('App', () => {
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
});
expect(lastFrame()).not.toContain('Tips for getting started');
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Action Required'); // From ToolConfirmationQueue
expect(lastFrame()).toContain('Composer');
+58 -1
View File
@@ -68,8 +68,10 @@ import {
writeToStdout,
disableMouseEvents,
enterAlternateScreen,
exitAlternateScreen,
enableMouseEvents,
disableLineWrapping,
enableLineWrapping,
shouldEnterAlternateScreen,
startupProfiler,
SessionStartSource,
@@ -213,7 +215,7 @@ export const AppContainer = (props: AppContainerProps) => {
});
useMemoryMonitor(historyManager);
const isAlternateBuffer = config.getUseAlternateBuffer();
const [isAlternateBuffer, setIsAlternateBuffer] = useState(config.getUseAlternateBuffer());
const [corgiMode, setCorgiMode] = useState(false);
const [forceRerenderKey, setForceRerenderKey] = useState(0);
const [debugMessage, setDebugMessage] = useState<string>('');
@@ -1550,6 +1552,23 @@ Logging in with Google... Restarting Gemini CLI to continue.
type: TransientMessageType;
}>(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 {
isFolderTrustDialogOpen,
discoveryResults: folderDiscoveryResults,
@@ -1700,6 +1719,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
return true;
}
if (keyMatchers[Command.TOGGLE_BUFFER_MODE](key)) {
toggleAlternateBuffer();
return true;
}
if (keyMatchers[Command.QUIT](key)) {
// If the user presses Ctrl+C, we want to cancel any ongoing requests.
// This should happen regardless of the count.
@@ -2204,8 +2228,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
};
}, [config, refreshStatic]);
const showIsAlternateBufferHint = (historyManager.history.length > 15 || buffer.text.length > 200 || buffer.text.includes('\n')) && !isAlternateBuffer;
const uiState: UIState = useMemo(
() => ({
isAlternateBuffer,
history: historyManager.history,
historyManager,
isThemeDialogOpen,
@@ -2331,6 +2358,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
adminSettingsChanged,
newAgents,
showIsExpandableHint,
showIsAlternateBufferHint,
hintMode:
config.isModelSteeringEnabled() && isToolExecuting(pendingHistoryItems),
hintBuffer: '',
@@ -2457,6 +2485,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
adminSettingsChanged,
newAgents,
showIsExpandableHint,
showIsAlternateBufferHint,
isAlternateBuffer,
],
);
@@ -2465,6 +2495,31 @@ Logging in with Google... Restarting Gemini CLI to continue.
[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(
() => ({
handleThemeSelect,
@@ -2476,6 +2531,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleEditorSelect,
exitEditorDialog,
exitPrivacyNotice,
toggleAlternateBuffer,
closeSettingsDialog,
closeModelDialog,
openAgentConfigDialog,
@@ -2614,6 +2670,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
config,
historyManager,
getPreferredEditor,
toggleAlternateBuffer,
],
);
@@ -11,11 +11,11 @@ exports[`App > Snapshots > renders default layout correctly 1`] = `
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
@@ -55,6 +55,12 @@ Footer
Gemini CLI v1.2.3
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
Composer
"
`;
@@ -112,6 +118,12 @@ exports[`App > should render ToolConfirmationQueue along with Composer when tool
Gemini CLI v1.2.3
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
HistoryItemDisplay
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
@@ -133,12 +145,6 @@ HistoryItemDisplay
Notifications
Composer
@@ -28,11 +28,9 @@
<text x="882" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="882" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="138" fill="#afafaf" textLength="414" lengthAdjust="spacingAndGlyphs">... first 44 lines hidden (Ctrl+O to show) ...</text>
<text x="882" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">45</text>
<text x="63" y="155" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
@@ -41,7 +39,6 @@
<text x="198" y="155" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="155" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="172" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">46</text>
<text x="63" y="172" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
@@ -50,7 +47,6 @@
<text x="198" y="172" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="234" y="172" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
<text x="882" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="189" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">47</text>
<text x="63" y="189" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

@@ -8,10 +8,10 @@ exports[`Full Terminal Tool Confirmation Snapshot > renders tool confirmation bo
│ Action Required │
│ │
│ ? Edit packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto… │
│ │
│ ... first 44 lines hidden (Ctrl+O to show) ... │
│ 45 const line45 = true; │
│ 46 const line46 = true; │
│ │
│ ... first 44 lines hidden (Ctrl+O to show) ... │
│ 45 const line45 = true; │
│ 46 const line46 = true; │
│ 47 const line47 = true; │█
│ 48 const line48 = true; │█
│ 49 const line49 = true; │█
@@ -8,10 +8,9 @@ import {
renderWithProviders,
persistentStateMock,
} from '../../test-utils/render.js';
import { createMockSettings } from '../../test-utils/settings.js';
import type { LoadedSettings } from '../../config/settings.js';
import { AppHeader } from './AppHeader.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { makeFakeConfig } from '@google/gemini-cli-core';
import crypto from 'node:crypto';
@@ -20,11 +19,6 @@ vi.mock('../utils/terminalSetup.js', () => ({
}));
describe('<AppHeader />', () => {
beforeEach(() => {
vi.clearAllMocks();
persistentStateMock.setData({ tipsShown: undefined });
});
it('should render the banner with default text', async () => {
const uiState = {
history: [],
@@ -43,32 +37,10 @@ describe('<AppHeader />', () => {
);
expect(lastFrame()).toContain('This is the default banner');
expect(lastFrame()).not.toContain('Tips for getting started');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('should render tips when hideTips is explicitly set to false', async () => {
const uiState = {
history: [],
bannerData: {
defaultText: 'This is the default banner',
warningText: '',
},
bannerVisible: true,
};
const { lastFrame, unmount } = await renderWithProviders(
<AppHeader version="1.0.0" />,
{
uiState,
},
);
expect(lastFrame()).not.toContain('Tips for getting started');
unmount();
});
it('should render the banner with warning text', async () => {
const uiState = {
history: [],
@@ -87,7 +59,6 @@ describe('<AppHeader />', () => {
);
expect(lastFrame()).toContain('There are capacity issues');
expect(lastFrame()).not.toContain('Tips for getting started');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
@@ -109,7 +80,6 @@ describe('<AppHeader />', () => {
);
expect(lastFrame()).not.toContain('Banner');
expect(lastFrame()).not.toContain('Tips for getting started');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
@@ -140,7 +110,6 @@ describe('<AppHeader />', () => {
);
expect(lastFrame()).not.toContain('This is the default banner');
expect(lastFrame()).not.toContain('Tips for getting started');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
@@ -154,6 +123,10 @@ describe('<AppHeader />', () => {
},
};
// Set tipsShown to 10 or more to prevent Tips from incrementing its count
// and interfering with the expected persistentState.set call.
persistentStateMock.setData({ tipsShown: 10 });
const { unmount } = await renderWithProviders(
<AppHeader version="1.0.0" />,
{
@@ -194,7 +167,7 @@ describe('<AppHeader />', () => {
unmount();
});
it('should render Tips when tipsShown is less than 10 and hideTips is false', async () => {
it('should render Tips when tipsShown is less than 10', async () => {
const uiState = {
history: [],
bannerData: {
@@ -210,7 +183,6 @@ describe('<AppHeader />', () => {
<AppHeader version="1.0.0" />,
{
uiState,
settings: createMockSettings({ ui: { hideTips: false } }),
},
);
@@ -219,30 +191,6 @@ describe('<AppHeader />', () => {
unmount();
});
it('should NOT render Tips when tipsShown is less than 10 but hideTips is true (default)', async () => {
const uiState = {
history: [],
bannerData: {
defaultText: 'First line\\nSecond line',
warningText: '',
},
bannerVisible: true,
};
persistentStateMock.setData({ tipsShown: 5 });
const { lastFrame, unmount } = await renderWithProviders(
<AppHeader version="1.0.0" />,
{
uiState,
},
);
expect(lastFrame()).not.toContain('Tips');
expect(persistentStateMock.set).not.toHaveBeenCalledWith('tipsShown', 6);
unmount();
});
it('should NOT render Tips when tipsShown is 10 or more', async () => {
const uiState = {
bannerData: {
@@ -265,7 +213,7 @@ describe('<AppHeader />', () => {
});
it('should show tips until they have been shown 10 times (persistence flow)', async () => {
persistentStateMock.set('tipsShown', 0);
persistentStateMock.setData({ tipsShown: 9 });
const uiState = {
history: [],
@@ -279,20 +227,19 @@ describe('<AppHeader />', () => {
// First session
const session1 = await renderWithProviders(<AppHeader version="1.0.0" />, {
uiState,
settings: createMockSettings({ ui: { hideTips: false } }),
});
expect(session1.lastFrame()).toContain('Tips');
expect(persistentStateMock.get('tipsShown')).toBe(1);
expect(persistentStateMock.get('tipsShown')).toBe(10);
session1.unmount();
// Second session - state is persisted in the fake
const session2 = await renderWithProviders(<AppHeader version="1.0.0" />, {
settings: createMockSettings({ ui: { hideTips: false } }),
});
const session2 = await renderWithProviders(
<AppHeader version="1.0.0" />,
{},
);
expect(session2.lastFrame()).toContain('Tips');
expect(persistentStateMock.get('tipsShown')).toBe(2);
expect(session2.lastFrame()).not.toContain('Tips');
session2.unmount();
});
+3 -5
View File
@@ -62,10 +62,7 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
const { terminalWidth, bannerData, bannerVisible, updateInfo } = useUIState();
const { bannerText } = useBanner(bannerData);
const isTipsVisible = !(
settings.merged.ui.hideTips || config.getScreenReader()
);
const { showTips } = useTips({ isVisible: isTipsVisible });
const { showTips } = useTips();
const authType = config.getContentGeneratorConfig()?.authType;
const loggedOut = !authType;
@@ -162,7 +159,8 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
/>
)}
{isTipsVisible && showTips && <Tips config={config} />}
{!(settings.merged.ui.hideTips || config.getScreenReader()) &&
showTips && <Tips config={config} />}
</Box>
);
};
@@ -35,7 +35,10 @@ vi.mock('./shared/ScrollableList.js', () => ({
describe('DetailedMessagesDisplay', () => {
beforeEach(() => {
vi.mocked(useConsoleMessages).mockReturnValue([]);
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: [],
clearConsoleMessages: vi.fn(),
});
});
it('renders nothing when messages are empty', async () => {
const { lastFrame, unmount } = await renderWithProviders(
@@ -55,7 +58,10 @@ describe('DetailedMessagesDisplay', () => {
{ type: 'error', content: 'Error message', count: 1 },
{ type: 'debug', content: 'Debug message', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue(messages);
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
@@ -73,7 +79,10 @@ describe('DetailedMessagesDisplay', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'error', content: 'Error message', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue(messages);
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
@@ -89,7 +98,10 @@ describe('DetailedMessagesDisplay', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'error', content: 'Error message', count: 1 },
];
vi.mocked(useConsoleMessages).mockReturnValue(messages);
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
@@ -105,7 +117,10 @@ describe('DetailedMessagesDisplay', () => {
const messages: ConsoleMessageItem[] = [
{ type: 'log', content: 'Repeated message', count: 5 },
];
vi.mocked(useConsoleMessages).mockReturnValue(messages);
vi.mocked(useConsoleMessages).mockReturnValue({
consoleMessages: messages,
clearConsoleMessages: vi.fn(),
});
const { lastFrame, unmount } = await renderWithProviders(
<DetailedMessagesDisplay maxHeight={10} width={80} hasFocus={false} />,
@@ -29,7 +29,7 @@ export const DetailedMessagesDisplay: React.FC<
> = ({ maxHeight, width, hasFocus }) => {
const scrollableListRef = useRef<ScrollableListRef<ConsoleMessageItem>>(null);
const consoleMessages = useConsoleMessages();
const { consoleMessages } = useConsoleMessages();
const config = useConfig();
const messages = useMemo(() => {
@@ -142,4 +142,38 @@ describe('<StatusRow />', () => {
await waitUntilReady();
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');
});
});
+6 -1
View File
@@ -206,7 +206,12 @@ export const StatusRow: React.FC<StatusRowProps> = ({
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 (
settings.merged.ui.showShortcutsHint &&
!hideUiDetailsForSuggestions &&
@@ -11,6 +11,12 @@ exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmat
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
Action Required (was prompted):
? confirming_tool Confirming tool description
@@ -27,6 +33,12 @@ exports[`AlternateBufferQuittingDisplay > renders with active and pending tool m
Gemini CLI v0.10.0
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │
│ │
@@ -50,6 +62,14 @@ exports[`AlternateBufferQuittingDisplay > renders with empty history and no pend
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v0.10.0
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
@@ -63,6 +83,12 @@ exports[`AlternateBufferQuittingDisplay > renders with history but no pending it
Gemini CLI v0.10.0
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │
│ │
@@ -84,6 +110,12 @@ exports[`AlternateBufferQuittingDisplay > renders with pending items but no hist
Gemini CLI v0.10.0
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ o tool3 Description for tool 3 │
│ │
@@ -101,6 +133,12 @@ exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages
Gemini CLI v0.10.0
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Hello Gemini
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
@@ -8,6 +8,14 @@ exports[`<AppHeader /> > should not render the banner when no flags are set 1`]
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.0.0
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
@@ -19,6 +27,14 @@ exports[`<AppHeader /> > should not render the default banner if shown count is
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v1.0.0
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
@@ -35,6 +51,12 @@ exports[`<AppHeader /> > should render the banner with default text 1`] = `
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ This is the default banner │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
@@ -51,6 +73,12 @@ exports[`<AppHeader /> > should render the banner with warning text 1`] = `
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ There are capacity issues │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
@@ -1,8 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="139" viewBox="0 0 920 139">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="275" viewBox="0 0 920 275">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="139" fill="#000000" />
<rect width="920" height="275" fill="#000000" />
<g transform="translate(10, 10)">
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
@@ -21,5 +21,14 @@
<text x="90" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.0.0</text>
<text x="0" y="172" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="189" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="189" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="189" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="206" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="206" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="206" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="223" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="240" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -1,8 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="139" viewBox="0 0 920 139">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="275" viewBox="0 0 920 275">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="139" fill="#000000" />
<rect width="920" height="275" fill="#000000" />
<g transform="translate(10, 10)">
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
@@ -22,5 +22,14 @@
<text x="81" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.0.0</text>
<text x="0" y="172" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="189" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="189" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="189" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="206" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="206" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="206" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="223" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="240" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -9,7 +9,13 @@ exports[`AppHeader Icon Rendering > renders the default icon in standard termina
Gemini CLI v1.0.0
"
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results"
`;
exports[`AppHeader Icon Rendering > renders the symmetric icon in Apple Terminal 1`] = `
@@ -21,5 +27,11 @@ exports[`AppHeader Icon Rendering > renders the symmetric icon in Apple Terminal
Gemini CLI v1.0.0
"
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results"
`;
@@ -205,6 +205,8 @@ const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
'\u03A9': 'z', // "Ω" Option+z
'\u00B8': 'Z', // "¸" Option+Shift+z
'\u2202': 'd', // "∂" delete word forward
'\u2020': 't', // "†" toggle full screen buffer
'\u00E5': 'a', // "å" Option+a for alternate buffer
};
function nonKeyboardEventFilter(
@@ -78,6 +78,7 @@ export interface UIActions {
setShortcutsHelpVisible: (visible: boolean) => void;
setCleanUiDetailsVisible: (visible: boolean) => void;
toggleCleanUiDetailsVisible: () => void;
toggleAlternateBuffer: () => void;
revealCleanUiDetailsTemporarily: (durationMs?: number) => void;
handleWarning: (message: string) => void;
setEmbeddedShellFocused: (value: boolean) => void;
@@ -118,6 +118,8 @@ export interface UIState {
isEditorDialogOpen: boolean;
showPrivacyNotice: boolean;
corgiMode: boolean;
isAlternateBuffer: boolean;
showIsAlternateBufferHint: boolean;
debugMessage: string;
quittingMessages: HistoryItem[] | null;
isSettingsDialogOpen: boolean;
@@ -11,49 +11,47 @@ import {
isAlternateBufferEnabled,
} from './useAlternateBuffer.js';
import type { Config } from '@google/gemini-cli-core';
import { useUIState } from '../contexts/UIStateContext.js';
vi.mock('../contexts/ConfigContext.js', () => ({
useConfig: vi.fn(),
}));
vi.mock('../contexts/UIStateContext.js');
const mockUseConfig = vi.mocked(
await import('../contexts/ConfigContext.js').then((m) => m.useConfig),
);
const mockUseUIState = vi.mocked(useUIState);
describe('useAlternateBuffer', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return false when config.getUseAlternateBuffer returns false', async () => {
mockUseConfig.mockReturnValue({
getUseAlternateBuffer: () => false,
} as unknown as ReturnType<typeof mockUseConfig>);
it('should return false when uiState.isAlternateBuffer is false', async () => {
mockUseUIState.mockReturnValue({
isAlternateBuffer: false,
} as unknown as ReturnType<typeof mockUseUIState>);
const { result } = await renderHook(() => useAlternateBuffer());
expect(result.current).toBe(false);
});
it('should return true when config.getUseAlternateBuffer returns true', async () => {
mockUseConfig.mockReturnValue({
getUseAlternateBuffer: () => true,
} as unknown as ReturnType<typeof mockUseConfig>);
it('should return true when uiState.isAlternateBuffer is true', async () => {
mockUseUIState.mockReturnValue({
isAlternateBuffer: true,
} as unknown as ReturnType<typeof mockUseUIState>);
const { result } = await renderHook(() => useAlternateBuffer());
expect(result.current).toBe(true);
});
it('should return the immutable config value, not react to settings changes', async () => {
const mockConfig = {
getUseAlternateBuffer: () => true,
} as unknown as ReturnType<typeof mockUseConfig>;
mockUseConfig.mockReturnValue(mockConfig);
it('should react to state changes', async () => {
mockUseUIState.mockReturnValue({
isAlternateBuffer: false,
} as unknown as ReturnType<typeof mockUseUIState>);
const { result, rerender } = await renderHook(() => useAlternateBuffer());
// Value should remain true even after rerender
expect(result.current).toBe(true);
expect(result.current).toBe(false);
mockUseUIState.mockReturnValue({
isAlternateBuffer: true,
} as unknown as ReturnType<typeof mockUseUIState>);
rerender();
@@ -4,14 +4,14 @@
* 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';
export const isAlternateBufferEnabled = (config: Config): boolean =>
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 => {
const config = useConfig();
return isAlternateBufferEnabled(config);
const uiState = useUIState();
return uiState.isAlternateBuffer;
};
@@ -7,93 +7,76 @@
import { act, useCallback } from 'react';
import { vi } from 'vitest';
import { render } from '../../test-utils/render.js';
import {
useConsoleMessages,
useErrorCount,
initializeConsoleStore,
} from './useConsoleMessages.js';
import { coreEvents } from '@google/gemini-cli-core';
import { useConsoleMessages } from './useConsoleMessages.js';
import { CoreEvent, type ConsoleLogPayload } from '@google/gemini-cli-core';
// Mock coreEvents
let consoleLogHandler: ((payload: ConsoleLogPayload) => void) | undefined;
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual = await importOriginal();
const handlers = new Map<string, (payload: unknown) => void>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actual = (await importOriginal()) as any;
return {
...(actual as Record<string, unknown>),
...actual,
coreEvents: {
...((actual as Record<string, unknown>)['coreEvents'] as Record<
string,
unknown
>),
on: vi.fn((event: string, handler: (payload: unknown) => void) => {
handlers.set(event, handler);
on: vi.fn((event, handler) => {
if (event === CoreEvent.ConsoleLog) {
consoleLogHandler = handler;
}
}),
off: vi.fn((event: string) => {
handlers.delete(event);
off: vi.fn((event) => {
if (event === CoreEvent.ConsoleLog) {
consoleLogHandler = undefined;
}
}),
// Helper for testing to trigger the handlers
_trigger: (event: string, payload: unknown) => {
handlers.get(event)?.(payload);
},
emitConsoleLog: vi.fn(),
},
};
});
describe('useConsoleMessages', () => {
let unmounts: Array<() => void> = [];
beforeEach(() => {
vi.useFakeTimers();
initializeConsoleStore();
consoleLogHandler = undefined;
});
afterEach(() => {
for (const unmount of unmounts) {
try {
unmount();
} catch (_e) {
// Ignore unmount errors
}
}
unmounts = [];
vi.runOnlyPendingTimers();
vi.useRealTimers();
vi.restoreAllMocks();
});
const useTestableConsoleMessages = () => {
const consoleMessages = useConsoleMessages();
const { ...rest } = useConsoleMessages();
const log = useCallback((content: string) => {
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'log', content });
if (consoleLogHandler) {
consoleLogHandler({ type: 'log', content });
}
}, []);
const error = useCallback((content: string) => {
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'error', content });
}, []);
const clearConsoleMessages = useCallback(() => {
initializeConsoleStore();
if (consoleLogHandler) {
consoleLogHandler({ type: 'error', content });
}
}, []);
return {
consoleMessages,
...rest,
log,
error,
clearConsoleMessages,
clearConsoleMessages: rest.clearConsoleMessages,
};
};
const renderConsoleMessagesHook = async () => {
let hookResult: ReturnType<typeof useTestableConsoleMessages> | undefined;
let hookResult: ReturnType<typeof useTestableConsoleMessages>;
function TestComponent() {
hookResult = useTestableConsoleMessages();
return null;
}
const { unmount } = await render(<TestComponent />);
unmounts.push(unmount);
return {
result: {
get current() {
return hookResult!;
return hookResult;
},
},
unmount,
@@ -110,7 +93,10 @@ describe('useConsoleMessages', () => {
act(() => {
result.current.log('Test message');
vi.runAllTimers();
});
await act(async () => {
await vi.advanceTimersByTimeAsync(60);
});
expect(result.current.consoleMessages).toEqual([
@@ -125,7 +111,10 @@ describe('useConsoleMessages', () => {
result.current.log('Test message');
result.current.log('Test message');
result.current.log('Test message');
vi.runAllTimers();
});
await act(async () => {
await vi.advanceTimersByTimeAsync(60);
});
expect(result.current.consoleMessages).toEqual([
@@ -139,7 +128,10 @@ describe('useConsoleMessages', () => {
act(() => {
result.current.log('First message');
result.current.error('Second message');
vi.runAllTimers();
});
await act(async () => {
await vi.advanceTimersByTimeAsync(60);
});
expect(result.current.consoleMessages).toEqual([
@@ -147,85 +139,53 @@ describe('useConsoleMessages', () => {
{ type: 'error', content: 'Second message', count: 1 },
]);
});
});
describe('useErrorCount', () => {
let unmounts: Array<() => void> = [];
beforeEach(() => {
vi.useFakeTimers();
initializeConsoleStore();
});
afterEach(() => {
for (const unmount of unmounts) {
try {
unmount();
} catch (_e) {
// Ignore unmount errors
}
}
unmounts = [];
vi.runOnlyPendingTimers();
vi.useRealTimers();
vi.restoreAllMocks();
});
const renderErrorCountHook = async () => {
let hookResult: ReturnType<typeof useErrorCount>;
function TestComponent() {
hookResult = useErrorCount();
return null;
}
const { unmount } = await render(<TestComponent />);
unmounts.push(unmount);
return {
result: {
get current() {
return hookResult;
},
},
unmount,
};
};
it('should initialize with an error count of 0', async () => {
const { result } = await renderErrorCountHook();
expect(result.current.errorCount).toBe(0);
});
it('should increment error count when an error is logged', async () => {
const { result } = await renderErrorCountHook();
act(() => {
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'error', content: 'error' });
vi.runAllTimers();
});
expect(result.current.errorCount).toBe(1);
});
it('should not increment error count for non-error logs', async () => {
const { result } = await renderErrorCountHook();
act(() => {
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'log', content: 'log' });
vi.runAllTimers();
});
expect(result.current.errorCount).toBe(0);
});
it('should clear the error count', async () => {
const { result } = await renderErrorCountHook();
act(() => {
// @ts-expect-error - internal testing helper
coreEvents._trigger('console-log', { type: 'error', content: 'error' });
vi.runAllTimers();
});
expect(result.current.errorCount).toBe(1);
it('should clear all messages when clearConsoleMessages is called', async () => {
const { result } = await renderConsoleMessagesHook();
act(() => {
result.current.clearErrorCount();
result.current.log('A message');
});
expect(result.current.errorCount).toBe(0);
await act(async () => {
await vi.advanceTimersByTimeAsync(60);
});
expect(result.current.consoleMessages).toHaveLength(1);
act(() => {
result.current.clearConsoleMessages();
});
expect(result.current.consoleMessages).toHaveLength(0);
});
it('should clear the pending timeout when clearConsoleMessages is called', async () => {
const { result } = await renderConsoleMessagesHook();
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
act(() => {
result.current.log('A message');
});
act(() => {
result.current.clearConsoleMessages();
});
expect(clearTimeoutSpy).toHaveBeenCalled();
// clearTimeoutSpy.mockRestore() is handled by afterEach restoreAllMocks
});
it('should clean up the timeout on unmount', async () => {
const { result, unmount } = await renderConsoleMessagesHook();
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
act(() => {
result.current.log('A message');
});
unmount();
expect(clearTimeoutSpy).toHaveBeenCalled();
});
});
+200 -157
View File
@@ -4,7 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useCallback, useSyncExternalStore } from 'react';
import {
useCallback,
useEffect,
useReducer,
useRef,
startTransition,
} from 'react';
import type { ConsoleMessageItem } from '../types.js';
import {
coreEvents,
@@ -12,170 +18,207 @@ import {
type ConsoleLogPayload,
} from '@google/gemini-cli-core';
export interface UseConsoleMessagesReturn {
consoleMessages: ConsoleMessageItem[];
clearConsoleMessages: () => void;
}
type Action =
| { type: 'ADD_MESSAGES'; payload: ConsoleMessageItem[] }
| { type: 'CLEAR' };
function consoleMessagesReducer(
state: ConsoleMessageItem[],
action: Action,
): ConsoleMessageItem[] {
const MAX_CONSOLE_MESSAGES = 1000;
switch (action.type) {
case 'ADD_MESSAGES': {
const newMessages = [...state];
for (const queuedMessage of action.payload) {
const lastMessage = newMessages[newMessages.length - 1];
if (
lastMessage &&
lastMessage.type === queuedMessage.type &&
lastMessage.content === queuedMessage.content
) {
// Create a new object for the last message to ensure React detects
// the change, preventing mutation of the existing state object.
newMessages[newMessages.length - 1] = {
...lastMessage,
count: lastMessage.count + 1,
};
} else {
newMessages.push({ ...queuedMessage, count: 1 });
}
}
// Limit the number of messages to prevent memory issues
if (newMessages.length > MAX_CONSOLE_MESSAGES) {
return newMessages.slice(newMessages.length - MAX_CONSOLE_MESSAGES);
}
return newMessages;
}
case 'CLEAR':
return [];
default:
return state;
}
}
export function useConsoleMessages(): UseConsoleMessagesReturn {
const [consoleMessages, dispatch] = useReducer(consoleMessagesReducer, []);
const messageQueueRef = useRef<ConsoleMessageItem[]>([]);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const isProcessingRef = useRef(false);
const processQueue = useCallback(() => {
if (messageQueueRef.current.length > 0) {
isProcessingRef.current = true;
const messagesToProcess = messageQueueRef.current;
messageQueueRef.current = [];
startTransition(() => {
dispatch({ type: 'ADD_MESSAGES', payload: messagesToProcess });
});
}
timeoutRef.current = null;
}, []);
const handleNewMessage = useCallback(
(message: ConsoleMessageItem) => {
messageQueueRef.current.push(message);
if (!isProcessingRef.current && !timeoutRef.current) {
// Batch updates using a timeout. 50ms is a reasonable delay to batch
// rapid-fire messages without noticeable lag while avoiding React update
// queue flooding.
timeoutRef.current = setTimeout(processQueue, 50);
}
},
[processQueue],
);
// Once the updated consoleMessages have been committed to the screen,
// we can safely process the next batch of queued messages if any exist.
// This completely eliminates overlapping concurrent updates to this state.
useEffect(() => {
isProcessingRef.current = false;
if (messageQueueRef.current.length > 0 && !timeoutRef.current) {
timeoutRef.current = setTimeout(processQueue, 50);
}
}, [consoleMessages, processQueue]);
useEffect(() => {
const handleConsoleLog = (payload: ConsoleLogPayload) => {
let content = payload.content;
const MAX_CONSOLE_MSG_LENGTH = 10000;
if (content.length > MAX_CONSOLE_MSG_LENGTH) {
content =
content.slice(0, MAX_CONSOLE_MSG_LENGTH) +
`... [Truncated ${content.length - MAX_CONSOLE_MSG_LENGTH} characters]`;
}
handleNewMessage({
type: payload.type,
content,
count: 1,
});
};
const handleOutput = (payload: {
isStderr: boolean;
chunk: Uint8Array | string;
}) => {
let content =
typeof payload.chunk === 'string'
? payload.chunk
: new TextDecoder().decode(payload.chunk);
const MAX_OUTPUT_CHUNK_LENGTH = 10000;
if (content.length > MAX_OUTPUT_CHUNK_LENGTH) {
content =
content.slice(0, MAX_OUTPUT_CHUNK_LENGTH) +
`... [Truncated ${content.length - MAX_OUTPUT_CHUNK_LENGTH} characters]`;
}
// It would be nice if we could show stderr as 'warn' but unfortunately
// we log non warning info to stderr before the app starts so that would
// be misleading.
handleNewMessage({ type: 'log', content, count: 1 });
};
coreEvents.on(CoreEvent.ConsoleLog, handleConsoleLog);
coreEvents.on(CoreEvent.Output, handleOutput);
return () => {
coreEvents.off(CoreEvent.ConsoleLog, handleConsoleLog);
coreEvents.off(CoreEvent.Output, handleOutput);
};
}, [handleNewMessage]);
const clearConsoleMessages = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
messageQueueRef.current = [];
isProcessingRef.current = true;
startTransition(() => {
dispatch({ type: 'CLEAR' });
});
}, []);
// Cleanup on unmount
useEffect(
() => () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
},
[],
);
return { consoleMessages, clearConsoleMessages };
}
export interface UseErrorCountReturn {
errorCount: number;
clearErrorCount: () => void;
}
// --- Global Console Store ---
const MAX_CONSOLE_MESSAGES = 1000;
let globalConsoleMessages: ConsoleMessageItem[] = [];
let globalErrorCount = 0;
const listeners = new Set<() => void>();
let messageQueue: ConsoleMessageItem[] = [];
let timeoutId: NodeJS.Timeout | null = null;
/**
* Initializes the global console store and subscribes to coreEvents.
* Acts as a safe reset function, making it idempotent and useful for test isolation.
* Must be called during application startup.
*/
export function initializeConsoleStore() {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
messageQueue = [];
globalConsoleMessages = [];
globalErrorCount = 0;
notifyListeners();
// Safely detach first to ensure idempotency and prevent listener leaks
coreEvents.off(CoreEvent.ConsoleLog, handleConsoleLog);
coreEvents.off(CoreEvent.Output, handleOutput);
coreEvents.on(CoreEvent.ConsoleLog, handleConsoleLog);
coreEvents.on(CoreEvent.Output, handleOutput);
}
function notifyListeners() {
for (const listener of listeners) {
listener();
}
}
function processQueue() {
if (messageQueue.length === 0) return;
// Create a new array to trigger React updates
const newMessages = [...globalConsoleMessages];
for (const queuedMessage of messageQueue) {
if (queuedMessage.type === 'error') {
globalErrorCount++;
}
// Coalesce consecutive identical messages
const prev = newMessages[newMessages.length - 1];
if (
prev &&
prev.type === queuedMessage.type &&
prev.content === queuedMessage.content
) {
newMessages[newMessages.length - 1] = {
...prev,
count: prev.count + 1,
};
} else {
newMessages.push({ ...queuedMessage, count: 1 });
}
}
globalConsoleMessages =
newMessages.length > MAX_CONSOLE_MESSAGES
? newMessages.slice(-MAX_CONSOLE_MESSAGES)
: newMessages;
messageQueue = [];
timeoutId = null;
notifyListeners();
}
function handleNewMessage(message: ConsoleMessageItem) {
messageQueue.push(message);
if (!timeoutId) {
// Batch updates using a timeout. 50ms is a reasonable delay to batch
// rapid-fire messages without noticeable lag while avoiding React update
// queue flooding.
timeoutId = setTimeout(processQueue, 50);
}
}
// --- Subscription API for useSyncExternalStore ---
function subscribe(listener: () => void) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
function getConsoleMessagesSnapshot() {
return globalConsoleMessages;
}
function getErrorCountSnapshot() {
return globalErrorCount;
}
// --- Core Event Listeners (Always active at module level) ---
const handleConsoleLog = (payload: ConsoleLogPayload) => {
let content = payload.content;
const MAX_CONSOLE_MSG_LENGTH = 10000;
if (content.length > MAX_CONSOLE_MSG_LENGTH) {
content =
content.slice(0, MAX_CONSOLE_MSG_LENGTH) +
`... [Truncated ${content.length - MAX_CONSOLE_MSG_LENGTH} characters]`;
}
handleNewMessage({
type: payload.type,
content,
count: 1,
});
};
const handleOutput = (payload: {
isStderr: boolean;
chunk: Uint8Array | string;
}) => {
let content =
typeof payload.chunk === 'string'
? payload.chunk
: new TextDecoder().decode(payload.chunk);
const MAX_OUTPUT_CHUNK_LENGTH = 10000;
if (content.length > MAX_OUTPUT_CHUNK_LENGTH) {
content =
content.slice(0, MAX_OUTPUT_CHUNK_LENGTH) +
`... [Truncated ${content.length - MAX_OUTPUT_CHUNK_LENGTH} characters]`;
}
handleNewMessage({ type: 'log', content, count: 1 });
};
/**
* Hook to access the global console message history.
* Decoupled from any component lifecycle to ensure history is preserved even
* when the UI is unmounted.
*/
export function useConsoleMessages(): ConsoleMessageItem[] {
return useSyncExternalStore(subscribe, getConsoleMessagesSnapshot);
}
/**
* Hook to access the global error count.
* Uses the same external store as useConsoleMessages for consistency.
*/
export function useErrorCount(): UseErrorCountReturn {
const errorCount = useSyncExternalStore(subscribe, getErrorCountSnapshot);
const [errorCount, dispatch] = useReducer(
(state: number, action: 'INCREMENT' | 'CLEAR') => {
switch (action) {
case 'INCREMENT':
return state + 1;
case 'CLEAR':
return 0;
default:
return state;
}
},
0,
);
useEffect(() => {
const handleConsoleLog = (payload: ConsoleLogPayload) => {
if (payload.type === 'error') {
startTransition(() => {
dispatch('INCREMENT');
});
}
};
coreEvents.on(CoreEvent.ConsoleLog, handleConsoleLog);
return () => {
coreEvents.off(CoreEvent.ConsoleLog, handleConsoleLog);
};
}, []);
const clearErrorCount = useCallback(() => {
globalErrorCount = 0;
notifyListeners();
startTransition(() => {
dispatch('CLEAR');
});
}, []);
return { errorCount, clearErrorCount };
-10
View File
@@ -14,7 +14,6 @@ import { useTips } from './useTips.js';
describe('useTips()', () => {
beforeEach(() => {
vi.clearAllMocks();
persistentStateMock.setData({ tipsShown: undefined });
});
it('should return false and call set(1) if state is undefined', async () => {
@@ -45,13 +44,4 @@ describe('useTips()', () => {
expect(persistentStateMock.set).not.toHaveBeenCalled();
expect(persistentStateMock.get('tipsShown')).toBe(10);
});
it('should NOT increment if isVisible is false', async () => {
const { result } = await renderHookWithProviders(() =>
useTips({ isVisible: false }),
);
expect(result.current.showTips).toBe(true);
expect(persistentStateMock.set).not.toHaveBeenCalled();
});
});
+3 -8
View File
@@ -11,21 +11,16 @@ interface UseTipsResult {
showTips: boolean;
}
interface UseTipsOptions {
isVisible?: boolean;
}
export function useTips(options: UseTipsOptions = {}): UseTipsResult {
const { isVisible = true } = options;
export function useTips(): UseTipsResult {
const [tipsCount] = useState(() => persistentState.get('tipsShown') ?? 0);
const showTips = tipsCount < 10;
useEffect(() => {
if (showTips && isVisible) {
if (showTips) {
persistentState.set('tipsShown', tipsCount + 1);
}
}, [tipsCount, showTips, isVisible]);
}, [tipsCount, showTips]);
return { showTips };
}
+3
View File
@@ -95,6 +95,7 @@ export enum Command {
RESTART_APP = 'app.restart',
SUSPEND_APP = 'app.suspend',
SHOW_SHELL_INPUT_UNFOCUS_WARNING = 'app.showShellUnfocusWarning',
TOGGLE_BUFFER_MODE = 'app.toggleBufferMode',
// Background Shell Controls
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.SUSPEND_APP, [new KeyBinding('ctrl+z')]],
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING, [new KeyBinding('tab')]],
[Command.TOGGLE_BUFFER_MODE, [new KeyBinding('alt+a')]],
// Background Shell Controls
[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.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.',
[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.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.',
@@ -1,8 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="241" viewBox="0 0 920 241">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="343" viewBox="0 0 920 343">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="241" fill="#000000" />
<rect width="920" height="343" fill="#000000" />
<g transform="translate(10, 10)">
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
@@ -21,16 +21,25 @@
<text x="90" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="0" y="138" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">google_web_search</text>
<text x="855" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="189" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Searching...</text>
<text x="855" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="0" y="155" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="172" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="172" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="172" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="189" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="189" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="189" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="206" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="223" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
<text x="0" y="240" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="257" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">google_web_search</text>
<text x="855" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="291" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Searching...</text>
<text x="855" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -1,8 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="241" viewBox="0 0 920 241">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="343" viewBox="0 0 920 343">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="241" fill="#000000" />
<rect width="920" height="343" fill="#000000" />
<g transform="translate(10, 10)">
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
@@ -21,16 +21,25 @@
<text x="90" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="0" y="138" fill="#87afff" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="155" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">run_shell_command</text>
<text x="855" y="155" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="172" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="189" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Running command...</text>
<text x="855" y="189" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#87afff" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="0" y="155" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="172" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="172" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="172" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="189" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="189" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="189" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="206" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="223" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
<text x="0" y="240" fill="#87afff" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="257" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="257" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">run_shell_command</text>
<text x="855" y="257" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="274" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="291" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Running command...</text>
<text x="855" y="291" fill="#87afff" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#87afff" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -1,8 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="241" viewBox="0 0 920 241">
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="343" viewBox="0 0 920 343">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="241" fill="#000000" />
<rect width="920" height="343" fill="#000000" />
<g transform="translate(10, 10)">
<text x="9" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
@@ -21,16 +21,25 @@
<text x="90" y="70" fill="#ffffff" textLength="297" lengthAdjust="spacingAndGlyphs"> ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀</text>
<text x="9" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">Gemini CLI</text>
<text x="99" y="104" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs"> v1.2.3</text>
<text x="0" y="138" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">google_web_search</text>
<text x="855" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="189" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Searching...</text>
<text x="855" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
<text x="0" y="155" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">Tips for getting started:</text>
<text x="0" y="172" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs">1. Create </text>
<text x="90" y="172" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs" font-weight="bold">GEMINI.md</text>
<text x="171" y="172" fill="#ffffff" textLength="333" lengthAdjust="spacingAndGlyphs"> files to customize your interactions</text>
<text x="0" y="189" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">2. </text>
<text x="27" y="189" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">/help</text>
<text x="72" y="189" fill="#ffffff" textLength="189" lengthAdjust="spacingAndGlyphs"> for more information</text>
<text x="0" y="206" fill="#ffffff" textLength="450" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands</text>
<text x="0" y="223" fill="#ffffff" textLength="315" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results</text>
<text x="0" y="240" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="257" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold">google_web_search</text>
<text x="855" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="855" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="291" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs">Searching...</text>
<text x="855" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#ffffaf" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -9,6 +9,12 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho
Gemini CLI v1.2.3
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊶ google_web_search │
│ │
@@ -25,6 +31,12 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho
Gemini CLI v1.2.3
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊶ run_shell_command │
│ │
@@ -41,6 +53,12 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho
Gemini CLI v1.2.3
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊶ google_web_search │
│ │
+2 -2
View File
@@ -299,8 +299,8 @@
"hideTips": {
"title": "Hide Tips",
"description": "Hide helpful tips in the UI",
"markdownDescription": "Hide helpful tips in the UI\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"markdownDescription": "Hide helpful tips in the UI\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"escapePastedAtSymbols": {