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

View File

@@ -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):

View File

@@ -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`,
);

View File

@@ -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),

View File

@@ -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,

View File

@@ -529,6 +529,7 @@ describe('startInteractiveUI', () => {
// Verify render options
expect(options).toEqual({
alternateBuffer: true,
exitOnCtrlC: false,
isScreenReaderEnabled: false,
onRender: expect.any(Function),

View File

@@ -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.

View File

@@ -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 │
│ │
╰─────────────────────────────────────────────────────────────────────────────╯"
`;

View File

@@ -1,11 +1,11 @@
// 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 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)"`;

View File

@@ -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 │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;

View File

@@ -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);
};

View File

@@ -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/);
});
});

View File

@@ -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 (
<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 */}
{showLineNumbers && useMaxSizedBox && (
<Text color={activeTheme.colors.Gray}>
@@ -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) => (
<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 */}
{showLineNumbers && useMaxSizedBox && (
<Text color={activeTheme.defaultColor}>

View File

@@ -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;

View File

@@ -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": {