Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 10 additions & 19 deletions packages/cli/src/gemini.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,18 +238,15 @@ vi.mock('./validateNonInterActiveAuth.js', () => ({
}));

describe('gemini.tsx main function', () => {
let originalEnvGeminiSandbox: string | undefined;
let originalEnvSandbox: string | undefined;
let originalIsTTY: boolean | undefined;
let initialUnhandledRejectionListeners: NodeJS.UnhandledRejectionListener[] =
[];

beforeEach(() => {
// Store and clear sandbox-related env variables to ensure a consistent test environment
originalEnvGeminiSandbox = process.env['GEMINI_SANDBOX'];
originalEnvSandbox = process.env['SANDBOX'];
delete process.env['GEMINI_SANDBOX'];
delete process.env['SANDBOX'];
vi.stubEnv('GEMINI_SANDBOX', '');
vi.stubEnv('SANDBOX', '');
vi.stubEnv('SHPOOL_SESSION_NAME', '');

initialUnhandledRejectionListeners =
process.listeners('unhandledRejection');
Expand All @@ -260,18 +257,6 @@ describe('gemini.tsx main function', () => {
});

afterEach(() => {
// Restore original env variables
if (originalEnvGeminiSandbox !== undefined) {
process.env['GEMINI_SANDBOX'] = originalEnvGeminiSandbox;
} else {
delete process.env['GEMINI_SANDBOX'];
}
if (originalEnvSandbox !== undefined) {
process.env['SANDBOX'] = originalEnvSandbox;
} else {
delete process.env['SANDBOX'];
}

const currentListeners = process.listeners('unhandledRejection');
currentListeners.forEach((listener) => {
if (!initialUnhandledRejectionListeners.includes(listener)) {
Expand All @@ -282,6 +267,7 @@ describe('gemini.tsx main function', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(process.stdin as any).isTTY = originalIsTTY;

vi.unstubAllEnvs();
vi.restoreAllMocks();
});

Expand Down Expand Up @@ -1209,7 +1195,12 @@ describe('startInteractiveUI', () => {
registerTelemetryConfig: vi.fn(),
}));

beforeEach(() => {
vi.stubEnv('SHPOOL_SESSION_NAME', '');
});

afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

Expand Down Expand Up @@ -1308,7 +1299,7 @@ describe('startInteractiveUI', () => {

// Verify all startup tasks were called
expect(getVersion).toHaveBeenCalledTimes(1);
expect(registerCleanup).toHaveBeenCalledTimes(3);
expect(registerCleanup).toHaveBeenCalledTimes(4);

// Verify cleanup handler is registered with unmount function
const cleanupFn = vi.mocked(registerCleanup).mock.calls[0][0];
Expand Down
53 changes: 33 additions & 20 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ import {
writeToStderr,
disableMouseEvents,
enableMouseEvents,
enterAlternateScreen,
disableLineWrapping,
enableLineWrapping,
shouldEnterAlternateScreen,
startupProfiler,
ExitCodes,
Expand Down Expand Up @@ -89,6 +89,7 @@ import { SessionStatsProvider } from './ui/contexts/SessionContext.js';
import { VimModeProvider } from './ui/contexts/VimModeContext.js';
import { KeypressProvider } from './ui/contexts/KeypressContext.js';
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js';
import { useTerminalSize } from './ui/hooks/useTerminalSize.js';
import {
relaunchAppInChildProcess,
relaunchOnExitCode,
Expand Down Expand Up @@ -214,9 +215,13 @@ export async function startInteractiveUI(

const { stdout: inkStdout, stderr: inkStderr } = createWorkingStdio();

const isShpool = !!process.env['SHPOOL_SESSION_NAME'];

// Create wrapper component to use hooks inside render
const AppWrapper = () => {
useKittyKeyboardProtocol();
const { columns, rows } = useTerminalSize();

return (
<SettingsContext.Provider value={settings}>
<KeypressProvider
Expand All @@ -234,6 +239,7 @@ export async function startInteractiveUI(
<SessionStatsProvider>
<VimModeProvider settings={settings}>
<AppContainer
key={`${columns}-${rows}`}
config={config}
startupWarnings={startupWarnings}
version={version}
Expand All @@ -250,6 +256,17 @@ export async function startInteractiveUI(
);
};

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>
Expand All @@ -273,10 +290,19 @@ export async function startInteractiveUI(
patchConsole: false,
alternateBuffer: useAlternateBuffer,
incrementalRendering:
settings.merged.ui.incrementalRendering !== false && useAlternateBuffer,
settings.merged.ui.incrementalRendering !== false &&
useAlternateBuffer &&
!isShpool,
},
);

if (useAlternateBuffer) {
disableLineWrapping();
registerCleanup(() => {
enableLineWrapping();
});
}

checkForUpdates(settings)
.then((info) => {
handleAutoUpdate(info, settings, config.getProjectRoot());
Expand Down Expand Up @@ -590,26 +616,13 @@ export async function main() {
// input showing up in the output.
process.stdin.setRawMode(true);

if (
shouldEnterAlternateScreen(
isAlternateBufferEnabled(settings),
config.getScreenReader(),
)
) {
enterAlternateScreen();
disableLineWrapping();

// Ink will cleanup so there is no need for us to manually cleanup.
}

// This cleanup isn't strictly needed but may help in certain situations.
const restoreRawMode = () => {
process.on('SIGTERM', () => {
process.stdin.setRawMode(wasRaw);
};
process.off('SIGTERM', restoreRawMode);
process.on('SIGTERM', restoreRawMode);
process.off('SIGINT', restoreRawMode);
process.on('SIGINT', restoreRawMode);
});
process.on('SIGINT', () => {
process.stdin.setRawMode(wasRaw);
});
}

await setupTerminalAndTheme(config, settings);
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,9 @@ export const AppContainer = (props: AppContainerProps) => {
(async () => {
// Note: the program will not work if this fails so let errors be
// handled by the global catch.
await config.initialize();
if (!config.isInitialized()) {
await config.initialize();
}
setConfigInitialized(true);
startupProfiler.flush(config);

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,10 @@ export class Config {
);
}

isInitialized(): boolean {
return this.initialized;
}

/**
* Must only be called once, throws if called again.
*/
Expand Down
Loading