feat(ui) Make useAlternateBuffer the default (#12976)

This commit is contained in:
Jacob Richman
2025-11-12 21:17:46 -08:00
committed by jacob314
parent 046b3011c2
commit b37c674f2b
14 changed files with 123 additions and 60 deletions
+1 -1
View File
@@ -232,7 +232,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`ui.useAlternateBuffer`** (boolean): - **`ui.useAlternateBuffer`** (boolean):
- **Description:** Use an alternate screen buffer for the UI, preserving shell - **Description:** Use an alternate screen buffer for the UI, preserving shell
history. history.
- **Default:** `false` - **Default:** `true`
- **Requires restart:** Yes - **Requires restart:** Yes
- **`ui.customWittyPhrases`** (array): - **`ui.customWittyPhrases`** (array):
+5 -2
View File
@@ -84,8 +84,11 @@ describe('extension reloading', () => {
await run.expectText('- hello'); await run.expectText('- hello');
// Update the extension, expect the list to update, and mcp servers as well. // Update the extension, expect the list to update, and mcp servers as well.
await run.sendText('/extensions update test-extension'); await run.sendKeys('/extensions update test-extension');
await run.type('\r'); 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( await run.expectText(
` * test-server (remote): http://localhost:${portB}/mcp`, ` * test-server (remote): http://localhost:${portB}/mcp`,
); );
+7 -2
View File
@@ -192,7 +192,12 @@ export class InteractiveRun {
timeout, timeout,
200, 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 // 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 = { const options: pty.IPtyForkOptions = {
name: 'xterm-color', name: 'xterm-color',
cols: 80, cols: 80,
rows: 24, rows: 80,
cwd: this.testDir!, cwd: this.testDir!,
env: Object.fromEntries( env: Object.fromEntries(
Object.entries(env).filter(([, v]) => v !== undefined), Object.entries(env).filter(([, v]) => v !== undefined),
+1 -1
View File
@@ -497,7 +497,7 @@ const SETTINGS_SCHEMA = {
label: 'Use Alternate Screen Buffer', label: 'Use Alternate Screen Buffer',
category: 'UI', category: 'UI',
requiresRestart: true, requiresRestart: true,
default: false, default: true,
description: description:
'Use an alternate screen buffer for the UI, preserving shell history.', 'Use an alternate screen buffer for the UI, preserving shell history.',
showInDialog: true, showInDialog: true,
+1
View File
@@ -529,6 +529,7 @@ describe('startInteractiveUI', () => {
// Verify render options // Verify render options
expect(options).toEqual({ expect(options).toEqual({
alternateBuffer: true,
exitOnCtrlC: false, exitOnCtrlC: false,
isScreenReaderEnabled: false, isScreenReaderEnabled: false,
onRender: expect.any(Function), onRender: expect.any(Function),
+5 -3
View File
@@ -76,6 +76,7 @@ import { requestConsentNonInteractive } from './config/extensions/consent.js';
import { disableMouseEvents, enableMouseEvents } from './ui/utils/mouse.js'; import { disableMouseEvents, enableMouseEvents } from './ui/utils/mouse.js';
import { ScrollProvider } from './ui/contexts/ScrollProvider.js'; import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
import ansiEscapes from 'ansi-escapes'; import ansiEscapes from 'ansi-escapes';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
const SLOW_RENDER_MS = 200; const SLOW_RENDER_MS = 200;
@@ -170,7 +171,8 @@ export async function startInteractiveUI(
process.stdout.write('\x1b[?7l'); process.stdout.write('\x1b[?7l');
} }
const mouseEventsEnabled = settings.merged.ui?.useAlternateBuffer === true; const useAlternateBuffer = isAlternateBufferEnabled(settings);
const mouseEventsEnabled = useAlternateBuffer;
if (mouseEventsEnabled) { if (mouseEventsEnabled) {
enableMouseEvents(); enableMouseEvents();
} }
@@ -236,7 +238,7 @@ export async function startInteractiveUI(
recordSlowRender(config, renderTime); recordSlowRender(config, renderTime);
} }
}, },
alternateBuffer: settings.merged.ui?.useAlternateBuffer, alternateBuffer: useAlternateBuffer,
}, },
); );
@@ -437,7 +439,7 @@ export async function main() {
// input showing up in the output. // input showing up in the output.
process.stdin.setRawMode(true); process.stdin.setRawMode(true);
if (settings.merged.ui?.useAlternateBuffer) { if (isAlternateBufferEnabled(settings)) {
process.stdout.write(ansiEscapes.enterAlternativeScreen); process.stdout.write(ansiEscapes.enterAlternativeScreen);
// Ink will cleanup so there is no need for us to manually cleanup. // Ink will cleanup so there is no need for us to manually cleanup.
@@ -16,16 +16,16 @@ Tips for getting started:
2. Be specific for the best results. 2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini. 3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information. 4. /help for more information.
╭───────────────────────────────────────────────────────────────────────────── ╭─────────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │ ✓ tool1 Description for tool 1 │
│ │
╰───────────────────────────────────────────────────────────────────────────── ╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────────── ╭─────────────────────────────────────────────────────────────────────────────╮
│ ✓ tool2 Description for tool 2 │ ✓ tool2 Description for tool 2 │
│ │
╰───────────────────────────────────────────────────────────────────────────── ╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────────── ╭─────────────────────────────────────────────────────────────────────────────╮
│ o tool3 Description for tool 3 │ o tool3 Description for tool 3 │
│ │
╰─────────────────────────────────────────────────────────────────────────────╯" ╰─────────────────────────────────────────────────────────────────────────────╯"
`; `;
@@ -1,11 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Footer /> > 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 /> > 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 /> > 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 /> > 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 /> > 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 /> > 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 /> > footer configuration filtering (golden snapshots) > renders footer with all optional sections hidden (minimal footer) > footer-minimal 1`] = `""`; exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with all optional sections hidden (minimal footer) > footer-minimal 1`] = `""`;
exports[`<Footer /> > 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 /> > 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)"`;
@@ -1,57 +1,57 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // 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`] = ` 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 →
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`] = ` 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 ←
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
llllllllllllllllllllllllllllllllllllllllllllllllll" llllllllllllllllllllllllllllllllllllllllllllllllll"
`; `;
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = ` 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" 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`] = ` 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" git commit -m "feat: add search" in src/app"
`; `;
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = ` 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`] = ` 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`] = ` 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`] = ` exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = `
"╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── "╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ > Type your message or @path/to/file │ > Type your message or @path/to/file │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`; `;
@@ -5,8 +5,12 @@
*/ */
import { useSettings } from '../contexts/SettingsContext.js'; 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 => { export const useAlternateBuffer = (): boolean => {
const settings = useSettings(); const settings = useSettings();
return settings.merged.ui?.useAlternateBuffer ?? false; return isAlternateBufferEnabled(settings);
}; };
@@ -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/);
});
});
+5 -7
View File
@@ -22,6 +22,7 @@ import {
} from '../components/shared/MaxSizedBox.js'; } from '../components/shared/MaxSizedBox.js';
import type { LoadedSettings } from '../../config/settings.js'; import type { LoadedSettings } from '../../config/settings.js';
import { debugLogger } from '@google/gemini-cli-core'; import { debugLogger } from '@google/gemini-cli-core';
import { isAlternateBufferEnabled } from '../hooks/useAlternateBuffer.js';
// Configure theming and parsing utilities. // Configure theming and parsing utilities.
const lowlight = createLowlight(common); const lowlight = createLowlight(common);
@@ -150,7 +151,7 @@ export function colorizeCode({
? false ? false
: (settings?.merged.ui?.showLineNumbers ?? true); : (settings?.merged.ui?.showLineNumbers ?? true);
const useMaxSizedBox = settings?.merged.ui?.useAlternateBuffer !== true; const useMaxSizedBox = !isAlternateBufferEnabled(settings);
try { try {
// Render the HAST tree using the adapted theme // Render the HAST tree using the adapted theme
// Apply the theme's default foreground color to the top-level Text element // Apply the theme's default foreground color to the top-level Text element
@@ -160,10 +161,7 @@ export function colorizeCode({
let hiddenLinesCount = 0; let hiddenLinesCount = 0;
// Optimization to avoid highlighting lines that cannot possibly be displayed. // Optimization to avoid highlighting lines that cannot possibly be displayed.
if ( if (availableHeight !== undefined && useMaxSizedBox) {
availableHeight !== undefined &&
settings?.merged.ui?.useAlternateBuffer === false
) {
availableHeight = Math.max(availableHeight, MINIMUM_MAX_HEIGHT); availableHeight = Math.max(availableHeight, MINIMUM_MAX_HEIGHT);
if (lines.length > availableHeight) { if (lines.length > availableHeight) {
const sliceIndex = lines.length - availableHeight; const sliceIndex = lines.length - availableHeight;
@@ -180,7 +178,7 @@ export function colorizeCode({
); );
return ( return (
<Box key={index}> <Box key={index} minHeight={useMaxSizedBox ? undefined : 1}>
{/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */} {/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
{showLineNumbers && useMaxSizedBox && ( {showLineNumbers && useMaxSizedBox && (
<Text color={activeTheme.colors.Gray}> <Text color={activeTheme.colors.Gray}>
@@ -238,7 +236,7 @@ export function colorizeCode({
const lines = codeToHighlight.split('\n'); const lines = codeToHighlight.split('\n');
const padWidth = String(lines.length).length; // Calculate padding width based on number of lines const padWidth = String(lines.length).length; // Calculate padding width based on number of lines
const fallbackLines = lines.map((line, index) => ( const fallbackLines = lines.map((line, index) => (
<Box key={index}> <Box key={index} minHeight={useMaxSizedBox ? undefined : 1}>
{/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */} {/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
{showLineNumbers && useMaxSizedBox && ( {showLineNumbers && useMaxSizedBox && (
<Text color={activeTheme.defaultColor}> <Text color={activeTheme.defaultColor}>
+2 -1
View File
@@ -6,6 +6,7 @@
import { lerp } from '../../utils/math.js'; import { lerp } from '../../utils/math.js';
import { type LoadedSettings } from '../../config/settings.js'; import { type LoadedSettings } from '../../config/settings.js';
import { isAlternateBufferEnabled } from '../hooks/useAlternateBuffer.js';
const getMainAreaWidthInternal = (terminalWidth: number): number => { const getMainAreaWidthInternal = (terminalWidth: number): number => {
if (terminalWidth <= 80) { if (terminalWidth <= 80) {
@@ -27,7 +28,7 @@ export const calculateMainAreaWidth = (
settings: LoadedSettings, settings: LoadedSettings,
): number => { ): number => {
if (settings.merged.ui?.useFullWidth !== false) { if (settings.merged.ui?.useFullWidth !== false) {
if (settings.merged.ui?.useAlternateBuffer) { if (isAlternateBufferEnabled(settings)) {
return terminalWidth - 1; return terminalWidth - 1;
} }
return terminalWidth; return terminalWidth;
+2 -2
View File
@@ -278,8 +278,8 @@
"useAlternateBuffer": { "useAlternateBuffer": {
"title": "Use Alternate Screen Buffer", "title": "Use Alternate Screen Buffer",
"description": "Use an alternate screen buffer for the UI, preserving shell history.", "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`", "markdownDescription": "Use an alternate screen buffer for the UI, preserving shell history.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`",
"default": false, "default": true,
"type": "boolean" "type": "boolean"
}, },
"customWittyPhrases": { "customWittyPhrases": {