mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 10:34:35 -07:00
Fix multiple bugs with auth flow including using the implemented but unused restart support. (#13565)
This commit is contained in:
@@ -8,23 +8,26 @@
|
|||||||
|
|
||||||
import './src/gemini.js';
|
import './src/gemini.js';
|
||||||
import { main } from './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 ---
|
// --- Global Entry Point ---
|
||||||
main().catch((error) => {
|
main().catch(async (error) => {
|
||||||
|
await runExitCleanup();
|
||||||
|
|
||||||
if (error instanceof FatalError) {
|
if (error instanceof FatalError) {
|
||||||
let errorMessage = error.message;
|
let errorMessage = error.message;
|
||||||
if (!process.env['NO_COLOR']) {
|
if (!process.env['NO_COLOR']) {
|
||||||
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
|
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
|
||||||
}
|
}
|
||||||
debugLogger.error(errorMessage);
|
writeToStderr(errorMessage + '\n');
|
||||||
process.exit(error.exitCode);
|
process.exit(error.exitCode);
|
||||||
}
|
}
|
||||||
debugLogger.error('An unexpected critical error occurred:');
|
writeToStderr('An unexpected critical error occurred:');
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
debugLogger.error(error.stack);
|
writeToStderr(error.stack + '\n');
|
||||||
} else {
|
} else {
|
||||||
debugLogger.error(String(error));
|
writeToStderr(String(error) + '\n');
|
||||||
}
|
}
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
+21
-12
@@ -33,13 +33,11 @@ import {
|
|||||||
runExitCleanup,
|
runExitCleanup,
|
||||||
} from './utils/cleanup.js';
|
} from './utils/cleanup.js';
|
||||||
import { getCliVersion } from './utils/version.js';
|
import { getCliVersion } from './utils/version.js';
|
||||||
import type {
|
|
||||||
Config,
|
|
||||||
ResumedSessionData,
|
|
||||||
OutputPayload,
|
|
||||||
ConsoleLogPayload,
|
|
||||||
} from '@google/gemini-cli-core';
|
|
||||||
import {
|
import {
|
||||||
|
type Config,
|
||||||
|
type ResumedSessionData,
|
||||||
|
type OutputPayload,
|
||||||
|
type ConsoleLogPayload,
|
||||||
sessionId,
|
sessionId,
|
||||||
logUserPrompt,
|
logUserPrompt,
|
||||||
AuthType,
|
AuthType,
|
||||||
@@ -53,6 +51,11 @@ import {
|
|||||||
patchStdio,
|
patchStdio,
|
||||||
writeToStdout,
|
writeToStdout,
|
||||||
writeToStderr,
|
writeToStderr,
|
||||||
|
disableMouseEvents,
|
||||||
|
enableMouseEvents,
|
||||||
|
enterAlternateScreen,
|
||||||
|
disableLineWrapping,
|
||||||
|
shouldEnterAlternateScreen,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
initializeApp,
|
initializeApp,
|
||||||
@@ -85,9 +88,7 @@ import { deleteSession, listSessions } from './utils/sessions.js';
|
|||||||
import { ExtensionManager } from './config/extension-manager.js';
|
import { ExtensionManager } from './config/extension-manager.js';
|
||||||
import { createPolicyUpdater } from './config/policy.js';
|
import { createPolicyUpdater } from './config/policy.js';
|
||||||
import { requestConsentNonInteractive } from './config/extensions/consent.js';
|
import { requestConsentNonInteractive } from './config/extensions/consent.js';
|
||||||
import { disableMouseEvents, enableMouseEvents } from './ui/utils/mouse.js';
|
|
||||||
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
|
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
|
||||||
import ansiEscapes from 'ansi-escapes';
|
|
||||||
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||||
|
|
||||||
import { profiler } from './ui/components/DebugProfiler.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
|
// 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
|
// and the Ink alternate buffer mode requires line wrapping harmful to
|
||||||
// screen readers.
|
// screen readers.
|
||||||
const useAlternateBuffer =
|
const useAlternateBuffer = shouldEnterAlternateScreen(
|
||||||
isAlternateBufferEnabled(settings) && !config.getScreenReader();
|
isAlternateBufferEnabled(settings),
|
||||||
|
config.getScreenReader(),
|
||||||
|
);
|
||||||
const mouseEventsEnabled = useAlternateBuffer;
|
const mouseEventsEnabled = useAlternateBuffer;
|
||||||
if (mouseEventsEnabled) {
|
if (mouseEventsEnabled) {
|
||||||
enableMouseEvents();
|
enableMouseEvents();
|
||||||
@@ -481,8 +484,14 @@ export async function main() {
|
|||||||
// input showing up in the output.
|
// input showing up in the output.
|
||||||
process.stdin.setRawMode(true);
|
process.stdin.setRawMode(true);
|
||||||
|
|
||||||
if (isAlternateBufferEnabled(settings)) {
|
if (
|
||||||
writeToStdout(ansiEscapes.enterAlternativeScreen);
|
shouldEnterAlternateScreen(
|
||||||
|
isAlternateBufferEnabled(settings),
|
||||||
|
config.getScreenReader(),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
enterAlternateScreen();
|
||||||
|
disableLineWrapping();
|
||||||
|
|
||||||
// Ink will cleanup so there is no need for us to manually cleanup.
|
// Ink will cleanup so there is no need for us to manually cleanup.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|||||||
stdout: process.stdout,
|
stdout: process.stdout,
|
||||||
stderr: process.stderr,
|
stderr: process.stderr,
|
||||||
})),
|
})),
|
||||||
|
enableMouseEvents: vi.fn(),
|
||||||
|
disableMouseEvents: vi.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
import type { LoadedSettings } from '../config/settings.js';
|
import type { LoadedSettings } from '../config/settings.js';
|
||||||
@@ -137,10 +139,6 @@ vi.mock('../utils/events.js');
|
|||||||
vi.mock('../utils/handleAutoUpdate.js');
|
vi.mock('../utils/handleAutoUpdate.js');
|
||||||
vi.mock('./utils/ConsolePatcher.js');
|
vi.mock('./utils/ConsolePatcher.js');
|
||||||
vi.mock('../utils/cleanup.js');
|
vi.mock('../utils/cleanup.js');
|
||||||
vi.mock('./utils/mouse.js', () => ({
|
|
||||||
enableMouseEvents: vi.fn(),
|
|
||||||
disableMouseEvents: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { useHistory } from './hooks/useHistoryManager.js';
|
import { useHistory } from './hooks/useHistoryManager.js';
|
||||||
import { useThemeCommand } from './hooks/useThemeCommand.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 { useKeypress, type Key } from './hooks/useKeypress.js';
|
||||||
import { measureElement } from 'ink';
|
import { measureElement } from 'ink';
|
||||||
import { useTerminalSize } from './hooks/useTerminalSize.js';
|
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 { type ExtensionManager } from '../config/extension-manager.js';
|
||||||
import { enableMouseEvents, disableMouseEvents } from './utils/mouse.js';
|
|
||||||
|
|
||||||
describe('AppContainer State Management', () => {
|
describe('AppContainer State Management', () => {
|
||||||
let mockConfig: Config;
|
let mockConfig: Config;
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ import {
|
|||||||
refreshServerHierarchicalMemory,
|
refreshServerHierarchicalMemory,
|
||||||
type ModelChangedPayload,
|
type ModelChangedPayload,
|
||||||
type MemoryChangedPayload,
|
type MemoryChangedPayload,
|
||||||
|
writeToStdout,
|
||||||
|
disableMouseEvents,
|
||||||
|
enterAlternateScreen,
|
||||||
|
enableMouseEvents,
|
||||||
|
disableLineWrapping,
|
||||||
|
shouldEnterAlternateScreen,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { validateAuthMethod } from '../config/auth.js';
|
import { validateAuthMethod } from '../config/auth.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
@@ -92,6 +98,7 @@ import { appEvents, AppEvent } from '../utils/events.js';
|
|||||||
import { type UpdateObject } from './utils/updateCheck.js';
|
import { type UpdateObject } from './utils/updateCheck.js';
|
||||||
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
||||||
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
|
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
|
||||||
|
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
|
||||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||||
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||||
import { useSessionStats } from './contexts/SessionContext.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 { requestConsentInteractive } from '../config/extensions/consent.js';
|
||||||
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
||||||
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
||||||
import { disableMouseEvents, enableMouseEvents } from './utils/mouse.js';
|
|
||||||
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
||||||
import { useSettings } from './contexts/SettingsContext.js';
|
import { useSettings } from './contexts/SettingsContext.js';
|
||||||
import { enableSupportedProtocol } from './utils/kittyProtocolDetector.js';
|
import { enableSupportedProtocol } from './utils/kittyProtocolDetector.js';
|
||||||
import { writeToStdout } from '@google/gemini-cli-core';
|
|
||||||
|
|
||||||
const WARNING_PROMPT_DURATION_MS = 1000;
|
const WARNING_PROMPT_DURATION_MS = 1000;
|
||||||
const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
|
const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
|
||||||
@@ -372,16 +377,19 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
setHistoryRemountKey((prev) => prev + 1);
|
setHistoryRemountKey((prev) => prev + 1);
|
||||||
}, [setHistoryRemountKey, isAlternateBuffer, stdout]);
|
}, [setHistoryRemountKey, isAlternateBuffer, stdout]);
|
||||||
const handleEditorClose = useCallback(() => {
|
const handleEditorClose = useCallback(() => {
|
||||||
if (isAlternateBuffer) {
|
if (
|
||||||
|
shouldEnterAlternateScreen(isAlternateBuffer, config.getScreenReader())
|
||||||
|
) {
|
||||||
// The editor may have exited alternate buffer mode so we need to
|
// The editor may have exited alternate buffer mode so we need to
|
||||||
// enter it again to be safe.
|
// enter it again to be safe.
|
||||||
writeToStdout(ansiEscapes.enterAlternativeScreen);
|
enterAlternateScreen();
|
||||||
enableMouseEvents();
|
enableMouseEvents();
|
||||||
|
disableLineWrapping();
|
||||||
app.rerender();
|
app.rerender();
|
||||||
}
|
}
|
||||||
enableSupportedProtocol();
|
enableSupportedProtocol();
|
||||||
refreshStatic();
|
refreshStatic();
|
||||||
}, [refreshStatic, isAlternateBuffer, app]);
|
}, [refreshStatic, isAlternateBuffer, app, config]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
coreEvents.on(CoreEvent.ExternalEditorClosed, handleEditorClose);
|
coreEvents.on(CoreEvent.ExternalEditorClosed, handleEditorClose);
|
||||||
@@ -458,12 +466,12 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
config.isBrowserLaunchSuppressed()
|
config.isBrowserLaunchSuppressed()
|
||||||
) {
|
) {
|
||||||
await runExitCleanup();
|
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);
|
setAuthState(AuthState.Authenticated);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { validateAuthMethodWithSettings } from './useAuth.js';
|
|||||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||||
import { clearCachedCredentialFile } from '@google/gemini-cli-core';
|
import { clearCachedCredentialFile } from '@google/gemini-cli-core';
|
||||||
import { Text } from 'ink';
|
import { Text } from 'ink';
|
||||||
|
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
||||||
|
|
||||||
// Mocks
|
// Mocks
|
||||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
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 () => {
|
it('exits process for Login with Google when browser is suppressed', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
const exitSpy = vi
|
const exitSpy = vi
|
||||||
.spyOn(process, 'exit')
|
.spyOn(process, 'exit')
|
||||||
.mockImplementation(() => undefined as never);
|
.mockImplementation(() => undefined as never);
|
||||||
@@ -241,14 +243,14 @@ describe('AuthDialog', () => {
|
|||||||
mockedRadioButtonSelect.mock.calls[0][0];
|
mockedRadioButtonSelect.mock.calls[0][0];
|
||||||
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
|
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
|
||||||
|
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
expect(mockedRunExitCleanup).toHaveBeenCalled();
|
expect(mockedRunExitCleanup).toHaveBeenCalled();
|
||||||
expect(logSpy).toHaveBeenCalledWith(
|
expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
|
||||||
expect.stringContaining('Please restart Gemini CLI'),
|
|
||||||
);
|
|
||||||
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
||||||
|
|
||||||
exitSpy.mockRestore();
|
exitSpy.mockRestore();
|
||||||
logSpy.mockRestore();
|
logSpy.mockRestore();
|
||||||
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
||||||
@@ -17,13 +17,13 @@ import { SettingScope } from '../../config/settings.js';
|
|||||||
import {
|
import {
|
||||||
AuthType,
|
AuthType,
|
||||||
clearCachedCredentialFile,
|
clearCachedCredentialFile,
|
||||||
debugLogger,
|
|
||||||
type Config,
|
type Config,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { AuthState } from '../types.js';
|
import { AuthState } from '../types.js';
|
||||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||||
import { validateAuthMethodWithSettings } from './useAuth.js';
|
import { validateAuthMethodWithSettings } from './useAuth.js';
|
||||||
|
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
||||||
|
|
||||||
interface AuthDialogProps {
|
interface AuthDialogProps {
|
||||||
config: Config;
|
config: Config;
|
||||||
@@ -40,6 +40,7 @@ export function AuthDialog({
|
|||||||
authError,
|
authError,
|
||||||
onAuthError,
|
onAuthError,
|
||||||
}: AuthDialogProps): React.JSX.Element {
|
}: AuthDialogProps): React.JSX.Element {
|
||||||
|
const [exiting, setExiting] = useState(false);
|
||||||
let items = [
|
let items = [
|
||||||
{
|
{
|
||||||
label: 'Login with Google',
|
label: 'Login with Google',
|
||||||
@@ -111,6 +112,9 @@ export function AuthDialog({
|
|||||||
|
|
||||||
const onSelect = useCallback(
|
const onSelect = useCallback(
|
||||||
async (authType: AuthType | undefined, scope: LoadableSettingScope) => {
|
async (authType: AuthType | undefined, scope: LoadableSettingScope) => {
|
||||||
|
if (exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (authType) {
|
if (authType) {
|
||||||
await clearCachedCredentialFile();
|
await clearCachedCredentialFile();
|
||||||
|
|
||||||
@@ -119,15 +123,12 @@ export function AuthDialog({
|
|||||||
authType === AuthType.LOGIN_WITH_GOOGLE &&
|
authType === AuthType.LOGIN_WITH_GOOGLE &&
|
||||||
config.isBrowserLaunchSuppressed()
|
config.isBrowserLaunchSuppressed()
|
||||||
) {
|
) {
|
||||||
runExitCleanup();
|
setExiting(true);
|
||||||
debugLogger.log(
|
setTimeout(async () => {
|
||||||
`
|
await runExitCleanup();
|
||||||
----------------------------------------------------------------
|
process.exit(RELAUNCH_EXIT_CODE);
|
||||||
Logging in with Google... Please restart Gemini CLI to continue.
|
}, 100);
|
||||||
----------------------------------------------------------------
|
return;
|
||||||
`,
|
|
||||||
);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (authType === AuthType.USE_GEMINI) {
|
if (authType === AuthType.USE_GEMINI) {
|
||||||
@@ -136,7 +137,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
}
|
}
|
||||||
setAuthState(AuthState.Unauthenticated);
|
setAuthState(AuthState.Unauthenticated);
|
||||||
},
|
},
|
||||||
[settings, config, setAuthState],
|
[settings, config, setAuthState, exiting],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAuthSelect = (authMethod: AuthType) => {
|
const handleAuthSelect = (authMethod: AuthType) => {
|
||||||
@@ -169,6 +170,23 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
{ isActive: true },
|
{ isActive: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (exiting) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor={theme.border.focused}
|
||||||
|
flexDirection="row"
|
||||||
|
padding={1}
|
||||||
|
width="100%"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
|
<Text color={theme.text.primary}>
|
||||||
|
Logging in with Google... Restarting Gemini CLI to continue.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
borderStyle="round"
|
borderStyle="round"
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { ApiAuthDialog } from '../auth/ApiAuthDialog.js';
|
|||||||
import { EditorSettingsDialog } from './EditorSettingsDialog.js';
|
import { EditorSettingsDialog } from './EditorSettingsDialog.js';
|
||||||
import { PrivacyNotice } from '../privacy/PrivacyNotice.js';
|
import { PrivacyNotice } from '../privacy/PrivacyNotice.js';
|
||||||
import { ProQuotaDialog } from './ProQuotaDialog.js';
|
import { ProQuotaDialog } from './ProQuotaDialog.js';
|
||||||
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||||
|
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
||||||
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
||||||
import { ModelDialog } from './ModelDialog.js';
|
import { ModelDialog } from './ModelDialog.js';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
@@ -137,7 +139,10 @@ export const DialogManager = ({
|
|||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onSelect={() => uiActions.closeSettingsDialog()}
|
onSelect={() => uiActions.closeSettingsDialog()}
|
||||||
onRestartRequest={() => process.exit(0)}
|
onRestartRequest={async () => {
|
||||||
|
await runExitCleanup();
|
||||||
|
process.exit(RELAUNCH_EXIT_CODE);
|
||||||
|
}}
|
||||||
availableTerminalHeight={terminalHeight - staticExtraHeight}
|
availableTerminalHeight={terminalHeight - staticExtraHeight}
|
||||||
config={config}
|
config={config}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ export async function detectAndEnableKittyProtocol(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import {
|
||||||
|
enableKittyKeyboardProtocol,
|
||||||
|
disableKittyKeyboardProtocol,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
|
|
||||||
export function isKittyProtocolEnabled(): boolean {
|
export function isKittyProtocolEnabled(): boolean {
|
||||||
return kittyEnabled;
|
return kittyEnabled;
|
||||||
}
|
}
|
||||||
@@ -104,8 +109,7 @@ export function isKittyProtocolEnabled(): boolean {
|
|||||||
function disableAllProtocols() {
|
function disableAllProtocols() {
|
||||||
try {
|
try {
|
||||||
if (kittyEnabled) {
|
if (kittyEnabled) {
|
||||||
// use writeSync to avoid race conditions
|
disableKittyKeyboardProtocol();
|
||||||
fs.writeSync(process.stdout.fd, '\x1b[<u');
|
|
||||||
kittyEnabled = false;
|
kittyEnabled = false;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -120,8 +124,7 @@ function disableAllProtocols() {
|
|||||||
export function enableSupportedProtocol(): void {
|
export function enableSupportedProtocol(): void {
|
||||||
try {
|
try {
|
||||||
if (kittySupported) {
|
if (kittySupported) {
|
||||||
// use writeSync to avoid race conditions
|
enableKittyKeyboardProtocol();
|
||||||
fs.writeSync(process.stdout.fd, '\x1b[>1u');
|
|
||||||
kittyEnabled = true;
|
kittyEnabled = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { writeToStdout } from '@google/gemini-cli-core';
|
import { enableMouseEvents, disableMouseEvents } from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
SGR_MOUSE_REGEX,
|
SGR_MOUSE_REGEX,
|
||||||
X11_MOUSE_REGEX,
|
X11_MOUSE_REGEX,
|
||||||
@@ -230,14 +230,4 @@ export function isIncompleteMouseSequence(buffer: string): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableMouseEvents() {
|
export { enableMouseEvents, disableMouseEvents };
|
||||||
// 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');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import readline from 'node:readline';
|
|||||||
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
|
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
|
||||||
import { GEMINI_DIR } from '../utils/paths.js';
|
import { GEMINI_DIR } from '../utils/paths.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
|
import { writeToStdout } from '../utils/stdio.js';
|
||||||
|
|
||||||
vi.mock('os', async (importOriginal) => {
|
vi.mock('os', async (importOriginal) => {
|
||||||
const os = await importOriginal<typeof import('os')>();
|
const os = await importOriginal<typeof import('os')>();
|
||||||
@@ -44,6 +45,19 @@ vi.mock('node:readline');
|
|||||||
vi.mock('../utils/browser.js', () => ({
|
vi.mock('../utils/browser.js', () => ({
|
||||||
shouldAttemptBrowserLaunch: () => true,
|
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', () => ({
|
vi.mock('./oauth-credential-storage.js', () => ({
|
||||||
OAuthCredentialStorage: {
|
OAuthCredentialStorage: {
|
||||||
@@ -238,13 +252,10 @@ describe('oauth2', () => {
|
|||||||
const mockReadline = {
|
const mockReadline = {
|
||||||
question: vi.fn((_query, callback) => callback(mockCode)),
|
question: vi.fn((_query, callback) => callback(mockCode)),
|
||||||
close: vi.fn(),
|
close: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
};
|
};
|
||||||
(readline.createInterface as Mock).mockReturnValue(mockReadline);
|
(readline.createInterface as Mock).mockReturnValue(mockReadline);
|
||||||
|
|
||||||
const consoleLogSpy = vi
|
|
||||||
.spyOn(debugLogger, 'log')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const client = await getOauthClient(
|
const client = await getOauthClient(
|
||||||
AuthType.LOGIN_WITH_GOOGLE,
|
AuthType.LOGIN_WITH_GOOGLE,
|
||||||
mockConfigWithNoBrowser,
|
mockConfigWithNoBrowser,
|
||||||
@@ -255,7 +266,7 @@ describe('oauth2', () => {
|
|||||||
// Verify the auth flow
|
// Verify the auth flow
|
||||||
expect(mockGenerateCodeVerifierAsync).toHaveBeenCalled();
|
expect(mockGenerateCodeVerifierAsync).toHaveBeenCalled();
|
||||||
expect(mockGenerateAuthUrl).toHaveBeenCalled();
|
expect(mockGenerateAuthUrl).toHaveBeenCalled();
|
||||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
expect(vi.mocked(writeToStdout)).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(mockAuthUrl),
|
expect.stringContaining(mockAuthUrl),
|
||||||
);
|
);
|
||||||
expect(mockReadline.question).toHaveBeenCalledWith(
|
expect(mockReadline.question).toHaveBeenCalledWith(
|
||||||
@@ -268,8 +279,6 @@ describe('oauth2', () => {
|
|||||||
redirect_uri: 'https://codeassist.google.com/authcode',
|
redirect_uri: 'https://codeassist.google.com/authcode',
|
||||||
});
|
});
|
||||||
expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens);
|
expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens);
|
||||||
|
|
||||||
consoleLogSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('in Cloud Shell', () => {
|
describe('in Cloud Shell', () => {
|
||||||
@@ -932,6 +941,7 @@ describe('oauth2', () => {
|
|||||||
const mockReadline = {
|
const mockReadline = {
|
||||||
question: vi.fn((_query, callback) => callback('invalid-code')),
|
question: vi.fn((_query, callback) => callback('invalid-code')),
|
||||||
close: vi.fn(),
|
close: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
};
|
};
|
||||||
(readline.createInterface as Mock).mockReturnValue(mockReadline);
|
(readline.createInterface as Mock).mockReturnValue(mockReadline);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ import open from 'open';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { promises as fs } from 'node:fs';
|
import { promises as fs } from 'node:fs';
|
||||||
import type { Config } from '../config/config.js';
|
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 { UserAccountManager } from '../utils/userAccountManager.js';
|
||||||
import { AuthType } from '../core/contentGenerator.js';
|
import { AuthType } from '../core/contentGenerator.js';
|
||||||
import readline from 'node:readline';
|
import readline from 'node:readline';
|
||||||
@@ -27,6 +31,19 @@ import { Storage } from '../config/storage.js';
|
|||||||
import { OAuthCredentialStorage } from './oauth-credential-storage.js';
|
import { OAuthCredentialStorage } from './oauth-credential-storage.js';
|
||||||
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
|
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.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();
|
const userAccountManager = new UserAccountManager();
|
||||||
|
|
||||||
@@ -185,16 +202,34 @@ async function initOauthClient(
|
|||||||
if (config.isBrowserLaunchSuppressed()) {
|
if (config.isBrowserLaunchSuppressed()) {
|
||||||
let success = false;
|
let success = false;
|
||||||
const maxRetries = 2;
|
const maxRetries = 2;
|
||||||
for (let i = 0; !success && i < maxRetries; i++) {
|
// Enter alternate buffer
|
||||||
success = await authWithUserCode(client);
|
enterAlternateScreen();
|
||||||
if (!success) {
|
// Clear screen and move cursor to top-left.
|
||||||
debugLogger.error(
|
writeToStdout('\u001B[2J\u001B[H');
|
||||||
'\nFailed to authenticate with user code.',
|
disableMouseEvents();
|
||||||
i === maxRetries - 1 ? '' : 'Retrying...\n',
|
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) {
|
if (!success) {
|
||||||
|
writeToStderr('Failed to authenticate with user code.\n');
|
||||||
throw new FatalAuthenticationError(
|
throw new FatalAuthenticationError(
|
||||||
'Failed to authenticate with user code.',
|
'Failed to authenticate with user code.',
|
||||||
);
|
);
|
||||||
@@ -202,11 +237,13 @@ async function initOauthClient(
|
|||||||
} else {
|
} else {
|
||||||
const webLogin = await authWithWeb(client);
|
const webLogin = await authWithWeb(client);
|
||||||
|
|
||||||
debugLogger.log(
|
coreEvents.emit(CoreEvent.UserFeedback, {
|
||||||
`\n\nCode Assist login required.\n` +
|
severity: 'info',
|
||||||
|
message:
|
||||||
|
`\n\nCode Assist login required.\n` +
|
||||||
`Attempting to open authentication page in your browser.\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 {
|
try {
|
||||||
// Attempt to open the authentication URL in the default browser.
|
// Attempt to open the authentication URL in the default browser.
|
||||||
// We do not use the `wait` option here because the main script's execution
|
// 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,
|
// in a minimal Docker container), it will emit an unhandled 'error' event,
|
||||||
// causing the entire Node.js process to crash.
|
// causing the entire Node.js process to crash.
|
||||||
childProcess.on('error', (error) => {
|
childProcess.on('error', (error) => {
|
||||||
debugLogger.error(
|
coreEvents.emit(CoreEvent.UserFeedback, {
|
||||||
`Failed to open browser with error:`,
|
severity: 'error',
|
||||||
getErrorMessage(error),
|
message:
|
||||||
`\nPlease try running again with NO_BROWSER=true set.`,
|
`Failed to open browser with error: ${getErrorMessage(error)}\n` +
|
||||||
);
|
`Please try running again with NO_BROWSER=true set.`,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugLogger.error(
|
coreEvents.emit(CoreEvent.UserFeedback, {
|
||||||
`Failed to open browser with error:`,
|
severity: 'error',
|
||||||
getErrorMessage(err),
|
message:
|
||||||
`\nPlease try running again with NO_BROWSER=true set.`,
|
`Failed to open browser with error: ${getErrorMessage(err)}\n` +
|
||||||
);
|
`Please try running again with NO_BROWSER=true set.`,
|
||||||
|
});
|
||||||
throw new FatalAuthenticationError(
|
throw new FatalAuthenticationError(
|
||||||
`Failed to open browser: ${getErrorMessage(err)}`,
|
`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
|
// Add timeout to prevent infinite waiting when browser tab gets stuck
|
||||||
const authTimeout = 5 * 60 * 1000; // 5 minutes timeout
|
const authTimeout = 5 * 60 * 1000; // 5 minutes timeout
|
||||||
@@ -250,6 +292,11 @@ async function initOauthClient(
|
|||||||
});
|
});
|
||||||
|
|
||||||
await Promise.race([webLogin.loginCompletePromise, timeoutPromise]);
|
await Promise.race([webLogin.loginCompletePromise, timeoutPromise]);
|
||||||
|
|
||||||
|
coreEvents.emit(CoreEvent.UserFeedback, {
|
||||||
|
severity: 'info',
|
||||||
|
message: 'Authentication succeeded\n',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
@@ -266,55 +313,77 @@ export async function getOauthClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
|
async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
|
||||||
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<string>((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 {
|
try {
|
||||||
const { tokens } = await client.getToken({
|
const redirectUri = 'https://codeassist.google.com/authcode';
|
||||||
code,
|
const codeVerifier = await client.generateCodeVerifierAsync();
|
||||||
codeVerifier: codeVerifier.codeVerifier,
|
const state = crypto.randomBytes(32).toString('hex');
|
||||||
|
const authUrl: string = client.generateAuthUrl({
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
|
access_type: 'offline',
|
||||||
|
scope: OAUTH_SCOPE,
|
||||||
|
code_challenge_method: CodeChallengeMethod.S256,
|
||||||
|
code_challenge: codeVerifier.codeChallenge,
|
||||||
|
state,
|
||||||
});
|
});
|
||||||
client.setCredentials(tokens);
|
writeToStdout(
|
||||||
} catch (error) {
|
'Please visit the following URL to authorize the application:\n\n' +
|
||||||
|
authUrl +
|
||||||
|
'\n\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
const code = await new Promise<string>((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(
|
debugLogger.error(
|
||||||
'Failed to authenticate with authorization code:',
|
'Failed to authenticate with user code:',
|
||||||
getErrorMessage(error),
|
getErrorMessage(err),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authWithWeb(client: OAuth2Client): Promise<OauthWebLogin> {
|
async function authWithWeb(client: OAuth2Client): Promise<OauthWebLogin> {
|
||||||
|
|||||||
@@ -147,3 +147,4 @@ export * from './hooks/types.js';
|
|||||||
|
|
||||||
// Export stdio utils
|
// Export stdio utils
|
||||||
export * from './utils/stdio.js';
|
export * from './utils/stdio.js';
|
||||||
|
export * from './utils/terminal.js';
|
||||||
|
|||||||
@@ -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[<u');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableLineWrapping() {
|
||||||
|
writeToStdout('\x1b[?7h');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disableLineWrapping() {
|
||||||
|
writeToStdout('\x1b[?7l');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enterAlternateScreen() {
|
||||||
|
writeToStdout('\x1b[?1049h');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitAlternateScreen() {
|
||||||
|
writeToStdout('\x1b[?1049l');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldEnterAlternateScreen(
|
||||||
|
useAlternateBuffer: boolean,
|
||||||
|
isScreenReader: boolean,
|
||||||
|
): boolean {
|
||||||
|
return useAlternateBuffer && !isScreenReader;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user