mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
refactor(cli): centralize tool mapping and decouple legacy scheduler (#17044)
This commit is contained in:
@@ -0,0 +1,236 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { mapCoreStatusToDisplayStatus, mapToDisplay } from './toolMapping.js';
|
||||||
|
import {
|
||||||
|
debugLogger,
|
||||||
|
type AnyDeclarativeTool,
|
||||||
|
type AnyToolInvocation,
|
||||||
|
type ToolCallRequestInfo,
|
||||||
|
type ToolCallResponseInfo,
|
||||||
|
type Status,
|
||||||
|
type ToolCall,
|
||||||
|
type ScheduledToolCall,
|
||||||
|
type SuccessfulToolCall,
|
||||||
|
type ExecutingToolCall,
|
||||||
|
type WaitingToolCall,
|
||||||
|
type CancelledToolCall,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
|
import { ToolCallStatus } from '../types.js';
|
||||||
|
|
||||||
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
debugLogger: {
|
||||||
|
warn: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toolMapping', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mapCoreStatusToDisplayStatus', () => {
|
||||||
|
it.each([
|
||||||
|
['validating', ToolCallStatus.Executing],
|
||||||
|
['awaiting_approval', ToolCallStatus.Confirming],
|
||||||
|
['executing', ToolCallStatus.Executing],
|
||||||
|
['success', ToolCallStatus.Success],
|
||||||
|
['cancelled', ToolCallStatus.Canceled],
|
||||||
|
['error', ToolCallStatus.Error],
|
||||||
|
['scheduled', ToolCallStatus.Pending],
|
||||||
|
] as const)('maps %s to %s', (coreStatus, expectedDisplayStatus) => {
|
||||||
|
expect(mapCoreStatusToDisplayStatus(coreStatus)).toBe(
|
||||||
|
expectedDisplayStatus,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs warning and defaults to Error for unknown status', () => {
|
||||||
|
const result = mapCoreStatusToDisplayStatus('unknown_status' as Status);
|
||||||
|
expect(result).toBe(ToolCallStatus.Error);
|
||||||
|
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||||
|
'Unknown core status encountered: unknown_status',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mapToDisplay', () => {
|
||||||
|
const mockRequest: ToolCallRequestInfo = {
|
||||||
|
callId: 'call-1',
|
||||||
|
name: 'test_tool',
|
||||||
|
args: { arg1: 'val1' },
|
||||||
|
isClientInitiated: false,
|
||||||
|
prompt_id: 'p1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTool = {
|
||||||
|
name: 'test_tool',
|
||||||
|
displayName: 'Test Tool',
|
||||||
|
isOutputMarkdown: true,
|
||||||
|
} as unknown as AnyDeclarativeTool;
|
||||||
|
|
||||||
|
const mockInvocation = {
|
||||||
|
getDescription: () => 'Calling test_tool with args...',
|
||||||
|
} as unknown as AnyToolInvocation;
|
||||||
|
|
||||||
|
const mockResponse: ToolCallResponseInfo = {
|
||||||
|
callId: 'call-1',
|
||||||
|
responseParts: [],
|
||||||
|
resultDisplay: 'Success output',
|
||||||
|
error: undefined,
|
||||||
|
errorType: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('handles a single tool call input', () => {
|
||||||
|
const toolCall: ScheduledToolCall = {
|
||||||
|
status: 'scheduled',
|
||||||
|
request: mockRequest,
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay(toolCall);
|
||||||
|
expect(result.type).toBe('tool_group');
|
||||||
|
expect(result.tools).toHaveLength(1);
|
||||||
|
expect(result.tools[0]?.callId).toBe('call-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles an array of tool calls', () => {
|
||||||
|
const toolCall1: ScheduledToolCall = {
|
||||||
|
status: 'scheduled',
|
||||||
|
request: mockRequest,
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
};
|
||||||
|
const toolCall2: ScheduledToolCall = {
|
||||||
|
status: 'scheduled',
|
||||||
|
request: { ...mockRequest, callId: 'call-2' },
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay([toolCall1, toolCall2]);
|
||||||
|
expect(result.tools).toHaveLength(2);
|
||||||
|
expect(result.tools[0]?.callId).toBe('call-1');
|
||||||
|
expect(result.tools[1]?.callId).toBe('call-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps successful tool call properties correctly', () => {
|
||||||
|
const toolCall: SuccessfulToolCall = {
|
||||||
|
status: 'success',
|
||||||
|
request: mockRequest,
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
response: {
|
||||||
|
...mockResponse,
|
||||||
|
outputFile: '/tmp/output.txt',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay(toolCall);
|
||||||
|
const displayTool = result.tools[0];
|
||||||
|
|
||||||
|
expect(displayTool).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
callId: 'call-1',
|
||||||
|
name: 'Test Tool',
|
||||||
|
description: 'Calling test_tool with args...',
|
||||||
|
renderOutputAsMarkdown: true,
|
||||||
|
status: ToolCallStatus.Success,
|
||||||
|
resultDisplay: 'Success output',
|
||||||
|
outputFile: '/tmp/output.txt',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps executing tool call properties correctly with live output and ptyId', () => {
|
||||||
|
const toolCall: ExecutingToolCall = {
|
||||||
|
status: 'executing',
|
||||||
|
request: mockRequest,
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
liveOutput: 'Loading...',
|
||||||
|
pid: 12345,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay(toolCall);
|
||||||
|
const displayTool = result.tools[0];
|
||||||
|
|
||||||
|
expect(displayTool.status).toBe(ToolCallStatus.Executing);
|
||||||
|
expect(displayTool.resultDisplay).toBe('Loading...');
|
||||||
|
expect(displayTool.ptyId).toBe(12345);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps awaiting_approval tool call properties with correlationId', () => {
|
||||||
|
const confirmationDetails = {
|
||||||
|
type: 'exec' as const,
|
||||||
|
title: 'Confirm Exec',
|
||||||
|
command: 'ls',
|
||||||
|
rootCommand: 'ls',
|
||||||
|
rootCommands: ['ls'],
|
||||||
|
onConfirm: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolCall: WaitingToolCall = {
|
||||||
|
status: 'awaiting_approval',
|
||||||
|
request: mockRequest,
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
confirmationDetails,
|
||||||
|
correlationId: 'corr-id-123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay(toolCall);
|
||||||
|
const displayTool = result.tools[0];
|
||||||
|
|
||||||
|
expect(displayTool.status).toBe(ToolCallStatus.Confirming);
|
||||||
|
expect(displayTool.confirmationDetails).toEqual(confirmationDetails);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps error tool call missing tool definition', () => {
|
||||||
|
// e.g. "TOOL_NOT_REGISTERED" errors
|
||||||
|
const toolCall: ToolCall = {
|
||||||
|
status: 'error',
|
||||||
|
request: mockRequest, // name: 'test_tool'
|
||||||
|
response: { ...mockResponse, resultDisplay: 'Tool not found' },
|
||||||
|
// notice: no `tool` or `invocation` defined here
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay(toolCall);
|
||||||
|
const displayTool = result.tools[0];
|
||||||
|
|
||||||
|
expect(displayTool.status).toBe(ToolCallStatus.Error);
|
||||||
|
expect(displayTool.name).toBe('test_tool'); // falls back to request.name
|
||||||
|
expect(displayTool.description).toBe('{"arg1":"val1"}'); // falls back to stringified args
|
||||||
|
expect(displayTool.resultDisplay).toBe('Tool not found');
|
||||||
|
expect(displayTool.renderOutputAsMarkdown).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps cancelled tool call properties correctly', () => {
|
||||||
|
const toolCall: CancelledToolCall = {
|
||||||
|
status: 'cancelled',
|
||||||
|
request: mockRequest,
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
response: {
|
||||||
|
...mockResponse,
|
||||||
|
resultDisplay: 'User cancelled', // Could be diff output for edits
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay(toolCall);
|
||||||
|
const displayTool = result.tools[0];
|
||||||
|
|
||||||
|
expect(displayTool.status).toBe(ToolCallStatus.Canceled);
|
||||||
|
expect(displayTool.resultDisplay).toBe('User cancelled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ToolCall,
|
||||||
|
type Status as CoreStatus,
|
||||||
|
type ToolCallConfirmationDetails,
|
||||||
|
type ToolResultDisplay,
|
||||||
|
debugLogger,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
|
import {
|
||||||
|
ToolCallStatus,
|
||||||
|
type HistoryItemToolGroup,
|
||||||
|
type IndividualToolCallDisplay,
|
||||||
|
} from '../types.js';
|
||||||
|
|
||||||
|
export function mapCoreStatusToDisplayStatus(
|
||||||
|
coreStatus: CoreStatus,
|
||||||
|
): ToolCallStatus {
|
||||||
|
switch (coreStatus) {
|
||||||
|
case 'validating':
|
||||||
|
return ToolCallStatus.Executing;
|
||||||
|
case 'awaiting_approval':
|
||||||
|
return ToolCallStatus.Confirming;
|
||||||
|
case 'executing':
|
||||||
|
return ToolCallStatus.Executing;
|
||||||
|
case 'success':
|
||||||
|
return ToolCallStatus.Success;
|
||||||
|
case 'cancelled':
|
||||||
|
return ToolCallStatus.Canceled;
|
||||||
|
case 'error':
|
||||||
|
return ToolCallStatus.Error;
|
||||||
|
case 'scheduled':
|
||||||
|
return ToolCallStatus.Pending;
|
||||||
|
default:
|
||||||
|
debugLogger.warn(`Unknown core status encountered: ${coreStatus}`);
|
||||||
|
return ToolCallStatus.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms `ToolCall` objects into `HistoryItemToolGroup` objects for UI
|
||||||
|
* display. This is a pure projection layer and does not track interaction
|
||||||
|
* state.
|
||||||
|
*/
|
||||||
|
export function mapToDisplay(
|
||||||
|
toolOrTools: ToolCall[] | ToolCall,
|
||||||
|
): HistoryItemToolGroup {
|
||||||
|
const toolCalls = Array.isArray(toolOrTools) ? toolOrTools : [toolOrTools];
|
||||||
|
|
||||||
|
const toolDisplays = toolCalls.map((call): IndividualToolCallDisplay => {
|
||||||
|
let description: string;
|
||||||
|
let renderOutputAsMarkdown = false;
|
||||||
|
|
||||||
|
const displayName = call.tool?.displayName ?? call.request.name;
|
||||||
|
|
||||||
|
if (call.status === 'error') {
|
||||||
|
description = JSON.stringify(call.request.args);
|
||||||
|
} else {
|
||||||
|
description = call.invocation.getDescription();
|
||||||
|
renderOutputAsMarkdown = call.tool.isOutputMarkdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseDisplayProperties = {
|
||||||
|
callId: call.request.callId,
|
||||||
|
name: displayName,
|
||||||
|
description,
|
||||||
|
renderOutputAsMarkdown,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resultDisplay: ToolResultDisplay | undefined = undefined;
|
||||||
|
let confirmationDetails: ToolCallConfirmationDetails | undefined =
|
||||||
|
undefined;
|
||||||
|
let outputFile: string | undefined = undefined;
|
||||||
|
let ptyId: number | undefined = undefined;
|
||||||
|
|
||||||
|
switch (call.status) {
|
||||||
|
case 'success':
|
||||||
|
resultDisplay = call.response.resultDisplay;
|
||||||
|
outputFile = call.response.outputFile;
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
case 'cancelled':
|
||||||
|
resultDisplay = call.response.resultDisplay;
|
||||||
|
break;
|
||||||
|
case 'awaiting_approval':
|
||||||
|
// Only map if it's the legacy callback-based details.
|
||||||
|
// Serializable details will be handled in a later milestone.
|
||||||
|
if (
|
||||||
|
call.confirmationDetails &&
|
||||||
|
'onConfirm' in call.confirmationDetails &&
|
||||||
|
typeof call.confirmationDetails.onConfirm === 'function'
|
||||||
|
) {
|
||||||
|
confirmationDetails =
|
||||||
|
call.confirmationDetails as ToolCallConfirmationDetails;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'executing':
|
||||||
|
resultDisplay = call.liveOutput;
|
||||||
|
ptyId = call.pid;
|
||||||
|
break;
|
||||||
|
case 'scheduled':
|
||||||
|
case 'validating':
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
const exhaustiveCheck: never = call;
|
||||||
|
debugLogger.warn(
|
||||||
|
`Unhandled tool call status in mapper: ${
|
||||||
|
(exhaustiveCheck as ToolCall).status
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseDisplayProperties,
|
||||||
|
status: mapCoreStatusToDisplayStatus(call.status),
|
||||||
|
resultDisplay,
|
||||||
|
confirmationDetails,
|
||||||
|
outputFile,
|
||||||
|
ptyId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'tool_group',
|
||||||
|
tools: toolDisplays,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -63,9 +63,9 @@ import { useStateAndRef } from './useStateAndRef.js';
|
|||||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||||
import { useLogger } from './useLogger.js';
|
import { useLogger } from './useLogger.js';
|
||||||
import { SHELL_COMMAND_NAME } from '../constants.js';
|
import { SHELL_COMMAND_NAME } from '../constants.js';
|
||||||
|
import { mapToDisplay as mapTrackedToolCallsToDisplay } from './toolMapping.js';
|
||||||
import {
|
import {
|
||||||
useReactToolScheduler,
|
useReactToolScheduler,
|
||||||
mapToDisplay as mapTrackedToolCallsToDisplay,
|
|
||||||
type TrackedToolCall,
|
type TrackedToolCall,
|
||||||
type TrackedCompletedToolCall,
|
type TrackedCompletedToolCall,
|
||||||
type TrackedCancelledToolCall,
|
type TrackedCancelledToolCall,
|
||||||
|
|||||||
@@ -7,33 +7,27 @@
|
|||||||
import type {
|
import type {
|
||||||
Config,
|
Config,
|
||||||
ToolCallRequestInfo,
|
ToolCallRequestInfo,
|
||||||
ExecutingToolCall,
|
|
||||||
ScheduledToolCall,
|
|
||||||
ValidatingToolCall,
|
|
||||||
WaitingToolCall,
|
|
||||||
CompletedToolCall,
|
|
||||||
CancelledToolCall,
|
|
||||||
OutputUpdateHandler,
|
OutputUpdateHandler,
|
||||||
AllToolCallsCompleteHandler,
|
AllToolCallsCompleteHandler,
|
||||||
ToolCallsUpdateHandler,
|
ToolCallsUpdateHandler,
|
||||||
ToolCall,
|
ToolCall,
|
||||||
ToolCallConfirmationDetails,
|
|
||||||
Status as CoreStatus,
|
|
||||||
EditorType,
|
EditorType,
|
||||||
|
CompletedToolCall,
|
||||||
|
ExecutingToolCall,
|
||||||
|
ScheduledToolCall,
|
||||||
|
ValidatingToolCall,
|
||||||
|
WaitingToolCall,
|
||||||
|
CancelledToolCall,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { CoreToolScheduler, debugLogger } from '@google/gemini-cli-core';
|
import { CoreToolScheduler } from '@google/gemini-cli-core';
|
||||||
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
|
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
|
||||||
import type {
|
|
||||||
HistoryItemToolGroup,
|
|
||||||
IndividualToolCallDisplay,
|
|
||||||
} from '../types.js';
|
|
||||||
import { ToolCallStatus } from '../types.js';
|
|
||||||
|
|
||||||
export type ScheduleFn = (
|
export type ScheduleFn = (
|
||||||
request: ToolCallRequestInfo | ToolCallRequestInfo[],
|
request: ToolCallRequestInfo | ToolCallRequestInfo[],
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
export type MarkToolsAsSubmittedFn = (callIds: string[]) => void;
|
export type MarkToolsAsSubmittedFn = (callIds: string[]) => void;
|
||||||
|
export type CancelAllFn = (signal: AbortSignal) => void;
|
||||||
|
|
||||||
export type TrackedScheduledToolCall = ScheduledToolCall & {
|
export type TrackedScheduledToolCall = ScheduledToolCall & {
|
||||||
responseSubmittedToGemini?: boolean;
|
responseSubmittedToGemini?: boolean;
|
||||||
@@ -63,8 +57,12 @@ export type TrackedToolCall =
|
|||||||
| TrackedCompletedToolCall
|
| TrackedCompletedToolCall
|
||||||
| TrackedCancelledToolCall;
|
| TrackedCancelledToolCall;
|
||||||
|
|
||||||
export type CancelAllFn = (signal: AbortSignal) => void;
|
/**
|
||||||
|
* Legacy scheduler implementation based on CoreToolScheduler callbacks.
|
||||||
|
*
|
||||||
|
* This is currently the default implementation used by useGeminiStream.
|
||||||
|
* It will be phased out once the event-driven scheduler migration is complete.
|
||||||
|
*/
|
||||||
export function useReactToolScheduler(
|
export function useReactToolScheduler(
|
||||||
onComplete: (tools: CompletedToolCall[]) => Promise<void>,
|
onComplete: (tools: CompletedToolCall[]) => Promise<void>,
|
||||||
config: Config,
|
config: Config,
|
||||||
@@ -82,7 +80,6 @@ export function useReactToolScheduler(
|
|||||||
>([]);
|
>([]);
|
||||||
const [lastToolOutputTime, setLastToolOutputTime] = useState<number>(0);
|
const [lastToolOutputTime, setLastToolOutputTime] = useState<number>(0);
|
||||||
|
|
||||||
// Store callbacks in refs to keep them up-to-date without causing re-renders.
|
|
||||||
const onCompleteRef = useRef(onComplete);
|
const onCompleteRef = useRef(onComplete);
|
||||||
const getPreferredEditorRef = useRef(getPreferredEditor);
|
const getPreferredEditorRef = useRef(getPreferredEditor);
|
||||||
|
|
||||||
@@ -131,7 +128,6 @@ export function useReactToolScheduler(
|
|||||||
existingTrackedCall?.responseSubmittedToGemini ?? false;
|
existingTrackedCall?.responseSubmittedToGemini ?? false;
|
||||||
|
|
||||||
if (coreTc.status === 'executing') {
|
if (coreTc.status === 'executing') {
|
||||||
// Preserve live output if it exists from a previous render.
|
|
||||||
const liveOutput = (existingTrackedCall as TrackedExecutingToolCall)
|
const liveOutput = (existingTrackedCall as TrackedExecutingToolCall)
|
||||||
?.liveOutput;
|
?.liveOutput;
|
||||||
return {
|
return {
|
||||||
@@ -215,135 +211,3 @@ export function useReactToolScheduler(
|
|||||||
lastToolOutputTime,
|
lastToolOutputTime,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a CoreToolScheduler status to the UI's ToolCallStatus enum.
|
|
||||||
*/
|
|
||||||
function mapCoreStatusToDisplayStatus(coreStatus: CoreStatus): ToolCallStatus {
|
|
||||||
switch (coreStatus) {
|
|
||||||
case 'validating':
|
|
||||||
return ToolCallStatus.Executing;
|
|
||||||
case 'awaiting_approval':
|
|
||||||
return ToolCallStatus.Confirming;
|
|
||||||
case 'executing':
|
|
||||||
return ToolCallStatus.Executing;
|
|
||||||
case 'success':
|
|
||||||
return ToolCallStatus.Success;
|
|
||||||
case 'cancelled':
|
|
||||||
return ToolCallStatus.Canceled;
|
|
||||||
case 'error':
|
|
||||||
return ToolCallStatus.Error;
|
|
||||||
case 'scheduled':
|
|
||||||
return ToolCallStatus.Pending;
|
|
||||||
default: {
|
|
||||||
const exhaustiveCheck: never = coreStatus;
|
|
||||||
debugLogger.warn(`Unknown core status encountered: ${exhaustiveCheck}`);
|
|
||||||
return ToolCallStatus.Error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms `TrackedToolCall` objects into `HistoryItemToolGroup` objects for UI display.
|
|
||||||
*/
|
|
||||||
export function mapToDisplay(
|
|
||||||
toolOrTools: TrackedToolCall[] | TrackedToolCall,
|
|
||||||
): HistoryItemToolGroup {
|
|
||||||
const toolCalls = Array.isArray(toolOrTools) ? toolOrTools : [toolOrTools];
|
|
||||||
|
|
||||||
const toolDisplays = toolCalls.map(
|
|
||||||
(trackedCall): IndividualToolCallDisplay => {
|
|
||||||
let displayName: string;
|
|
||||||
let description: string;
|
|
||||||
let renderOutputAsMarkdown = false;
|
|
||||||
|
|
||||||
if (trackedCall.status === 'error') {
|
|
||||||
displayName =
|
|
||||||
trackedCall.tool === undefined
|
|
||||||
? trackedCall.request.name
|
|
||||||
: trackedCall.tool.displayName;
|
|
||||||
description = JSON.stringify(trackedCall.request.args);
|
|
||||||
} else {
|
|
||||||
displayName = trackedCall.tool.displayName;
|
|
||||||
description = trackedCall.invocation.getDescription();
|
|
||||||
renderOutputAsMarkdown = trackedCall.tool.isOutputMarkdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseDisplayProperties: Omit<
|
|
||||||
IndividualToolCallDisplay,
|
|
||||||
'status' | 'resultDisplay' | 'confirmationDetails'
|
|
||||||
> = {
|
|
||||||
callId: trackedCall.request.callId,
|
|
||||||
name: displayName,
|
|
||||||
description,
|
|
||||||
renderOutputAsMarkdown,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (trackedCall.status) {
|
|
||||||
case 'success':
|
|
||||||
return {
|
|
||||||
...baseDisplayProperties,
|
|
||||||
status: mapCoreStatusToDisplayStatus(trackedCall.status),
|
|
||||||
resultDisplay: trackedCall.response.resultDisplay,
|
|
||||||
confirmationDetails: undefined,
|
|
||||||
outputFile: trackedCall.response.outputFile,
|
|
||||||
};
|
|
||||||
case 'error':
|
|
||||||
return {
|
|
||||||
...baseDisplayProperties,
|
|
||||||
status: mapCoreStatusToDisplayStatus(trackedCall.status),
|
|
||||||
resultDisplay: trackedCall.response.resultDisplay,
|
|
||||||
confirmationDetails: undefined,
|
|
||||||
};
|
|
||||||
case 'cancelled':
|
|
||||||
return {
|
|
||||||
...baseDisplayProperties,
|
|
||||||
status: mapCoreStatusToDisplayStatus(trackedCall.status),
|
|
||||||
resultDisplay: trackedCall.response.resultDisplay,
|
|
||||||
confirmationDetails: undefined,
|
|
||||||
};
|
|
||||||
case 'awaiting_approval':
|
|
||||||
return {
|
|
||||||
...baseDisplayProperties,
|
|
||||||
status: mapCoreStatusToDisplayStatus(trackedCall.status),
|
|
||||||
resultDisplay: undefined,
|
|
||||||
confirmationDetails:
|
|
||||||
trackedCall.confirmationDetails as ToolCallConfirmationDetails,
|
|
||||||
};
|
|
||||||
case 'executing':
|
|
||||||
return {
|
|
||||||
...baseDisplayProperties,
|
|
||||||
status: mapCoreStatusToDisplayStatus(trackedCall.status),
|
|
||||||
resultDisplay: trackedCall.liveOutput ?? undefined,
|
|
||||||
confirmationDetails: undefined,
|
|
||||||
ptyId: trackedCall.pid,
|
|
||||||
};
|
|
||||||
case 'validating': // Fallthrough
|
|
||||||
case 'scheduled':
|
|
||||||
return {
|
|
||||||
...baseDisplayProperties,
|
|
||||||
status: mapCoreStatusToDisplayStatus(trackedCall.status),
|
|
||||||
resultDisplay: undefined,
|
|
||||||
confirmationDetails: undefined,
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
const exhaustiveCheck: never = trackedCall;
|
|
||||||
return {
|
|
||||||
callId: (exhaustiveCheck as TrackedToolCall).request.callId,
|
|
||||||
name: 'Unknown Tool',
|
|
||||||
description: 'Encountered an unknown tool call state.',
|
|
||||||
status: ToolCallStatus.Error,
|
|
||||||
resultDisplay: 'Unknown tool call state',
|
|
||||||
confirmationDetails: undefined,
|
|
||||||
renderOutputAsMarkdown: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'tool_group',
|
|
||||||
tools: toolDisplays,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ import type { Mock } from 'vitest';
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { act } from 'react';
|
import { act } from 'react';
|
||||||
import { renderHook } from '../../test-utils/render.js';
|
import { renderHook } from '../../test-utils/render.js';
|
||||||
import {
|
import { useReactToolScheduler } from './useReactToolScheduler.js';
|
||||||
useReactToolScheduler,
|
import { mapToDisplay } from './toolMapping.js';
|
||||||
mapToDisplay,
|
|
||||||
} from './useReactToolScheduler.js';
|
|
||||||
import type { PartUnion, FunctionResponse } from '@google/genai';
|
import type { PartUnion, FunctionResponse } from '@google/genai';
|
||||||
import type {
|
import type {
|
||||||
Config,
|
Config,
|
||||||
|
|||||||
Reference in New Issue
Block a user