mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-17 13:30:53 -07:00
feat: add config and keybindings for new ink terminal buffer mode
test: update test utils and resolve snapshot differences for ink changes feat: refactor VirtualizedList to support static rendering and terminal buffers feat: wire up AppContainer mouse mode and recording state for ink buffer Fix stale ref in ScrollProvider breaking scrolling. The ScrollProvider was getting stuck with a stale reference because the `scrollables` Map was being registered and unregistered whenever the entry identity changed. The underlying `ScrollableList` component dynamically created a new ID which caused React remount cycles, while the `ScrollProvider` itself suffered from state lag where the event handler ref was one tick behind the actual registration. This commit resolves these issues by: 1. Adding a stable `id` and `key` to `ScrollableList` in `MainContent.tsx` 2. Making `scrollablesRef` synchronously update in `ScrollProvider.tsx` 3. Using a proxy entry in `useScrollable` to avoid constant re-registering. chore: add useEffect cleanup for ResizeObservers Checkpoint fixing terminal buffer support. Termina Serializer Optimization
This commit is contained in:
@@ -661,6 +661,8 @@ export interface ConfigParameters {
|
||||
trustedFolder?: boolean;
|
||||
useBackgroundColor?: boolean;
|
||||
useAlternateBuffer?: boolean;
|
||||
useTerminalBuffer?: boolean;
|
||||
useRenderProcess?: boolean;
|
||||
useRipgrep?: boolean;
|
||||
enableInteractiveShell?: boolean;
|
||||
shellBackgroundCompletionBehavior?: string;
|
||||
@@ -878,6 +880,8 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly skipNextSpeakerCheck: boolean;
|
||||
private readonly useBackgroundColor: boolean;
|
||||
private readonly useAlternateBuffer: boolean;
|
||||
private readonly useTerminalBuffer: boolean;
|
||||
private readonly useRenderProcess: boolean;
|
||||
private shellExecutionConfig: ShellExecutionConfig;
|
||||
private readonly extensionManagement: boolean = true;
|
||||
private readonly extensionRegistryURI: string | undefined;
|
||||
@@ -1218,6 +1222,8 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.useRipgrep = params.useRipgrep ?? true;
|
||||
this.useBackgroundColor = params.useBackgroundColor ?? true;
|
||||
this.useAlternateBuffer = params.useAlternateBuffer ?? false;
|
||||
this.useTerminalBuffer = params.useTerminalBuffer ?? false;
|
||||
this.useRenderProcess = params.useRenderProcess ?? true;
|
||||
this.enableInteractiveShell = params.enableInteractiveShell ?? false;
|
||||
|
||||
const requestedBehavior = params.shellBackgroundCompletionBehavior;
|
||||
@@ -3243,6 +3249,14 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.useAlternateBuffer;
|
||||
}
|
||||
|
||||
getUseTerminalBuffer(): boolean {
|
||||
return this.useTerminalBuffer;
|
||||
}
|
||||
|
||||
getUseRenderProcess(): boolean {
|
||||
return this.useRenderProcess;
|
||||
}
|
||||
|
||||
getEnableInteractiveShell(): boolean {
|
||||
return this.enableInteractiveShell;
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('terminalSerializer', () => {
|
||||
await writeToTerminal(terminal, '\x1b[7mInverse text\x1b[0m');
|
||||
const result = serializeTerminalToObject(terminal);
|
||||
expect(result[0][0].inverse).toBe(true);
|
||||
expect(result[0][0].text).toBe('Inverse text');
|
||||
expect(result[0][0].text.trim()).toBe('Inverse text');
|
||||
});
|
||||
|
||||
it('should handle foreground colors', async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { IBufferCell, Terminal } from '@xterm/headless';
|
||||
import type { Terminal } from '@xterm/headless';
|
||||
export interface AnsiToken {
|
||||
text: string;
|
||||
bold: boolean;
|
||||
@@ -19,129 +19,12 @@ export interface AnsiToken {
|
||||
export type AnsiLine = AnsiToken[];
|
||||
export type AnsiOutput = AnsiLine[];
|
||||
|
||||
const enum Attribute {
|
||||
inverse = 1,
|
||||
bold = 2,
|
||||
italic = 4,
|
||||
underline = 8,
|
||||
dim = 16,
|
||||
}
|
||||
|
||||
export const enum ColorMode {
|
||||
DEFAULT = 0,
|
||||
PALETTE = 1,
|
||||
RGB = 2,
|
||||
}
|
||||
|
||||
class Cell {
|
||||
private cell: IBufferCell | null = null;
|
||||
private x = 0;
|
||||
private y = 0;
|
||||
private cursorX = 0;
|
||||
private cursorY = 0;
|
||||
private attributes: number = 0;
|
||||
fg = 0;
|
||||
bg = 0;
|
||||
fgColorMode: ColorMode = ColorMode.DEFAULT;
|
||||
bgColorMode: ColorMode = ColorMode.DEFAULT;
|
||||
|
||||
constructor(
|
||||
cell: IBufferCell | null,
|
||||
x: number,
|
||||
y: number,
|
||||
cursorX: number,
|
||||
cursorY: number,
|
||||
) {
|
||||
this.update(cell, x, y, cursorX, cursorY);
|
||||
}
|
||||
|
||||
update(
|
||||
cell: IBufferCell | null,
|
||||
x: number,
|
||||
y: number,
|
||||
cursorX: number,
|
||||
cursorY: number,
|
||||
) {
|
||||
this.cell = cell;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.cursorX = cursorX;
|
||||
this.cursorY = cursorY;
|
||||
this.attributes = 0;
|
||||
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cell.isInverse()) {
|
||||
this.attributes += Attribute.inverse;
|
||||
}
|
||||
if (cell.isBold()) {
|
||||
this.attributes += Attribute.bold;
|
||||
}
|
||||
if (cell.isItalic()) {
|
||||
this.attributes += Attribute.italic;
|
||||
}
|
||||
if (cell.isUnderline()) {
|
||||
this.attributes += Attribute.underline;
|
||||
}
|
||||
if (cell.isDim()) {
|
||||
this.attributes += Attribute.dim;
|
||||
}
|
||||
|
||||
if (cell.isFgRGB()) {
|
||||
this.fgColorMode = ColorMode.RGB;
|
||||
} else if (cell.isFgPalette()) {
|
||||
this.fgColorMode = ColorMode.PALETTE;
|
||||
} else {
|
||||
this.fgColorMode = ColorMode.DEFAULT;
|
||||
}
|
||||
|
||||
if (cell.isBgRGB()) {
|
||||
this.bgColorMode = ColorMode.RGB;
|
||||
} else if (cell.isBgPalette()) {
|
||||
this.bgColorMode = ColorMode.PALETTE;
|
||||
} else {
|
||||
this.bgColorMode = ColorMode.DEFAULT;
|
||||
}
|
||||
|
||||
if (this.fgColorMode === ColorMode.DEFAULT) {
|
||||
this.fg = -1;
|
||||
} else {
|
||||
this.fg = cell.getFgColor();
|
||||
}
|
||||
|
||||
if (this.bgColorMode === ColorMode.DEFAULT) {
|
||||
this.bg = -1;
|
||||
} else {
|
||||
this.bg = cell.getBgColor();
|
||||
}
|
||||
}
|
||||
|
||||
isCursor(): boolean {
|
||||
return this.x === this.cursorX && this.y === this.cursorY;
|
||||
}
|
||||
|
||||
getChars(): string {
|
||||
return this.cell?.getChars() || ' ';
|
||||
}
|
||||
|
||||
isAttribute(attribute: Attribute): boolean {
|
||||
return (this.attributes & attribute) !== 0;
|
||||
}
|
||||
|
||||
equals(other: Cell): boolean {
|
||||
return (
|
||||
this.attributes === other.attributes &&
|
||||
this.fg === other.fg &&
|
||||
this.bg === other.bg &&
|
||||
this.fgColorMode === other.fgColorMode &&
|
||||
this.bgColorMode === other.bgColorMode &&
|
||||
this.isCursor() === other.isCursor()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeTerminalToObject(
|
||||
terminal: Terminal,
|
||||
startLine?: number,
|
||||
@@ -155,10 +38,6 @@ export function serializeTerminalToObject(
|
||||
|
||||
const result: AnsiOutput = [];
|
||||
|
||||
// Reuse cell instances
|
||||
const lastCell = new Cell(null, -1, -1, cursorX, cursorY);
|
||||
const currentCell = new Cell(null, -1, -1, cursorX, cursorY);
|
||||
|
||||
const effectiveStart = startLine ?? buffer.viewportY;
|
||||
const effectiveEnd = endLine ?? buffer.viewportY + terminal.rows;
|
||||
|
||||
@@ -170,49 +49,129 @@ export function serializeTerminalToObject(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset lastCell for new line
|
||||
lastCell.update(null, -1, -1, cursorX, cursorY);
|
||||
let currentText = '';
|
||||
let currentBold = false;
|
||||
let currentItalic = false;
|
||||
let currentUnderline = false;
|
||||
let currentDim = false;
|
||||
let currentInverse = false;
|
||||
let currentFgMode = ColorMode.DEFAULT;
|
||||
let currentFgColor = -1;
|
||||
let currentBgMode = ColorMode.DEFAULT;
|
||||
let currentBgColor = -1;
|
||||
let currentFg = '';
|
||||
let currentBg = '';
|
||||
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const cellData = line.getCell(x);
|
||||
currentCell.update(cellData || null, x, y, cursorX, cursorY);
|
||||
|
||||
if (x > 0 && !currentCell.equals(lastCell)) {
|
||||
if (currentText) {
|
||||
const token: AnsiToken = {
|
||||
text: currentText,
|
||||
bold: lastCell.isAttribute(Attribute.bold),
|
||||
italic: lastCell.isAttribute(Attribute.italic),
|
||||
underline: lastCell.isAttribute(Attribute.underline),
|
||||
dim: lastCell.isAttribute(Attribute.dim),
|
||||
inverse:
|
||||
lastCell.isAttribute(Attribute.inverse) || lastCell.isCursor(),
|
||||
fg: convertColorToHex(lastCell.fg, lastCell.fgColorMode, defaultFg),
|
||||
bg: convertColorToHex(lastCell.bg, lastCell.bgColorMode, defaultBg),
|
||||
};
|
||||
currentLine.push(token);
|
||||
}
|
||||
currentText = '';
|
||||
const isCursor = x === cursorX && y === cursorY;
|
||||
const bold = cellData ? !!cellData.isBold() : false;
|
||||
const italic = cellData ? !!cellData.isItalic() : false;
|
||||
const underline = cellData ? !!cellData.isUnderline() : false;
|
||||
const dim = cellData ? !!cellData.isDim() : false;
|
||||
const inverse = (cellData ? !!cellData.isInverse() : false) || isCursor;
|
||||
|
||||
let fgMode = ColorMode.DEFAULT;
|
||||
let bgMode = ColorMode.DEFAULT;
|
||||
let fgColor = -1;
|
||||
let bgColor = -1;
|
||||
|
||||
if (cellData) {
|
||||
if (cellData.isFgRGB()) fgMode = ColorMode.RGB;
|
||||
else if (cellData.isFgPalette()) fgMode = ColorMode.PALETTE;
|
||||
|
||||
if (cellData.isBgRGB()) bgMode = ColorMode.RGB;
|
||||
else if (cellData.isBgPalette()) bgMode = ColorMode.PALETTE;
|
||||
|
||||
if (fgMode !== ColorMode.DEFAULT) fgColor = cellData.getFgColor();
|
||||
if (bgMode !== ColorMode.DEFAULT) bgColor = cellData.getBgColor();
|
||||
}
|
||||
|
||||
// Handle wide characters correctly. Wide characters take 2 cells.
|
||||
// The second cell has a width of 0 and an empty string for getChars().
|
||||
// For a regular empty cell (width=1), we output a space.
|
||||
let char = ' ';
|
||||
if (cellData) {
|
||||
if (cellData.getWidth() === 0) {
|
||||
char = '';
|
||||
} else {
|
||||
char = cellData.getChars() || ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if (x === 0) {
|
||||
currentText = char;
|
||||
currentBold = bold;
|
||||
currentItalic = italic;
|
||||
currentUnderline = underline;
|
||||
currentDim = dim;
|
||||
currentInverse = inverse;
|
||||
currentFgMode = fgMode;
|
||||
currentFgColor = fgColor;
|
||||
currentBgMode = bgMode;
|
||||
currentBgColor = bgColor;
|
||||
currentFg = convertColorToHex(fgColor, fgMode, defaultFg);
|
||||
currentBg = convertColorToHex(bgColor, bgMode, defaultBg);
|
||||
} else {
|
||||
if (
|
||||
currentBold !== bold ||
|
||||
currentItalic !== italic ||
|
||||
currentUnderline !== underline ||
|
||||
currentDim !== dim ||
|
||||
currentInverse !== inverse ||
|
||||
currentFgMode !== fgMode ||
|
||||
currentFgColor !== fgColor ||
|
||||
currentBgMode !== bgMode ||
|
||||
currentBgColor !== bgColor
|
||||
) {
|
||||
if (currentText) {
|
||||
currentLine.push({
|
||||
text: currentText,
|
||||
bold: currentBold,
|
||||
italic: currentItalic,
|
||||
underline: currentUnderline,
|
||||
dim: currentDim,
|
||||
inverse: currentInverse,
|
||||
fg: currentFg,
|
||||
bg: currentBg,
|
||||
});
|
||||
}
|
||||
currentText = char;
|
||||
currentBold = bold;
|
||||
currentItalic = italic;
|
||||
currentUnderline = underline;
|
||||
currentDim = dim;
|
||||
currentInverse = inverse;
|
||||
|
||||
if (currentFgMode !== fgMode || currentFgColor !== fgColor) {
|
||||
currentFg = convertColorToHex(fgColor, fgMode, defaultFg);
|
||||
currentFgMode = fgMode;
|
||||
currentFgColor = fgColor;
|
||||
}
|
||||
|
||||
if (currentBgMode !== bgMode || currentBgColor !== bgColor) {
|
||||
currentBg = convertColorToHex(bgColor, bgMode, defaultBg);
|
||||
currentBgMode = bgMode;
|
||||
currentBgColor = bgColor;
|
||||
}
|
||||
} else {
|
||||
currentText += char;
|
||||
}
|
||||
}
|
||||
currentText += currentCell.getChars();
|
||||
// Copy state from currentCell to lastCell. Since we can't easily deep copy
|
||||
// without allocating, we just update lastCell with the same data.
|
||||
lastCell.update(cellData || null, x, y, cursorX, cursorY);
|
||||
}
|
||||
|
||||
if (currentText) {
|
||||
const token: AnsiToken = {
|
||||
currentLine.push({
|
||||
text: currentText,
|
||||
bold: lastCell.isAttribute(Attribute.bold),
|
||||
italic: lastCell.isAttribute(Attribute.italic),
|
||||
underline: lastCell.isAttribute(Attribute.underline),
|
||||
dim: lastCell.isAttribute(Attribute.dim),
|
||||
inverse: lastCell.isAttribute(Attribute.inverse) || lastCell.isCursor(),
|
||||
fg: convertColorToHex(lastCell.fg, lastCell.fgColorMode, defaultFg),
|
||||
bg: convertColorToHex(lastCell.bg, lastCell.bgColorMode, defaultBg),
|
||||
};
|
||||
currentLine.push(token);
|
||||
bold: currentBold,
|
||||
italic: currentItalic,
|
||||
underline: currentUnderline,
|
||||
dim: currentDim,
|
||||
inverse: currentInverse,
|
||||
fg: currentFg,
|
||||
bg: currentBg,
|
||||
});
|
||||
}
|
||||
|
||||
result.push(currentLine);
|
||||
|
||||
Reference in New Issue
Block a user