diff --git a/esbuild.config.js b/esbuild.config.js
index 49d158ec36..f0d55e3ca6 100644
--- a/esbuild.config.js
+++ b/esbuild.config.js
@@ -82,11 +82,14 @@ const commonAliases = {
const cliConfig = {
...baseConfig,
banner: {
- js: `const require = (await import('node:module')).createRequire(import.meta.url); globalThis.__filename = (await import('node:url')).fileURLToPath(import.meta.url); globalThis.__dirname = (await import('node:path')).dirname(globalThis.__filename);`,
+ js: `const require = (await import('node:module')).createRequire(import.meta.url); const __chunk_filename = (await import('node:url')).fileURLToPath(import.meta.url); const __chunk_dirname = (await import('node:path')).dirname(__chunk_filename);`,
},
- entryPoints: ['packages/cli/index.ts'],
- outfile: 'bundle/gemini.js',
+ entryPoints: { gemini: 'packages/cli/index.ts' },
+ outdir: 'bundle',
+ splitting: true,
define: {
+ __filename: '__chunk_filename',
+ __dirname: '__chunk_dirname',
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
'process.env.GEMINI_SANDBOX_IMAGE_DEFAULT': JSON.stringify(
pkg.config?.sandboxImageUri,
@@ -103,11 +106,13 @@ const cliConfig = {
const a2aServerConfig = {
...baseConfig,
banner: {
- js: `const require = (await import('node:module')).createRequire(import.meta.url); globalThis.__filename = (await import('node:url')).fileURLToPath(import.meta.url); globalThis.__dirname = (await import('node:path')).dirname(globalThis.__filename);`,
+ js: `const require = (await import('node:module')).createRequire(import.meta.url); const __chunk_filename = (await import('node:url')).fileURLToPath(import.meta.url); const __chunk_dirname = (await import('node:path')).dirname(__chunk_filename);`,
},
entryPoints: ['packages/a2a-server/src/http/server.ts'],
outfile: 'packages/a2a-server/dist/a2a-server.mjs',
define: {
+ __filename: '__chunk_filename',
+ __dirname: '__chunk_dirname',
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
},
plugins: createWasmPlugins(),
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 2985e20358..04a370d7e9 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -4,13 +4,38 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React from 'react';
-import { render } from 'ink';
-import { AppContainer } from './ui/AppContainer.js';
+import {
+ type StartupWarning,
+ WarningPriority,
+ type Config,
+ type ResumedSessionData,
+ type OutputPayload,
+ type ConsoleLogPayload,
+ type UserFeedbackPayload,
+ sessionId,
+ logUserPrompt,
+ AuthType,
+ UserPromptEvent,
+ coreEvents,
+ CoreEvent,
+ getOauthClient,
+ patchStdio,
+ writeToStdout,
+ writeToStderr,
+ shouldEnterAlternateScreen,
+ startupProfiler,
+ ExitCodes,
+ SessionStartSource,
+ SessionEndReason,
+ ValidationCancelledError,
+ ValidationRequiredError,
+ type AdminControlsSettings,
+ debugLogger,
+} from '@google/gemini-cli-core';
+
import { loadCliConfig, parseArguments } from './config/config.js';
import * as cliConfig from './config/config.js';
import { readStdin } from './utils/readStdin.js';
-import { basename } from 'node:path';
import { createHash } from 'node:crypto';
import v8 from 'node:v8';
import os from 'node:os';
@@ -37,47 +62,11 @@ import {
runExitCleanup,
registerTelemetryConfig,
setupSignalHandlers,
- setupTtyCheck,
} from './utils/cleanup.js';
import {
cleanupToolOutputFiles,
cleanupExpiredSessions,
} from './utils/sessionCleanup.js';
-import {
- type StartupWarning,
- WarningPriority,
- type Config,
- type ResumedSessionData,
- type OutputPayload,
- type ConsoleLogPayload,
- type UserFeedbackPayload,
- sessionId,
- logUserPrompt,
- AuthType,
- getOauthClient,
- UserPromptEvent,
- debugLogger,
- recordSlowRender,
- coreEvents,
- CoreEvent,
- createWorkingStdio,
- patchStdio,
- writeToStdout,
- writeToStderr,
- disableMouseEvents,
- enableMouseEvents,
- disableLineWrapping,
- enableLineWrapping,
- shouldEnterAlternateScreen,
- startupProfiler,
- ExitCodes,
- SessionStartSource,
- SessionEndReason,
- getVersion,
- ValidationCancelledError,
- ValidationRequiredError,
- type AdminControlsSettings,
-} from '@google/gemini-cli-core';
import {
initializeApp,
type InitializationResult,
@@ -85,21 +74,9 @@ import {
import { validateAuthMethod } from './config/auth.js';
import { runAcpClient } from './acp/acpClient.js';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
-import { checkForUpdates } from './ui/utils/updateCheck.js';
-import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
import { appEvents, AppEvent } from './utils/events.js';
import { SessionError, SessionSelector } from './utils/sessionUtils.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 {
relaunchAppInChildProcess,
relaunchOnExitCode,
@@ -107,19 +84,13 @@ import {
import { loadSandboxConfig } from './config/sandboxConfig.js';
import { deleteSession, listSessions } from './utils/sessions.js';
import { createPolicyUpdater } from './config/policy.js';
-import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
-import { TerminalProvider } from './ui/contexts/TerminalContext.js';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
-import { OverflowProvider } from './ui/contexts/OverflowContext.js';
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
-import { profiler } from './ui/components/DebugProfiler.js';
import { runDeferredCommand } from './deferred.js';
import { cleanupBackgroundLogs } from './utils/logCleanup.js';
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
-const SLOW_RENDER_MS = 200;
-
export function validateDnsResolutionOrder(
order: string | undefined,
): DnsResolutionOrder {
@@ -198,147 +169,16 @@ export async function startInteractiveUI(
resumedSessionData: ResumedSessionData | undefined,
initializationResult: InitializationResult,
) {
- // 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(
- isAlternateBufferEnabled(config),
- config.getScreenReader(),
+ // Dynamically import the heavy UI module so React/Ink are only parsed when needed
+ const { startInteractiveUI: doStartUI } = await import('./interactiveCli.js');
+ await doStartUI(
+ config,
+ settings,
+ startupWarnings,
+ workspaceRoot,
+ resumedSessionData,
+ initializationResult,
);
- 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();
- registerCleanup(consolePatcher.cleanup);
-
- 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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- };
-
- if (isShpool) {
- // Wait a moment for shpool to stabilize terminal size and state.
- // shpool is a persistence tool that restores terminal state by replaying it.
- // This delay gives shpool time to finish its restoration replay and send
- // the actual terminal size (often via an immediate SIGWINCH) before we
- // render the first TUI frame. Without this, the first frame may be
- // garbled or rendered at an incorrect size, which disabling incremental
- // rendering alone cannot fix for the initial frame.
- await new Promise((resolve) => setTimeout(resolve, 100));
- }
-
- const instance = render(
- process.env['DEBUG'] ? (
-
-
-
- ) : (
-
- ),
- {
- stdout: inkStdout,
- stderr: inkStderr,
- stdin: process.stdin,
- exitOnCtrlC: false,
- isScreenReaderEnabled: config.getScreenReader(),
- onRender: ({ renderTime }: { renderTime: number }) => {
- if (renderTime > SLOW_RENDER_MS) {
- recordSlowRender(config, renderTime);
- }
- profiler.reportFrameRendered();
- },
- patchConsole: false,
- alternateBuffer: useAlternateBuffer,
- incrementalRendering:
- settings.merged.ui.incrementalRendering !== false &&
- useAlternateBuffer &&
- !isShpool,
- },
- );
-
- if (useAlternateBuffer) {
- disableLineWrapping();
- registerCleanup(() => {
- enableLineWrapping();
- });
- }
-
- checkForUpdates(settings)
- .then((info) => {
- handleAutoUpdate(info, settings, config.getProjectRoot());
- })
- .catch((err) => {
- // Silently ignore update check errors.
- if (config.getDebugMode()) {
- debugLogger.warn('Update check failed:', err);
- }
- });
-
- registerCleanup(() => instance.unmount());
-
- registerCleanup(setupTtyCheck());
}
export async function main() {
@@ -845,25 +685,6 @@ export async function main() {
}
}
-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`);
- });
- }
-}
-
export function initializeOutputListenersAndFlush() {
// If there are no listeners for output, make sure we flush so output is not
// lost.
diff --git a/packages/cli/src/interactiveCli.tsx b/packages/cli/src/interactiveCli.tsx
new file mode 100644
index 0000000000..a27cdbbb78
--- /dev/null
+++ b/packages/cli/src/interactiveCli.tsx
@@ -0,0 +1,214 @@
+/**
+ * @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, 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 { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
+import { OverflowProvider } from './ui/contexts/OverflowContext.js';
+import { profiler } from './ui/components/DebugProfiler.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,
+) {
+ // 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(
+ isAlternateBufferEnabled(config),
+ 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();
+ registerCleanup(consolePatcher.cleanup);
+
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ 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'] ? (
+
+
+
+ ) : (
+
+ ),
+ {
+ stdout: inkStdout,
+ stderr: inkStderr,
+ stdin: process.stdin,
+ exitOnCtrlC: false,
+ isScreenReaderEnabled: config.getScreenReader(),
+ onRender: ({ renderTime }: { renderTime: number }) => {
+ if (renderTime > SLOW_RENDER_MS) {
+ recordSlowRender(config, renderTime);
+ }
+ profiler.reportFrameRendered();
+ },
+ patchConsole: false,
+ alternateBuffer: useAlternateBuffer,
+ incrementalRendering:
+ settings.merged.ui.incrementalRendering !== false &&
+ useAlternateBuffer &&
+ !isShpool,
+ },
+ );
+
+ if (useAlternateBuffer) {
+ disableLineWrapping();
+ registerCleanup(() => {
+ enableLineWrapping();
+ });
+ }
+
+ checkForUpdates(settings)
+ .then((info) => {
+ handleAutoUpdate(info, settings, config.getProjectRoot());
+ })
+ .catch((err) => {
+ // Silently ignore update check errors.
+ if (config.getDebugMode()) {
+ debugLogger.warn('Update check failed:', err);
+ }
+ });
+
+ registerCleanup(() => instance.unmount());
+
+ registerCleanup(setupTtyCheck());
+}
+
+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`);
+ });
+ }
+}
diff --git a/scripts/build_binary.js b/scripts/build_binary.js
index d4aa578925..7d0fd815c1 100644
--- a/scripts/build_binary.js
+++ b/scripts/build_binary.js
@@ -228,23 +228,35 @@ const packageJson = JSON.parse(
// Helper to calc hash
const sha256 = (content) => createHash('sha256').update(content).digest('hex');
-// Read Main Bundle
-const geminiBundlePath = join(root, 'bundle/gemini.js');
-const geminiContent = readFileSync(geminiBundlePath);
-const geminiHash = sha256(geminiContent);
-
const assets = {
- 'gemini.mjs': geminiBundlePath, // Use .js source but map to .mjs for runtime ESM
'manifest.json': 'bundle/manifest.json',
};
const manifest = {
main: 'gemini.mjs',
- mainHash: geminiHash,
+ mainHash: '',
version: packageJson.version,
files: [],
};
+// Add all javascript chunks from the bundle directory
+const jsFiles = globSync('*.js', { cwd: bundleDir });
+for (const jsFile of jsFiles) {
+ const fsPath = join(bundleDir, jsFile);
+ const content = readFileSync(fsPath);
+ const hash = sha256(content);
+
+ // Node SEA requires the main entry point to be explicitly mapped
+ if (jsFile === 'gemini.js') {
+ assets['gemini.mjs'] = fsPath;
+ manifest.mainHash = hash;
+ } else {
+ // Other chunks need to be mapped exactly as they are named so dynamic imports find them
+ assets[jsFile] = fsPath;
+ manifest.files.push({ key: jsFile, path: jsFile, hash: hash });
+ }
+}
+
// Helper to recursively find files from STAGING
function addAssetsFromDir(baseDir, runtimePrefix) {
const fullDir = join(stagingDir, baseDir);
@@ -346,6 +358,22 @@ const targetBinaryPath = join(targetDir, binaryName);
console.log(`Copying node binary from ${nodeBinary} to ${targetBinaryPath}...`);
copyFileSync(nodeBinary, targetBinaryPath);
+if (platform === 'darwin') {
+ console.log(`Thinning universal binary for ${arch}...`);
+ try {
+ // Attempt to thin the binary. Will fail safely if it's not a fat binary.
+ runCommand('lipo', [
+ targetBinaryPath,
+ '-thin',
+ arch,
+ '-output',
+ targetBinaryPath,
+ ]);
+ } catch (e) {
+ console.log(`Skipping lipo thinning: ${e.message}`);
+ }
+}
+
// Remove existing signature using helper
removeSignature(targetBinaryPath);
@@ -357,9 +385,7 @@ if (existsSync(bundleDir)) {
// Clean up source JS files from output (we only want embedded)
const filesToRemove = [
- 'gemini.js',
'gemini.mjs',
- 'gemini.js.map',
'gemini.mjs.map',
'gemini-sea.cjs',
'sea-launch.cjs',
@@ -373,6 +399,12 @@ filesToRemove.forEach((f) => {
if (existsSync(p)) rmSync(p, { recursive: true, force: true });
});
+// Remove all chunk and entry .js/.js.map files
+const jsFilesToRemove = globSync('*.{js,js.map}', { cwd: targetDir });
+for (const f of jsFilesToRemove) {
+ rmSync(join(targetDir, f));
+}
+
// Remove .sb files from targetDir
const sbFilesToRemove = globSync('sandbox-macos-*.sb', { cwd: targetDir });
for (const f of sbFilesToRemove) {