mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-19 16:23:06 -07:00
4ee4bfcb02
Checkpoint optimizing virtualized list Fixes for fallback rendering where terminalBuffer=false Change terminalBuffer false back to the default while we fix performance with very large chats. Checkpoint changes to virtualized list. Fix virtualized list NO commit Update ink version. Fix UI snapshot mismatch in MainContent tests and VirtualizedList computation Checkpoint.
260 lines
8.0 KiB
TypeScript
260 lines
8.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { render } from 'ink';
|
|
import { basename } from 'node:path';
|
|
import { AppContainer } from './ui/AppContainer.js';
|
|
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
|
import {
|
|
registerCleanup,
|
|
removeCleanup,
|
|
setupTtyCheck,
|
|
} from './utils/cleanup.js';
|
|
import {
|
|
type StartupWarning,
|
|
type Config,
|
|
type ResumedSessionData,
|
|
coreEvents,
|
|
createWorkingStdio,
|
|
disableMouseEvents,
|
|
enableMouseEvents,
|
|
disableLineWrapping,
|
|
enableLineWrapping,
|
|
shouldEnterAlternateScreen,
|
|
recordSlowRender,
|
|
writeToStdout,
|
|
getVersion,
|
|
debugLogger,
|
|
} from '@google/gemini-cli-core';
|
|
import type { InitializationResult } from './core/initializer.js';
|
|
import type { LoadedSettings } from './config/settings.js';
|
|
import { checkForUpdates } from './ui/utils/updateCheck.js';
|
|
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
|
|
import { SettingsContext } from './ui/contexts/SettingsContext.js';
|
|
import { MouseProvider } from './ui/contexts/MouseContext.js';
|
|
import { StreamingState } from './ui/types.js';
|
|
import { computeTerminalTitle } from './utils/windowTitle.js';
|
|
|
|
import { SessionStatsProvider } from './ui/contexts/SessionContext.js';
|
|
import { VimModeProvider } from './ui/contexts/VimModeContext.js';
|
|
import { KeyMatchersProvider } from './ui/hooks/useKeyMatchers.js';
|
|
import { loadKeyMatchers } from './ui/key/keyMatchers.js';
|
|
import { KeypressProvider } from './ui/contexts/KeypressContext.js';
|
|
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js';
|
|
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
|
|
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
|
|
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
|
|
import { profiler } from './ui/components/DebugProfiler.js';
|
|
import { initializeConsoleStore } from './ui/hooks/useConsoleMessages.js';
|
|
|
|
const SLOW_RENDER_MS = 200;
|
|
|
|
export async function startInteractiveUI(
|
|
config: Config,
|
|
settings: LoadedSettings,
|
|
startupWarnings: StartupWarning[],
|
|
workspaceRoot: string = process.cwd(),
|
|
resumedSessionData: ResumedSessionData | undefined,
|
|
initializationResult: InitializationResult,
|
|
) {
|
|
initializeConsoleStore();
|
|
// Never enter Ink alternate buffer mode when screen reader mode is enabled
|
|
// as there is no benefit of alternate buffer mode when using a screen reader
|
|
// and the Ink alternate buffer mode requires line wrapping harmful to
|
|
// screen readers.
|
|
const useAlternateBuffer = shouldEnterAlternateScreen(
|
|
config.getUseAlternateBuffer(),
|
|
config.getScreenReader(),
|
|
);
|
|
const mouseEventsEnabled = useAlternateBuffer;
|
|
if (mouseEventsEnabled) {
|
|
enableMouseEvents();
|
|
registerCleanup(() => {
|
|
disableMouseEvents();
|
|
});
|
|
}
|
|
|
|
const { matchers, errors } = await loadKeyMatchers();
|
|
errors.forEach((error) => {
|
|
coreEvents.emitFeedback('warning', error);
|
|
});
|
|
|
|
const version = await getVersion();
|
|
setWindowTitle(basename(workspaceRoot), settings);
|
|
|
|
const consolePatcher = new ConsolePatcher({
|
|
onNewMessage: (msg) => {
|
|
coreEvents.emitConsoleLog(msg.type, msg.content);
|
|
},
|
|
debugMode: config.getDebugMode(),
|
|
});
|
|
consolePatcher.patch();
|
|
|
|
const { stdout: inkStdout, stderr: inkStderr } = createWorkingStdio();
|
|
|
|
const isShpool = !!process.env['SHPOOL_SESSION_NAME'];
|
|
|
|
// Create wrapper component to use hooks inside render
|
|
const AppWrapper = () => {
|
|
useKittyKeyboardProtocol();
|
|
|
|
return (
|
|
<SettingsContext.Provider value={settings}>
|
|
<KeyMatchersProvider value={matchers}>
|
|
<KeypressProvider config={config}>
|
|
<MouseProvider mouseEventsEnabled={mouseEventsEnabled}>
|
|
<TerminalProvider>
|
|
<ScrollProvider>
|
|
<OverflowProvider>
|
|
<SessionStatsProvider sessionId={config.getSessionId()}>
|
|
<VimModeProvider>
|
|
<AppContainer
|
|
config={config}
|
|
startupWarnings={startupWarnings}
|
|
version={version}
|
|
resumedSessionData={resumedSessionData}
|
|
initializationResult={initializationResult}
|
|
/>
|
|
</VimModeProvider>
|
|
</SessionStatsProvider>
|
|
</OverflowProvider>
|
|
</ScrollProvider>
|
|
</TerminalProvider>
|
|
</MouseProvider>
|
|
</KeypressProvider>
|
|
</KeyMatchersProvider>
|
|
</SettingsContext.Provider>
|
|
);
|
|
};
|
|
|
|
if (isShpool) {
|
|
// Wait a moment for shpool to stabilize terminal size and state.
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
const instance = render(
|
|
process.env['DEBUG'] ? (
|
|
<React.StrictMode>
|
|
<AppWrapper />
|
|
</React.StrictMode>
|
|
) : (
|
|
<AppWrapper />
|
|
),
|
|
{
|
|
stdout: inkStdout,
|
|
stderr: inkStderr,
|
|
stdin: process.stdin,
|
|
exitOnCtrlC: false,
|
|
isScreenReaderEnabled: config.getScreenReader(),
|
|
onRender: ({ renderTime }: { renderTime: number }) => {
|
|
if (renderTime > SLOW_RENDER_MS) {
|
|
recordSlowRender(config, Math.round(renderTime));
|
|
}
|
|
profiler.reportFrameRendered();
|
|
},
|
|
standardReactLayoutTiming: false,
|
|
patchConsole: false,
|
|
alternateBuffer: useAlternateBuffer,
|
|
terminalBuffer: config.getUseTerminalBuffer(),
|
|
renderProcess:
|
|
config.getUseRenderProcess() && config.getUseTerminalBuffer(),
|
|
incrementalRendering:
|
|
settings.merged.ui.incrementalRendering !== false &&
|
|
useAlternateBuffer &&
|
|
!isShpool,
|
|
debugRainbow: settings.merged.ui.debugRainbow === true,
|
|
|
|
// @ts-expect-error Custom option in our fork of ink
|
|
maxScrollbackLength: settings.merged.ui.maxScrollbackLength,
|
|
},
|
|
);
|
|
|
|
let cleanupLineWrapping: (() => void) | undefined;
|
|
if (useAlternateBuffer) {
|
|
disableLineWrapping();
|
|
cleanupLineWrapping = () => enableLineWrapping();
|
|
registerCleanup(cleanupLineWrapping);
|
|
}
|
|
|
|
checkForUpdates(settings)
|
|
.then((info) => {
|
|
handleAutoUpdate(
|
|
info,
|
|
settings,
|
|
config.getProjectRoot(),
|
|
config.getSandboxEnabled(),
|
|
);
|
|
})
|
|
.catch((err) => {
|
|
// Silently ignore update check errors.
|
|
if (config.getDebugMode()) {
|
|
debugLogger.warn('Update check failed:', err);
|
|
}
|
|
});
|
|
|
|
const cleanupUnmount = () => instance.unmount();
|
|
registerCleanup(cleanupUnmount);
|
|
|
|
const cleanupTtyCheck = setupTtyCheck();
|
|
registerCleanup(cleanupTtyCheck);
|
|
|
|
const cleanupConsolePatcher = () => consolePatcher.cleanup();
|
|
registerCleanup(cleanupConsolePatcher);
|
|
|
|
try {
|
|
await instance.waitUntilExit();
|
|
} finally {
|
|
try {
|
|
removeCleanup(cleanupConsolePatcher);
|
|
cleanupConsolePatcher();
|
|
} catch (e: unknown) {
|
|
debugLogger.error('Error cleaning up console patcher:', e);
|
|
}
|
|
|
|
try {
|
|
removeCleanup(cleanupUnmount);
|
|
instance.unmount();
|
|
} catch (e: unknown) {
|
|
debugLogger.error('Error unmounting Ink instance:', e);
|
|
}
|
|
|
|
try {
|
|
removeCleanup(cleanupTtyCheck);
|
|
cleanupTtyCheck();
|
|
} catch (e: unknown) {
|
|
debugLogger.error('Error in TTY cleanup:', e);
|
|
}
|
|
|
|
if (cleanupLineWrapping) {
|
|
try {
|
|
removeCleanup(cleanupLineWrapping);
|
|
cleanupLineWrapping();
|
|
} catch (e: unknown) {
|
|
debugLogger.error('Error restoring line wrapping:', e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function setWindowTitle(title: string, settings: LoadedSettings) {
|
|
if (!settings.merged.ui.hideWindowTitle) {
|
|
// Initial state before React loop starts
|
|
const windowTitle = computeTerminalTitle({
|
|
streamingState: StreamingState.Idle,
|
|
isConfirming: false,
|
|
isSilentWorking: false,
|
|
folderName: title,
|
|
showThoughts: !!settings.merged.ui.showStatusInTitle,
|
|
useDynamicTitle: settings.merged.ui.dynamicWindowTitle,
|
|
});
|
|
writeToStdout(`\x1b]0;${windowTitle}\x07`);
|
|
|
|
process.on('exit', () => {
|
|
writeToStdout(`\x1b]0;\x07`);
|
|
});
|
|
}
|
|
}
|