diff --git a/packages/cli/index.ts b/packages/cli/index.ts
index 8f9a066327..894b5c1fd4 100644
--- a/packages/cli/index.ts
+++ b/packages/cli/index.ts
@@ -8,23 +8,26 @@
import './src/gemini.js';
import { main } from './src/gemini.js';
-import { debugLogger, FatalError } from '@google/gemini-cli-core';
+import { FatalError, writeToStderr } from '@google/gemini-cli-core';
+import { runExitCleanup } from './src/utils/cleanup.js';
// --- Global Entry Point ---
-main().catch((error) => {
+main().catch(async (error) => {
+ await runExitCleanup();
+
if (error instanceof FatalError) {
let errorMessage = error.message;
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
}
- debugLogger.error(errorMessage);
+ writeToStderr(errorMessage + '\n');
process.exit(error.exitCode);
}
- debugLogger.error('An unexpected critical error occurred:');
+ writeToStderr('An unexpected critical error occurred:');
if (error instanceof Error) {
- debugLogger.error(error.stack);
+ writeToStderr(error.stack + '\n');
} else {
- debugLogger.error(String(error));
+ writeToStderr(String(error) + '\n');
}
process.exit(1);
});
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 15617fa9a8..b4983e9401 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -33,13 +33,11 @@ import {
runExitCleanup,
} from './utils/cleanup.js';
import { getCliVersion } from './utils/version.js';
-import type {
- Config,
- ResumedSessionData,
- OutputPayload,
- ConsoleLogPayload,
-} from '@google/gemini-cli-core';
import {
+ type Config,
+ type ResumedSessionData,
+ type OutputPayload,
+ type ConsoleLogPayload,
sessionId,
logUserPrompt,
AuthType,
@@ -53,6 +51,11 @@ import {
patchStdio,
writeToStdout,
writeToStderr,
+ disableMouseEvents,
+ enableMouseEvents,
+ enterAlternateScreen,
+ disableLineWrapping,
+ shouldEnterAlternateScreen,
} from '@google/gemini-cli-core';
import {
initializeApp,
@@ -85,9 +88,7 @@ import { deleteSession, listSessions } from './utils/sessions.js';
import { ExtensionManager } from './config/extension-manager.js';
import { createPolicyUpdater } from './config/policy.js';
import { requestConsentNonInteractive } from './config/extensions/consent.js';
-import { disableMouseEvents, enableMouseEvents } from './ui/utils/mouse.js';
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
-import ansiEscapes from 'ansi-escapes';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
import { profiler } from './ui/components/DebugProfiler.js';
@@ -176,8 +177,10 @@ export async function startInteractiveUI(
// 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 =
- isAlternateBufferEnabled(settings) && !config.getScreenReader();
+ const useAlternateBuffer = shouldEnterAlternateScreen(
+ isAlternateBufferEnabled(settings),
+ config.getScreenReader(),
+ );
const mouseEventsEnabled = useAlternateBuffer;
if (mouseEventsEnabled) {
enableMouseEvents();
@@ -481,8 +484,14 @@ export async function main() {
// input showing up in the output.
process.stdin.setRawMode(true);
- if (isAlternateBufferEnabled(settings)) {
- writeToStdout(ansiEscapes.enterAlternativeScreen);
+ if (
+ shouldEnterAlternateScreen(
+ isAlternateBufferEnabled(settings),
+ config.getScreenReader(),
+ )
+ ) {
+ enterAlternateScreen();
+ disableLineWrapping();
// Ink will cleanup so there is no need for us to manually cleanup.
}
diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx
index 43a0de7c92..24f3b12885 100644
--- a/packages/cli/src/ui/AppContainer.test.tsx
+++ b/packages/cli/src/ui/AppContainer.test.tsx
@@ -69,6 +69,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
stdout: process.stdout,
stderr: process.stderr,
})),
+ enableMouseEvents: vi.fn(),
+ disableMouseEvents: vi.fn(),
};
});
import type { LoadedSettings } from '../config/settings.js';
@@ -137,10 +139,6 @@ vi.mock('../utils/events.js');
vi.mock('../utils/handleAutoUpdate.js');
vi.mock('./utils/ConsolePatcher.js');
vi.mock('../utils/cleanup.js');
-vi.mock('./utils/mouse.js', () => ({
- enableMouseEvents: vi.fn(),
- disableMouseEvents: vi.fn(),
-}));
import { useHistory } from './hooks/useHistoryManager.js';
import { useThemeCommand } from './hooks/useThemeCommand.js';
@@ -165,9 +163,13 @@ import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useKeypress, type Key } from './hooks/useKeypress.js';
import { measureElement } from 'ink';
import { useTerminalSize } from './hooks/useTerminalSize.js';
-import { ShellExecutionService, writeToStdout } from '@google/gemini-cli-core';
+import {
+ ShellExecutionService,
+ writeToStdout,
+ enableMouseEvents,
+ disableMouseEvents,
+} from '@google/gemini-cli-core';
import { type ExtensionManager } from '../config/extension-manager.js';
-import { enableMouseEvents, disableMouseEvents } from './utils/mouse.js';
describe('AppContainer State Management', () => {
let mockConfig: Config;
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 1ae809a96e..ffe7aa51d0 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -52,6 +52,12 @@ import {
refreshServerHierarchicalMemory,
type ModelChangedPayload,
type MemoryChangedPayload,
+ writeToStdout,
+ disableMouseEvents,
+ enterAlternateScreen,
+ enableMouseEvents,
+ disableLineWrapping,
+ shouldEnterAlternateScreen,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
@@ -92,6 +98,7 @@ import { appEvents, AppEvent } from '../utils/events.js';
import { type UpdateObject } from './utils/updateCheck.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
+import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
import { useSessionStats } from './contexts/SessionContext.js';
@@ -106,11 +113,9 @@ import { type ExtensionManager } from '../config/extension-manager.js';
import { requestConsentInteractive } from '../config/extensions/consent.js';
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
-import { disableMouseEvents, enableMouseEvents } from './utils/mouse.js';
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
import { useSettings } from './contexts/SettingsContext.js';
import { enableSupportedProtocol } from './utils/kittyProtocolDetector.js';
-import { writeToStdout } from '@google/gemini-cli-core';
const WARNING_PROMPT_DURATION_MS = 1000;
const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
@@ -372,16 +377,19 @@ export const AppContainer = (props: AppContainerProps) => {
setHistoryRemountKey((prev) => prev + 1);
}, [setHistoryRemountKey, isAlternateBuffer, stdout]);
const handleEditorClose = useCallback(() => {
- if (isAlternateBuffer) {
+ if (
+ shouldEnterAlternateScreen(isAlternateBuffer, config.getScreenReader())
+ ) {
// The editor may have exited alternate buffer mode so we need to
// enter it again to be safe.
- writeToStdout(ansiEscapes.enterAlternativeScreen);
+ enterAlternateScreen();
enableMouseEvents();
+ disableLineWrapping();
app.rerender();
}
enableSupportedProtocol();
refreshStatic();
- }, [refreshStatic, isAlternateBuffer, app]);
+ }, [refreshStatic, isAlternateBuffer, app, config]);
useEffect(() => {
coreEvents.on(CoreEvent.ExternalEditorClosed, handleEditorClose);
@@ -458,12 +466,12 @@ export const AppContainer = (props: AppContainerProps) => {
config.isBrowserLaunchSuppressed()
) {
await runExitCleanup();
- debugLogger.log(`
+ writeToStdout(`
----------------------------------------------------------------
-Logging in with Google... Please restart Gemini CLI to continue.
+Logging in with Google... Restarting Gemini CLI to continue.
----------------------------------------------------------------
`);
- process.exit(0);
+ process.exit(RELAUNCH_EXIT_CODE);
}
}
setAuthState(AuthState.Authenticated);
diff --git a/packages/cli/src/ui/auth/AuthDialog.test.tsx b/packages/cli/src/ui/auth/AuthDialog.test.tsx
index 2b4bcca128..ca9e235ed5 100644
--- a/packages/cli/src/ui/auth/AuthDialog.test.tsx
+++ b/packages/cli/src/ui/auth/AuthDialog.test.tsx
@@ -25,6 +25,7 @@ import { validateAuthMethodWithSettings } from './useAuth.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { clearCachedCredentialFile } from '@google/gemini-cli-core';
import { Text } from 'ink';
+import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
// Mocks
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
@@ -229,6 +230,7 @@ describe('AuthDialog', () => {
});
it('exits process for Login with Google when browser is suppressed', async () => {
+ vi.useFakeTimers();
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
@@ -241,14 +243,14 @@ describe('AuthDialog', () => {
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
+ await vi.runAllTimersAsync();
+
expect(mockedRunExitCleanup).toHaveBeenCalled();
- expect(logSpy).toHaveBeenCalledWith(
- expect.stringContaining('Please restart Gemini CLI'),
- );
- expect(exitSpy).toHaveBeenCalledWith(0);
+ expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
exitSpy.mockRestore();
logSpy.mockRestore();
+ vi.useRealTimers();
});
});
diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx
index ecd51f6ed4..da5b6d7dff 100644
--- a/packages/cli/src/ui/auth/AuthDialog.tsx
+++ b/packages/cli/src/ui/auth/AuthDialog.tsx
@@ -5,7 +5,7 @@
*/
import type React from 'react';
-import { useCallback } from 'react';
+import { useCallback, useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
@@ -17,13 +17,13 @@ import { SettingScope } from '../../config/settings.js';
import {
AuthType,
clearCachedCredentialFile,
- debugLogger,
type Config,
} from '@google/gemini-cli-core';
import { useKeypress } from '../hooks/useKeypress.js';
import { AuthState } from '../types.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { validateAuthMethodWithSettings } from './useAuth.js';
+import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
interface AuthDialogProps {
config: Config;
@@ -40,6 +40,7 @@ export function AuthDialog({
authError,
onAuthError,
}: AuthDialogProps): React.JSX.Element {
+ const [exiting, setExiting] = useState(false);
let items = [
{
label: 'Login with Google',
@@ -111,6 +112,9 @@ export function AuthDialog({
const onSelect = useCallback(
async (authType: AuthType | undefined, scope: LoadableSettingScope) => {
+ if (exiting) {
+ return;
+ }
if (authType) {
await clearCachedCredentialFile();
@@ -119,15 +123,12 @@ export function AuthDialog({
authType === AuthType.LOGIN_WITH_GOOGLE &&
config.isBrowserLaunchSuppressed()
) {
- runExitCleanup();
- debugLogger.log(
- `
-----------------------------------------------------------------
-Logging in with Google... Please restart Gemini CLI to continue.
-----------------------------------------------------------------
- `,
- );
- process.exit(0);
+ setExiting(true);
+ setTimeout(async () => {
+ await runExitCleanup();
+ process.exit(RELAUNCH_EXIT_CODE);
+ }, 100);
+ return;
}
}
if (authType === AuthType.USE_GEMINI) {
@@ -136,7 +137,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
}
setAuthState(AuthState.Unauthenticated);
},
- [settings, config, setAuthState],
+ [settings, config, setAuthState, exiting],
);
const handleAuthSelect = (authMethod: AuthType) => {
@@ -169,6 +170,23 @@ Logging in with Google... Please restart Gemini CLI to continue.
{ isActive: true },
);
+ if (exiting) {
+ return (
+
+
+ Logging in with Google... Restarting Gemini CLI to continue.
+
+
+ );
+ }
+
return (
uiActions.closeSettingsDialog()}
- onRestartRequest={() => process.exit(0)}
+ onRestartRequest={async () => {
+ await runExitCleanup();
+ process.exit(RELAUNCH_EXIT_CODE);
+ }}
availableTerminalHeight={terminalHeight - staticExtraHeight}
config={config}
/>
diff --git a/packages/cli/src/ui/utils/kittyProtocolDetector.ts b/packages/cli/src/ui/utils/kittyProtocolDetector.ts
index c15e5a052d..a590eedef4 100644
--- a/packages/cli/src/ui/utils/kittyProtocolDetector.ts
+++ b/packages/cli/src/ui/utils/kittyProtocolDetector.ts
@@ -97,6 +97,11 @@ export async function detectAndEnableKittyProtocol(): Promise {
});
}
+import {
+ enableKittyKeyboardProtocol,
+ disableKittyKeyboardProtocol,
+} from '@google/gemini-cli-core';
+
export function isKittyProtocolEnabled(): boolean {
return kittyEnabled;
}
@@ -104,8 +109,7 @@ export function isKittyProtocolEnabled(): boolean {
function disableAllProtocols() {
try {
if (kittyEnabled) {
- // use writeSync to avoid race conditions
- fs.writeSync(process.stdout.fd, '\x1b[1u');
+ enableKittyKeyboardProtocol();
kittyEnabled = true;
}
} catch {
diff --git a/packages/cli/src/ui/utils/mouse.ts b/packages/cli/src/ui/utils/mouse.ts
index e5f2cd8241..3485e5a78f 100644
--- a/packages/cli/src/ui/utils/mouse.ts
+++ b/packages/cli/src/ui/utils/mouse.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { writeToStdout } from '@google/gemini-cli-core';
+import { enableMouseEvents, disableMouseEvents } from '@google/gemini-cli-core';
import {
SGR_MOUSE_REGEX,
X11_MOUSE_REGEX,
@@ -230,14 +230,4 @@ export function isIncompleteMouseSequence(buffer: string): boolean {
return true;
}
-export function enableMouseEvents() {
- // Enable mouse tracking with SGR format
- // ?1002h = button event tracking (clicks + drags + scroll wheel)
- // ?1006h = SGR extended mouse mode (better coordinate handling)
- writeToStdout('\u001b[?1002h\u001b[?1006h');
-}
-
-export function disableMouseEvents() {
- // Disable mouse tracking with SGR format
- writeToStdout('\u001b[?1006l\u001b[?1002l');
-}
+export { enableMouseEvents, disableMouseEvents };
diff --git a/packages/core/src/code_assist/oauth2.test.ts b/packages/core/src/code_assist/oauth2.test.ts
index 697a1a52d9..4a6758105f 100644
--- a/packages/core/src/code_assist/oauth2.test.ts
+++ b/packages/core/src/code_assist/oauth2.test.ts
@@ -27,6 +27,7 @@ import readline from 'node:readline';
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
import { GEMINI_DIR } from '../utils/paths.js';
import { debugLogger } from '../utils/debugLogger.js';
+import { writeToStdout } from '../utils/stdio.js';
vi.mock('os', async (importOriginal) => {
const os = await importOriginal();
@@ -44,6 +45,19 @@ vi.mock('node:readline');
vi.mock('../utils/browser.js', () => ({
shouldAttemptBrowserLaunch: () => true,
}));
+vi.mock('../utils/stdio.js', () => ({
+ writeToStdout: vi.fn(),
+ writeToStderr: vi.fn(),
+ createInkStdio: vi.fn(() => ({
+ stdout: process.stdout,
+ stderr: process.stderr,
+ })),
+ enterAlternateScreen: vi.fn(),
+ exitAlternateScreen: vi.fn(),
+ enableLineWrapping: vi.fn(),
+ disableMouseEvents: vi.fn(),
+ disableKittyKeyboardProtocol: vi.fn(),
+}));
vi.mock('./oauth-credential-storage.js', () => ({
OAuthCredentialStorage: {
@@ -238,13 +252,10 @@ describe('oauth2', () => {
const mockReadline = {
question: vi.fn((_query, callback) => callback(mockCode)),
close: vi.fn(),
+ on: vi.fn(),
};
(readline.createInterface as Mock).mockReturnValue(mockReadline);
- const consoleLogSpy = vi
- .spyOn(debugLogger, 'log')
- .mockImplementation(() => {});
-
const client = await getOauthClient(
AuthType.LOGIN_WITH_GOOGLE,
mockConfigWithNoBrowser,
@@ -255,7 +266,7 @@ describe('oauth2', () => {
// Verify the auth flow
expect(mockGenerateCodeVerifierAsync).toHaveBeenCalled();
expect(mockGenerateAuthUrl).toHaveBeenCalled();
- expect(consoleLogSpy).toHaveBeenCalledWith(
+ expect(vi.mocked(writeToStdout)).toHaveBeenCalledWith(
expect.stringContaining(mockAuthUrl),
);
expect(mockReadline.question).toHaveBeenCalledWith(
@@ -268,8 +279,6 @@ describe('oauth2', () => {
redirect_uri: 'https://codeassist.google.com/authcode',
});
expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens);
-
- consoleLogSpy.mockRestore();
});
describe('in Cloud Shell', () => {
@@ -932,6 +941,7 @@ describe('oauth2', () => {
const mockReadline = {
question: vi.fn((_query, callback) => callback('invalid-code')),
close: vi.fn(),
+ on: vi.fn(),
};
(readline.createInterface as Mock).mockReturnValue(mockReadline);
diff --git a/packages/core/src/code_assist/oauth2.ts b/packages/core/src/code_assist/oauth2.ts
index 4382195adb..b0a4cb4baa 100644
--- a/packages/core/src/code_assist/oauth2.ts
+++ b/packages/core/src/code_assist/oauth2.ts
@@ -19,7 +19,11 @@ import open from 'open';
import path from 'node:path';
import { promises as fs } from 'node:fs';
import type { Config } from '../config/config.js';
-import { getErrorMessage, FatalAuthenticationError } from '../utils/errors.js';
+import {
+ getErrorMessage,
+ FatalAuthenticationError,
+ FatalCancellationError,
+} from '../utils/errors.js';
import { UserAccountManager } from '../utils/userAccountManager.js';
import { AuthType } from '../core/contentGenerator.js';
import readline from 'node:readline';
@@ -27,6 +31,19 @@ import { Storage } from '../config/storage.js';
import { OAuthCredentialStorage } from './oauth-credential-storage.js';
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
import { debugLogger } from '../utils/debugLogger.js';
+import {
+ writeToStdout,
+ createInkStdio,
+ writeToStderr,
+} from '../utils/stdio.js';
+import {
+ enableLineWrapping,
+ disableMouseEvents,
+ disableKittyKeyboardProtocol,
+ enterAlternateScreen,
+ exitAlternateScreen,
+} from '../utils/terminal.js';
+import { coreEvents, CoreEvent } from '../utils/events.js';
const userAccountManager = new UserAccountManager();
@@ -185,16 +202,34 @@ async function initOauthClient(
if (config.isBrowserLaunchSuppressed()) {
let success = false;
const maxRetries = 2;
- for (let i = 0; !success && i < maxRetries; i++) {
- success = await authWithUserCode(client);
- if (!success) {
- debugLogger.error(
- '\nFailed to authenticate with user code.',
- i === maxRetries - 1 ? '' : 'Retrying...\n',
- );
+ // Enter alternate buffer
+ enterAlternateScreen();
+ // Clear screen and move cursor to top-left.
+ writeToStdout('\u001B[2J\u001B[H');
+ disableMouseEvents();
+ disableKittyKeyboardProtocol();
+ enableLineWrapping();
+
+ try {
+ for (let i = 0; !success && i < maxRetries; i++) {
+ success = await authWithUserCode(client);
+ if (!success) {
+ writeToStderr(
+ '\nFailed to authenticate with user code.' +
+ (i === maxRetries - 1 ? '' : ' Retrying...\n'),
+ );
+ }
}
+ } finally {
+ exitAlternateScreen();
+ // If this was triggered from an active Gemini CLI TUI this event ensures
+ // the TUI will re-initialize the terminal state just like it will when
+ // another editor like VIM may have modified the buffer of settings.
+ coreEvents.emit(CoreEvent.ExternalEditorClosed);
}
+
if (!success) {
+ writeToStderr('Failed to authenticate with user code.\n');
throw new FatalAuthenticationError(
'Failed to authenticate with user code.',
);
@@ -202,11 +237,13 @@ async function initOauthClient(
} else {
const webLogin = await authWithWeb(client);
- debugLogger.log(
- `\n\nCode Assist login required.\n` +
+ coreEvents.emit(CoreEvent.UserFeedback, {
+ severity: 'info',
+ message:
+ `\n\nCode Assist login required.\n` +
`Attempting to open authentication page in your browser.\n` +
- `Otherwise navigate to:\n\n${webLogin.authUrl}\n\n`,
- );
+ `Otherwise navigate to:\n\n${webLogin.authUrl}\n\n\n`,
+ });
try {
// Attempt to open the authentication URL in the default browser.
// We do not use the `wait` option here because the main script's execution
@@ -218,23 +255,28 @@ async function initOauthClient(
// in a minimal Docker container), it will emit an unhandled 'error' event,
// causing the entire Node.js process to crash.
childProcess.on('error', (error) => {
- debugLogger.error(
- `Failed to open browser with error:`,
- getErrorMessage(error),
- `\nPlease try running again with NO_BROWSER=true set.`,
- );
+ coreEvents.emit(CoreEvent.UserFeedback, {
+ severity: 'error',
+ message:
+ `Failed to open browser with error: ${getErrorMessage(error)}\n` +
+ `Please try running again with NO_BROWSER=true set.`,
+ });
});
} catch (err) {
- debugLogger.error(
- `Failed to open browser with error:`,
- getErrorMessage(err),
- `\nPlease try running again with NO_BROWSER=true set.`,
- );
+ coreEvents.emit(CoreEvent.UserFeedback, {
+ severity: 'error',
+ message:
+ `Failed to open browser with error: ${getErrorMessage(err)}\n` +
+ `Please try running again with NO_BROWSER=true set.`,
+ });
throw new FatalAuthenticationError(
`Failed to open browser: ${getErrorMessage(err)}`,
);
}
- debugLogger.log('Waiting for authentication...');
+ coreEvents.emit(CoreEvent.UserFeedback, {
+ severity: 'info',
+ message: 'Waiting for authentication...\n',
+ });
// Add timeout to prevent infinite waiting when browser tab gets stuck
const authTimeout = 5 * 60 * 1000; // 5 minutes timeout
@@ -250,6 +292,11 @@ async function initOauthClient(
});
await Promise.race([webLogin.loginCompletePromise, timeoutPromise]);
+
+ coreEvents.emit(CoreEvent.UserFeedback, {
+ severity: 'info',
+ message: 'Authentication succeeded\n',
+ });
}
return client;
@@ -266,55 +313,77 @@ export async function getOauthClient(
}
async function authWithUserCode(client: OAuth2Client): Promise {
- const redirectUri = 'https://codeassist.google.com/authcode';
- const codeVerifier = await client.generateCodeVerifierAsync();
- const state = crypto.randomBytes(32).toString('hex');
- const authUrl: string = client.generateAuthUrl({
- redirect_uri: redirectUri,
- access_type: 'offline',
- scope: OAUTH_SCOPE,
- code_challenge_method: CodeChallengeMethod.S256,
- code_challenge: codeVerifier.codeChallenge,
- state,
- });
- debugLogger.log(
- 'Please visit the following URL to authorize the application:',
- );
- debugLogger.log('');
- debugLogger.log(authUrl);
- debugLogger.log('');
-
- const code = await new Promise((resolve) => {
- const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
- });
- rl.question('Enter the authorization code: ', (code) => {
- rl.close();
- resolve(code.trim());
- });
- });
-
- if (!code) {
- debugLogger.error('Authorization code is required.');
- return false;
- }
-
try {
- const { tokens } = await client.getToken({
- code,
- codeVerifier: codeVerifier.codeVerifier,
+ const redirectUri = 'https://codeassist.google.com/authcode';
+ const codeVerifier = await client.generateCodeVerifierAsync();
+ const state = crypto.randomBytes(32).toString('hex');
+ const authUrl: string = client.generateAuthUrl({
redirect_uri: redirectUri,
+ access_type: 'offline',
+ scope: OAUTH_SCOPE,
+ code_challenge_method: CodeChallengeMethod.S256,
+ code_challenge: codeVerifier.codeChallenge,
+ state,
});
- client.setCredentials(tokens);
- } catch (error) {
+ writeToStdout(
+ 'Please visit the following URL to authorize the application:\n\n' +
+ authUrl +
+ '\n\n',
+ );
+
+ const code = await new Promise((resolve, _) => {
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: createInkStdio().stdout,
+ terminal: true,
+ });
+
+ rl.question('Enter the authorization code: ', (code) => {
+ rl.close();
+ resolve(code.trim());
+ });
+ });
+
+ if (!code) {
+ writeToStderr('Authorization code is required.\n');
+ debugLogger.error('Authorization code is required.');
+ return false;
+ }
+
+ try {
+ const { tokens } = await client.getToken({
+ code,
+ codeVerifier: codeVerifier.codeVerifier,
+ redirect_uri: redirectUri,
+ });
+ client.setCredentials(tokens);
+ } catch (error) {
+ writeToStderr(
+ 'Failed to authenticate with authorization code:' +
+ getErrorMessage(error) +
+ '\n',
+ );
+
+ debugLogger.error(
+ 'Failed to authenticate with authorization code:',
+ getErrorMessage(error),
+ );
+ return false;
+ }
+ return true;
+ } catch (err) {
+ if (err instanceof FatalCancellationError) {
+ throw err;
+ }
+ writeToStderr(
+ 'Failed to authenticate with user code:' + getErrorMessage(err) + '\n',
+ );
debugLogger.error(
- 'Failed to authenticate with authorization code:',
- getErrorMessage(error),
+ 'Failed to authenticate with user code:',
+ getErrorMessage(err),
);
return false;
}
- return true;
}
async function authWithWeb(client: OAuth2Client): Promise {
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index ad227b8db7..4640729922 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -147,3 +147,4 @@ export * from './hooks/types.js';
// Export stdio utils
export * from './utils/stdio.js';
+export * from './utils/terminal.js';
diff --git a/packages/core/src/utils/terminal.ts b/packages/core/src/utils/terminal.ts
new file mode 100644
index 0000000000..008919ea49
--- /dev/null
+++ b/packages/core/src/utils/terminal.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { writeToStdout } from './stdio.js';
+
+export function enableMouseEvents() {
+ // Enable mouse tracking with SGR format
+ // ?1002h = button event tracking (clicks + drags + scroll wheel)
+ // ?1006h = SGR extended mouse mode (better coordinate handling)
+ writeToStdout('\u001b[?1002h\u001b[?1006h');
+}
+
+export function disableMouseEvents() {
+ // Disable mouse tracking with SGR format
+ writeToStdout('\u001b[?1006l\u001b[?1002l');
+}
+
+export function enableKittyKeyboardProtocol() {
+ writeToStdout('\x1b[>1u');
+}
+
+export function disableKittyKeyboardProtocol() {
+ writeToStdout('\x1b[