From 45a4a7054e2ad06ee421a2ad5069e78639066848 Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Wed, 11 Mar 2026 20:00:03 +0000 Subject: [PATCH 1/4] chore: allow 'gemini-3.1' in sensitive keyword linter (#22065) --- scripts/lint.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/lint.js b/scripts/lint.js index 049e89fca1..279421a979 100644 --- a/scripts/lint.js +++ b/scripts/lint.js @@ -246,6 +246,7 @@ export function runSensitiveKeywordLinter() { console.log('\nRunning sensitive keyword linter...'); const SENSITIVE_PATTERN = /gemini-\d+(\.\d+)?/g; const ALLOWED_KEYWORDS = new Set([ + 'gemini-3.1', 'gemini-3', 'gemini-3.0', 'gemini-2.5', From e802776c96077c414f279ef9acfcec1371d096c8 Mon Sep 17 00:00:00 2001 From: M Junaid Shaukat <154750865+junaiddshaukat@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:10:29 +0500 Subject: [PATCH 2/4] feat(core): support custom base URL via env vars (#21561) Co-authored-by: Spencer --- .../core/src/core/contentGenerator.test.ts | 141 ++++++++++++++++++ packages/core/src/core/contentGenerator.ts | 30 +++- 2 files changed, 169 insertions(+), 2 deletions(-) diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts index d86eb6f738..c5dcc6e22a 100644 --- a/packages/core/src/core/contentGenerator.test.ts +++ b/packages/core/src/core/contentGenerator.test.ts @@ -10,6 +10,7 @@ import { AuthType, createContentGeneratorConfig, type ContentGenerator, + validateBaseUrl, } from './contentGenerator.js'; import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js'; import { GoogleGenAI } from '@google/genai'; @@ -442,6 +443,116 @@ describe('createContentGenerator', () => { ); }); + it('should pass GOOGLE_GEMINI_BASE_URL as httpOptions.baseUrl for Gemini API', async () => { + const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); + vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'https://my-gemini-proxy.example.com'); + + await createContentGenerator( + { + apiKey: 'test-api-key', + authType: AuthType.USE_GEMINI, + }, + mockConfig, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + httpOptions: expect.objectContaining({ + baseUrl: 'https://my-gemini-proxy.example.com', + }), + }), + ); + }); + + it('should pass GOOGLE_VERTEX_BASE_URL as httpOptions.baseUrl for Vertex AI', async () => { + const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); + vi.stubEnv('GOOGLE_VERTEX_BASE_URL', 'https://my-vertex-proxy.example.com'); + + await createContentGenerator( + { + apiKey: 'test-api-key', + vertexai: true, + authType: AuthType.USE_VERTEX_AI, + }, + mockConfig, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.objectContaining({ + httpOptions: expect.objectContaining({ + baseUrl: 'https://my-vertex-proxy.example.com', + }), + }), + ); + }); + + it('should not include baseUrl in httpOptions when GOOGLE_GEMINI_BASE_URL is not set', async () => { + const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + } as unknown as Config; + + const mockGenerator = { + models: {}, + } as unknown as GoogleGenAI; + vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never); + + await createContentGenerator( + { + apiKey: 'test-api-key', + authType: AuthType.USE_GEMINI, + }, + mockConfig, + ); + + expect(GoogleGenAI).toHaveBeenCalledWith( + expect.not.objectContaining({ + httpOptions: expect.objectContaining({ + baseUrl: expect.any(String), + }), + }), + ); + }); + + it('should reject an insecure GOOGLE_GEMINI_BASE_URL for non-local hosts', async () => { + const mockConfig = { + getModel: vi.fn().mockReturnValue('gemini-pro'), + getProxy: vi.fn().mockReturnValue(undefined), + getUsageStatisticsEnabled: () => false, + } as unknown as Config; + + vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'http://evil-proxy.example.com'); + + await expect( + createContentGenerator( + { + apiKey: 'test-api-key', + authType: AuthType.USE_GEMINI, + }, + mockConfig, + ), + ).rejects.toThrow('Custom base URL must use HTTPS unless it is localhost.'); + }); + it('should pass apiVersion for Vertex AI when GOOGLE_GENAI_API_VERSION is set', async () => { const mockConfig = { getModel: vi.fn().mockReturnValue('gemini-pro'), @@ -560,3 +671,33 @@ describe('createContentGeneratorConfig', () => { expect(config.vertexai).toBeUndefined(); }); }); + +describe('validateBaseUrl', () => { + it('should accept a valid HTTPS URL', () => { + expect(() => validateBaseUrl('https://my-proxy.example.com')).not.toThrow(); + }); + + it('should accept HTTP for localhost', () => { + expect(() => validateBaseUrl('http://localhost:8080')).not.toThrow(); + }); + + it('should accept HTTP for 127.0.0.1', () => { + expect(() => validateBaseUrl('http://127.0.0.1:3000')).not.toThrow(); + }); + + it('should accept HTTP for ::1', () => { + expect(() => validateBaseUrl('http://[::1]:8080')).not.toThrow(); + }); + + it('should reject HTTP for non-local hosts', () => { + expect(() => validateBaseUrl('http://my-proxy.example.com')).toThrow( + 'Custom base URL must use HTTPS unless it is localhost.', + ); + }); + + it('should reject an invalid URL', () => { + expect(() => validateBaseUrl('not-a-url')).toThrow( + 'Invalid custom base URL: not-a-url', + ); + }); +}); diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 2ce5420335..d7da9fb064 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -225,13 +225,25 @@ export async function createContentGenerator( 'x-gemini-api-privileged-user-id': `${installationId}`, }; } + let baseUrl = config.baseUrl; + if (!baseUrl) { + const envBaseUrl = config.vertexai + ? process.env['GOOGLE_VERTEX_BASE_URL'] + : process.env['GOOGLE_GEMINI_BASE_URL']; + if (envBaseUrl) { + validateBaseUrl(envBaseUrl); + baseUrl = envBaseUrl; + } + } else { + validateBaseUrl(baseUrl); + } const httpOptions: { baseUrl?: string; headers: Record; } = { headers }; - if (config.baseUrl) { - httpOptions.baseUrl = config.baseUrl; + if (baseUrl) { + httpOptions.baseUrl = baseUrl; } const googleGenAI = new GoogleGenAI({ @@ -253,3 +265,17 @@ export async function createContentGenerator( return generator; } + +const LOCAL_HOSTNAMES = ['localhost', '127.0.0.1', '[::1]']; + +export function validateBaseUrl(baseUrl: string): void { + let url: URL; + try { + url = new URL(baseUrl); + } catch { + throw new Error(`Invalid custom base URL: ${baseUrl}`); + } + if (url.protocol !== 'https:' && !LOCAL_HOSTNAMES.includes(url.hostname)) { + throw new Error('Custom base URL must use HTTPS unless it is localhost.'); + } +} From be16caece222d058141bd21b4bdd6baf4f9ea7a5 Mon Sep 17 00:00:00 2001 From: nityam Date: Thu, 12 Mar 2026 01:44:12 +0530 Subject: [PATCH 3/4] merge duplicate imports packages/cli/src subtask2 (#22051) --- packages/cli/src/ui/IdeIntegrationNudge.tsx | 6 ++- packages/cli/src/ui/auth/AuthDialog.tsx | 8 ++-- .../src/ui/components/AgentConfigDialog.tsx | 8 ++-- .../cli/src/ui/components/AskUserDialog.tsx | 3 +- packages/cli/src/ui/components/Checklist.tsx | 2 +- .../ui/components/DetailedMessagesDisplay.tsx | 2 +- .../components/EditorSettingsDialog.test.tsx | 3 +- .../ui/components/EditorSettingsDialog.tsx | 10 ++--- .../src/ui/components/FolderTrustDialog.tsx | 6 ++- .../ui/components/GradientRegression.test.tsx | 2 +- packages/cli/src/ui/components/Help.test.tsx | 3 +- .../ui/components/HistoryItemDisplay.test.tsx | 5 +-- .../src/ui/components/InputPrompt.test.tsx | 43 +++++++++++++------ .../cli/src/ui/components/InputPrompt.tsx | 13 +++--- .../components/LogoutConfirmationDialog.tsx | 6 ++- .../components/LoopDetectionConfirmation.tsx | 6 ++- .../ui/components/ModelStatsDisplay.test.tsx | 4 +- .../ui/components/MultiFolderTrustDialog.tsx | 8 ++-- .../PermissionsModifyTrustDialog.test.tsx | 11 ++++- .../src/ui/components/PolicyUpdateDialog.tsx | 10 +++-- .../src/ui/components/RewindConfirmation.tsx | 6 ++- .../src/ui/components/SessionBrowser.test.tsx | 7 ++- .../components/SessionSummaryDisplay.test.tsx | 2 +- .../cli/src/ui/components/SettingsDialog.tsx | 11 +++-- .../src/ui/components/ShellInputPrompt.tsx | 2 +- .../src/ui/components/StatsDisplay.test.tsx | 2 +- .../cli/src/ui/components/StatsDisplay.tsx | 6 ++- .../ui/components/ToolStatsDisplay.test.tsx | 2 +- .../messages/CompressionMessage.test.tsx | 8 ++-- .../src/ui/components/messages/Todo.test.tsx | 8 ++-- .../cli/src/ui/components/messages/Todo.tsx | 2 +- .../messages/ToolConfirmationMessage.tsx | 6 ++- .../messages/ToolMessageRawMarkdown.test.tsx | 3 +- .../components/shared/BaseSelectionList.tsx | 7 +-- .../src/ui/components/shared/EnumSelector.tsx | 2 +- .../shared/RadioButtonSelect.test.tsx | 3 +- .../src/ui/components/shared/TextInput.tsx | 6 +-- .../ui/components/triage/TriageDuplicates.tsx | 8 +++- .../src/ui/components/triage/TriageIssues.tsx | 8 +++- .../views/ExtensionRegistryView.tsx | 1 - .../cli/src/ui/components/views/McpStatus.tsx | 3 +- 41 files changed, 153 insertions(+), 109 deletions(-) diff --git a/packages/cli/src/ui/IdeIntegrationNudge.tsx b/packages/cli/src/ui/IdeIntegrationNudge.tsx index 409a6469f6..37823cf8a8 100644 --- a/packages/cli/src/ui/IdeIntegrationNudge.tsx +++ b/packages/cli/src/ui/IdeIntegrationNudge.tsx @@ -6,8 +6,10 @@ import type { IdeInfo } from '@google/gemini-cli-core'; import { Box, Text } from 'ink'; -import type { RadioSelectItem } from './components/shared/RadioButtonSelect.js'; -import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from './components/shared/RadioButtonSelect.js'; import { useKeypress } from './hooks/useKeypress.js'; import { theme } from './semantic-colors.js'; diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx index 4e523d6b11..c823f606c6 100644 --- a/packages/cli/src/ui/auth/AuthDialog.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.tsx @@ -9,11 +9,11 @@ import { useCallback, useState } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js'; -import type { - LoadableSettingScope, - LoadedSettings, +import { + SettingScope, + type LoadableSettingScope, + type LoadedSettings, } from '../../config/settings.js'; -import { SettingScope } from '../../config/settings.js'; import { AuthType, clearCachedCredentialFile, diff --git a/packages/cli/src/ui/components/AgentConfigDialog.tsx b/packages/cli/src/ui/components/AgentConfigDialog.tsx index 819b32d7b0..3f5d348a45 100644 --- a/packages/cli/src/ui/components/AgentConfigDialog.tsx +++ b/packages/cli/src/ui/components/AgentConfigDialog.tsx @@ -8,11 +8,11 @@ import type React from 'react'; import { useState, useEffect, useMemo, useCallback } from 'react'; import { Text } from 'ink'; import { theme } from '../semantic-colors.js'; -import type { - LoadableSettingScope, - LoadedSettings, +import { + SettingScope, + type LoadableSettingScope, + type LoadedSettings, } from '../../config/settings.js'; -import { SettingScope } from '../../config/settings.js'; import type { AgentDefinition, AgentOverride } from '@google/gemini-cli-core'; import { getCachedStringWidth } from '../utils/textUtils.js'; import { diff --git a/packages/cli/src/ui/components/AskUserDialog.tsx b/packages/cli/src/ui/components/AskUserDialog.tsx index 4233616144..eec633b7de 100644 --- a/packages/cli/src/ui/components/AskUserDialog.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.tsx @@ -15,13 +15,12 @@ import { } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; -import type { Question } from '@google/gemini-cli-core'; +import { checkExhaustive, type Question } from '@google/gemini-cli-core'; import { BaseSelectionList } from './shared/BaseSelectionList.js'; import type { SelectionListItem } from '../hooks/useSelectionList.js'; import { TabHeader, type Tab } from './shared/TabHeader.js'; import { useKeypress, type Key } from '../hooks/useKeypress.js'; import { Command } from '../key/keyMatchers.js'; -import { checkExhaustive } from '@google/gemini-cli-core'; import { TextInput } from './shared/TextInput.js'; import { formatCommand } from '../key/keybindingUtils.js'; import { diff --git a/packages/cli/src/ui/components/Checklist.tsx b/packages/cli/src/ui/components/Checklist.tsx index cfbd4268fd..d9fb51278c 100644 --- a/packages/cli/src/ui/components/Checklist.tsx +++ b/packages/cli/src/ui/components/Checklist.tsx @@ -5,9 +5,9 @@ */ import type React from 'react'; +import { useMemo } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; -import { useMemo } from 'react'; import { ChecklistItem, type ChecklistItemData } from './ChecklistItem.js'; export interface ChecklistProps { diff --git a/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx index ff88afa888..13f3872e5d 100644 --- a/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx +++ b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useRef, useCallback } from 'react'; import type React from 'react'; +import { useRef, useCallback } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import type { ConsoleMessageItem } from '../types.js'; diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx index 36832c1662..6ebe22d982 100644 --- a/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/EditorSettingsDialog.test.tsx @@ -7,8 +7,7 @@ import { render } from '../../test-utils/render.js'; import { EditorSettingsDialog } from './EditorSettingsDialog.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { SettingScope } from '../../config/settings.js'; -import type { LoadedSettings } from '../../config/settings.js'; +import { SettingScope, type LoadedSettings } from '../../config/settings.js'; import { KeypressProvider } from '../contexts/KeypressContext.js'; import { act } from 'react'; import { waitFor } from '../../test-utils/async.js'; diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.tsx index f75b1c27b8..7fa0d2a2cf 100644 --- a/packages/cli/src/ui/components/EditorSettingsDialog.tsx +++ b/packages/cli/src/ui/components/EditorSettingsDialog.tsx @@ -13,18 +13,18 @@ import { type EditorDisplay, } from '../editors/editorSettingsManager.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; -import type { - LoadableSettingScope, - LoadedSettings, +import { + SettingScope, + type LoadableSettingScope, + type LoadedSettings, } from '../../config/settings.js'; -import { SettingScope } from '../../config/settings.js'; import { type EditorType, isEditorAvailable, EDITOR_DISPLAY_NAMES, + coreEvents, } from '@google/gemini-cli-core'; import { useKeypress } from '../hooks/useKeypress.js'; -import { coreEvents } from '@google/gemini-cli-core'; interface EditorDialogProps { onSelect: ( diff --git a/packages/cli/src/ui/components/FolderTrustDialog.tsx b/packages/cli/src/ui/components/FolderTrustDialog.tsx index 5bb748b28f..6c1c0d9e8c 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.tsx @@ -9,8 +9,10 @@ import type React from 'react'; import { useEffect, useState, useCallback } from 'react'; import { theme } from '../semantic-colors.js'; import stripAnsi from 'strip-ansi'; -import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; -import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from './shared/RadioButtonSelect.js'; import { MaxSizedBox } from './shared/MaxSizedBox.js'; import { Scrollable } from './shared/Scrollable.js'; import { useKeypress } from '../hooks/useKeypress.js'; diff --git a/packages/cli/src/ui/components/GradientRegression.test.tsx b/packages/cli/src/ui/components/GradientRegression.test.tsx index bc836a1102..ba118e6bcb 100644 --- a/packages/cli/src/ui/components/GradientRegression.test.tsx +++ b/packages/cli/src/ui/components/GradientRegression.test.tsx @@ -7,7 +7,7 @@ import { describe, it, expect, vi } from 'vitest'; import { renderWithProviders } from '../../test-utils/render.js'; import * as SessionContext from '../contexts/SessionContext.js'; -import type { SessionStatsState } from '../contexts/SessionContext.js'; +import { type SessionStatsState } from '../contexts/SessionContext.js'; import { Banner } from './Banner.js'; import { Footer } from './Footer.js'; import { Header } from './Header.js'; diff --git a/packages/cli/src/ui/components/Help.test.tsx b/packages/cli/src/ui/components/Help.test.tsx index 666593f04f..dc86cb70dc 100644 --- a/packages/cli/src/ui/components/Help.test.tsx +++ b/packages/cli/src/ui/components/Help.test.tsx @@ -7,8 +7,7 @@ import { render } from '../../test-utils/render.js'; import { describe, it, expect } from 'vitest'; import { Help } from './Help.js'; -import type { SlashCommand } from '../commands/types.js'; -import { CommandKind } from '../commands/types.js'; +import { CommandKind, type SlashCommand } from '../commands/types.js'; const mockCommands: readonly SlashCommand[] = [ { diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx index a574a9f311..f049ffe15e 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx @@ -6,13 +6,12 @@ import { describe, it, expect, vi } from 'vitest'; import { HistoryItemDisplay } from './HistoryItemDisplay.js'; -import { type HistoryItem } from '../types.js'; -import { MessageType } from '../types.js'; +import { MessageType, type HistoryItem } from '../types.js'; import { SessionStatsProvider } from '../contexts/SessionContext.js'; import { + CoreToolCallStatus, type Config, type ToolExecuteConfirmationDetails, - CoreToolCallStatus, } from '@google/gemini-cli-core'; import { ToolGroupMessage } from './messages/ToolGroupMessage.js'; import { renderWithProviders } from '../../test-utils/render.js'; diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 260455c782..15f6e2f8c4 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -8,31 +8,46 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { createMockSettings } from '../../test-utils/settings.js'; import { waitFor } from '../../test-utils/async.js'; import { act, useState } from 'react'; -import type { InputPromptProps } from './InputPrompt.js'; -import { InputPrompt, tryTogglePasteExpansion } from './InputPrompt.js'; -import type { TextBuffer } from './shared/text-buffer.js'; +import { + InputPrompt, + tryTogglePasteExpansion, + type InputPromptProps, +} from './InputPrompt.js'; import { calculateTransformationsForLine, calculateTransformedLine, + type TextBuffer, } from './shared/text-buffer.js'; -import type { Config } from '@google/gemini-cli-core'; -import { ApprovalMode, debugLogger } from '@google/gemini-cli-core'; +import { + ApprovalMode, + debugLogger, + type Config, +} from '@google/gemini-cli-core'; import * as path from 'node:path'; -import type { CommandContext, SlashCommand } from '../commands/types.js'; -import { CommandKind } from '../commands/types.js'; +import { + CommandKind, + type CommandContext, + type SlashCommand, +} from '../commands/types.js'; import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { Text } from 'ink'; -import type { UseShellHistoryReturn } from '../hooks/useShellHistory.js'; -import { useShellHistory } from '../hooks/useShellHistory.js'; -import type { UseCommandCompletionReturn } from '../hooks/useCommandCompletion.js'; +import { + useShellHistory, + type UseShellHistoryReturn, +} from '../hooks/useShellHistory.js'; import { useCommandCompletion, CompletionMode, + type UseCommandCompletionReturn, } from '../hooks/useCommandCompletion.js'; -import type { UseInputHistoryReturn } from '../hooks/useInputHistory.js'; -import { useInputHistory } from '../hooks/useInputHistory.js'; -import type { UseReverseSearchCompletionReturn } from '../hooks/useReverseSearchCompletion.js'; -import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js'; +import { + useInputHistory, + type UseInputHistoryReturn, +} from '../hooks/useInputHistory.js'; +import { + useReverseSearchCompletion, + type UseReverseSearchCompletionReturn, +} from '../hooks/useReverseSearchCompletion.js'; import clipboardy from 'clipboardy'; import * as clipboardUtils from '../utils/clipboardUtils.js'; import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js'; diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 1cfa2d4215..94b1d2dc00 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -5,8 +5,8 @@ */ import type React from 'react'; -import clipboardy from 'clipboardy'; import { useCallback, useEffect, useState, useRef, useMemo } from 'react'; +import clipboardy from 'clipboardy'; import { Box, Text, useStdout, type DOMElement } from 'ink'; import { SuggestionsDisplay, MAX_WIDTH } from './SuggestionsDisplay.js'; import { theme } from '../semantic-colors.js'; @@ -34,13 +34,16 @@ import { useCommandCompletion, CompletionMode, } from '../hooks/useCommandCompletion.js'; -import type { Key } from '../hooks/useKeypress.js'; -import { useKeypress } from '../hooks/useKeypress.js'; +import { useKeypress, type Key } from '../hooks/useKeypress.js'; import { Command } from '../key/keyMatchers.js'; import { formatCommand } from '../key/keybindingUtils.js'; import type { CommandContext, SlashCommand } from '../commands/types.js'; -import type { Config } from '@google/gemini-cli-core'; -import { ApprovalMode, coreEvents, debugLogger } from '@google/gemini-cli-core'; +import { + ApprovalMode, + coreEvents, + debugLogger, + type Config, +} from '@google/gemini-cli-core'; import { parseInputForHighlighting, parseSegmentsFromTokens, diff --git a/packages/cli/src/ui/components/LogoutConfirmationDialog.tsx b/packages/cli/src/ui/components/LogoutConfirmationDialog.tsx index fbe4c30bd0..44726f9bc2 100644 --- a/packages/cli/src/ui/components/LogoutConfirmationDialog.tsx +++ b/packages/cli/src/ui/components/LogoutConfirmationDialog.tsx @@ -7,8 +7,10 @@ import { Box, Text } from 'ink'; import type React from 'react'; import { theme } from '../semantic-colors.js'; -import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; -import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from './shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; export enum LogoutChoice { diff --git a/packages/cli/src/ui/components/LoopDetectionConfirmation.tsx b/packages/cli/src/ui/components/LoopDetectionConfirmation.tsx index 5d4690e51b..37523d5387 100644 --- a/packages/cli/src/ui/components/LoopDetectionConfirmation.tsx +++ b/packages/cli/src/ui/components/LoopDetectionConfirmation.tsx @@ -5,8 +5,10 @@ */ import { Box, Text } from 'ink'; -import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; -import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from './shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { theme } from '../semantic-colors.js'; diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx index 73c51fd0d1..5da3c3a6d2 100644 --- a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx @@ -9,8 +9,8 @@ import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; import { ModelStatsDisplay } from './ModelStatsDisplay.js'; import * as SessionContext from '../contexts/SessionContext.js'; import * as SettingsContext from '../contexts/SettingsContext.js'; -import type { LoadedSettings } from '../../config/settings.js'; -import type { SessionMetrics } from '../contexts/SessionContext.js'; +import { type LoadedSettings } from '../../config/settings.js'; +import { type SessionMetrics } from '../contexts/SessionContext.js'; import { ToolCallDecision, LlmRole } from '@google/gemini-cli-core'; // Mock the context to provide controlled data for testing diff --git a/packages/cli/src/ui/components/MultiFolderTrustDialog.tsx b/packages/cli/src/ui/components/MultiFolderTrustDialog.tsx index 0c2c4e362d..deab70c0ce 100644 --- a/packages/cli/src/ui/components/MultiFolderTrustDialog.tsx +++ b/packages/cli/src/ui/components/MultiFolderTrustDialog.tsx @@ -8,14 +8,16 @@ import { Box, Text } from 'ink'; import type React from 'react'; import { useState } from 'react'; import { theme } from '../semantic-colors.js'; -import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; -import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from './shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { loadTrustedFolders, TrustLevel } from '../../config/trustedFolders.js'; import { expandHomeDir } from '../utils/directoryUtils.js'; import * as path from 'node:path'; import { MessageType, type HistoryItem } from '../types.js'; -import type { Config } from '@google/gemini-cli-core'; +import { type Config } from '@google/gemini-cli-core'; export enum MultiFolderTrustChoice { YES, diff --git a/packages/cli/src/ui/components/PermissionsModifyTrustDialog.test.tsx b/packages/cli/src/ui/components/PermissionsModifyTrustDialog.test.tsx index 9ffaa8797b..3047922e09 100644 --- a/packages/cli/src/ui/components/PermissionsModifyTrustDialog.test.tsx +++ b/packages/cli/src/ui/components/PermissionsModifyTrustDialog.test.tsx @@ -4,8 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import type { Mock } from 'vitest'; +import { + describe, + it, + expect, + vi, + beforeEach, + afterEach, + type Mock, +} from 'vitest'; import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js'; diff --git a/packages/cli/src/ui/components/PolicyUpdateDialog.tsx b/packages/cli/src/ui/components/PolicyUpdateDialog.tsx index 6b24908560..c54f4ebf39 100644 --- a/packages/cli/src/ui/components/PolicyUpdateDialog.tsx +++ b/packages/cli/src/ui/components/PolicyUpdateDialog.tsx @@ -5,16 +5,18 @@ */ import { Box, Text } from 'ink'; -import { useCallback, useRef } from 'react'; import type React from 'react'; +import { useCallback, useRef } from 'react'; import { + PolicyIntegrityManager, type Config, type PolicyUpdateConfirmationRequest, - PolicyIntegrityManager, } from '@google/gemini-cli-core'; import { theme } from '../semantic-colors.js'; -import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; -import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from './shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { Command } from '../key/keyMatchers.js'; import { useKeyMatchers } from '../hooks/useKeyMatchers.js'; diff --git a/packages/cli/src/ui/components/RewindConfirmation.tsx b/packages/cli/src/ui/components/RewindConfirmation.tsx index a3a58db6f9..653d1e401a 100644 --- a/packages/cli/src/ui/components/RewindConfirmation.tsx +++ b/packages/cli/src/ui/components/RewindConfirmation.tsx @@ -8,8 +8,10 @@ import { Box, Text, useIsScreenReaderEnabled } from 'ink'; import type React from 'react'; import { useMemo } from 'react'; import { theme } from '../semantic-colors.js'; -import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; -import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from './shared/RadioButtonSelect.js'; import type { FileChangeStats } from '../utils/rewindFileOps.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { formatTimeAgo } from '../utils/formatters.js'; diff --git a/packages/cli/src/ui/components/SessionBrowser.test.tsx b/packages/cli/src/ui/components/SessionBrowser.test.tsx index e97ae310bd..83e3ae1aaa 100644 --- a/packages/cli/src/ui/components/SessionBrowser.test.tsx +++ b/packages/cli/src/ui/components/SessionBrowser.test.tsx @@ -8,10 +8,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { act } from 'react'; import { render } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; -import type { Config } from '@google/gemini-cli-core'; -import { SessionBrowser } from './SessionBrowser.js'; -import type { SessionBrowserProps } from './SessionBrowser.js'; -import type { SessionInfo } from '../../utils/sessionUtils.js'; +import { type Config } from '@google/gemini-cli-core'; +import { SessionBrowser, type SessionBrowserProps } from './SessionBrowser.js'; +import { type SessionInfo } from '../../utils/sessionUtils.js'; // Collect key handlers registered via useKeypress so tests can // simulate input without going through the full stdin pipeline. diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx index 2ed71762b7..3be3cb09f5 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx @@ -8,7 +8,7 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SessionSummaryDisplay } from './SessionSummaryDisplay.js'; import * as SessionContext from '../contexts/SessionContext.js'; -import type { SessionMetrics } from '../contexts/SessionContext.js'; +import { type SessionMetrics } from '../contexts/SessionContext.js'; import { ToolCallDecision, getShellConfiguration, diff --git a/packages/cli/src/ui/components/SettingsDialog.tsx b/packages/cli/src/ui/components/SettingsDialog.tsx index b8136254f3..82965bda71 100644 --- a/packages/cli/src/ui/components/SettingsDialog.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.tsx @@ -4,14 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type React from 'react'; import { useState, useMemo, useCallback, useEffect } from 'react'; +import type React from 'react'; import { Text } from 'ink'; import { AsyncFzf } from 'fzf'; -import type { Key } from '../hooks/useKeypress.js'; +import { type Key } from '../hooks/useKeypress.js'; import { theme } from '../semantic-colors.js'; -import type { LoadableSettingScope, Settings } from '../../config/settings.js'; -import { SettingScope } from '../../config/settings.js'; +import { + SettingScope, + type LoadableSettingScope, + type Settings, +} from '../../config/settings.js'; import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js'; import { getDialogSettingKeys, diff --git a/packages/cli/src/ui/components/ShellInputPrompt.tsx b/packages/cli/src/ui/components/ShellInputPrompt.tsx index 8f5831c1ef..82366abdb0 100644 --- a/packages/cli/src/ui/components/ShellInputPrompt.tsx +++ b/packages/cli/src/ui/components/ShellInputPrompt.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useCallback } from 'react'; import type React from 'react'; +import { useCallback } from 'react'; import { useKeypress } from '../hooks/useKeypress.js'; import { ShellExecutionService } from '@google/gemini-cli-core'; import { keyToAnsi, type Key } from '../key/keyToAnsi.js'; diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx index 2b4422e69c..cb9aa55cc5 100644 --- a/packages/cli/src/ui/components/StatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx @@ -8,7 +8,7 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { describe, it, expect, vi } from 'vitest'; import { StatsDisplay } from './StatsDisplay.js'; import * as SessionContext from '../contexts/SessionContext.js'; -import type { SessionMetrics } from '../contexts/SessionContext.js'; +import { type SessionMetrics } from '../contexts/SessionContext.js'; import { ToolCallDecision, type RetrieveUserQuotaResponse, diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index d369374d95..320203f3dc 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -9,8 +9,10 @@ import { Box, Text, useStdout } from 'ink'; import { ThemedGradient } from './ThemedGradient.js'; import { theme } from '../semantic-colors.js'; import { formatDuration, formatResetTime } from '../utils/formatters.js'; -import type { ModelMetrics } from '../contexts/SessionContext.js'; -import { useSessionStats } from '../contexts/SessionContext.js'; +import { + useSessionStats, + type ModelMetrics, +} from '../contexts/SessionContext.js'; import { getStatusColor, TOOL_SUCCESS_RATE_HIGH, diff --git a/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx b/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx index 72a1f12cff..197c7d84d5 100644 --- a/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx @@ -8,7 +8,7 @@ import { render } from '../../test-utils/render.js'; import { describe, it, expect, vi } from 'vitest'; import { ToolStatsDisplay } from './ToolStatsDisplay.js'; import * as SessionContext from '../contexts/SessionContext.js'; -import type { SessionMetrics } from '../contexts/SessionContext.js'; +import { type SessionMetrics } from '../contexts/SessionContext.js'; import { ToolCallDecision } from '@google/gemini-cli-core'; // Mock the context to provide controlled data for testing diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx index 7a4b9bc42d..a4f5212289 100644 --- a/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/CompressionMessage.test.tsx @@ -5,10 +5,12 @@ */ import { renderWithProviders } from '../../../test-utils/render.js'; -import type { CompressionDisplayProps } from './CompressionMessage.js'; -import { CompressionMessage } from './CompressionMessage.js'; +import { + CompressionMessage, + type CompressionDisplayProps, +} from './CompressionMessage.js'; import { CompressionStatus } from '@google/gemini-cli-core'; -import type { CompressionProps } from '../../types.js'; +import { type CompressionProps } from '../../types.js'; import { describe, it, expect } from 'vitest'; describe('', () => { diff --git a/packages/cli/src/ui/components/messages/Todo.test.tsx b/packages/cli/src/ui/components/messages/Todo.test.tsx index b6413e496a..17c4f623bf 100644 --- a/packages/cli/src/ui/components/messages/Todo.test.tsx +++ b/packages/cli/src/ui/components/messages/Todo.test.tsx @@ -8,11 +8,9 @@ import { render } from '../../../test-utils/render.js'; import { describe, it, expect } from 'vitest'; import { Box } from 'ink'; import { TodoTray } from './Todo.js'; -import type { Todo } from '@google/gemini-cli-core'; -import type { UIState } from '../../contexts/UIStateContext.js'; -import { UIStateContext } from '../../contexts/UIStateContext.js'; -import type { HistoryItem } from '../../types.js'; -import { CoreToolCallStatus } from '@google/gemini-cli-core'; +import { CoreToolCallStatus, type Todo } from '@google/gemini-cli-core'; +import { UIStateContext, type UIState } from '../../contexts/UIStateContext.js'; +import { type HistoryItem } from '../../types.js'; const createTodoHistoryItem = (todos: Todo[]): HistoryItem => ({ diff --git a/packages/cli/src/ui/components/messages/Todo.tsx b/packages/cli/src/ui/components/messages/Todo.tsx index cbc2405ac0..a7201b12fb 100644 --- a/packages/cli/src/ui/components/messages/Todo.tsx +++ b/packages/cli/src/ui/components/messages/Todo.tsx @@ -5,9 +5,9 @@ */ import type React from 'react'; +import { useMemo } from 'react'; import { type TodoList } from '@google/gemini-cli-core'; import { useUIState } from '../../contexts/UIStateContext.js'; -import { useMemo } from 'react'; import type { HistoryItemToolGroup } from '../../types.js'; import { Checklist } from '../Checklist.js'; import type { ChecklistItemData } from '../ChecklistItem.js'; diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 113852cb8d..8bc329f3df 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -18,9 +18,11 @@ import { hasRedirection, debugLogger, } from '@google/gemini-cli-core'; -import type { RadioSelectItem } from '../shared/RadioButtonSelect.js'; import { useToolActions } from '../../contexts/ToolActionsContext.js'; -import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from '../shared/RadioButtonSelect.js'; import { MaxSizedBox, MINIMUM_MAX_HEIGHT } from '../shared/MaxSizedBox.js'; import { sanitizeForDisplay, diff --git a/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx b/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx index b81c951978..2375be7f0e 100644 --- a/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessageRawMarkdown.test.tsx @@ -5,8 +5,7 @@ */ import { describe, it, expect } from 'vitest'; -import type { ToolMessageProps } from './ToolMessage.js'; -import { ToolMessage } from './ToolMessage.js'; +import { ToolMessage, type ToolMessageProps } from './ToolMessage.js'; import { StreamingState } from '../../types.js'; import { StreamingContext } from '../../contexts/StreamingContext.js'; import { renderWithProviders } from '../../../test-utils/render.js'; diff --git a/packages/cli/src/ui/components/shared/BaseSelectionList.tsx b/packages/cli/src/ui/components/shared/BaseSelectionList.tsx index 7efb40b3ae..1090d4010d 100644 --- a/packages/cli/src/ui/components/shared/BaseSelectionList.tsx +++ b/packages/cli/src/ui/components/shared/BaseSelectionList.tsx @@ -8,9 +8,10 @@ import type React from 'react'; import { useEffect, useState } from 'react'; import { Text, Box } from 'ink'; import { theme } from '../../semantic-colors.js'; -import { useSelectionList } from '../../hooks/useSelectionList.js'; - -import type { SelectionListItem } from '../../hooks/useSelectionList.js'; +import { + useSelectionList, + type SelectionListItem, +} from '../../hooks/useSelectionList.js'; export interface RenderItemContext { isSelected: boolean; diff --git a/packages/cli/src/ui/components/shared/EnumSelector.tsx b/packages/cli/src/ui/components/shared/EnumSelector.tsx index a86efd8ff1..5553e6ff0d 100644 --- a/packages/cli/src/ui/components/shared/EnumSelector.tsx +++ b/packages/cli/src/ui/components/shared/EnumSelector.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useState, useEffect } from 'react'; import type React from 'react'; +import { useState, useEffect } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import type { SettingEnumOption } from '../../../config/settingsSchema.js'; diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.test.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.test.tsx index 00607e522a..393dd63d44 100644 --- a/packages/cli/src/ui/components/shared/RadioButtonSelect.test.tsx +++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.test.tsx @@ -6,9 +6,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderWithProviders } from '../../../test-utils/render.js'; -import type { Text } from 'ink'; -import { Box } from 'ink'; import type React from 'react'; +import { Box, type Text } from 'ink'; import { RadioButtonSelect, type RadioSelectItem, diff --git a/packages/cli/src/ui/components/shared/TextInput.tsx b/packages/cli/src/ui/components/shared/TextInput.tsx index 277d5e9723..479757d8f3 100644 --- a/packages/cli/src/ui/components/shared/TextInput.tsx +++ b/packages/cli/src/ui/components/shared/TextInput.tsx @@ -6,13 +6,11 @@ import type React from 'react'; import { useCallback } from 'react'; -import type { Key } from '../../hooks/useKeypress.js'; import { Text, Box } from 'ink'; -import { useKeypress } from '../../hooks/useKeypress.js'; +import { useKeypress, type Key } from '../../hooks/useKeypress.js'; import chalk from 'chalk'; import { theme } from '../../semantic-colors.js'; -import type { TextBuffer } from './text-buffer.js'; -import { expandPastePlaceholders } from './text-buffer.js'; +import { expandPastePlaceholders, type TextBuffer } from './text-buffer.js'; import { cpSlice, cpIndexToOffset } from '../../utils/textUtils.js'; import { Command } from '../../key/keyMatchers.js'; import { useKeyMatchers } from '../../hooks/useKeyMatchers.js'; diff --git a/packages/cli/src/ui/components/triage/TriageDuplicates.tsx b/packages/cli/src/ui/components/triage/TriageDuplicates.tsx index 73d0ae701f..d77bf45243 100644 --- a/packages/cli/src/ui/components/triage/TriageDuplicates.tsx +++ b/packages/cli/src/ui/components/triage/TriageDuplicates.tsx @@ -7,8 +7,12 @@ import { useState, useEffect, useCallback } from 'react'; import { Box, Text } from 'ink'; import Spinner from 'ink-spinner'; -import type { Config } from '@google/gemini-cli-core'; -import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core'; +import { + debugLogger, + spawnAsync, + LlmRole, + type Config, +} from '@google/gemini-cli-core'; import { useKeypress } from '../../hooks/useKeypress.js'; import { Command } from '../../key/keyMatchers.js'; import { useKeyMatchers } from '../../hooks/useKeyMatchers.js'; diff --git a/packages/cli/src/ui/components/triage/TriageIssues.tsx b/packages/cli/src/ui/components/triage/TriageIssues.tsx index 477be8a363..62c0f50e1c 100644 --- a/packages/cli/src/ui/components/triage/TriageIssues.tsx +++ b/packages/cli/src/ui/components/triage/TriageIssues.tsx @@ -7,8 +7,12 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { Box, Text } from 'ink'; import Spinner from 'ink-spinner'; -import type { Config } from '@google/gemini-cli-core'; -import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core'; +import { + debugLogger, + spawnAsync, + LlmRole, + type Config, +} from '@google/gemini-cli-core'; import { useKeypress } from '../../hooks/useKeypress.js'; import { Command } from '../../key/keyMatchers.js'; import { TextInput } from '../shared/TextInput.js'; diff --git a/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx b/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx index 3e9b8a3469..0539437fc3 100644 --- a/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx +++ b/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx @@ -8,7 +8,6 @@ import type React from 'react'; import { useMemo, useCallback, useState } from 'react'; import { Box, Text } from 'ink'; import type { RegistryExtension } from '../../../config/extensionRegistryClient.js'; - import { SearchableList, type GenericListItem, diff --git a/packages/cli/src/ui/components/views/McpStatus.tsx b/packages/cli/src/ui/components/views/McpStatus.tsx index c007d14635..1f14c0b5c5 100644 --- a/packages/cli/src/ui/components/views/McpStatus.tsx +++ b/packages/cli/src/ui/components/views/McpStatus.tsx @@ -4,8 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { MCPServerConfig } from '@google/gemini-cli-core'; -import { MCPServerStatus } from '@google/gemini-cli-core'; +import { MCPServerStatus, type MCPServerConfig } from '@google/gemini-cli-core'; import { Box, Text } from 'ink'; import type React from 'react'; import { MAX_MCP_RESOURCES_TO_SHOW } from '../../constants.js'; From 775bcbf3a6144b989b3588f6492caaa8933b6177 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 11 Mar 2026 16:40:06 -0400 Subject: [PATCH 4/4] fix(core): silently retry API errors up to 3 times before halting session (#21989) --- packages/core/src/core/geminiChat.test.ts | 6 +-- packages/core/src/core/geminiChat.ts | 41 +++++++++++-------- .../src/core/geminiChat_network_retry.test.ts | 31 ++++++++++---- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts index ec375e88be..275e02118a 100644 --- a/packages/core/src/core/geminiChat.test.ts +++ b/packages/core/src/core/geminiChat.test.ts @@ -1380,11 +1380,11 @@ describe('GeminiChat', () => { } }).rejects.toThrow(InvalidStreamError); - // Should be called 2 times (initial + 1 retry) + // Should be called 4 times (initial + 3 retries) expect(mockContentGenerator.generateContentStream).toHaveBeenCalledTimes( - 2, + 4, ); - expect(mockLogContentRetry).toHaveBeenCalledTimes(1); + expect(mockLogContentRetry).toHaveBeenCalledTimes(3); expect(mockLogContentRetryFailure).toHaveBeenCalledTimes(1); // History should still contain the user message. diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 4dc586e156..c8f4897a38 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -79,17 +79,17 @@ export type StreamEvent = | { type: StreamEventType.AGENT_EXECUTION_BLOCKED; reason: string }; /** - * Options for retrying due to invalid content from the model. + * Options for retrying mid-stream errors (e.g. invalid content or API disconnects). */ -interface ContentRetryOptions { +interface MidStreamRetryOptions { /** Total number of attempts to make (1 initial + N retries). */ maxAttempts: number; /** The base delay in milliseconds for linear backoff. */ initialDelayMs: number; } -const INVALID_CONTENT_RETRY_OPTIONS: ContentRetryOptions = { - maxAttempts: 2, // 1 initial call + 1 retry +const MID_STREAM_RETRY_OPTIONS: MidStreamRetryOptions = { + maxAttempts: 4, // 1 initial call + 3 retries mid-stream initialDelayMs: 500, }; @@ -350,7 +350,7 @@ export class GeminiChat { this: GeminiChat, ): AsyncGenerator { try { - const maxAttempts = INVALID_CONTENT_RETRY_OPTIONS.maxAttempts; + const maxAttempts = this.config.getMaxAttempts(); for (let attempt = 0; attempt < maxAttempts; attempt++) { let isConnectionPhase = true; @@ -402,21 +402,19 @@ export class GeminiChat { return; // Stop the generator } + if (isConnectionPhase) { + // Connection phase errors have already been retried by retryWithBackoff. + // If they bubble up here, they are exhausted or fatal. + throw error; + } + // Check if the error is retryable (e.g., transient SSL errors - // like ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC) + // like ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC or ApiError) const isRetryable = isRetryableError( error, this.config.getRetryFetchErrors(), ); - // For connection phase errors, only retryable errors should continue - if (isConnectionPhase) { - if (!isRetryable || signal.aborted) { - throw error; - } - // Fall through to retry logic for retryable connection errors - } - const isContentError = error instanceof InvalidStreamError; const errorType = isContentError ? error.type @@ -426,9 +424,16 @@ export class GeminiChat { (isContentError && isGemini2Model(model)) || (isRetryable && !signal.aborted) ) { - // Check if we have more attempts left. - if (attempt < maxAttempts - 1) { - const delayMs = INVALID_CONTENT_RETRY_OPTIONS.initialDelayMs; + // The issue requests exactly 3 retries (4 attempts) for API errors during stream iteration. + // Regardless of the global maxAttempts (e.g. 10), we only want to retry these mid-stream API errors + // up to 3 times before finally throwing the error to the user. + const maxMidStreamAttempts = MID_STREAM_RETRY_OPTIONS.maxAttempts; + + if ( + attempt < maxAttempts - 1 && + attempt < maxMidStreamAttempts - 1 + ) { + const delayMs = MID_STREAM_RETRY_OPTIONS.initialDelayMs; if (isContentError) { logContentRetry( @@ -449,7 +454,7 @@ export class GeminiChat { } coreEvents.emitRetryAttempt({ attempt: attempt + 1, - maxAttempts, + maxAttempts: Math.min(maxAttempts, maxMidStreamAttempts), delayMs: delayMs * (attempt + 1), error: errorType, model, diff --git a/packages/core/src/core/geminiChat_network_retry.test.ts b/packages/core/src/core/geminiChat_network_retry.test.ts index 2f7cf69dd8..2426cfd483 100644 --- a/packages/core/src/core/geminiChat_network_retry.test.ts +++ b/packages/core/src/core/geminiChat_network_retry.test.ts @@ -292,6 +292,14 @@ describe('GeminiChat Network Retries', () => { (sslError as NodeJS.ErrnoException).code = 'ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC'; + // Instead of outer loop, connection retries are handled by retryWithBackoff. + // Simulate retryWithBackoff attempting it twice: first throws, second succeeds. + mockRetryWithBackoff.mockImplementation( + async (apiCall) => + // Execute the apiCall to trigger mockContentGenerator + await apiCall(), + ); + vi.mocked(mockContentGenerator.generateContentStream) // First call: throw SSL error immediately (connection phase) .mockRejectedValueOnce(sslError) @@ -309,6 +317,15 @@ describe('GeminiChat Network Retries', () => { })(), ); + // Because retryWithBackoff is mocked and we just want to test GeminiChat's integration, + // we need to actually execute the real retryWithBackoff logic for this test to see it work. + // So let's restore the real retryWithBackoff for this test. + const { retryWithBackoff } = + await vi.importActual( + '../utils/retry.js', + ); + mockRetryWithBackoff.mockImplementation(retryWithBackoff); + const stream = await chat.sendMessageStream( { model: 'test-model' }, 'test message', @@ -322,10 +339,6 @@ describe('GeminiChat Network Retries', () => { events.push(event); } - // Should have retried and succeeded - const retryEvent = events.find((e) => e.type === StreamEventType.RETRY); - expect(retryEvent).toBeDefined(); - const successChunk = events.find( (e) => e.type === StreamEventType.CHUNK && @@ -342,6 +355,12 @@ describe('GeminiChat Network Retries', () => { const connectionError = new Error('read ECONNRESET'); (connectionError as NodeJS.ErrnoException).code = 'ECONNRESET'; + const { retryWithBackoff } = + await vi.importActual( + '../utils/retry.js', + ); + mockRetryWithBackoff.mockImplementation(retryWithBackoff); + vi.mocked(mockContentGenerator.generateContentStream) .mockRejectedValueOnce(connectionError) .mockImplementationOnce(async () => @@ -372,9 +391,6 @@ describe('GeminiChat Network Retries', () => { events.push(event); } - const retryEvent = events.find((e) => e.type === StreamEventType.RETRY); - expect(retryEvent).toBeDefined(); - const successChunk = events.find( (e) => e.type === StreamEventType.CHUNK && @@ -382,6 +398,7 @@ describe('GeminiChat Network Retries', () => { 'Success after connection retry', ); expect(successChunk).toBeDefined(); + expect(mockContentGenerator.generateContentStream).toHaveBeenCalledTimes(2); }); it('should NOT retry on non-retryable error during connection phase', async () => {