Skip to content
15 changes: 15 additions & 0 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
import { getCliVersion } from './utils/version.js';
import { writeStderrLine } from './utils/stdioHelpers.js';
import { computeWindowTitle } from './utils/windowTitle.js';
import {
startEarlyInputCapture,
stopAndGetCapturedInput,
} from './utils/earlyInputCapture.js';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { showResumeSessionPicker } from './ui/components/StandaloneSessionPicker.js';
import { initializeLlmOutputLanguage } from './utils/languageUtils.js';
Expand Down Expand Up @@ -147,6 +151,11 @@ export async function startInteractiveUI(
const version = await getCliVersion();
setWindowTitle(basename(workspaceRoot), settings);

// Drain the early-captured input exactly once, before any React rendering.
// Must be outside any component/effect so StrictMode's mount/cleanup/remount
// always reads from the same stable prop rather than the (now empty) module buffer.
const initialCapturedInput = stopAndGetCapturedInput();

// Create wrapper component to use hooks inside render
const AppWrapper = () => {
const kittyProtocolStatus = useKittyKeyboardProtocol();
Expand All @@ -160,6 +169,7 @@ export async function startInteractiveUI(
pasteWorkaround={
process.platform === 'win32' || nodeMajorVersion < 20
}
initialCapturedInput={initialCapturedInput}
>
<SessionStatsProvider sessionId={config.getSessionId()}>
<VimModeProvider settings={settings}>
Expand Down Expand Up @@ -382,6 +392,11 @@ export async function main() {
// input showing up in the output.
process.stdin.setRawMode(true);

// Startup optimization: start early input capture
startEarlyInputCapture();
Comment thread
doudouOUC marked this conversation as resolved.
// Ensure the stdin listener is removed on any exit path (error, signal, etc.)
registerCleanup(() => stopAndGetCapturedInput());

// This cleanup isn't strictly needed but may help in certain situations.
process.on('SIGTERM', () => {
process.stdin.setRawMode(wasRaw);
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/ui/contexts/KeypressContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,14 @@ export function KeypressProvider({
pasteWorkaround = false,
config,
debugKeystrokeLogging,
initialCapturedInput,
}: {
children?: React.ReactNode;
kittyProtocolEnabled: boolean;
pasteWorkaround?: boolean;
config?: Config;
debugKeystrokeLogging?: boolean;
initialCapturedInput?: Buffer;
}) {
const { stdin, setRawMode } = useStdin();
const subscribers = useRef<Set<KeypressHandler>>(new Set()).current;
Expand All @@ -158,6 +160,11 @@ export function KeypressProvider({
setRawMode(true);
}

// Use pre-drained captured input passed from outside React.
// Draining happens before render() so StrictMode's mount/cleanup/remount
// always reads from the stable prop reference, not the (already empty) module buffer.
const capturedInput = initialCapturedInput ?? Buffer.alloc(0);

const keypressStream = new PassThrough();
let usePassthrough = false;
// Use passthrough mode when pasteWorkaround is enabled,
Expand Down Expand Up @@ -985,7 +992,30 @@ export function KeypressProvider({
stdin.on('keypress', handleKeypress);
}

// Startup optimization: replay captured input if available
let replayPending = false;
if (capturedInput.length > 0) {
debugLogger.debug(
`Replaying ${capturedInput.length} bytes of captured input`,
);
// Process in next event loop tick to ensure subscribers are ready.
// Always emit on stdin so that handleRawKeypress processes paste markers
// correctly in passthrough mode.
// In non-passthrough mode, readline.emitKeypressEvents installs an internal
// 'data' listener on stdin that converts data events to keypress events.
replayPending = true;
setImmediate(() => {
if (!replayPending) return;
try {
stdin.emit('data', capturedInput);
} catch (err) {
debugLogger.error('Failed to replay captured input:', err);
}
});
}

return () => {
replayPending = false;
if (usePassthrough) {
keypressStream.removeListener('keypress', handleKeypress);
stdin.removeListener('data', handleRawKeypress);
Expand Down Expand Up @@ -1033,6 +1063,7 @@ export function KeypressProvider({
pasteWorkaround,
config,
subscribers,
initialCapturedInput,
]);

return (
Expand Down
Loading
Loading