mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 21:21:09 -07:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1a54496dd | |||
| 7d5b3584f3 | |||
| 03fd454b31 | |||
| 6339fd95fa | |||
| c0c1df12f9 | |||
| 3e31372710 | |||
| c6a695d1b5 | |||
| 78f3d705dd | |||
| 7d233ddd5f | |||
| a5befa04f5 |
Generated
+9
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -18077,7 +18077,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -18206,7 +18206,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
@@ -18354,7 +18354,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
@@ -18665,7 +18665,7 @@
|
||||
},
|
||||
"packages/devtools": {
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
@@ -18680,7 +18680,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18711,7 +18711,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -18743,7 +18743,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"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.41.0-nightly.20260423.gaa05b4583"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
||||
@@ -106,6 +107,7 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// Simulate A2A client confirmation
|
||||
@@ -148,7 +150,11 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// Simulate Rejection (Cancel)
|
||||
const handled = await (
|
||||
@@ -174,7 +180,11 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
correlationId: 'corr-2',
|
||||
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
||||
};
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall2] });
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall2],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// Simulate ModifyWithEditor
|
||||
const handled2 = await (
|
||||
@@ -215,7 +225,11 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// Simulate ProceedOnce for MCP
|
||||
const handled = await (
|
||||
@@ -255,7 +269,11 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
@@ -294,7 +312,11 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
@@ -333,7 +355,11 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
@@ -376,7 +402,11 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
const handler = (yoloMessageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// Should NOT auto-publish ProceedOnce anymore, because PolicyEngine handles it directly
|
||||
expect(yoloMessageBus.publish).not.toHaveBeenCalledWith(
|
||||
@@ -419,6 +449,7 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// Should publish artifact update for output
|
||||
@@ -453,7 +484,11 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// The tool should be complete and registered appropriately, eventually
|
||||
// triggering the toolCompletionPromise resolution when all clear.
|
||||
@@ -533,6 +568,7 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall1, toolCall2],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// Confirm first tool call
|
||||
@@ -600,6 +636,7 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall1, toolCall2],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// Should NOT transition to input-required yet
|
||||
@@ -621,6 +658,7 @@ describe('Task Event-Driven Scheduler', () => {
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall1Complete, toolCall2],
|
||||
schedulerId: 'task-id',
|
||||
});
|
||||
|
||||
// Now it should transition
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
type ToolCallRequestInfo,
|
||||
type GitService,
|
||||
type CompletedToolCall,
|
||||
type ToolCall,
|
||||
type ToolCallsUpdateMessage,
|
||||
MessageBusType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
|
||||
@@ -460,4 +463,204 @@ describe('Task', () => {
|
||||
expect(task.currentPromptId).toBe(expectedPromptId2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Race Condition Fix', () => {
|
||||
const mockConfig = createMockConfig();
|
||||
const mockEventBus: ExecutionEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should NOT transition to input-required if a tool is still validating', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
// Manually register two tool calls
|
||||
task['_registerToolCall']('tool-1', 'awaiting_approval');
|
||||
task['_registerToolCall']('tool-2', 'validating');
|
||||
|
||||
// Call checkInputRequiredState (private)
|
||||
task['checkInputRequiredState']();
|
||||
|
||||
// Verify task state did NOT change to input-required
|
||||
expect(task.taskState).not.toBe('input-required');
|
||||
expect(mockEventBus.publish).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: expect.objectContaining({ state: 'input-required' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should transition to input-required if all active tools are awaiting approval', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
// Transition from submitted to working first to simulate normal flow
|
||||
task.taskState = 'working';
|
||||
|
||||
// Manually register tool calls
|
||||
task['_registerToolCall']('tool-1', 'awaiting_approval');
|
||||
|
||||
// Call checkInputRequiredState
|
||||
task['checkInputRequiredState']();
|
||||
|
||||
// Verify task state changed to input-required
|
||||
expect(task.taskState).toBe('input-required');
|
||||
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: expect.objectContaining({ state: 'input-required' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('handleEventDrivenToolCallsUpdate should ignore events for other schedulers', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
const handleEventDrivenToolCallSpy = vi.spyOn(
|
||||
task as unknown as {
|
||||
handleEventDrivenToolCall: Task['handleEventDrivenToolCall'];
|
||||
},
|
||||
'handleEventDrivenToolCall',
|
||||
);
|
||||
|
||||
const otherEvent: ToolCallsUpdateMessage = {
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [
|
||||
{ request: { callId: '1' }, status: 'executing' } as ToolCall,
|
||||
],
|
||||
schedulerId: 'other-task-id',
|
||||
};
|
||||
|
||||
task['handleEventDrivenToolCallsUpdate'](otherEvent);
|
||||
|
||||
expect(handleEventDrivenToolCallSpy).not.toHaveBeenCalled();
|
||||
|
||||
const ownEvent: ToolCallsUpdateMessage = {
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [
|
||||
{ request: { callId: '1' }, status: 'executing' } as ToolCall,
|
||||
],
|
||||
schedulerId: 'task-id',
|
||||
};
|
||||
|
||||
task['handleEventDrivenToolCallsUpdate'](ownEvent);
|
||||
|
||||
expect(handleEventDrivenToolCallSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Serialization and Mapping', () => {
|
||||
it('should map internal "validating" status to "scheduled" for the client and include outcome', async () => {
|
||||
const mockConfig = createMockConfig();
|
||||
const mockEventBus: ExecutionEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
const mockToolCall = {
|
||||
request: { callId: 'tool-1' },
|
||||
status: 'validating',
|
||||
outcome: 'accepted',
|
||||
tool: { name: 'test-tool' },
|
||||
};
|
||||
|
||||
const message = task['toolStatusMessage'](
|
||||
mockToolCall as unknown as ToolCall,
|
||||
'task-id',
|
||||
'context-id',
|
||||
);
|
||||
const serialized = (
|
||||
message.parts![0] as {
|
||||
data: { status: string; outcome: string };
|
||||
}
|
||||
).data;
|
||||
|
||||
expect(serialized.status).toBe('scheduled');
|
||||
expect(serialized.outcome).toBe('accepted');
|
||||
});
|
||||
|
||||
it('should correctly detect changes when status or outcome changes', async () => {
|
||||
const mockConfig = createMockConfig();
|
||||
const mockEventBus: ExecutionEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task(
|
||||
'task-id',
|
||||
'context-id',
|
||||
mockConfig as Config,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
const toolCall1 = {
|
||||
request: { callId: 'tool-1' },
|
||||
status: 'awaiting_approval',
|
||||
};
|
||||
|
||||
// First update - should trigger change
|
||||
const changed1 = task['handleEventDrivenToolCall'](
|
||||
toolCall1 as unknown as ToolCall,
|
||||
);
|
||||
expect(changed1).toBe(true);
|
||||
|
||||
// Second update with same status - should NOT trigger change
|
||||
const changed2 = task['handleEventDrivenToolCall'](
|
||||
toolCall1 as unknown as ToolCall,
|
||||
);
|
||||
expect(changed2).toBe(false);
|
||||
|
||||
// Update with new outcome - SHOULD trigger change
|
||||
const toolCall2 = {
|
||||
request: { callId: 'tool-1' },
|
||||
status: 'awaiting_approval',
|
||||
outcome: 'accepted',
|
||||
};
|
||||
const changed3 = task['handleEventDrivenToolCall'](
|
||||
toolCall2 as unknown as ToolCall,
|
||||
);
|
||||
expect(changed3).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
GeminiEventType,
|
||||
ToolConfirmationOutcome,
|
||||
ApprovalMode,
|
||||
CoreToolCallStatus,
|
||||
getAllMCPServerStatuses,
|
||||
MCPServerStatus,
|
||||
isNodeError,
|
||||
@@ -95,6 +96,8 @@ export class Task {
|
||||
|
||||
// For tool waiting logic
|
||||
private pendingToolCalls: Map<string, string> = new Map(); //toolCallId --> status
|
||||
private pendingOutcomes: Map<string, ToolConfirmationOutcome | undefined> =
|
||||
new Map(); // toolCallId --> outcome
|
||||
private toolsAlreadyConfirmed: Set<string> = new Set();
|
||||
private toolCompletionPromise?: Promise<void>;
|
||||
private toolCompletionNotifier?: {
|
||||
@@ -413,7 +416,10 @@ export class Task {
|
||||
private handleEventDrivenToolCallsUpdate(
|
||||
event: ToolCallsUpdateMessage,
|
||||
): void {
|
||||
if (event.type !== MessageBusType.TOOL_CALLS_UPDATE) {
|
||||
if (
|
||||
event.type !== MessageBusType.TOOL_CALLS_UPDATE ||
|
||||
event.schedulerId !== this.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -426,7 +432,7 @@ export class Task {
|
||||
this.checkInputRequiredState();
|
||||
}
|
||||
|
||||
private handleEventDrivenToolCall(tc: ToolCall): void {
|
||||
private handleEventDrivenToolCall(tc: ToolCall): boolean {
|
||||
const callId = tc.request.callId;
|
||||
|
||||
// Do not process events for tools that have already been finalized.
|
||||
@@ -436,11 +442,16 @@ export class Task {
|
||||
this.processedToolCallIds.has(callId) ||
|
||||
this.completedToolCalls.some((c) => c.request.callId === callId)
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousStatus = this.pendingToolCalls.get(callId);
|
||||
const hasChanged = previousStatus !== tc.status;
|
||||
const previousOutcome = this.pendingOutcomes.get(callId);
|
||||
const hasChanged =
|
||||
previousStatus !== tc.status || previousOutcome !== tc.outcome;
|
||||
|
||||
// Update outcome tracking
|
||||
this.pendingOutcomes.set(callId, tc.outcome);
|
||||
|
||||
// 1. Handle Output
|
||||
if (tc.status === 'executing' && tc.liveOutput) {
|
||||
@@ -454,6 +465,7 @@ export class Task {
|
||||
tc.status === 'cancelled'
|
||||
) {
|
||||
this.toolsAlreadyConfirmed.delete(callId);
|
||||
this.pendingOutcomes.delete(callId);
|
||||
if (hasChanged) {
|
||||
logger.info(
|
||||
`[Task] Tool call ${callId} completed with status: ${tc.status}`,
|
||||
@@ -496,6 +508,8 @@ export class Task {
|
||||
);
|
||||
this.eventBus?.publish(statusUpdate);
|
||||
}
|
||||
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
private checkInputRequiredState(): void {
|
||||
@@ -508,12 +522,14 @@ export class Task {
|
||||
let isExecuting = false;
|
||||
|
||||
for (const [callId, status] of this.pendingToolCalls.entries()) {
|
||||
if (status === 'executing' || status === 'scheduled') {
|
||||
isExecuting = true;
|
||||
} else if (
|
||||
status === 'awaiting_approval' &&
|
||||
!this.toolsAlreadyConfirmed.has(callId)
|
||||
if (
|
||||
status === CoreToolCallStatus.Executing ||
|
||||
status === CoreToolCallStatus.Scheduled ||
|
||||
status === CoreToolCallStatus.Validating ||
|
||||
this.toolsAlreadyConfirmed.has(callId)
|
||||
) {
|
||||
isExecuting = true;
|
||||
} else if (status === CoreToolCallStatus.AwaitingApproval) {
|
||||
isAwaitingApproval = true;
|
||||
}
|
||||
}
|
||||
@@ -574,8 +590,14 @@ export class Task {
|
||||
'confirmationDetails',
|
||||
'liveOutput',
|
||||
'response',
|
||||
'outcome',
|
||||
);
|
||||
|
||||
// Map internal 'validating' status to 'scheduled' for the client
|
||||
if (serializableToolCall.status === CoreToolCallStatus.Validating) {
|
||||
serializableToolCall.status = CoreToolCallStatus.Scheduled;
|
||||
}
|
||||
|
||||
if (tc.tool) {
|
||||
const toolFields = this._pickFields(
|
||||
tc.tool,
|
||||
|
||||
@@ -228,7 +228,7 @@ describe('E2E Tests', () => {
|
||||
expect(toolCallUpdateEvent.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
status: 'validating',
|
||||
status: 'scheduled',
|
||||
request: { callId: 'test-call-id' },
|
||||
},
|
||||
},
|
||||
@@ -330,7 +330,7 @@ describe('E2E Tests', () => {
|
||||
expect(toolCallValidateEvent1.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
status: 'validating',
|
||||
status: 'scheduled',
|
||||
request: { callId: 'test-call-id-1' },
|
||||
},
|
||||
},
|
||||
@@ -352,7 +352,7 @@ describe('E2E Tests', () => {
|
||||
kind: 'state-change',
|
||||
});
|
||||
|
||||
// 4. Tool 1 is validating.
|
||||
// 4. Tool 1 is scheduled.
|
||||
const toolCallUpdate1 = events[3].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallUpdate1.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
@@ -361,12 +361,12 @@ describe('E2E Tests', () => {
|
||||
{
|
||||
data: {
|
||||
request: { callId: 'test-call-id-1' },
|
||||
status: 'validating',
|
||||
status: 'scheduled',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 5. Tool 2 is validating.
|
||||
// 5. Tool 2 is scheduled.
|
||||
const toolCallUpdate2 = events[4].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallUpdate2.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
@@ -375,17 +375,17 @@ describe('E2E Tests', () => {
|
||||
{
|
||||
data: {
|
||||
request: { callId: 'test-call-id-2' },
|
||||
status: 'validating',
|
||||
status: 'scheduled',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 6. Tool 1 is awaiting approval.
|
||||
const toolCallAwaitEvent = events[5].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallAwaitEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
const toolCallAwaitEvent1 = events[5].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallAwaitEvent1.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-confirmation',
|
||||
});
|
||||
expect(toolCallAwaitEvent.status.message?.parts).toMatchObject([
|
||||
expect(toolCallAwaitEvent1.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
request: { callId: 'test-call-id-1' },
|
||||
@@ -394,14 +394,28 @@ describe('E2E Tests', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
// 7. The final event is "input-required".
|
||||
const finalEvent = events[6].result as TaskStatusUpdateEvent;
|
||||
// 7. Tool 2 is awaiting approval.
|
||||
const toolCallAwaitEvent2 = events[6].result as TaskStatusUpdateEvent;
|
||||
expect(toolCallAwaitEvent2.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-confirmation',
|
||||
});
|
||||
expect(toolCallAwaitEvent2.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
request: { callId: 'test-call-id-2' },
|
||||
status: 'awaiting_approval',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 8. The final event is "input-required".
|
||||
const finalEvent = events[7].result as TaskStatusUpdateEvent;
|
||||
expect(finalEvent.final).toBe(true);
|
||||
expect(finalEvent.status.state).toBe('input-required');
|
||||
|
||||
// The scheduler now waits for approval, so no more events are sent.
|
||||
assertUniqueFinalEventIsLast(events);
|
||||
expect(events.length).toBe(7);
|
||||
expect(events.length).toBe(8);
|
||||
});
|
||||
|
||||
it('should handle multiple tool calls sequentially in YOLO mode', async () => {
|
||||
@@ -499,7 +513,7 @@ describe('E2E Tests', () => {
|
||||
// Tool 1 Lifecycle
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'validating',
|
||||
status: 'scheduled',
|
||||
callId: 'test-call-id-1',
|
||||
},
|
||||
{
|
||||
@@ -520,7 +534,7 @@ describe('E2E Tests', () => {
|
||||
// Tool 2 Lifecycle
|
||||
{
|
||||
kind: 'tool-call-update',
|
||||
status: 'validating',
|
||||
status: 'scheduled',
|
||||
callId: 'test-call-id-2',
|
||||
},
|
||||
{
|
||||
@@ -603,26 +617,40 @@ describe('E2E Tests', () => {
|
||||
expect(workingEvent2.kind).toBe('status-update');
|
||||
expect(workingEvent2.status.state).toBe('working');
|
||||
|
||||
// Status update: tool-call-update (validating)
|
||||
const validatingEvent = events[3].result as TaskStatusUpdateEvent;
|
||||
expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
// Status update: tool-call-update (scheduled)
|
||||
const scheduledEvent1 = events[3].result as TaskStatusUpdateEvent;
|
||||
expect(scheduledEvent1.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
expect(validatingEvent.status.message?.parts).toMatchObject([
|
||||
expect(scheduledEvent1.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
status: 'validating',
|
||||
status: 'scheduled',
|
||||
request: { callId: 'test-call-id-no-approval' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Status update: tool-call-update (scheduled)
|
||||
const scheduledEvent = events[4].result as TaskStatusUpdateEvent;
|
||||
expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
const scheduledEvent2 = events[4].result as TaskStatusUpdateEvent;
|
||||
expect(scheduledEvent2.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
expect(scheduledEvent.status.message?.parts).toMatchObject([
|
||||
expect(scheduledEvent2.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
status: 'scheduled',
|
||||
request: { callId: 'test-call-id-no-approval' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Status update: tool-call-update (scheduled)
|
||||
const scheduledEvent3 = events[5].result as TaskStatusUpdateEvent;
|
||||
expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
expect(scheduledEvent3.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
status: 'scheduled',
|
||||
@@ -632,7 +660,7 @@ describe('E2E Tests', () => {
|
||||
]);
|
||||
|
||||
// Status update: tool-call-update (executing)
|
||||
const executingEvent = events[5].result as TaskStatusUpdateEvent;
|
||||
const executingEvent = events[6].result as TaskStatusUpdateEvent;
|
||||
expect(executingEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
@@ -646,7 +674,7 @@ describe('E2E Tests', () => {
|
||||
]);
|
||||
|
||||
// Status update: tool-call-update (success)
|
||||
const successEvent = events[6].result as TaskStatusUpdateEvent;
|
||||
const successEvent = events[7].result as TaskStatusUpdateEvent;
|
||||
expect(successEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
@@ -660,12 +688,12 @@ describe('E2E Tests', () => {
|
||||
]);
|
||||
|
||||
// Status update: working (before sending tool result to LLM)
|
||||
const workingEvent3 = events[7].result as TaskStatusUpdateEvent;
|
||||
const workingEvent3 = events[8].result as TaskStatusUpdateEvent;
|
||||
expect(workingEvent3.kind).toBe('status-update');
|
||||
expect(workingEvent3.status.state).toBe('working');
|
||||
|
||||
// Status update: text-content (final LLM response)
|
||||
const textContentEvent = events[8].result as TaskStatusUpdateEvent;
|
||||
const textContentEvent = events[9].result as TaskStatusUpdateEvent;
|
||||
expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'text-content',
|
||||
});
|
||||
@@ -674,7 +702,7 @@ describe('E2E Tests', () => {
|
||||
]);
|
||||
|
||||
assertUniqueFinalEventIsLast(events);
|
||||
expect(events.length).toBe(10);
|
||||
expect(events.length).toBe(11);
|
||||
});
|
||||
|
||||
it('should bypass tool approval in YOLO mode', async () => {
|
||||
@@ -734,15 +762,15 @@ describe('E2E Tests', () => {
|
||||
expect(workingEvent2.kind).toBe('status-update');
|
||||
expect(workingEvent2.status.state).toBe('working');
|
||||
|
||||
// Status update: tool-call-update (validating)
|
||||
const validatingEvent = events[3].result as TaskStatusUpdateEvent;
|
||||
expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
// Status update: tool-call-update (scheduled)
|
||||
const scheduledEvent = events[3].result as TaskStatusUpdateEvent;
|
||||
expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
expect(validatingEvent.status.message?.parts).toMatchObject([
|
||||
expect(scheduledEvent.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
status: 'validating',
|
||||
status: 'scheduled',
|
||||
request: { callId: 'test-call-id-yolo' },
|
||||
},
|
||||
},
|
||||
@@ -762,8 +790,22 @@ describe('E2E Tests', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
// Status update: tool-call-update (scheduled)
|
||||
const scheduledEvent3 = events[5].result as TaskStatusUpdateEvent;
|
||||
expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
expect(scheduledEvent3.status.message?.parts).toMatchObject([
|
||||
{
|
||||
data: {
|
||||
status: 'scheduled',
|
||||
request: { callId: 'test-call-id-yolo' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Status update: tool-call-update (executing)
|
||||
const executingEvent = events[5].result as TaskStatusUpdateEvent;
|
||||
const executingEvent = events[6].result as TaskStatusUpdateEvent;
|
||||
expect(executingEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
@@ -777,7 +819,7 @@ describe('E2E Tests', () => {
|
||||
]);
|
||||
|
||||
// Status update: tool-call-update (success)
|
||||
const successEvent = events[6].result as TaskStatusUpdateEvent;
|
||||
const successEvent = events[7].result as TaskStatusUpdateEvent;
|
||||
expect(successEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'tool-call-update',
|
||||
});
|
||||
@@ -791,12 +833,12 @@ describe('E2E Tests', () => {
|
||||
]);
|
||||
|
||||
// Status update: working (before sending tool result to LLM)
|
||||
const workingEvent3 = events[7].result as TaskStatusUpdateEvent;
|
||||
const workingEvent3 = events[8].result as TaskStatusUpdateEvent;
|
||||
expect(workingEvent3.kind).toBe('status-update');
|
||||
expect(workingEvent3.status.state).toBe('working');
|
||||
|
||||
// Status update: text-content (final LLM response)
|
||||
const textContentEvent = events[8].result as TaskStatusUpdateEvent;
|
||||
const textContentEvent = events[9].result as TaskStatusUpdateEvent;
|
||||
expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({
|
||||
kind: 'text-content',
|
||||
});
|
||||
@@ -805,7 +847,7 @@ describe('E2E Tests', () => {
|
||||
]);
|
||||
|
||||
assertUniqueFinalEventIsLast(events);
|
||||
expect(events.length).toBe(10);
|
||||
expect(events.length).toBe(11);
|
||||
});
|
||||
|
||||
it('should include traceId in status updates when available', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -27,7 +27,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.0-nightly.20260423.gaa05b4583"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.41.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
|
||||
@@ -326,6 +326,36 @@ describe('SettingsDialog', () => {
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render the bottom border correctly when height is constrained', async () => {
|
||||
const settings = createMockSettings();
|
||||
const onSelect = vi.fn();
|
||||
const constrainedHeight = 15;
|
||||
|
||||
const renderResult = await renderDialog(settings, onSelect, {
|
||||
availableTerminalHeight: constrainedHeight,
|
||||
});
|
||||
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
const output = renderResult.lastFrame();
|
||||
const lines = output.trim().split('\n');
|
||||
|
||||
// Verify height constraint
|
||||
expect(lines.length).toBeLessThanOrEqual(constrainedHeight);
|
||||
|
||||
// Verify bottom border existence in the last line of the output
|
||||
const lastLine = lines[lines.length - 1];
|
||||
// 'round' border characters: ─, ╰, ╯
|
||||
expect(lastLine).toMatch(/[─╰╯]/);
|
||||
});
|
||||
|
||||
// SVG snapshot ensures visual layout and border rendering are preserved
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
|
||||
renderResult.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Setting Descriptions', () => {
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="275" viewBox="0 0 920 275">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="275" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#878787" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="0" y="19" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="19" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Settings </text>
|
||||
<text x="891" y="36" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="53" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#d7ffd7" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="891" y="70" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#d7ffd7" textLength="18" lengthAdjust="spacingAndGlyphs">╰─</text>
|
||||
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
|
||||
<text x="45" y="87" fill="#afafaf" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
|
||||
<text x="180" y="87" fill="#d7ffd7" textLength="702" lengthAdjust="spacingAndGlyphs">─────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="87" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="104" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">▲</text>
|
||||
<text x="891" y="104" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="119" width="9" height="17" fill="#005f00" />
|
||||
<text x="27" y="121" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="119" width="9" height="17" fill="#005f00" />
|
||||
<rect x="45" y="119" width="72" height="17" fill="#005f00" />
|
||||
<text x="45" y="121" fill="#d7ffd7" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
|
||||
<rect x="117" y="119" width="711" height="17" fill="#005f00" />
|
||||
<rect x="828" y="119" width="45" height="17" fill="#005f00" />
|
||||
<text x="828" y="121" fill="#d7ffd7" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
|
||||
<text x="891" y="121" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="138" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">▼</text>
|
||||
<rect x="45" y="136" width="198" height="17" fill="#005f00" />
|
||||
<text x="45" y="138" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
|
||||
<text x="891" y="138" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="155" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="172" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
|
||||
<text x="891" y="172" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="187" width="9" height="17" fill="#005f00" />
|
||||
<text x="27" y="189" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="187" width="9" height="17" fill="#005f00" />
|
||||
<rect x="45" y="187" width="117" height="17" fill="#005f00" />
|
||||
<text x="45" y="189" fill="#d7ffd7" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="162" y="187" width="711" height="17" fill="#005f00" />
|
||||
<text x="891" y="189" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="206" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="206" fill="#afafaf" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
|
||||
<text x="891" y="206" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="223" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="223" fill="#878787" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="240" fill="#878787" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -46,6 +46,24 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Initial Rendering > should render the bottom border correctly when height is constrained 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ ╰─Search to filter─────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ ▼ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings enabled' correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
|
||||
@@ -425,7 +425,7 @@ export function BaseSettingsDialog({
|
||||
flexDirection="row"
|
||||
padding={1}
|
||||
width="100%"
|
||||
height="100%"
|
||||
maxHeight={availableHeight}
|
||||
>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{/* Title */}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"description": "Gemini CLI Core",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1898,6 +1898,30 @@ describe('PolicyEngine', () => {
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
|
||||
it('should NOT downgrade to ASK_USER for redirected commands in YOLO mode even without sandbox', async () => {
|
||||
const rules: PolicyRule[] = [
|
||||
{
|
||||
toolName: 'run_shell_command',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 10,
|
||||
},
|
||||
];
|
||||
|
||||
engine = new PolicyEngine({
|
||||
rules,
|
||||
approvalMode: ApprovalMode.YOLO,
|
||||
sandboxManager: new NoopSandboxManager(),
|
||||
});
|
||||
|
||||
const command = 'npm test 2>&1 | tail -80';
|
||||
const { decision } = await engine.check(
|
||||
{ name: 'run_shell_command', args: { command } },
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
|
||||
it('should return ALLOW in YOLO mode even if shell command parsing fails', async () => {
|
||||
const { splitCommands } = await import('../utils/shell-utils.js');
|
||||
const rules: PolicyRule[] = [
|
||||
|
||||
@@ -288,12 +288,11 @@ export class PolicyEngine {
|
||||
if (allowRedirection) return false;
|
||||
if (!hasRedirection(command)) return false;
|
||||
|
||||
// Do not downgrade (do not ask user) if sandboxing is enabled and in AUTO_EDIT or YOLO
|
||||
const sandboxEnabled = !(this.sandboxManager instanceof NoopSandboxManager);
|
||||
// Do not downgrade (do not ask user) if in AUTO_EDIT or YOLO mode.
|
||||
// These modes trust the agent's actions (YOLO) or specific task (AUTO_EDIT).
|
||||
if (
|
||||
sandboxEnabled &&
|
||||
(this.approvalMode === ApprovalMode.AUTO_EDIT ||
|
||||
this.approvalMode === ApprovalMode.YOLO)
|
||||
this.approvalMode === ApprovalMode.AUTO_EDIT ||
|
||||
this.approvalMode === ApprovalMode.YOLO
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import type { Config } from '../config/config.js';
|
||||
import * as sdk from './sdk.js';
|
||||
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||
import { EventMetadataKey } from './clearcut-logger/event-metadata-key.js';
|
||||
|
||||
vi.mock('@opentelemetry/api-logs');
|
||||
vi.mock('./sdk.js');
|
||||
@@ -144,4 +145,174 @@ describe('conseca-logger', () => {
|
||||
|
||||
expect(mockLogger.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should omit user_prompt/trusted_content/policy from OTEL when logPrompts is disabled', () => {
|
||||
const configNoPrompts = {
|
||||
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||
} as unknown as Config;
|
||||
|
||||
const event = new ConsecaPolicyGenerationEvent(
|
||||
'sensitive prompt',
|
||||
'sensitive content',
|
||||
'sensitive policy',
|
||||
);
|
||||
|
||||
logConsecaPolicyGeneration(configNoPrompts, event);
|
||||
|
||||
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(attrs['user_prompt']).toBeUndefined();
|
||||
expect(attrs['trusted_content']).toBeUndefined();
|
||||
expect(attrs['policy']).toBeUndefined();
|
||||
expect(attrs['event.name']).toBe(EVENT_CONSECA_POLICY_GENERATION);
|
||||
});
|
||||
|
||||
it('should omit user_prompt/trusted_content/policy from Clearcut when logPrompts is disabled', () => {
|
||||
const configNoPrompts = {
|
||||
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||
} as unknown as Config;
|
||||
|
||||
const event = new ConsecaPolicyGenerationEvent(
|
||||
'sensitive prompt',
|
||||
'sensitive content',
|
||||
'sensitive policy',
|
||||
'some error',
|
||||
);
|
||||
|
||||
logConsecaPolicyGeneration(configNoPrompts, event);
|
||||
|
||||
expect(mockClearcutLogger.createLogEvent).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
[
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
|
||||
value: 'some error',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('should include user_prompt/trusted_content/policy in OTEL when logPrompts is enabled', () => {
|
||||
const event = new ConsecaPolicyGenerationEvent(
|
||||
'visible prompt',
|
||||
'visible content',
|
||||
'visible policy',
|
||||
);
|
||||
|
||||
logConsecaPolicyGeneration(mockConfig, event);
|
||||
|
||||
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(attrs['user_prompt']).toBe('visible prompt');
|
||||
expect(attrs['trusted_content']).toBe('visible content');
|
||||
expect(attrs['policy']).toBe('visible policy');
|
||||
});
|
||||
|
||||
it('should omit sensitive fields from verdict OTEL when logPrompts is disabled', () => {
|
||||
const configNoPrompts = {
|
||||
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||
} as unknown as Config;
|
||||
|
||||
const event = new ConsecaVerdictEvent(
|
||||
'sensitive prompt',
|
||||
'sensitive policy',
|
||||
'sensitive tool call',
|
||||
'allow',
|
||||
'sensitive rationale',
|
||||
);
|
||||
|
||||
logConsecaVerdict(configNoPrompts, event);
|
||||
|
||||
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(attrs['user_prompt']).toBeUndefined();
|
||||
expect(attrs['policy']).toBeUndefined();
|
||||
expect(attrs['tool_call']).toBeUndefined();
|
||||
expect(attrs['verdict_rationale']).toBeUndefined();
|
||||
// verdict (the allow/deny result) is not sensitive and should be present
|
||||
expect(attrs['verdict']).toBe('allow');
|
||||
});
|
||||
|
||||
it('should omit sensitive fields from verdict Clearcut when logPrompts is disabled', () => {
|
||||
const configNoPrompts = {
|
||||
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||
} as unknown as Config;
|
||||
|
||||
const event = new ConsecaVerdictEvent(
|
||||
'sensitive prompt',
|
||||
'sensitive policy',
|
||||
'sensitive tool call',
|
||||
'allow',
|
||||
'sensitive rationale',
|
||||
'some error',
|
||||
);
|
||||
|
||||
logConsecaVerdict(configNoPrompts, event);
|
||||
|
||||
expect(mockClearcutLogger.createLogEvent).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
[
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RESULT,
|
||||
value: '"allow"',
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
|
||||
value: 'some error',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('should include sensitive fields in verdict OTEL when logPrompts is enabled', () => {
|
||||
const event = new ConsecaVerdictEvent(
|
||||
'visible prompt',
|
||||
'visible policy',
|
||||
'visible tool call',
|
||||
'deny',
|
||||
'visible rationale',
|
||||
);
|
||||
|
||||
logConsecaVerdict(mockConfig, event);
|
||||
|
||||
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(attrs['user_prompt']).toBe('visible prompt');
|
||||
expect(attrs['policy']).toBe('visible policy');
|
||||
expect(attrs['tool_call']).toBe('visible tool call');
|
||||
expect(attrs['verdict_rationale']).toBe('visible rationale');
|
||||
expect(attrs['verdict']).toBe('deny');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { isTelemetrySdkInitialized } from './sdk.js';
|
||||
import {
|
||||
ClearcutLogger,
|
||||
EventNames,
|
||||
type EventValue,
|
||||
} from './clearcut-logger/clearcut-logger.js';
|
||||
import { EventMetadataKey } from './clearcut-logger/event-metadata-key.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
@@ -27,20 +28,24 @@ export function logConsecaPolicyGeneration(
|
||||
debugLogger.debug('Conseca Policy Generation Event:', event);
|
||||
const clearcutLogger = ClearcutLogger.getInstance(config);
|
||||
if (clearcutLogger) {
|
||||
const data = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
||||
value: safeJsonStringify(event.user_prompt),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_TRUSTED_CONTENT,
|
||||
value: safeJsonStringify(event.trusted_content),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
||||
value: safeJsonStringify(event.policy),
|
||||
},
|
||||
];
|
||||
const data: EventValue[] = [];
|
||||
|
||||
if (config.getTelemetryLogPromptsEnabled()) {
|
||||
data.push(
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
||||
value: safeJsonStringify(event.user_prompt),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_TRUSTED_CONTENT,
|
||||
value: safeJsonStringify(event.trusted_content),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
||||
value: safeJsonStringify(event.policy),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (event.error) {
|
||||
data.push({
|
||||
@@ -71,29 +76,34 @@ export function logConsecaVerdict(
|
||||
debugLogger.debug('Conseca Verdict Event:', event);
|
||||
const clearcutLogger = ClearcutLogger.getInstance(config);
|
||||
if (clearcutLogger) {
|
||||
const data = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
||||
value: safeJsonStringify(event.user_prompt),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
||||
value: safeJsonStringify(event.policy),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
|
||||
value: safeJsonStringify(event.tool_call),
|
||||
},
|
||||
const data: EventValue[] = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RESULT,
|
||||
value: safeJsonStringify(event.verdict),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RATIONALE,
|
||||
value: event.verdict_rationale,
|
||||
},
|
||||
];
|
||||
|
||||
if (config.getTelemetryLogPromptsEnabled()) {
|
||||
data.push(
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
||||
value: safeJsonStringify(event.user_prompt),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
||||
value: safeJsonStringify(event.policy),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
|
||||
value: safeJsonStringify(event.tool_call),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RATIONALE,
|
||||
value: event.verdict_rationale,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (event.error) {
|
||||
data.push({
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
|
||||
|
||||
@@ -642,6 +642,54 @@ describe('loggers', () => {
|
||||
}),
|
||||
});
|
||||
});
|
||||
it('should not include response_text when logPrompts is disabled', () => {
|
||||
const mockConfigNoPrompts = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTargetDir: () => 'target-dir',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const event = new ApiResponseEvent(
|
||||
'test-model',
|
||||
100,
|
||||
{ prompt_id: 'prompt-id-noprompts', contents: [] },
|
||||
{ candidates: [] },
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
{},
|
||||
'this response should be hidden',
|
||||
);
|
||||
|
||||
logApiResponse(mockConfigNoPrompts, event);
|
||||
|
||||
const firstEmitCall = mockLogger.emit.mock.calls[0][0];
|
||||
expect(firstEmitCall.attributes['response_text']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include response_text when logPrompts is enabled', () => {
|
||||
const event = new ApiResponseEvent(
|
||||
'test-model',
|
||||
100,
|
||||
{ prompt_id: 'prompt-id-withprompts', contents: [] },
|
||||
{ candidates: [] },
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
{},
|
||||
'this response should be visible',
|
||||
);
|
||||
|
||||
logApiResponse(mockConfig, event);
|
||||
|
||||
const firstEmitCall = mockLogger.emit.mock.calls[0][0];
|
||||
expect(firstEmitCall.attributes['response_text']).toBe(
|
||||
'this response should be visible',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logApiError', () => {
|
||||
@@ -1076,6 +1124,10 @@ describe('loggers', () => {
|
||||
expect(attributes['gen_ai.provider.name']).toBe('gcp.vertex_ai');
|
||||
// Ensure prompt messages are NOT included
|
||||
expect(attributes['gen_ai.input.messages']).toBeUndefined();
|
||||
|
||||
// Ensure request_text is also NOT included in the first (toLogRecord) log
|
||||
const firstLogCall = mockLogger.emit.mock.calls[0][0];
|
||||
expect(firstLogCall.attributes['request_text']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should correctly derive model from prompt details if available in semantic log', () => {
|
||||
@@ -1373,16 +1425,20 @@ describe('loggers', () => {
|
||||
error_type: undefined,
|
||||
mcp_server_name: undefined,
|
||||
extension_id: undefined,
|
||||
metadata: {
|
||||
model_added_lines: 1,
|
||||
model_removed_lines: 2,
|
||||
model_added_chars: 3,
|
||||
model_removed_chars: 4,
|
||||
user_added_lines: 5,
|
||||
user_removed_lines: 6,
|
||||
user_added_chars: 7,
|
||||
user_removed_chars: 8,
|
||||
},
|
||||
metadata: JSON.stringify(
|
||||
{
|
||||
model_added_lines: 1,
|
||||
model_removed_lines: 2,
|
||||
model_added_chars: 3,
|
||||
model_removed_chars: 4,
|
||||
user_added_lines: 5,
|
||||
user_removed_lines: 6,
|
||||
user_added_chars: 7,
|
||||
user_removed_chars: 8,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
content_length: 13,
|
||||
},
|
||||
});
|
||||
@@ -1455,12 +1511,16 @@ describe('loggers', () => {
|
||||
body: 'Tool call: ask_user. Decision: accept. Success: true. Duration: 100ms.',
|
||||
attributes: expect.objectContaining({
|
||||
function_name: 'ask_user',
|
||||
metadata: expect.objectContaining({
|
||||
ask_user: {
|
||||
question_types: ['choice'],
|
||||
dismissed: false,
|
||||
metadata: JSON.stringify(
|
||||
{
|
||||
ask_user: {
|
||||
question_types: ['choice'],
|
||||
dismissed: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -1867,6 +1927,99 @@ describe('loggers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('logToolCall — logPrompts flag', () => {
|
||||
it('should omit function_args when logPrompts is disabled', () => {
|
||||
const mockConfigNoPrompts = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTargetDir: () => 'target-dir',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const call: CompletedToolCall = {
|
||||
status: CoreToolCallStatus.Success,
|
||||
request: {
|
||||
name: 'run_bash',
|
||||
args: { command: 'echo sensitive' },
|
||||
callId: 'call-1',
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-noprompts',
|
||||
},
|
||||
response: {
|
||||
callId: 'call-1',
|
||||
responseParts: [],
|
||||
resultDisplay: undefined,
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: undefined,
|
||||
},
|
||||
tool: undefined as unknown as AnyDeclarativeTool,
|
||||
invocation: {} as AnyToolInvocation,
|
||||
durationMs: 50,
|
||||
};
|
||||
const event = new ToolCallEvent(call);
|
||||
logToolCall(mockConfigNoPrompts, event);
|
||||
|
||||
const emitted = mockLogger.emit.mock.calls[0][0] as {
|
||||
attributes: Record<string, unknown>;
|
||||
};
|
||||
expect(emitted.attributes['function_args']).toBeUndefined();
|
||||
expect(emitted.attributes['function_name']).toBe('run_bash');
|
||||
});
|
||||
|
||||
it('should include function_args when logPrompts is enabled', () => {
|
||||
const mockConfigWithPrompts = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getTargetDir: () => 'target-dir',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getTelemetryTracesEnabled: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const call: CompletedToolCall = {
|
||||
status: CoreToolCallStatus.Success,
|
||||
request: {
|
||||
name: 'run_bash',
|
||||
args: { command: 'echo visible' },
|
||||
callId: 'call-2',
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-withprompts',
|
||||
},
|
||||
response: {
|
||||
callId: 'call-2',
|
||||
responseParts: [],
|
||||
resultDisplay: undefined,
|
||||
error: undefined,
|
||||
errorType: undefined,
|
||||
contentLength: undefined,
|
||||
},
|
||||
tool: undefined as unknown as AnyDeclarativeTool,
|
||||
invocation: {} as AnyToolInvocation,
|
||||
durationMs: 50,
|
||||
};
|
||||
const event = new ToolCallEvent(call);
|
||||
logToolCall(mockConfigWithPrompts, event);
|
||||
|
||||
const emitted = mockLogger.emit.mock.calls[0][0] as {
|
||||
attributes: Record<string, unknown>;
|
||||
};
|
||||
expect(emitted.attributes['function_args']).toBe(
|
||||
JSON.stringify({ command: 'echo visible' }, null, 2),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logMalformedJsonResponse', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(ClearcutLogger.prototype, 'logMalformedJsonResponseEvent');
|
||||
|
||||
@@ -231,6 +231,17 @@ export class UserPromptEvent implements BaseTelemetryEvent {
|
||||
}
|
||||
|
||||
export const EVENT_TOOL_CALL = 'gemini_cli.tool_call';
|
||||
|
||||
const TOOL_CALL_METADATA_SAFE_KEYS = [
|
||||
'model_added_lines',
|
||||
'model_removed_lines',
|
||||
'model_added_chars',
|
||||
'model_removed_chars',
|
||||
'user_added_lines',
|
||||
'user_removed_lines',
|
||||
'user_added_chars',
|
||||
'user_removed_chars',
|
||||
] as const;
|
||||
export class ToolCallEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'tool_call';
|
||||
'event.timestamp': string;
|
||||
@@ -355,7 +366,6 @@ export class ToolCallEvent implements BaseTelemetryEvent {
|
||||
'event.name': EVENT_TOOL_CALL,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
function_name: this.function_name,
|
||||
function_args: safeJsonStringify(this.function_args, 2),
|
||||
duration_ms: this.duration_ms,
|
||||
success: this.success,
|
||||
decision: this.decision,
|
||||
@@ -367,8 +377,22 @@ export class ToolCallEvent implements BaseTelemetryEvent {
|
||||
extension_id: this.extension_id,
|
||||
start_time: this.start_time,
|
||||
end_time: this.end_time,
|
||||
metadata: this.metadata,
|
||||
};
|
||||
if (config.getTelemetryLogPromptsEnabled() && this.function_args) {
|
||||
attributes['function_args'] = safeJsonStringify(this.function_args, 2);
|
||||
}
|
||||
if (this.metadata) {
|
||||
const metadata = config.getTelemetryLogPromptsEnabled()
|
||||
? this.metadata
|
||||
: Object.fromEntries(
|
||||
Object.entries(this.metadata).filter(([k]) =>
|
||||
(TOOL_CALL_METADATA_SAFE_KEYS as readonly string[]).includes(k),
|
||||
),
|
||||
);
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
attributes['metadata'] = safeJsonStringify(metadata, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
attributes[CoreToolCallStatus.Error] = this.error;
|
||||
@@ -423,8 +447,10 @@ export class ApiRequestEvent implements BaseTelemetryEvent {
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
model: this.model,
|
||||
prompt_id: this.prompt.prompt_id,
|
||||
request_text: this.request_text,
|
||||
};
|
||||
if (config.getTelemetryLogPromptsEnabled() && this.request_text) {
|
||||
attributes['request_text'] = this.request_text;
|
||||
}
|
||||
if (this.role) {
|
||||
attributes['role'] = this.role;
|
||||
}
|
||||
@@ -692,7 +718,7 @@ export class ApiResponseEvent implements BaseTelemetryEvent {
|
||||
if (this.role) {
|
||||
attributes['role'] = this.role;
|
||||
}
|
||||
if (this.response_text) {
|
||||
if (config.getTelemetryLogPromptsEnabled() && this.response_text) {
|
||||
attributes['response_text'] = this.response_text;
|
||||
}
|
||||
if (this.status_code) {
|
||||
@@ -954,11 +980,20 @@ export class ConsecaPolicyGenerationEvent implements BaseTelemetryEvent {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_CONSECA_POLICY_GENERATION,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
user_prompt: this.user_prompt,
|
||||
trusted_content: this.trusted_content,
|
||||
policy: this.policy,
|
||||
};
|
||||
|
||||
if (config.getTelemetryLogPromptsEnabled()) {
|
||||
if (this.user_prompt) {
|
||||
attributes['user_prompt'] = this.user_prompt;
|
||||
}
|
||||
if (this.trusted_content) {
|
||||
attributes['trusted_content'] = this.trusted_content;
|
||||
}
|
||||
if (this.policy) {
|
||||
attributes['policy'] = this.policy;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
attributes['error'] = this.error;
|
||||
}
|
||||
@@ -1005,13 +1040,24 @@ export class ConsecaVerdictEvent implements BaseTelemetryEvent {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_CONSECA_VERDICT,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
user_prompt: this.user_prompt,
|
||||
policy: this.policy,
|
||||
tool_call: this.tool_call,
|
||||
verdict: this.verdict,
|
||||
verdict_rationale: this.verdict_rationale,
|
||||
};
|
||||
|
||||
if (config.getTelemetryLogPromptsEnabled()) {
|
||||
if (this.user_prompt) {
|
||||
attributes['user_prompt'] = this.user_prompt;
|
||||
}
|
||||
if (this.policy) {
|
||||
attributes['policy'] = this.policy;
|
||||
}
|
||||
if (this.tool_call) {
|
||||
attributes['tool_call'] = this.tool_call;
|
||||
}
|
||||
if (this.verdict_rationale) {
|
||||
attributes['verdict_rationale'] = this.verdict_rationale;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
attributes['error'] = this.error;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"description": "Gemini CLI SDK",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"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.41.0-nightly.20260423.gaa05b4583",
|
||||
"version": "0.41.1",
|
||||
"publisher": "google",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user