Compare commits

...

7 Commits

Author SHA1 Message Date
gemini-cli-robot b1f7a7e6f7 chore(release): v0.25.0-preview.2 2026-01-20 05:59:38 +00:00
N. Taylor Mullen ea0e3de430 fix(core): deduplicate ModelInfo emission in GeminiClient (#17075) 2026-01-19 21:26:09 -08:00
N. Taylor Mullen f6a5fa0e03 fix(ui): ensure rationale renders before tool calls (#17043) 2026-01-19 21:25:49 -08:00
gemini-cli-robot de86bccd0d chore(release): v0.25.0-preview.1 2026-01-19 01:50:22 +00:00
christine betts 46079d9daa Patch #16730 into v0.25.0 preview (#16882)
Co-authored-by: Sehoon Shon <sshon@google.com>
2026-01-16 20:36:17 +00:00
gemini-cli-robot 1d5e792a41 fix(patch): cherry-pick cfdc4cf to release/v0.25.0-preview.0-pr-16759 to patch version v0.25.0-preview.0 and create version 0.25.0-preview.1 (#16866)
Co-authored-by: christine betts <chrstn@uw.edu>
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-01-16 11:20:33 -08:00
gemini-cli-robot cd7a5c9604 chore(release): v0.25.0-preview.0 2026-01-14 13:13:18 +00:00
14 changed files with 213 additions and 76 deletions
+7 -7
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.25.0-nightly.20260107.59a18e710",
"version": "0.25.0-preview.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.25.0-nightly.20260107.59a18e710",
"version": "0.25.0-preview.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.0-preview.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.0-preview.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.0-preview.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.0-preview.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.0-preview.2",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.25.0-nightly.20260107.59a18e710",
"version": "0.25.0-preview.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.0-preview.2"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.25.0-nightly.20260107.59a18e710",
"version": "0.25.0-preview.2",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.25.0-nightly.20260107.59a18e710",
"version": "0.25.0-preview.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.0-preview.2"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.11.0",
@@ -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 = [
+9 -2
View File
@@ -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(
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.25.0-nightly.20260107.59a18e710",
"version": "0.25.0-preview.2",
"description": "Gemini CLI Core",
"license": "Apache-2.0",
"repository": {
+5 -6
View File
@@ -1649,7 +1649,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 +1736,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);
+3 -2
View File
@@ -626,9 +626,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;
+5 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.25.0-nightly.20260107.59a18e710",
"version": "0.25.0-preview.2",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -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.0-preview.2",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {