mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
perf(cli): enable code splitting and deferred UI loading (#22117)
This commit is contained in:
+9
-4
@@ -82,11 +82,14 @@ const commonAliases = {
|
|||||||
const cliConfig = {
|
const cliConfig = {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
banner: {
|
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'],
|
entryPoints: { gemini: 'packages/cli/index.ts' },
|
||||||
outfile: 'bundle/gemini.js',
|
outdir: 'bundle',
|
||||||
|
splitting: true,
|
||||||
define: {
|
define: {
|
||||||
|
__filename: '__chunk_filename',
|
||||||
|
__dirname: '__chunk_dirname',
|
||||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||||
'process.env.GEMINI_SANDBOX_IMAGE_DEFAULT': JSON.stringify(
|
'process.env.GEMINI_SANDBOX_IMAGE_DEFAULT': JSON.stringify(
|
||||||
pkg.config?.sandboxImageUri,
|
pkg.config?.sandboxImageUri,
|
||||||
@@ -103,11 +106,13 @@ const cliConfig = {
|
|||||||
const a2aServerConfig = {
|
const a2aServerConfig = {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
banner: {
|
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'],
|
entryPoints: ['packages/a2a-server/src/http/server.ts'],
|
||||||
outfile: 'packages/a2a-server/dist/a2a-server.mjs',
|
outfile: 'packages/a2a-server/dist/a2a-server.mjs',
|
||||||
define: {
|
define: {
|
||||||
|
__filename: '__chunk_filename',
|
||||||
|
__dirname: '__chunk_dirname',
|
||||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
plugins: createWasmPlugins(),
|
plugins: createWasmPlugins(),
|
||||||
|
|||||||
+38
-217
@@ -4,13 +4,38 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import {
|
||||||
import { render } from 'ink';
|
type StartupWarning,
|
||||||
import { AppContainer } from './ui/AppContainer.js';
|
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 { loadCliConfig, parseArguments } from './config/config.js';
|
||||||
import * as cliConfig from './config/config.js';
|
import * as cliConfig from './config/config.js';
|
||||||
import { readStdin } from './utils/readStdin.js';
|
import { readStdin } from './utils/readStdin.js';
|
||||||
import { basename } from 'node:path';
|
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import v8 from 'node:v8';
|
import v8 from 'node:v8';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
@@ -37,47 +62,11 @@ import {
|
|||||||
runExitCleanup,
|
runExitCleanup,
|
||||||
registerTelemetryConfig,
|
registerTelemetryConfig,
|
||||||
setupSignalHandlers,
|
setupSignalHandlers,
|
||||||
setupTtyCheck,
|
|
||||||
} from './utils/cleanup.js';
|
} from './utils/cleanup.js';
|
||||||
import {
|
import {
|
||||||
cleanupToolOutputFiles,
|
cleanupToolOutputFiles,
|
||||||
cleanupExpiredSessions,
|
cleanupExpiredSessions,
|
||||||
} from './utils/sessionCleanup.js';
|
} 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 {
|
import {
|
||||||
initializeApp,
|
initializeApp,
|
||||||
type InitializationResult,
|
type InitializationResult,
|
||||||
@@ -85,21 +74,9 @@ import {
|
|||||||
import { validateAuthMethod } from './config/auth.js';
|
import { validateAuthMethod } from './config/auth.js';
|
||||||
import { runAcpClient } from './acp/acpClient.js';
|
import { runAcpClient } from './acp/acpClient.js';
|
||||||
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.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 { appEvents, AppEvent } from './utils/events.js';
|
||||||
import { SessionError, SessionSelector } from './utils/sessionUtils.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 {
|
import {
|
||||||
relaunchAppInChildProcess,
|
relaunchAppInChildProcess,
|
||||||
relaunchOnExitCode,
|
relaunchOnExitCode,
|
||||||
@@ -107,19 +84,13 @@ import {
|
|||||||
import { loadSandboxConfig } from './config/sandboxConfig.js';
|
import { loadSandboxConfig } from './config/sandboxConfig.js';
|
||||||
import { deleteSession, listSessions } from './utils/sessions.js';
|
import { deleteSession, listSessions } from './utils/sessions.js';
|
||||||
import { createPolicyUpdater } from './config/policy.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 { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||||
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
|
|
||||||
|
|
||||||
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
|
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
|
||||||
import { profiler } from './ui/components/DebugProfiler.js';
|
|
||||||
import { runDeferredCommand } from './deferred.js';
|
import { runDeferredCommand } from './deferred.js';
|
||||||
import { cleanupBackgroundLogs } from './utils/logCleanup.js';
|
import { cleanupBackgroundLogs } from './utils/logCleanup.js';
|
||||||
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
|
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
|
||||||
|
|
||||||
const SLOW_RENDER_MS = 200;
|
|
||||||
|
|
||||||
export function validateDnsResolutionOrder(
|
export function validateDnsResolutionOrder(
|
||||||
order: string | undefined,
|
order: string | undefined,
|
||||||
): DnsResolutionOrder {
|
): DnsResolutionOrder {
|
||||||
@@ -198,147 +169,16 @@ export async function startInteractiveUI(
|
|||||||
resumedSessionData: ResumedSessionData | undefined,
|
resumedSessionData: ResumedSessionData | undefined,
|
||||||
initializationResult: InitializationResult,
|
initializationResult: InitializationResult,
|
||||||
) {
|
) {
|
||||||
// Never enter Ink alternate buffer mode when screen reader mode is enabled
|
// Dynamically import the heavy UI module so React/Ink are only parsed when needed
|
||||||
// as there is no benefit of alternate buffer mode when using a screen reader
|
const { startInteractiveUI: doStartUI } = await import('./interactiveCli.js');
|
||||||
// and the Ink alternate buffer mode requires line wrapping harmful to
|
await doStartUI(
|
||||||
// screen readers.
|
config,
|
||||||
const useAlternateBuffer = shouldEnterAlternateScreen(
|
settings,
|
||||||
isAlternateBufferEnabled(config),
|
startupWarnings,
|
||||||
config.getScreenReader(),
|
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 (
|
|
||||||
<SettingsContext.Provider value={settings}>
|
|
||||||
<KeyMatchersProvider value={matchers}>
|
|
||||||
<KeypressProvider
|
|
||||||
config={config}
|
|
||||||
debugKeystrokeLogging={
|
|
||||||
settings.merged.general.debugKeystrokeLogging
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MouseProvider
|
|
||||||
mouseEventsEnabled={mouseEventsEnabled}
|
|
||||||
debugKeystrokeLogging={
|
|
||||||
settings.merged.general.debugKeystrokeLogging
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TerminalProvider>
|
|
||||||
<ScrollProvider>
|
|
||||||
<OverflowProvider>
|
|
||||||
<SessionStatsProvider>
|
|
||||||
<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.
|
|
||||||
// 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'] ? (
|
|
||||||
<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, 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() {
|
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() {
|
export function initializeOutputListenersAndFlush() {
|
||||||
// If there are no listeners for output, make sure we flush so output is not
|
// If there are no listeners for output, make sure we flush so output is not
|
||||||
// lost.
|
// lost.
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<SettingsContext.Provider value={settings}>
|
||||||
|
<KeyMatchersProvider value={matchers}>
|
||||||
|
<KeypressProvider
|
||||||
|
config={config}
|
||||||
|
debugKeystrokeLogging={
|
||||||
|
settings.merged.general.debugKeystrokeLogging
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MouseProvider
|
||||||
|
mouseEventsEnabled={mouseEventsEnabled}
|
||||||
|
debugKeystrokeLogging={
|
||||||
|
settings.merged.general.debugKeystrokeLogging
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TerminalProvider>
|
||||||
|
<ScrollProvider>
|
||||||
|
<OverflowProvider>
|
||||||
|
<SessionStatsProvider>
|
||||||
|
<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, 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`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
-9
@@ -228,23 +228,35 @@ const packageJson = JSON.parse(
|
|||||||
// Helper to calc hash
|
// Helper to calc hash
|
||||||
const sha256 = (content) => createHash('sha256').update(content).digest('hex');
|
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 = {
|
const assets = {
|
||||||
'gemini.mjs': geminiBundlePath, // Use .js source but map to .mjs for runtime ESM
|
|
||||||
'manifest.json': 'bundle/manifest.json',
|
'manifest.json': 'bundle/manifest.json',
|
||||||
};
|
};
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
main: 'gemini.mjs',
|
main: 'gemini.mjs',
|
||||||
mainHash: geminiHash,
|
mainHash: '',
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
files: [],
|
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
|
// Helper to recursively find files from STAGING
|
||||||
function addAssetsFromDir(baseDir, runtimePrefix) {
|
function addAssetsFromDir(baseDir, runtimePrefix) {
|
||||||
const fullDir = join(stagingDir, baseDir);
|
const fullDir = join(stagingDir, baseDir);
|
||||||
@@ -346,6 +358,22 @@ const targetBinaryPath = join(targetDir, binaryName);
|
|||||||
console.log(`Copying node binary from ${nodeBinary} to ${targetBinaryPath}...`);
|
console.log(`Copying node binary from ${nodeBinary} to ${targetBinaryPath}...`);
|
||||||
copyFileSync(nodeBinary, 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
|
// Remove existing signature using helper
|
||||||
removeSignature(targetBinaryPath);
|
removeSignature(targetBinaryPath);
|
||||||
|
|
||||||
@@ -357,9 +385,7 @@ if (existsSync(bundleDir)) {
|
|||||||
|
|
||||||
// Clean up source JS files from output (we only want embedded)
|
// Clean up source JS files from output (we only want embedded)
|
||||||
const filesToRemove = [
|
const filesToRemove = [
|
||||||
'gemini.js',
|
|
||||||
'gemini.mjs',
|
'gemini.mjs',
|
||||||
'gemini.js.map',
|
|
||||||
'gemini.mjs.map',
|
'gemini.mjs.map',
|
||||||
'gemini-sea.cjs',
|
'gemini-sea.cjs',
|
||||||
'sea-launch.cjs',
|
'sea-launch.cjs',
|
||||||
@@ -373,6 +399,12 @@ filesToRemove.forEach((f) => {
|
|||||||
if (existsSync(p)) rmSync(p, { recursive: true, force: true });
|
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
|
// Remove .sb files from targetDir
|
||||||
const sbFilesToRemove = globSync('sandbox-macos-*.sb', { cwd: targetDir });
|
const sbFilesToRemove = globSync('sandbox-macos-*.sb', { cwd: targetDir });
|
||||||
for (const f of sbFilesToRemove) {
|
for (const f of sbFilesToRemove) {
|
||||||
|
|||||||
Reference in New Issue
Block a user