diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md
index ecc31d609b..7a433caf8a 100644
--- a/docs/get-started/configuration.md
+++ b/docs/get-started/configuration.md
@@ -232,7 +232,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`ui.useAlternateBuffer`** (boolean):
- **Description:** Use an alternate screen buffer for the UI, preserving shell
history.
- - **Default:** `false`
+ - **Default:** `true`
- **Requires restart:** Yes
- **`ui.customWittyPhrases`** (array):
diff --git a/integration-tests/extensions-reload.test.ts b/integration-tests/extensions-reload.test.ts
index d28097f2c0..10a06b1bab 100644
--- a/integration-tests/extensions-reload.test.ts
+++ b/integration-tests/extensions-reload.test.ts
@@ -84,8 +84,11 @@ describe('extension reloading', () => {
await run.expectText('- hello');
// Update the extension, expect the list to update, and mcp servers as well.
- await run.sendText('/extensions update test-extension');
- await run.type('\r');
+ await run.sendKeys('/extensions update test-extension');
+ await run.expectText('/extensions update test-extension');
+ await run.sendKeys('\r');
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ await run.sendKeys('\r');
await run.expectText(
` * test-server (remote): http://localhost:${portB}/mcp`,
);
diff --git a/integration-tests/test-helper.ts b/integration-tests/test-helper.ts
index a202caf9ee..507ffbccfd 100644
--- a/integration-tests/test-helper.ts
+++ b/integration-tests/test-helper.ts
@@ -192,7 +192,12 @@ export class InteractiveRun {
timeout,
200,
);
- expect(found, `Did not find expected text: "${text}"`).toBe(true);
+ expect(
+ found,
+ `Did not find expected text: "${text}". Output was:\n${stripAnsi(
+ this.output,
+ )}`,
+ ).toBe(true);
}
// This types slowly to make sure command is correct, but only work for short
@@ -1004,7 +1009,7 @@ export class TestRig {
const options: pty.IPtyForkOptions = {
name: 'xterm-color',
cols: 80,
- rows: 24,
+ rows: 80,
cwd: this.testDir!,
env: Object.fromEntries(
Object.entries(env).filter(([, v]) => v !== undefined),
diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index 0a715906de..5f070d1a26 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -497,7 +497,7 @@ const SETTINGS_SCHEMA = {
label: 'Use Alternate Screen Buffer',
category: 'UI',
requiresRestart: true,
- default: false,
+ default: true,
description:
'Use an alternate screen buffer for the UI, preserving shell history.',
showInDialog: true,
diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx
index ffa5996630..ecb117de4c 100644
--- a/packages/cli/src/gemini.test.tsx
+++ b/packages/cli/src/gemini.test.tsx
@@ -529,6 +529,7 @@ describe('startInteractiveUI', () => {
// Verify render options
expect(options).toEqual({
+ alternateBuffer: true,
exitOnCtrlC: false,
isScreenReaderEnabled: false,
onRender: expect.any(Function),
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 4d96c64daf..c9225bbf69 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -76,6 +76,7 @@ import { requestConsentNonInteractive } from './config/extensions/consent.js';
import { disableMouseEvents, enableMouseEvents } from './ui/utils/mouse.js';
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
import ansiEscapes from 'ansi-escapes';
+import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
const SLOW_RENDER_MS = 200;
@@ -170,7 +171,8 @@ export async function startInteractiveUI(
process.stdout.write('\x1b[?7l');
}
- const mouseEventsEnabled = settings.merged.ui?.useAlternateBuffer === true;
+ const useAlternateBuffer = isAlternateBufferEnabled(settings);
+ const mouseEventsEnabled = useAlternateBuffer;
if (mouseEventsEnabled) {
enableMouseEvents();
}
@@ -236,7 +238,7 @@ export async function startInteractiveUI(
recordSlowRender(config, renderTime);
}
},
- alternateBuffer: settings.merged.ui?.useAlternateBuffer,
+ alternateBuffer: useAlternateBuffer,
},
);
@@ -437,7 +439,7 @@ export async function main() {
// input showing up in the output.
process.stdin.setRawMode(true);
- if (settings.merged.ui?.useAlternateBuffer) {
+ if (isAlternateBufferEnabled(settings)) {
process.stdout.write(ansiEscapes.enterAlternativeScreen);
// Ink will cleanup so there is no need for us to manually cleanup.
diff --git a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap
index a46007c568..e9d55cc795 100644
--- a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap
@@ -16,16 +16,16 @@ Tips for getting started:
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
-╭──────────────────────────────────────────────────────────────────────────────╮
-│ ✓ tool1 Description for tool 1 │
-│ │
-╰──────────────────────────────────────────────────────────────────────────────╯
-╭──────────────────────────────────────────────────────────────────────────────╮
-│ ✓ tool2 Description for tool 2 │
-│ │
-╰──────────────────────────────────────────────────────────────────────────────╯
-╭──────────────────────────────────────────────────────────────────────────────╮
-│ o tool3 Description for tool 3 │
-│ │
-╰──────────────────────────────────────────────────────────────────────────────╯"
+╭─────────────────────────────────────────────────────────────────────────────╮
+│ ✓ tool1 Description for tool 1 │
+│ │
+╰─────────────────────────────────────────────────────────────────────────────╯
+╭─────────────────────────────────────────────────────────────────────────────╮
+│ ✓ tool2 Description for tool 2 │
+│ │
+╰─────────────────────────────────────────────────────────────────────────────╯
+╭─────────────────────────────────────────────────────────────────────────────╮
+│ o tool3 Description for tool 3 │
+│ │
+╰─────────────────────────────────────────────────────────────────────────────╯"
`;
diff --git a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
index 292e3b9bc3..83c0fb0dba 100644
--- a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
@@ -1,11 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[` > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `" ...s/to/make/it/long no sandbox gemini-pro (100%)"`;
+exports[` > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `" ...s/to/make/it/long no sandbox gemini-pro (100%)"`;
-exports[` > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `" ...directories/to/make/it/long no sandbox (see /docs) gemini-pro (100% context left)"`;
+exports[` > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `" ...irectories/to/make/it/long no sandbox (see /docs) gemini-pro (100% context left)"`;
-exports[` > footer configuration filtering (golden snapshots) > renders footer with CWD and model info hidden to test alignment (only sandbox visible) > footer-only-sandbox 1`] = `" no sandbox (see /docs)"`;
+exports[` > footer configuration filtering (golden snapshots) > renders footer with CWD and model info hidden to test alignment (only sandbox visible) > footer-only-sandbox 1`] = `" no sandbox (see /docs)"`;
exports[` > footer configuration filtering (golden snapshots) > renders footer with all optional sections hidden (minimal footer) > footer-minimal 1`] = `""`;
-exports[` > footer configuration filtering (golden snapshots) > renders footer with only model info hidden (partial filtering) > footer-no-model 1`] = `" ...directories/to/make/it/long no sandbox (see /docs)"`;
+exports[` > footer configuration filtering (golden snapshots) > renders footer with only model info hidden (partial filtering) > footer-no-model 1`] = `" ...irectories/to/make/it/long no sandbox (see /docs)"`;
diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap
index e71999c99d..8391f25e88 100644
--- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap
@@ -1,57 +1,57 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-collapsed-match 1`] = `
-"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-│ (r:) Type your message or @path/to/file │
-╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+"╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ (r:) Type your message or @path/to/file │
+╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll →
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
..."
`;
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-expanded-match 1`] = `
-"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-│ (r:) Type your message or @path/to/file │
-╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+"╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ (r:) Type your message or @path/to/file │
+╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ←
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
llllllllllllllllllllllllllllllllllllllllllllllllll"
`;
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = `
-"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-│ (r:) commit │
-╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+"╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ (r:) commit │
+╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
git commit -m "feat: add search" in src/app"
`;
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 1`] = `
-"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-│ (r:) commit │
-╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+"╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ (r:) commit │
+╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
git commit -m "feat: add search" in src/app"
`;
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
-"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-│ > Type your message or @path/to/file │
-╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
+"╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ > Type your message or @path/to/file │
+╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = `
-"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-│ ! Type your message or @path/to/file │
-╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
+"╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ ! Type your message or @path/to/file │
+╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`InputPrompt > snapshots > should render correctly in yolo mode 1`] = `
-"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-│ * Type your message or @path/to/file │
-╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
+"╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ * Type your message or @path/to/file │
+╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = `
-"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
-│ > Type your message or @path/to/file │
-╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
+"╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ > Type your message or @path/to/file │
+╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
diff --git a/packages/cli/src/ui/hooks/useAlternateBuffer.ts b/packages/cli/src/ui/hooks/useAlternateBuffer.ts
index 5b6a55b215..efa8fbf3cf 100644
--- a/packages/cli/src/ui/hooks/useAlternateBuffer.ts
+++ b/packages/cli/src/ui/hooks/useAlternateBuffer.ts
@@ -5,8 +5,12 @@
*/
import { useSettings } from '../contexts/SettingsContext.js';
+import type { LoadedSettings } from '../../config/settings.js';
+
+export const isAlternateBufferEnabled = (settings: LoadedSettings): boolean =>
+ settings.merged.ui?.useAlternateBuffer !== false;
export const useAlternateBuffer = (): boolean => {
const settings = useSettings();
- return settings.merged.ui?.useAlternateBuffer ?? false;
+ return isAlternateBufferEnabled(settings);
};
diff --git a/packages/cli/src/ui/utils/CodeColorizer.test.tsx b/packages/cli/src/ui/utils/CodeColorizer.test.tsx
new file mode 100644
index 0000000000..641567c00b
--- /dev/null
+++ b/packages/cli/src/ui/utils/CodeColorizer.test.tsx
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import { colorizeCode } from './CodeColorizer.js';
+import { renderWithProviders } from '../../test-utils/render.js';
+import { LoadedSettings } from '../../config/settings.js';
+
+describe('colorizeCode', () => {
+ it('renders empty lines correctly when useAlternateBuffer is true', () => {
+ const code = 'line 1\n\nline 3';
+ const settings = new LoadedSettings(
+ { path: '', settings: {}, originalSettings: {} },
+ { path: '', settings: {}, originalSettings: {} },
+ {
+ path: '',
+ settings: { ui: { useAlternateBuffer: true, showLineNumbers: false } },
+ originalSettings: {
+ ui: { useAlternateBuffer: true, showLineNumbers: false },
+ },
+ },
+ { path: '', settings: {}, originalSettings: {} },
+ true,
+ new Set(),
+ );
+
+ const result = colorizeCode({
+ code,
+ language: 'javascript',
+ maxWidth: 80,
+ settings,
+ hideLineNumbers: true,
+ });
+
+ const { lastFrame } = renderWithProviders(<>{result}>);
+ // We expect the output to preserve the empty line.
+ // If the bug exists, it might look like "line 1\nline 3"
+ // If fixed, it should look like "line 1\n \nline 3" (if we use space) or just have the newline.
+
+ // We can check if the output matches the code (ignoring color codes if any, but lastFrame returns plain text usually unless configured otherwise)
+ // Actually lastFrame() returns string with ANSI codes stripped by default in some setups, or not.
+ // But ink-testing-library usually returns the visual representation.
+
+ expect(lastFrame()).toMatch(/line 1\s*\n\s*\n\s*line 3/);
+ });
+});
diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx
index 870b82535a..ae05eb8ea2 100644
--- a/packages/cli/src/ui/utils/CodeColorizer.tsx
+++ b/packages/cli/src/ui/utils/CodeColorizer.tsx
@@ -22,6 +22,7 @@ import {
} from '../components/shared/MaxSizedBox.js';
import type { LoadedSettings } from '../../config/settings.js';
import { debugLogger } from '@google/gemini-cli-core';
+import { isAlternateBufferEnabled } from '../hooks/useAlternateBuffer.js';
// Configure theming and parsing utilities.
const lowlight = createLowlight(common);
@@ -150,7 +151,7 @@ export function colorizeCode({
? false
: (settings?.merged.ui?.showLineNumbers ?? true);
- const useMaxSizedBox = settings?.merged.ui?.useAlternateBuffer !== true;
+ const useMaxSizedBox = !isAlternateBufferEnabled(settings);
try {
// Render the HAST tree using the adapted theme
// Apply the theme's default foreground color to the top-level Text element
@@ -160,10 +161,7 @@ export function colorizeCode({
let hiddenLinesCount = 0;
// Optimization to avoid highlighting lines that cannot possibly be displayed.
- if (
- availableHeight !== undefined &&
- settings?.merged.ui?.useAlternateBuffer === false
- ) {
+ if (availableHeight !== undefined && useMaxSizedBox) {
availableHeight = Math.max(availableHeight, MINIMUM_MAX_HEIGHT);
if (lines.length > availableHeight) {
const sliceIndex = lines.length - availableHeight;
@@ -180,7 +178,7 @@ export function colorizeCode({
);
return (
-
+
{/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
{showLineNumbers && useMaxSizedBox && (
@@ -238,7 +236,7 @@ export function colorizeCode({
const lines = codeToHighlight.split('\n');
const padWidth = String(lines.length).length; // Calculate padding width based on number of lines
const fallbackLines = lines.map((line, index) => (
-
+
{/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
{showLineNumbers && useMaxSizedBox && (
diff --git a/packages/cli/src/ui/utils/ui-sizing.ts b/packages/cli/src/ui/utils/ui-sizing.ts
index 84a3efd7ef..be85fe88b9 100644
--- a/packages/cli/src/ui/utils/ui-sizing.ts
+++ b/packages/cli/src/ui/utils/ui-sizing.ts
@@ -6,6 +6,7 @@
import { lerp } from '../../utils/math.js';
import { type LoadedSettings } from '../../config/settings.js';
+import { isAlternateBufferEnabled } from '../hooks/useAlternateBuffer.js';
const getMainAreaWidthInternal = (terminalWidth: number): number => {
if (terminalWidth <= 80) {
@@ -27,7 +28,7 @@ export const calculateMainAreaWidth = (
settings: LoadedSettings,
): number => {
if (settings.merged.ui?.useFullWidth !== false) {
- if (settings.merged.ui?.useAlternateBuffer) {
+ if (isAlternateBufferEnabled(settings)) {
return terminalWidth - 1;
}
return terminalWidth;
diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json
index c8a10c5ecf..66314d3704 100644
--- a/schemas/settings.schema.json
+++ b/schemas/settings.schema.json
@@ -278,8 +278,8 @@
"useAlternateBuffer": {
"title": "Use Alternate Screen Buffer",
"description": "Use an alternate screen buffer for the UI, preserving shell history.",
- "markdownDescription": "Use an alternate screen buffer for the UI, preserving shell history.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `false`",
- "default": false,
+ "markdownDescription": "Use an alternate screen buffer for the UI, preserving shell history.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`",
+ "default": true,
"type": "boolean"
},
"customWittyPhrases": {