mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
17 Commits
buffer-toggle
...
v0.25.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 83a3b07050 | |||
| 18e854c330 | |||
| 29d4b1e6b8 | |||
| 2a8e1a8cc1 | |||
| c9dbf70043 | |||
| eb88343419 | |||
| 217f277580 | |||
| 02e68e4554 | |||
| 982fd1fc29 | |||
| 6289c3ee3f | |||
| b1f7a7e6f7 | |||
| ea0e3de430 | |||
| f6a5fa0e03 | |||
| de86bccd0d | |||
| 46079d9daa | |||
| 1d5e792a41 | |||
| cd7a5c9604 |
@@ -830,6 +830,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionConfig`** (boolean):
|
||||
- **Description:** Enable requesting and fetching of extension settings.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionReloading`** (boolean):
|
||||
- **Description:** Enables extension loading/unloading within the CLI session.
|
||||
- **Default:** `false`
|
||||
|
||||
Generated
+7
-7
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -18382,7 +18382,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.7",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -18692,7 +18692,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.11.0",
|
||||
@@ -18795,7 +18795,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.7",
|
||||
@@ -18954,7 +18954,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18971,7 +18971,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.25.0-nightly.20260107.59a18e710"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.25.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.25.0-nightly.20260107.59a18e710"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.25.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.11.0",
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
getScopedEnvContents,
|
||||
} from '../../config/extensions/extensionSettings.js';
|
||||
import { getExtensionAndManager, getExtensionManager } from './utils.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { debugLogger, coreEvents } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import prompts from 'prompts';
|
||||
import type { ExtensionConfig } from '../../config/extension.js';
|
||||
@@ -43,6 +44,16 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
|
||||
}),
|
||||
handler: async (args) => {
|
||||
const { name, setting, scope } = args;
|
||||
const settings = loadSettings(process.cwd()).merged;
|
||||
|
||||
if (!(settings.experimental?.extensionConfig ?? true)) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'Extension configuration is currently disabled. Enable it by setting "experimental.extensionConfig" to true.',
|
||||
);
|
||||
await exitCli();
|
||||
return;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
if (name.includes('/') || name.includes('\\') || name.includes('..')) {
|
||||
|
||||
@@ -109,6 +109,9 @@ describe('ExtensionManager Settings Scope', () => {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: {
|
||||
extensionConfig: true,
|
||||
},
|
||||
} as Settings,
|
||||
});
|
||||
|
||||
@@ -148,6 +151,9 @@ describe('ExtensionManager Settings Scope', () => {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: {
|
||||
extensionConfig: true,
|
||||
},
|
||||
} as Settings,
|
||||
});
|
||||
|
||||
@@ -185,6 +191,9 @@ describe('ExtensionManager Settings Scope', () => {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: {
|
||||
extensionConfig: true,
|
||||
},
|
||||
} as Settings,
|
||||
});
|
||||
|
||||
|
||||
@@ -287,7 +287,10 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(destinationPath, { recursive: true });
|
||||
if (this.requestSetting) {
|
||||
if (
|
||||
this.requestSetting &&
|
||||
(this.settings.experimental?.extensionConfig ?? false)
|
||||
) {
|
||||
if (isUpdate) {
|
||||
await maybePromptForSettings(
|
||||
newExtensionConfig,
|
||||
@@ -305,11 +308,14 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
}
|
||||
|
||||
const missingSettings = await getMissingSettings(
|
||||
newExtensionConfig,
|
||||
extensionId,
|
||||
this.workspaceDir,
|
||||
);
|
||||
const missingSettings =
|
||||
(this.settings.experimental?.extensionConfig ?? false)
|
||||
? await getMissingSettings(
|
||||
newExtensionConfig,
|
||||
extensionId,
|
||||
this.workspaceDir,
|
||||
)
|
||||
: [];
|
||||
if (missingSettings.length > 0) {
|
||||
const message = `Extension "${newExtensionConfig.name}" has missing settings: ${missingSettings
|
||||
.map((s) => s.name)
|
||||
@@ -526,23 +532,31 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
const extensionId = getExtensionId(config, installMetadata);
|
||||
|
||||
const userSettings = await getScopedEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
ExtensionSettingScope.USER,
|
||||
);
|
||||
const workspaceSettings = await getScopedEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
this.workspaceDir,
|
||||
);
|
||||
let userSettings: Record<string, string> = {};
|
||||
let workspaceSettings: Record<string, string> = {};
|
||||
|
||||
if (this.settings.experimental?.extensionConfig ?? false) {
|
||||
userSettings = await getScopedEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
ExtensionSettingScope.USER,
|
||||
);
|
||||
workspaceSettings = await getScopedEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
ExtensionSettingScope.WORKSPACE,
|
||||
this.workspaceDir,
|
||||
);
|
||||
}
|
||||
|
||||
const customEnv = { ...userSettings, ...workspaceSettings };
|
||||
config = resolveEnvVarsInObject(config, customEnv);
|
||||
|
||||
const resolvedSettings: ResolvedExtensionSetting[] = [];
|
||||
if (config.settings) {
|
||||
if (
|
||||
config.settings &&
|
||||
(this.settings.experimental?.extensionConfig ?? false)
|
||||
) {
|
||||
for (const setting of config.settings) {
|
||||
const value = customEnv[setting.envVar];
|
||||
let scope: 'user' | 'workspace' | undefined;
|
||||
|
||||
@@ -200,11 +200,13 @@ describe('extension tests', () => {
|
||||
source: undefined,
|
||||
});
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||
const settings = loadSettings(tempWorkspaceDir).merged;
|
||||
(settings.experimental ??= {}).extensionConfig = true;
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
requestSetting: mockPromptForSettings,
|
||||
settings: loadSettings(tempWorkspaceDir).merged,
|
||||
settings,
|
||||
});
|
||||
resetTrustedFoldersForTesting();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as fs from 'node:fs';
|
||||
import { getMissingSettings } from './extensionSettings.js';
|
||||
import type { ExtensionConfig } from '../extension.js';
|
||||
import { ExtensionStorage } from './storage.js';
|
||||
import type { Settings } from '../settings.js';
|
||||
import {
|
||||
KeychainTokenStorage,
|
||||
debugLogger,
|
||||
@@ -245,8 +246,13 @@ describe('extensionUpdates', () => {
|
||||
|
||||
const manager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
settings: { telemetry: {} } as any,
|
||||
|
||||
settings: {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: { extensionConfig: true },
|
||||
} as unknown as Settings,
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: null, // Simulate non-interactive
|
||||
});
|
||||
|
||||
@@ -1413,6 +1413,15 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable extension management features.',
|
||||
showInDialog: false,
|
||||
},
|
||||
extensionConfig: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Configuration',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable requesting and fetching of extension settings.',
|
||||
showInDialog: false,
|
||||
},
|
||||
extensionReloading: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Reloading',
|
||||
|
||||
@@ -1065,6 +1065,8 @@ describe('gemini.tsx main function exit codes', () => {
|
||||
vi.mocked(loadSandboxConfig).mockResolvedValue({} as any);
|
||||
vi.mocked(loadCliConfig).mockResolvedValue({
|
||||
refreshAuth: vi.fn().mockRejectedValue(new Error('Auth failed')),
|
||||
getRemoteAdminSettings: vi.fn().mockReturnValue(undefined),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
} as unknown as Config);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
merged: {
|
||||
|
||||
@@ -363,6 +363,7 @@ export async function main() {
|
||||
// Refresh auth to fetch remote admin settings from CCPA and before entering
|
||||
// the sandbox because the sandbox will interfere with the Oauth2 web
|
||||
// redirect.
|
||||
let initialAuthFailed = false;
|
||||
if (
|
||||
settings.merged.security?.auth?.selectedType &&
|
||||
!settings.merged.security?.auth?.useExternal
|
||||
@@ -390,8 +391,7 @@ export async function main() {
|
||||
}
|
||||
} catch (err) {
|
||||
debugLogger.error('Error authenticating:', err);
|
||||
await runExitCleanup();
|
||||
process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR);
|
||||
initialAuthFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,6 +414,10 @@ export async function main() {
|
||||
// another way to decouple refreshAuth from requiring a config.
|
||||
|
||||
if (sandboxConfig) {
|
||||
if (initialAuthFailed) {
|
||||
await runExitCleanup();
|
||||
process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR);
|
||||
}
|
||||
let stdinData = '';
|
||||
if (!process.stdin.isTTY) {
|
||||
stdinData = await readStdin();
|
||||
|
||||
@@ -2175,11 +2175,9 @@ export function useTextBuffer({
|
||||
|
||||
if (!command) {
|
||||
command =
|
||||
(process.env['VISUAL'] ??
|
||||
process.env['VISUAL'] ??
|
||||
process.env['EDITOR'] ??
|
||||
process.platform === 'win32')
|
||||
? 'notepad'
|
||||
: 'vi';
|
||||
(process.platform === 'win32' ? 'notepad' : 'vi');
|
||||
}
|
||||
|
||||
dispatch({ type: 'create_undo_snapshot' });
|
||||
|
||||
@@ -2205,6 +2205,98 @@ describe('useGeminiStream', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should flush pending text rationale before scheduling tool calls to ensure correct history order', async () => {
|
||||
const addItemOrder: string[] = [];
|
||||
let capturedOnComplete: any;
|
||||
|
||||
const mockScheduleToolCalls = vi.fn(async (requests) => {
|
||||
addItemOrder.push('scheduleToolCalls_START');
|
||||
// Simulate tools completing and triggering onComplete immediately.
|
||||
// This mimics the behavior that caused the regression where tool results
|
||||
// were added to history during the await scheduleToolCalls(...) block.
|
||||
const tools = requests.map((r: any) => ({
|
||||
request: r,
|
||||
status: 'success',
|
||||
tool: { displayName: r.name, name: r.name },
|
||||
invocation: { getDescription: () => 'desc' },
|
||||
response: { responseParts: [], resultDisplay: 'done' },
|
||||
startTime: Date.now(),
|
||||
endTime: Date.now(),
|
||||
}));
|
||||
await capturedOnComplete(tools);
|
||||
addItemOrder.push('scheduleToolCalls_END');
|
||||
});
|
||||
|
||||
mockAddItem.mockImplementation((item: any) => {
|
||||
addItemOrder.push(`addItem:${item.type}`);
|
||||
});
|
||||
|
||||
// We need to capture the onComplete callback from useReactToolScheduler
|
||||
const mockUseReactToolScheduler = useReactToolScheduler as Mock;
|
||||
mockUseReactToolScheduler.mockImplementation((onComplete) => {
|
||||
capturedOnComplete = onComplete;
|
||||
return [
|
||||
[], // toolCalls
|
||||
mockScheduleToolCalls,
|
||||
vi.fn(), // markToolsAsSubmitted
|
||||
vi.fn(), // setToolCallsForDisplay
|
||||
vi.fn(), // cancelAllToolCalls
|
||||
0, // lastToolOutputTime
|
||||
];
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useGeminiStream(
|
||||
new MockedGeminiClientClass(mockConfig),
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockLoadedSettings,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
false,
|
||||
() => 'vscode' as EditorType,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
false,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
80,
|
||||
24,
|
||||
),
|
||||
);
|
||||
|
||||
const mockStream = (async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Rationale rationale.',
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.ToolCallRequest,
|
||||
value: { callId: '1', name: 'test_tool', args: {} },
|
||||
};
|
||||
})();
|
||||
mockSendMessageStream.mockReturnValue(mockStream);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('test input');
|
||||
});
|
||||
|
||||
// Expectation: addItem:gemini (rationale) MUST happen before scheduleToolCalls_START
|
||||
const rationaleIndex = addItemOrder.indexOf('addItem:gemini');
|
||||
const scheduleIndex = addItemOrder.indexOf('scheduleToolCalls_START');
|
||||
const toolGroupIndex = addItemOrder.indexOf('addItem:tool_group');
|
||||
|
||||
expect(rationaleIndex).toBeGreaterThan(-1);
|
||||
expect(scheduleIndex).toBeGreaterThan(-1);
|
||||
expect(toolGroupIndex).toBeGreaterThan(-1);
|
||||
|
||||
// This is the core fix validation: Rationale comes before tools are even scheduled (awaited)
|
||||
expect(rationaleIndex).toBeLessThan(scheduleIndex);
|
||||
expect(rationaleIndex).toBeLessThan(toolGroupIndex);
|
||||
});
|
||||
|
||||
it('should process @include commands, adding user turn after processing to prevent race conditions', async () => {
|
||||
const rawQuery = '@include file.txt Summarize this.';
|
||||
const processedQueryParts = [
|
||||
|
||||
@@ -458,7 +458,7 @@ export const useGeminiStream = (
|
||||
isClientInitiated: true,
|
||||
prompt_id,
|
||||
};
|
||||
scheduleToolCalls([toolCallRequest], abortSignal);
|
||||
await scheduleToolCalls([toolCallRequest], abortSignal);
|
||||
return { queryToSend: null, shouldProceed: false };
|
||||
}
|
||||
case 'submit_prompt': {
|
||||
@@ -921,7 +921,11 @@ export const useGeminiStream = (
|
||||
}
|
||||
}
|
||||
if (toolCallRequests.length > 0) {
|
||||
scheduleToolCalls(toolCallRequests, signal);
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
await scheduleToolCalls(toolCallRequests, signal);
|
||||
}
|
||||
return StreamProcessingStatus.Completed;
|
||||
},
|
||||
@@ -938,6 +942,9 @@ export const useGeminiStream = (
|
||||
handleChatModelEvent,
|
||||
handleAgentExecutionStoppedEvent,
|
||||
handleAgentExecutionBlockedEvent,
|
||||
addItem,
|
||||
pendingHistoryItemRef,
|
||||
setPendingHistoryItem,
|
||||
],
|
||||
);
|
||||
const submitQuery = useCallback(
|
||||
|
||||
@@ -166,11 +166,6 @@ describe('useQuotaAndFallback', () => {
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('retry_always');
|
||||
|
||||
// Verify activateFallbackMode was called
|
||||
expect(mockConfig.activateFallbackMode).toHaveBeenCalledWith(
|
||||
'gemini-flash',
|
||||
);
|
||||
|
||||
// The pending request should be cleared from the state
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
@@ -282,11 +277,6 @@ describe('useQuotaAndFallback', () => {
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('retry_always');
|
||||
|
||||
// Verify activateFallbackMode was called
|
||||
expect(mockConfig.activateFallbackMode).toHaveBeenCalledWith(
|
||||
'model-B',
|
||||
);
|
||||
|
||||
// The pending request should be cleared from the state
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
expect(mockConfig.setQuotaErrorOccurred).toHaveBeenCalledWith(true);
|
||||
@@ -342,11 +332,6 @@ To disable gemini-3-pro-preview, disable "Preview features" in /settings.`,
|
||||
const intent = await promise!;
|
||||
expect(intent).toBe('retry_always');
|
||||
|
||||
// Verify activateFallbackMode was called
|
||||
expect(mockConfig.activateFallbackMode).toHaveBeenCalledWith(
|
||||
'gemini-2.5-pro',
|
||||
);
|
||||
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -430,11 +415,6 @@ To disable gemini-3-pro-preview, disable "Preview features" in /settings.`,
|
||||
expect(intent).toBe('retry_always');
|
||||
expect(result.current.proQuotaRequest).toBeNull();
|
||||
|
||||
// Verify activateFallbackMode was called
|
||||
expect(mockConfig.activateFallbackMode).toHaveBeenCalledWith(
|
||||
'gemini-flash',
|
||||
);
|
||||
|
||||
// Verify quota error flags are reset
|
||||
expect(mockSetModelSwitchedFromQuotaError).toHaveBeenCalledWith(false);
|
||||
expect(mockConfig.setQuotaErrorOccurred).toHaveBeenCalledWith(false);
|
||||
|
||||
@@ -135,10 +135,6 @@ export function useQuotaAndFallback({
|
||||
config.setQuotaErrorOccurred(false);
|
||||
|
||||
if (choice === 'retry_always') {
|
||||
// Set the model to the fallback model for the current session.
|
||||
// This ensures the Footer updates and future turns use this model.
|
||||
// The change is not persisted, so the original model is restored on restart.
|
||||
config.activateFallbackMode(proQuotaRequest.fallbackModel);
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
|
||||
@@ -31,7 +31,7 @@ import { ToolCallStatus } from '../types.js';
|
||||
export type ScheduleFn = (
|
||||
request: ToolCallRequestInfo | ToolCallRequestInfo[],
|
||||
signal: AbortSignal,
|
||||
) => void;
|
||||
) => Promise<void>;
|
||||
export type MarkToolsAsSubmittedFn = (callIds: string[]) => void;
|
||||
|
||||
export type TrackedScheduledToolCall = ScheduledToolCall & {
|
||||
@@ -180,7 +180,7 @@ export function useReactToolScheduler(
|
||||
signal: AbortSignal,
|
||||
) => {
|
||||
setToolCallsForDisplay([]);
|
||||
void scheduler.schedule(request, signal);
|
||||
return scheduler.schedule(request, signal);
|
||||
},
|
||||
[scheduler, setToolCallsForDisplay],
|
||||
);
|
||||
|
||||
@@ -172,8 +172,8 @@ describe('useReactToolScheduler in YOLO Mode', () => {
|
||||
args: { data: 'any data' },
|
||||
} as any;
|
||||
|
||||
act(() => {
|
||||
schedule(request, new AbortController().signal);
|
||||
await act(async () => {
|
||||
await schedule(request, new AbortController().signal);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -229,11 +229,11 @@ describe('useReactToolScheduler', () => {
|
||||
schedule: (
|
||||
req: ToolCallRequestInfo | ToolCallRequestInfo[],
|
||||
signal: AbortSignal,
|
||||
) => void,
|
||||
) => Promise<void>,
|
||||
request: ToolCallRequestInfo | ToolCallRequestInfo[],
|
||||
) => {
|
||||
act(() => {
|
||||
schedule(request, new AbortController().signal);
|
||||
await act(async () => {
|
||||
await schedule(request, new AbortController().signal);
|
||||
});
|
||||
|
||||
await advanceAndSettle();
|
||||
@@ -322,10 +322,13 @@ describe('useReactToolScheduler', () => {
|
||||
|
||||
it('should clear previous tool calls when scheduling new ones', async () => {
|
||||
mockToolRegistry.getTool.mockReturnValue(mockTool);
|
||||
(mockTool.execute as Mock).mockResolvedValue({
|
||||
llmContent: 'Tool output',
|
||||
returnDisplay: 'Formatted tool output',
|
||||
} as ToolResult);
|
||||
(mockTool.execute as Mock).mockImplementation(async () => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
return {
|
||||
llmContent: 'Tool output',
|
||||
returnDisplay: 'Formatted tool output',
|
||||
};
|
||||
});
|
||||
|
||||
const { result } = renderScheduler();
|
||||
const schedule = result.current[1];
|
||||
@@ -346,10 +349,13 @@ describe('useReactToolScheduler', () => {
|
||||
name: 'mockTool',
|
||||
args: {},
|
||||
} as any;
|
||||
act(() => {
|
||||
schedule(newRequest, new AbortController().signal);
|
||||
let schedulePromise: Promise<void>;
|
||||
await act(async () => {
|
||||
schedulePromise = schedule(newRequest, new AbortController().signal);
|
||||
});
|
||||
|
||||
await advanceAndSettle();
|
||||
|
||||
// After scheduling, the old call should be gone,
|
||||
// and the new one should be in the display in its initial state.
|
||||
expect(result.current[0].length).toBe(1);
|
||||
@@ -358,14 +364,13 @@ describe('useReactToolScheduler', () => {
|
||||
|
||||
// Let the new call finish.
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await vi.advanceTimersByTimeAsync(20);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await schedulePromise;
|
||||
});
|
||||
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -388,16 +393,14 @@ describe('useReactToolScheduler', () => {
|
||||
args: {},
|
||||
} as any;
|
||||
|
||||
act(() => {
|
||||
schedule(request, new AbortController().signal);
|
||||
});
|
||||
let schedulePromise: Promise<void>;
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
}); // validation
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0); // Process scheduling
|
||||
schedulePromise = schedule(request, new AbortController().signal);
|
||||
});
|
||||
|
||||
await advanceAndSettle(); // validation
|
||||
await advanceAndSettle(); // Process scheduling
|
||||
|
||||
// At this point, the tool is 'executing' and waiting on the promise.
|
||||
expect(result.current[0][0].status).toBe('executing');
|
||||
|
||||
@@ -406,9 +409,7 @@ describe('useReactToolScheduler', () => {
|
||||
cancelAllToolCalls(cancelController.signal);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
await advanceAndSettle();
|
||||
|
||||
expect(onComplete).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
@@ -421,6 +422,11 @@ describe('useReactToolScheduler', () => {
|
||||
await act(async () => {
|
||||
resolveExecute({ llmContent: 'output', returnDisplay: 'display' });
|
||||
});
|
||||
|
||||
// Now await the schedule promise
|
||||
await act(async () => {
|
||||
await schedulePromise;
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -520,8 +526,9 @@ describe('useReactToolScheduler', () => {
|
||||
args: { data: 'sensitive' },
|
||||
} as any;
|
||||
|
||||
act(() => {
|
||||
schedule(request, new AbortController().signal);
|
||||
let schedulePromise: Promise<void>;
|
||||
await act(async () => {
|
||||
schedulePromise = schedule(request, new AbortController().signal);
|
||||
});
|
||||
await advanceAndSettle();
|
||||
|
||||
@@ -535,8 +542,11 @@ describe('useReactToolScheduler', () => {
|
||||
});
|
||||
|
||||
await advanceAndSettle();
|
||||
await advanceAndSettle();
|
||||
await advanceAndSettle();
|
||||
|
||||
// Now await the schedule promise as it should complete
|
||||
await act(async () => {
|
||||
await schedulePromise;
|
||||
});
|
||||
|
||||
expect(mockOnUserConfirmForToolConfirmation).toHaveBeenCalledWith(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
@@ -567,8 +577,9 @@ describe('useReactToolScheduler', () => {
|
||||
args: {},
|
||||
} as any;
|
||||
|
||||
act(() => {
|
||||
schedule(request, new AbortController().signal);
|
||||
let schedulePromise: Promise<void>;
|
||||
await act(async () => {
|
||||
schedulePromise = schedule(request, new AbortController().signal);
|
||||
});
|
||||
await advanceAndSettle();
|
||||
|
||||
@@ -580,8 +591,13 @@ describe('useReactToolScheduler', () => {
|
||||
await act(async () => {
|
||||
await capturedOnConfirmForTest?.(ToolConfirmationOutcome.Cancel);
|
||||
});
|
||||
|
||||
await advanceAndSettle();
|
||||
await advanceAndSettle();
|
||||
|
||||
// Now await the schedule promise
|
||||
await act(async () => {
|
||||
await schedulePromise;
|
||||
});
|
||||
|
||||
expect(mockOnUserConfirmForToolConfirmation).toHaveBeenCalledWith(
|
||||
ToolConfirmationOutcome.Cancel,
|
||||
@@ -628,8 +644,12 @@ describe('useReactToolScheduler', () => {
|
||||
args: {},
|
||||
} as any;
|
||||
|
||||
act(() => {
|
||||
result.current[1](request, new AbortController().signal);
|
||||
let schedulePromise: Promise<void>;
|
||||
await act(async () => {
|
||||
schedulePromise = result.current[1](
|
||||
request,
|
||||
new AbortController().signal,
|
||||
);
|
||||
});
|
||||
await advanceAndSettle();
|
||||
|
||||
@@ -653,7 +673,11 @@ describe('useReactToolScheduler', () => {
|
||||
} as ToolResult);
|
||||
});
|
||||
await advanceAndSettle();
|
||||
await advanceAndSettle();
|
||||
|
||||
// Now await schedule
|
||||
await act(async () => {
|
||||
await schedulePromise;
|
||||
});
|
||||
|
||||
const completedCalls = onComplete.mock.calls[0][0] as ToolCall[];
|
||||
expect(completedCalls[0].status).toBe('success');
|
||||
@@ -699,8 +723,8 @@ describe('useReactToolScheduler', () => {
|
||||
{ callId: 'multi2', name: 'tool2', args: { p: 2 } } as any,
|
||||
];
|
||||
|
||||
act(() => {
|
||||
schedule(requests, new AbortController().signal);
|
||||
await act(async () => {
|
||||
await schedule(requests, new AbortController().signal);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
@@ -791,24 +815,30 @@ describe('useReactToolScheduler', () => {
|
||||
args: {},
|
||||
} as any;
|
||||
|
||||
act(() => {
|
||||
schedule(request1, new AbortController().signal);
|
||||
let schedulePromise1: Promise<void>;
|
||||
let schedulePromise2: Promise<void>;
|
||||
|
||||
await act(async () => {
|
||||
schedulePromise1 = schedule(request1, new AbortController().signal);
|
||||
});
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
schedule(request2, new AbortController().signal);
|
||||
await act(async () => {
|
||||
schedulePromise2 = schedule(request2, new AbortController().signal);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for first to complete
|
||||
await act(async () => {
|
||||
await schedulePromise1;
|
||||
});
|
||||
|
||||
expect(onComplete).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
status: 'success',
|
||||
@@ -816,13 +846,17 @@ describe('useReactToolScheduler', () => {
|
||||
response: expect.objectContaining({ resultDisplay: 'done display' }),
|
||||
}),
|
||||
]);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for second to complete
|
||||
await act(async () => {
|
||||
await schedulePromise2;
|
||||
});
|
||||
|
||||
expect(onComplete).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
status: 'success',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"description": "Gemini CLI Core",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1955,9 +1955,8 @@ export class Config {
|
||||
*/
|
||||
async dispose(): Promise<void> {
|
||||
coreEvents.off(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed);
|
||||
if (this.agentRegistry) {
|
||||
this.agentRegistry.dispose();
|
||||
}
|
||||
this.agentRegistry?.dispose();
|
||||
this.geminiClient?.dispose();
|
||||
if (this.mcpClientManager) {
|
||||
await this.mcpClientManager.stop();
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import type {
|
||||
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
||||
import * as policyCatalog from '../availability/policyCatalog.js';
|
||||
import { partToString } from '../utils/partUtils.js';
|
||||
import { coreEvents } from '../utils/events.js';
|
||||
|
||||
vi.mock('../services/chatCompressionService.js');
|
||||
|
||||
@@ -290,6 +291,7 @@ describe('Gemini Client (client.ts)', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.dispose();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -1579,6 +1581,55 @@ ${JSON.stringify(
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
});
|
||||
|
||||
it('should re-route within the same prompt when the configured model changes', async () => {
|
||||
mockTurnRunFn.mockClear();
|
||||
mockTurnRunFn.mockImplementation(async function* () {
|
||||
yield { type: 'content', value: 'Hello' };
|
||||
});
|
||||
|
||||
mockRouterService.route.mockResolvedValueOnce({
|
||||
model: 'original-model',
|
||||
reason: 'test',
|
||||
});
|
||||
|
||||
let stream = client.sendMessageStream(
|
||||
[{ text: 'Hi' }],
|
||||
new AbortController().signal,
|
||||
'prompt-1',
|
||||
);
|
||||
await fromAsync(stream);
|
||||
|
||||
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
|
||||
expect(mockTurnRunFn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ model: 'original-model' },
|
||||
[{ text: 'Hi' }],
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
|
||||
mockRouterService.route.mockResolvedValue({
|
||||
model: 'fallback-model',
|
||||
reason: 'test',
|
||||
});
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue('gemini-2.5-flash');
|
||||
coreEvents.emitModelChanged('gemini-2.5-flash');
|
||||
|
||||
stream = client.sendMessageStream(
|
||||
[{ text: 'Continue' }],
|
||||
new AbortController().signal,
|
||||
'prompt-1',
|
||||
);
|
||||
await fromAsync(stream);
|
||||
|
||||
expect(mockRouterService.route).toHaveBeenCalledTimes(2);
|
||||
expect(mockTurnRunFn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ model: 'fallback-model' },
|
||||
[{ text: 'Continue' }],
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use getGlobalMemory for system instruction when JIT is enabled', async () => {
|
||||
@@ -1649,7 +1700,6 @@ ${JSON.stringify(
|
||||
expect(events).toEqual([
|
||||
{ type: GeminiEventType.ModelInfo, value: 'default-routed-model' },
|
||||
{ type: GeminiEventType.InvalidStream },
|
||||
{ type: GeminiEventType.ModelInfo, value: 'default-routed-model' },
|
||||
{ type: GeminiEventType.Content, value: 'Continued content' },
|
||||
]);
|
||||
|
||||
@@ -1737,13 +1787,13 @@ ${JSON.stringify(
|
||||
const events = await fromAsync(stream);
|
||||
|
||||
// Assert
|
||||
// We expect 4 events (model_info + original + model_info + 1 retry)
|
||||
expect(events.length).toBe(4);
|
||||
// We expect 3 events (model_info + original + 1 retry)
|
||||
expect(events.length).toBe(3);
|
||||
expect(
|
||||
events
|
||||
.filter((e) => e.type !== GeminiEventType.ModelInfo)
|
||||
.every((e) => e.type === GeminiEventType.InvalidStream),
|
||||
).toBe(true);
|
||||
.filter((e) => e.type === GeminiEventType.ModelInfo)
|
||||
.map((e) => e.value),
|
||||
).toEqual(['default-routed-model']);
|
||||
|
||||
// Verify that turn.run was called twice
|
||||
expect(mockTurnRunFn).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
import { resolveModel } from '../config/models.js';
|
||||
import type { RetryAvailabilityContext } from '../utils/retry.js';
|
||||
import { partToString } from '../utils/partUtils.js';
|
||||
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||
|
||||
const MAX_TURNS = 100;
|
||||
|
||||
@@ -94,8 +95,14 @@ export class GeminiClient {
|
||||
this.loopDetector = new LoopDetectionService(config);
|
||||
this.compressionService = new ChatCompressionService();
|
||||
this.lastPromptId = this.config.getSessionId();
|
||||
|
||||
coreEvents.on(CoreEvent.ModelChanged, this.handleModelChanged);
|
||||
}
|
||||
|
||||
private handleModelChanged = () => {
|
||||
this.currentSequenceModel = null;
|
||||
};
|
||||
|
||||
// Hook state to deduplicate BeforeAgent calls and track response for
|
||||
// AfterAgent
|
||||
private hookStateMap = new Map<
|
||||
@@ -253,6 +260,10 @@ export class GeminiClient {
|
||||
this.updateTelemetryTokenCount();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
coreEvents.off(CoreEvent.ModelChanged, this.handleModelChanged);
|
||||
}
|
||||
|
||||
async resumeChat(
|
||||
history: Content[],
|
||||
resumedSessionData?: ResumedSessionData,
|
||||
@@ -626,9 +637,10 @@ export class GeminiClient {
|
||||
);
|
||||
modelToUse = finalModel;
|
||||
|
||||
if (!signal.aborted && !this.currentSequenceModel) {
|
||||
yield { type: GeminiEventType.ModelInfo, value: modelToUse };
|
||||
}
|
||||
this.currentSequenceModel = modelToUse;
|
||||
yield { type: GeminiEventType.ModelInfo, value: modelToUse };
|
||||
|
||||
const resultStream = turn.run(modelConfigKey, request, linkedSignal);
|
||||
let isError = false;
|
||||
let isInvalidStream = false;
|
||||
|
||||
@@ -754,6 +754,108 @@ describe('Turn', () => {
|
||||
|
||||
expect(events).toEqual([expectedEvent]);
|
||||
});
|
||||
|
||||
it('should process all parts when thought is first part in chunk', async () => {
|
||||
const mockResponseStream = (async function* () {
|
||||
yield {
|
||||
type: StreamEventType.CHUNK,
|
||||
value: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{ text: '**Planning** the solution', thought: 'planning' },
|
||||
{ text: 'I will help you with that.' },
|
||||
],
|
||||
},
|
||||
citationMetadata: {
|
||||
citations: [{ uri: 'https://example.com', title: 'Source' }],
|
||||
},
|
||||
finishReason: 'STOP',
|
||||
},
|
||||
],
|
||||
functionCalls: [
|
||||
{
|
||||
id: 'fc1',
|
||||
name: 'ReadFile',
|
||||
args: { path: 'file.txt' },
|
||||
},
|
||||
],
|
||||
responseId: 'trace-789',
|
||||
} as unknown as GenerateContentResponse,
|
||||
};
|
||||
})();
|
||||
mockSendMessageStream.mockResolvedValue(mockResponseStream);
|
||||
|
||||
const events = [];
|
||||
for await (const event of turn.run(
|
||||
{ model: 'gemini' },
|
||||
[{ text: 'Test mixed content' }],
|
||||
new AbortController().signal,
|
||||
)) {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
// Should yield:
|
||||
// 1. Thought event (from first part)
|
||||
// 2. Content event (from second part)
|
||||
// 3. ToolCallRequest event (from functionCalls)
|
||||
// 4. Citation event (from citationMetadata, emitted with finishReason)
|
||||
// 5. Finished event (from finishReason)
|
||||
|
||||
expect(events.length).toBe(5);
|
||||
|
||||
const thoughtEvent = events.find(
|
||||
(e) => e.type === GeminiEventType.Thought,
|
||||
);
|
||||
expect(thoughtEvent).toBeDefined();
|
||||
expect(thoughtEvent).toMatchObject({
|
||||
type: GeminiEventType.Thought,
|
||||
value: { subject: 'Planning', description: 'the solution' },
|
||||
traceId: 'trace-789',
|
||||
});
|
||||
|
||||
const contentEvent = events.find(
|
||||
(e) => e.type === GeminiEventType.Content,
|
||||
);
|
||||
expect(contentEvent).toBeDefined();
|
||||
expect(contentEvent).toMatchObject({
|
||||
type: GeminiEventType.Content,
|
||||
value: 'I will help you with that.',
|
||||
traceId: 'trace-789',
|
||||
});
|
||||
|
||||
const toolCallEvent = events.find(
|
||||
(e) => e.type === GeminiEventType.ToolCallRequest,
|
||||
);
|
||||
expect(toolCallEvent).toBeDefined();
|
||||
expect(toolCallEvent).toMatchObject({
|
||||
type: GeminiEventType.ToolCallRequest,
|
||||
value: expect.objectContaining({
|
||||
callId: 'fc1',
|
||||
name: 'ReadFile',
|
||||
args: { path: 'file.txt' },
|
||||
}),
|
||||
});
|
||||
|
||||
const citationEvent = events.find(
|
||||
(e) => e.type === GeminiEventType.Citation,
|
||||
);
|
||||
expect(citationEvent).toBeDefined();
|
||||
expect(citationEvent).toMatchObject({
|
||||
type: GeminiEventType.Citation,
|
||||
value: expect.stringContaining('https://example.com'),
|
||||
});
|
||||
|
||||
const finishedEvent = events.find(
|
||||
(e) => e.type === GeminiEventType.Finished,
|
||||
);
|
||||
expect(finishedEvent).toBeDefined();
|
||||
expect(finishedEvent).toMatchObject({
|
||||
type: GeminiEventType.Finished,
|
||||
value: { reason: 'STOP' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDebugResponses', () => {
|
||||
|
||||
@@ -290,15 +290,16 @@ export class Turn {
|
||||
|
||||
const traceId = resp.responseId;
|
||||
|
||||
const thoughtPart = resp.candidates?.[0]?.content?.parts?.[0];
|
||||
if (thoughtPart?.thought) {
|
||||
const thought = parseThought(thoughtPart.text ?? '');
|
||||
yield {
|
||||
type: GeminiEventType.Thought,
|
||||
value: thought,
|
||||
traceId,
|
||||
};
|
||||
continue;
|
||||
const parts = resp.candidates?.[0]?.content?.parts ?? [];
|
||||
for (const part of parts) {
|
||||
if (part.thought) {
|
||||
const thought = parseThought(part.text ?? '');
|
||||
yield {
|
||||
type: GeminiEventType.Thought,
|
||||
value: thought,
|
||||
traceId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const text = getResponseText(resp);
|
||||
|
||||
@@ -65,6 +65,8 @@ const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
fallbackHandler: undefined,
|
||||
getFallbackModelHandler: vi.fn(),
|
||||
setActiveModel: vi.fn(),
|
||||
setModel: vi.fn(),
|
||||
activateFallbackMode: vi.fn(),
|
||||
getModelAvailabilityService: vi.fn(() =>
|
||||
createAvailabilityServiceMock({
|
||||
selectedModel: FALLBACK_MODEL,
|
||||
@@ -198,7 +200,7 @@ describe('handleFallback', () => {
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(policyConfig.getFallbackModelHandler).not.toHaveBeenCalled();
|
||||
expect(policyConfig.setActiveModel).toHaveBeenCalledWith(
|
||||
expect(policyConfig.activateFallbackMode).toHaveBeenCalledWith(
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
);
|
||||
} finally {
|
||||
@@ -273,7 +275,7 @@ describe('handleFallback', () => {
|
||||
expect(openBrowserSecurely).toHaveBeenCalledWith(
|
||||
'https://goo.gle/set-up-gemini-code-assist',
|
||||
);
|
||||
expect(policyConfig.setActiveModel).not.toHaveBeenCalled();
|
||||
expect(policyConfig.activateFallbackMode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should catch errors from the handler, log an error, and return null', async () => {
|
||||
@@ -378,7 +380,7 @@ describe('handleFallback', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('calls setActiveModel and logs telemetry when handler returns "retry_always"', async () => {
|
||||
it('calls activateFallbackMode when handler returns "retry_always"', async () => {
|
||||
policyHandler.mockResolvedValue('retry_always');
|
||||
vi.mocked(policyConfig.getModel).mockReturnValue(
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
@@ -391,11 +393,13 @@ describe('handleFallback', () => {
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(policyConfig.setActiveModel).toHaveBeenCalledWith(FALLBACK_MODEL);
|
||||
expect(policyConfig.activateFallbackMode).toHaveBeenCalledWith(
|
||||
FALLBACK_MODEL,
|
||||
);
|
||||
// TODO: add logging expect statement
|
||||
});
|
||||
|
||||
it('does NOT call setActiveModel when handler returns "stop"', async () => {
|
||||
it('does NOT call activateFallbackMode when handler returns "stop"', async () => {
|
||||
policyHandler.mockResolvedValue('stop');
|
||||
|
||||
const result = await handleFallback(
|
||||
@@ -405,11 +409,11 @@ describe('handleFallback', () => {
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(policyConfig.setActiveModel).not.toHaveBeenCalled();
|
||||
expect(policyConfig.activateFallbackMode).not.toHaveBeenCalled();
|
||||
// TODO: add logging expect statement
|
||||
});
|
||||
|
||||
it('does NOT call setActiveModel when handler returns "retry_once"', async () => {
|
||||
it('does NOT call activateFallbackMode when handler returns "retry_once"', async () => {
|
||||
policyHandler.mockResolvedValue('retry_once');
|
||||
|
||||
const result = await handleFallback(
|
||||
@@ -419,7 +423,7 @@ describe('handleFallback', () => {
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(policyConfig.setActiveModel).not.toHaveBeenCalled();
|
||||
expect(policyConfig.activateFallbackMode).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ async function processIntent(
|
||||
case 'retry_always':
|
||||
// TODO(telemetry): Implement generic fallback event logging. Existing
|
||||
// logFlashFallback is specific to a single Model.
|
||||
config.setActiveModel(fallbackModel);
|
||||
config.activateFallbackMode(fallbackModel);
|
||||
return true;
|
||||
|
||||
case 'retry_once':
|
||||
|
||||
@@ -81,7 +81,7 @@ export function getResponseText(
|
||||
candidate.content.parts.length > 0
|
||||
) {
|
||||
return candidate.content.parts
|
||||
.filter((part) => part.text)
|
||||
.filter((part) => part.text && !part.thought)
|
||||
.map((part) => part.text)
|
||||
.join('');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
"outDir": "dist",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
||||
"composite": true,
|
||||
"types": ["node", "vitest/globals"]
|
||||
"types": ["node", "vitest/globals"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@google/gemini-cli-core": ["./index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["index.ts", "src/**/*.ts", "src/**/*.json"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"displayName": "Gemini CLI Companion",
|
||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||
"version": "0.25.0-nightly.20260107.59a18e710",
|
||||
"version": "0.25.2",
|
||||
"publisher": "google",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
@@ -1393,6 +1393,13 @@
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"extensionConfig": {
|
||||
"title": "Extension Configuration",
|
||||
"description": "Enable requesting and fetching of extension settings.",
|
||||
"markdownDescription": "Enable requesting and fetching of extension settings.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"extensionReloading": {
|
||||
"title": "Extension Reloading",
|
||||
"description": "Enables extension loading/unloading within the CLI session.",
|
||||
|
||||
Reference in New Issue
Block a user