mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-19 00:02:51 -07:00
fix(cli,core): resolve lint and type errors in agent stream and core types
This commit is contained in:
@@ -1093,6 +1093,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}, [config]);
|
||||
|
||||
const useAgentProtocol = config.getExperimentalUseAgentProtocol();
|
||||
const useActiveStream = useAgentProtocol ? useAgentStream : useGeminiStream;
|
||||
|
||||
const {
|
||||
streamingState,
|
||||
@@ -1113,50 +1114,27 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
backgroundShells,
|
||||
dismissBackgroundShell,
|
||||
retryStatus,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
} = useAgentProtocol
|
||||
? useAgentStream(
|
||||
config.getGeminiClient(),
|
||||
historyManager.history,
|
||||
historyManager.addItem,
|
||||
config,
|
||||
settings,
|
||||
setDebugMessage,
|
||||
handleSlashCommand,
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
onAuthError,
|
||||
performMemoryRefresh,
|
||||
modelSwitchedFromQuotaError,
|
||||
setModelSwitchedFromQuotaError,
|
||||
onCancelSubmit,
|
||||
setEmbeddedShellFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
embeddedShellFocused,
|
||||
consumePendingHints,
|
||||
)
|
||||
: useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
historyManager.history,
|
||||
historyManager.addItem,
|
||||
config,
|
||||
settings,
|
||||
setDebugMessage,
|
||||
handleSlashCommand,
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
onAuthError,
|
||||
performMemoryRefresh,
|
||||
modelSwitchedFromQuotaError,
|
||||
setModelSwitchedFromQuotaError,
|
||||
onCancelSubmit,
|
||||
setEmbeddedShellFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
embeddedShellFocused,
|
||||
consumePendingHints,
|
||||
);
|
||||
} = useActiveStream(
|
||||
config.getGeminiClient(),
|
||||
historyManager.history,
|
||||
historyManager.addItem,
|
||||
config,
|
||||
settings,
|
||||
setDebugMessage,
|
||||
handleSlashCommand,
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
onAuthError,
|
||||
performMemoryRefresh,
|
||||
modelSwitchedFromQuotaError,
|
||||
setModelSwitchedFromQuotaError,
|
||||
onCancelSubmit,
|
||||
setEmbeddedShellFocused,
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
embeddedShellFocused,
|
||||
consumePendingHints,
|
||||
);
|
||||
|
||||
const pendingHistoryItems = useMemo(
|
||||
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
|
||||
@@ -1729,7 +1707,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
if (keyMatchers[Command.QUIT](key)) {
|
||||
// If the user presses Ctrl+C, we want to cancel any ongoing requests.
|
||||
// This should happen regardless of the count.
|
||||
cancelOngoingRequest?.();
|
||||
void cancelOngoingRequest?.();
|
||||
|
||||
handleCtrlCPress();
|
||||
return true;
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
} from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import {
|
||||
type Config,
|
||||
type GeminiClient,
|
||||
LegacyAgentSession as MockLegacyAgentSession,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type LoadedSettings } from '../../config/settings.js';
|
||||
import { renderHookWithProviders } from '../../test-utils/render.js';
|
||||
|
||||
// --- MOCKS ---
|
||||
@@ -47,9 +47,9 @@ vi.mock('./useLogger.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
...(actual as any),
|
||||
...actual,
|
||||
useSessionStats: vi.fn(() => ({
|
||||
startNewPrompt: vi.fn(),
|
||||
})),
|
||||
@@ -58,19 +58,18 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
|
||||
// Mock core classes properly
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual = await importOriginal() as any;
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
...actual,
|
||||
LegacyAgentSession: vi.fn().mockImplementation(() => mockLegacyAgentSession),
|
||||
LegacyAgentSession: vi
|
||||
.fn()
|
||||
.mockImplementation(() => mockLegacyAgentSession),
|
||||
};
|
||||
});
|
||||
|
||||
// --- END MOCKS ---
|
||||
|
||||
import { useAgentStream } from './useAgentStream.js';
|
||||
import {
|
||||
LegacyAgentSession as MockLegacyAgentSession,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { MessageType, StreamingState } from '../types.js';
|
||||
|
||||
describe('useAgentStream', () => {
|
||||
@@ -89,14 +88,14 @@ describe('useAgentStream', () => {
|
||||
getExperimentalUseAgentProtocol: () => true,
|
||||
getApprovalMode: () => 'default',
|
||||
getMessageBus: () => ({}),
|
||||
} as any;
|
||||
} as Config;
|
||||
|
||||
const mockSettings = {
|
||||
merged: {
|
||||
billing: { overageStrategy: 'stop' },
|
||||
ui: { errorVerbosity: 'full' },
|
||||
},
|
||||
} as any;
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -105,7 +104,7 @@ describe('useAgentStream', () => {
|
||||
it('should initialize LegacyAgentSession on mount', async () => {
|
||||
await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as any,
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
@@ -132,7 +131,7 @@ describe('useAgentStream', () => {
|
||||
it('should call session.send when submitQuery is called', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as any,
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
@@ -168,7 +167,7 @@ describe('useAgentStream', () => {
|
||||
it('should update streamingState based on agent_start and agent_end events', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as any,
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
@@ -188,7 +187,8 @@ describe('useAgentStream', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const eventHandler = (mockLegacyAgentSession.subscribe as any).mock.calls[0][0];
|
||||
const eventHandler = vi.mocked(mockLegacyAgentSession.subscribe).mock
|
||||
.calls[0][0];
|
||||
|
||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||
|
||||
@@ -206,7 +206,7 @@ describe('useAgentStream', () => {
|
||||
it('should accumulate text content and update pendingHistoryItems', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as any,
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
@@ -226,7 +226,8 @@ describe('useAgentStream', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const eventHandler = (mockLegacyAgentSession.subscribe as any).mock.calls[0][0];
|
||||
const eventHandler = vi.mocked(mockLegacyAgentSession.subscribe).mock
|
||||
.calls[0][0];
|
||||
|
||||
act(() => {
|
||||
eventHandler({
|
||||
@@ -256,7 +257,7 @@ describe('useAgentStream', () => {
|
||||
it('should process thought events and update thought state', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as any,
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
@@ -276,7 +277,8 @@ describe('useAgentStream', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const eventHandler = (mockLegacyAgentSession.subscribe as any).mock.calls[0][0];
|
||||
const eventHandler = vi.mocked(mockLegacyAgentSession.subscribe).mock
|
||||
.calls[0][0];
|
||||
|
||||
act(() => {
|
||||
eventHandler({
|
||||
@@ -293,15 +295,17 @@ describe('useAgentStream', () => {
|
||||
});
|
||||
|
||||
it('should display error message when a tool call requires approval', async () => {
|
||||
let eventHandler: (event: any) => void = () => {};
|
||||
vi.spyOn(mockLegacyAgentSession, 'subscribe').mockImplementation((handler) => {
|
||||
eventHandler = handler;
|
||||
return () => {};
|
||||
});
|
||||
let eventHandler: (event: unknown) => void = () => {};
|
||||
vi.spyOn(mockLegacyAgentSession, 'subscribe').mockImplementation(
|
||||
(handler) => {
|
||||
eventHandler = handler;
|
||||
return () => {};
|
||||
},
|
||||
);
|
||||
|
||||
await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as any,
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
@@ -325,7 +329,8 @@ describe('useAgentStream', () => {
|
||||
eventHandler({
|
||||
type: 'error',
|
||||
status: 'UNIMPLEMENTED',
|
||||
message: 'TODO: Tool approvals not yet implemented, please switch to YOLO mode to test.',
|
||||
message:
|
||||
'TODO: Tool approvals not yet implemented, please switch to YOLO mode to test.',
|
||||
fatal: true,
|
||||
});
|
||||
});
|
||||
@@ -342,7 +347,7 @@ describe('useAgentStream', () => {
|
||||
it('should call session.abort when cancelOngoingRequest is called', async () => {
|
||||
const { result } = await renderHookWithProviders(() =>
|
||||
useAgentStream(
|
||||
{} as any,
|
||||
{} as GeminiClient,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
|
||||
@@ -8,25 +8,30 @@ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
getErrorMessage,
|
||||
MessageSenderType,
|
||||
ApprovalMode,
|
||||
debugLogger,
|
||||
LegacyAgentSession,
|
||||
geminiPartsToContentParts,
|
||||
parseThought,
|
||||
CoreToolCallStatus,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Config,
|
||||
EditorType,
|
||||
GeminiClient,
|
||||
ThoughtSummary,
|
||||
RetryAttemptPayload,
|
||||
AgentEvent,
|
||||
import {
|
||||
type Config,
|
||||
type GeminiClient,
|
||||
type ApprovalMode,
|
||||
Kind,
|
||||
type EditorType,
|
||||
type ThoughtSummary,
|
||||
type RetryAttemptPayload,
|
||||
type AgentEvent,
|
||||
BaseDeclarativeTool,
|
||||
type ToolResult,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type PartListUnion } from '@google/genai';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryItemWithoutId,
|
||||
LoopDetectionConfirmationRequest,
|
||||
SlashCommandProcessorResult,
|
||||
} from '../types.js';
|
||||
import { StreamingState, MessageType } from '../types.js';
|
||||
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
|
||||
@@ -35,18 +40,47 @@ import { type BackgroundShell } from './shellCommandProcessor.js';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { useLogger } from './useLogger.js';
|
||||
import { mapToDisplay as mapTrackedToolCallsToDisplay } from './toolMapping.js';
|
||||
import {
|
||||
useToolScheduler,
|
||||
} from './useToolScheduler.js';
|
||||
import type {
|
||||
TrackedToolCall,
|
||||
} from './useToolScheduler.js';
|
||||
import { useToolScheduler } from './useToolScheduler.js';
|
||||
import type { TrackedToolCall } from './useToolScheduler.js';
|
||||
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import type { SlashCommandProcessorResult } from '../types.js';
|
||||
import { useStateAndRef } from './useStateAndRef.js';
|
||||
|
||||
class DummyTool extends BaseDeclarativeTool<
|
||||
Record<string, unknown>,
|
||||
ToolResult
|
||||
> {
|
||||
constructor(
|
||||
name: string,
|
||||
description: string,
|
||||
displayName: string,
|
||||
isOutputMarkdown: boolean,
|
||||
kind: Kind,
|
||||
messageBus: import('@google/gemini-cli-core').MessageBus,
|
||||
) {
|
||||
super(
|
||||
name,
|
||||
displayName,
|
||||
description,
|
||||
kind,
|
||||
undefined,
|
||||
messageBus,
|
||||
isOutputMarkdown,
|
||||
false,
|
||||
);
|
||||
}
|
||||
protected createInvocation(params: Record<string, unknown>) {
|
||||
return {
|
||||
getDescription: () => this.description,
|
||||
params,
|
||||
execute: async () => ({ llmContent: [], returnDisplay: '' }),
|
||||
toolLocations: () => [],
|
||||
shouldConfirmExecute: async (): Promise<false> => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* useAgentStream implements the interactive agent loop using the LegacyAgentSession (AgentProtocol).
|
||||
* It attempts to maintain parity with useGeminiStream while consolidating model/tool orchestration
|
||||
@@ -67,7 +101,9 @@ export const useAgentStream = (
|
||||
_onAuthError: (error: string) => void,
|
||||
_performMemoryRefresh: () => Promise<void>,
|
||||
_modelSwitchedFromQuotaError: boolean,
|
||||
_setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
_setModelSwitchedFromQuotaError: React.Dispatch<
|
||||
React.SetStateAction<boolean>
|
||||
>,
|
||||
onCancelSubmit: (shouldRestorePrompt?: boolean) => void,
|
||||
_setShellInputFocused: (value: boolean) => void,
|
||||
_terminalWidth: number,
|
||||
@@ -76,9 +112,7 @@ export const useAgentStream = (
|
||||
_consumeUserHint?: () => string | null,
|
||||
) => {
|
||||
const [initError] = useState<string | null>(null);
|
||||
const [retryStatus] = useState<RetryAttemptPayload | null>(
|
||||
null,
|
||||
);
|
||||
const [retryStatus] = useState<RetryAttemptPayload | null>(null);
|
||||
const [streamingState, setStreamingState] = useState<StreamingState>(
|
||||
StreamingState.Idle,
|
||||
);
|
||||
@@ -92,8 +126,9 @@ export const useAgentStream = (
|
||||
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
|
||||
useStateAndRef<HistoryItemWithoutId | null>(null);
|
||||
|
||||
const [trackedTools, , setTrackedTools] =
|
||||
useStateAndRef<TrackedToolCall[]>([]);
|
||||
const [trackedTools, , setTrackedTools] = useStateAndRef<TrackedToolCall[]>(
|
||||
[],
|
||||
);
|
||||
const [pushedToolCallIds, pushedToolCallIdsRef, setPushedToolCallIds] =
|
||||
useStateAndRef<Set<string>>(new Set());
|
||||
const [_isFirstToolInGroup, isFirstToolInGroupRef, setIsFirstToolInGroup] =
|
||||
@@ -123,20 +158,19 @@ export const useAgentStream = (
|
||||
const isBackgroundShellVisible = false;
|
||||
const toggleBackgroundShell = useCallback(() => {}, []);
|
||||
const backgroundCurrentShell = undefined;
|
||||
const backgroundShells = new Map<number, BackgroundShell>();
|
||||
const backgroundShells = useMemo(
|
||||
() => new Map<number, BackgroundShell>(),
|
||||
[],
|
||||
);
|
||||
const dismissBackgroundShell = useCallback(async (_pid: number) => {}, []);
|
||||
|
||||
// TODO: Support LoopDetection confirmation requests
|
||||
const [
|
||||
loopDetectionConfirmationRequest,
|
||||
] = useState<LoopDetectionConfirmationRequest | null>(null);
|
||||
const [loopDetectionConfirmationRequest] =
|
||||
useState<LoopDetectionConfirmationRequest | null>(null);
|
||||
|
||||
const flushPendingText = useCallback(() => {
|
||||
if (pendingHistoryItemRef.current) {
|
||||
addItem(
|
||||
pendingHistoryItemRef.current,
|
||||
userMessageTimestampRef.current,
|
||||
);
|
||||
addItem(pendingHistoryItemRef.current, userMessageTimestampRef.current);
|
||||
setPendingHistoryItem(null);
|
||||
geminiMessageBufferRef.current = '';
|
||||
}
|
||||
@@ -206,84 +240,201 @@ export const useAgentStream = (
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'tool_request':
|
||||
case 'tool_request': {
|
||||
flushPendingText();
|
||||
setTrackedTools((prev) => [
|
||||
...prev,
|
||||
{
|
||||
request: {
|
||||
callId: event.requestId,
|
||||
name: event.name,
|
||||
args: event.args,
|
||||
isClientInitiated: false,
|
||||
originalRequestName: event.name,
|
||||
},
|
||||
status: 'scheduled',
|
||||
tool: {
|
||||
displayName: (event._meta?.['displayName'] as string) ?? event.name,
|
||||
isOutputMarkdown: (event._meta?.['isOutputMarkdown'] as boolean) ?? false,
|
||||
kind: event._meta?.['kind'] as any,
|
||||
},
|
||||
invocation: {
|
||||
getDescription: () => (event._meta?.['description'] as string) ?? '',
|
||||
},
|
||||
} as unknown as TrackedToolCall,
|
||||
]);
|
||||
const legacyState = event._meta?.legacyState;
|
||||
const displayName = legacyState?.displayName ?? event.name;
|
||||
const isOutputMarkdown = legacyState?.isOutputMarkdown ?? false;
|
||||
const desc = legacyState?.description ?? '';
|
||||
|
||||
const args =
|
||||
event.args && typeof event.args === 'object' ? event.args : {};
|
||||
const fallbackKind = Kind.Other;
|
||||
const messageBus = config.getMessageBus();
|
||||
|
||||
const tool =
|
||||
config.getToolRegistry().getTool(event.name) ||
|
||||
new DummyTool(
|
||||
event.name,
|
||||
desc,
|
||||
displayName,
|
||||
isOutputMarkdown,
|
||||
fallbackKind,
|
||||
messageBus,
|
||||
);
|
||||
const invocation = tool.build(args);
|
||||
|
||||
const newCall: TrackedToolCall = {
|
||||
request: {
|
||||
callId: event.requestId,
|
||||
name: event.name,
|
||||
args,
|
||||
isClientInitiated: false,
|
||||
originalRequestName: event.name,
|
||||
prompt_id: '',
|
||||
},
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
tool,
|
||||
invocation,
|
||||
};
|
||||
setTrackedTools((prev) => [...prev, newCall]);
|
||||
break;
|
||||
case 'tool_update':
|
||||
}
|
||||
case 'tool_update': {
|
||||
setTrackedTools((prev) =>
|
||||
prev.map((tc) =>
|
||||
tc.request.callId === event.requestId
|
||||
? ({
|
||||
prev.map((tc): TrackedToolCall => {
|
||||
if (tc.request.callId !== event.requestId) return tc;
|
||||
|
||||
const legacyState = event._meta?.legacyState;
|
||||
const evtStatus = legacyState?.status;
|
||||
|
||||
let status = tc.status;
|
||||
if (evtStatus === 'executing')
|
||||
status = CoreToolCallStatus.Executing;
|
||||
else if (evtStatus === 'error') status = CoreToolCallStatus.Error;
|
||||
else if (evtStatus === 'success')
|
||||
status = CoreToolCallStatus.Success;
|
||||
|
||||
const liveOutput =
|
||||
event.displayContent?.[0]?.type === 'text'
|
||||
? event.displayContent[0].text
|
||||
: 'liveOutput' in tc
|
||||
? tc.liveOutput
|
||||
: undefined;
|
||||
const progressMessage =
|
||||
legacyState?.progressMessage ??
|
||||
('progressMessage' in tc ? tc.progressMessage : undefined);
|
||||
const progress =
|
||||
legacyState?.progress ??
|
||||
('progress' in tc ? tc.progress : undefined);
|
||||
const progressTotal =
|
||||
legacyState?.progressTotal ??
|
||||
('progressTotal' in tc ? tc.progressTotal : undefined);
|
||||
const pid =
|
||||
legacyState?.pid ?? ('pid' in tc ? tc.pid : undefined);
|
||||
const desc =
|
||||
legacyState?.description ??
|
||||
('invocation' in tc && tc.invocation
|
||||
? tc.invocation.getDescription()
|
||||
: '');
|
||||
const invocation =
|
||||
'invocation' in tc && tc.invocation
|
||||
? { ...tc.invocation, getDescription: () => desc }
|
||||
: undefined;
|
||||
|
||||
const inProgressFields = {
|
||||
pid,
|
||||
liveOutput,
|
||||
progress,
|
||||
progressTotal,
|
||||
progressMessage,
|
||||
invocation,
|
||||
};
|
||||
|
||||
const response =
|
||||
'response' in tc && tc.response
|
||||
? tc.response
|
||||
: { callId: tc.request.callId, responseParts: [] };
|
||||
const responseSubmittedToGemini =
|
||||
'responseSubmittedToGemini' in tc
|
||||
? tc.responseSubmittedToGemini
|
||||
: false;
|
||||
|
||||
switch (status) {
|
||||
case CoreToolCallStatus.Executing:
|
||||
return {
|
||||
...tc,
|
||||
status: (event.data?.['status'] as any) ?? tc.status,
|
||||
liveOutput:
|
||||
event.displayContent?.[0]?.type === 'text'
|
||||
? event.displayContent[0].text
|
||||
: (tc as any).liveOutput,
|
||||
progressMessage:
|
||||
(event.data?.['progressMessage'] as string | undefined) ??
|
||||
(tc as any).progressMessage,
|
||||
progress:
|
||||
(event.data?.['progress'] as number | undefined) ??
|
||||
(tc as any).progress,
|
||||
progressTotal:
|
||||
(event.data?.['progressTotal'] as number | undefined) ??
|
||||
(tc as any).progressTotal,
|
||||
pid:
|
||||
(event.data?.['pid'] as number | undefined) ??
|
||||
(tc as any).pid,
|
||||
invocation: {
|
||||
getDescription: () =>
|
||||
(event._meta?.['description'] as string) ??
|
||||
(tc as any).invocation?.getDescription(),
|
||||
},
|
||||
} as unknown as TrackedToolCall)
|
||||
: tc,
|
||||
),
|
||||
...inProgressFields,
|
||||
status: CoreToolCallStatus.Executing,
|
||||
};
|
||||
case CoreToolCallStatus.Error:
|
||||
return {
|
||||
...tc,
|
||||
...inProgressFields,
|
||||
status: CoreToolCallStatus.Error,
|
||||
response,
|
||||
responseSubmittedToGemini,
|
||||
};
|
||||
case CoreToolCallStatus.Success:
|
||||
return {
|
||||
...tc,
|
||||
...inProgressFields,
|
||||
status: CoreToolCallStatus.Success,
|
||||
response,
|
||||
responseSubmittedToGemini,
|
||||
};
|
||||
case CoreToolCallStatus.Scheduled:
|
||||
return {
|
||||
...tc,
|
||||
...inProgressFields,
|
||||
status: CoreToolCallStatus.Scheduled,
|
||||
};
|
||||
case CoreToolCallStatus.Validating:
|
||||
return {
|
||||
...tc,
|
||||
...inProgressFields,
|
||||
status: CoreToolCallStatus.Validating,
|
||||
};
|
||||
case CoreToolCallStatus.AwaitingApproval:
|
||||
return {
|
||||
...tc,
|
||||
...inProgressFields,
|
||||
status: CoreToolCallStatus.AwaitingApproval,
|
||||
};
|
||||
case CoreToolCallStatus.Cancelled:
|
||||
return {
|
||||
...tc,
|
||||
...inProgressFields,
|
||||
status: CoreToolCallStatus.Cancelled,
|
||||
};
|
||||
default:
|
||||
return tc;
|
||||
}
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'tool_response':
|
||||
}
|
||||
case 'tool_response': {
|
||||
setTrackedTools((prev) =>
|
||||
prev.map((tc) =>
|
||||
tc.request.callId === event.requestId
|
||||
? ({
|
||||
...tc,
|
||||
status: event.isError ? 'error' : 'success',
|
||||
response: {
|
||||
resultDisplay:
|
||||
event._meta?.['resultDisplay'] ??
|
||||
(event.displayContent?.[0]?.type === 'text'
|
||||
? event.displayContent[0].text
|
||||
: undefined),
|
||||
outputFile: event._meta?.['outputFile'] as string | undefined,
|
||||
},
|
||||
responseSubmittedToGemini: true,
|
||||
} as unknown as TrackedToolCall)
|
||||
: tc,
|
||||
),
|
||||
prev.map((tc): TrackedToolCall => {
|
||||
if (tc.request.callId !== event.requestId) return tc;
|
||||
|
||||
const legacyState = event._meta?.legacyState;
|
||||
const outputFile = legacyState?.outputFile;
|
||||
const resultDisplay =
|
||||
event.displayContent?.[0]?.type === 'text'
|
||||
? event.displayContent[0].text
|
||||
: undefined;
|
||||
|
||||
const response = {
|
||||
callId: tc.request.callId,
|
||||
responseParts: [],
|
||||
resultDisplay,
|
||||
outputFile,
|
||||
...(event.isError
|
||||
? { error: 'Tool error', errorType: 'UNKNOWN' }
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (event.isError) {
|
||||
return {
|
||||
...tc,
|
||||
status: CoreToolCallStatus.Error,
|
||||
response,
|
||||
responseSubmittedToGemini: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...tc,
|
||||
status: CoreToolCallStatus.Success,
|
||||
response,
|
||||
responseSubmittedToGemini: true,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'error':
|
||||
addItem(
|
||||
{ type: MessageType.ERROR, text: event.message },
|
||||
@@ -294,7 +445,7 @@ export const useAgentStream = (
|
||||
break;
|
||||
}
|
||||
},
|
||||
[addItem, flushPendingText, pendingHistoryItemRef, setPendingHistoryItem],
|
||||
[addItem, flushPendingText, setPendingHistoryItem, setTrackedTools, config],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -316,7 +467,7 @@ export const useAgentStream = (
|
||||
|
||||
const submitQuery = useCallback(
|
||||
async (
|
||||
query: PartListUnion,
|
||||
query: Array<import('@google/gemini-cli-core').Part> | string,
|
||||
options?: { isContinuation: boolean },
|
||||
_prompt_id?: string,
|
||||
) => {
|
||||
@@ -335,7 +486,7 @@ export const useAgentStream = (
|
||||
}
|
||||
|
||||
const parts = geminiPartsToContentParts(
|
||||
typeof query === 'string' ? [{ text: query }] : (query as any[]),
|
||||
typeof query === 'string' ? [{ text: query }] : query,
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -399,13 +550,14 @@ export const useAgentStream = (
|
||||
}
|
||||
|
||||
const isLastInBatch =
|
||||
toolsToPush[toolsToPush.length - 1] === trackedTools[trackedTools.length - 1];
|
||||
toolsToPush[toolsToPush.length - 1] ===
|
||||
trackedTools[trackedTools.length - 1];
|
||||
|
||||
const historyItem = mapTrackedToolCallsToDisplay(toolsToPush, {
|
||||
borderTop: isFirstToolInGroupRef.current,
|
||||
borderBottom: isLastInBatch,
|
||||
...getToolGroupBorderAppearance(
|
||||
{ type: 'tool_group', tools: trackedTools as any[] },
|
||||
{ type: 'tool_group', tools: trackedTools },
|
||||
activePtyId,
|
||||
!!_isShellFocused,
|
||||
[],
|
||||
@@ -437,7 +589,7 @@ export const useAgentStream = (
|
||||
const items: HistoryItemWithoutId[] = [];
|
||||
|
||||
const appearance = getToolGroupBorderAppearance(
|
||||
{ type: 'tool_group', tools: trackedTools as any[] },
|
||||
{ type: 'tool_group', tools: trackedTools },
|
||||
activePtyId,
|
||||
!!_isShellFocused,
|
||||
[],
|
||||
@@ -488,17 +640,18 @@ export const useAgentStream = (
|
||||
}, [
|
||||
trackedTools,
|
||||
pushedToolCallIds,
|
||||
isFirstToolInGroupRef,
|
||||
activePtyId,
|
||||
_isShellFocused,
|
||||
backgroundShells,
|
||||
]);
|
||||
|
||||
const pendingHistoryItems = useMemo(() => {
|
||||
return [pendingHistoryItem, ...pendingToolGroupItems].filter(
|
||||
(i): i is HistoryItemWithoutId => i !== undefined && i !== null,
|
||||
);
|
||||
}, [pendingHistoryItem, pendingToolGroupItems]);
|
||||
const pendingHistoryItems = useMemo(
|
||||
() =>
|
||||
[pendingHistoryItem, ...pendingToolGroupItems].filter(
|
||||
(i): i is HistoryItemWithoutId => i !== undefined && i !== null,
|
||||
),
|
||||
[pendingHistoryItem, pendingToolGroupItems],
|
||||
);
|
||||
|
||||
return {
|
||||
streamingState,
|
||||
|
||||
@@ -31,9 +31,10 @@ export type CancelAllFn = (signal: AbortSignal) => void;
|
||||
* The shape expected by useGeminiStream.
|
||||
* It matches the Core ToolCall structure + the UI metadata flag.
|
||||
*/
|
||||
export type TrackedToolCall = ToolCall & {
|
||||
responseSubmittedToGemini?: boolean;
|
||||
};
|
||||
export type Tracked<T> = T extends unknown
|
||||
? T & { responseSubmittedToGemini?: boolean }
|
||||
: never;
|
||||
export type TrackedToolCall = Tracked<ToolCall>;
|
||||
|
||||
// Narrowed types for specific statuses (used by useGeminiStream)
|
||||
export type TrackedScheduledToolCall = Extract<
|
||||
|
||||
@@ -211,7 +211,9 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
this._emit(toolUpdates);
|
||||
};
|
||||
|
||||
this._config.getMessageBus().subscribe(MessageBusType.TOOL_CALLS_UPDATE, handleToolCallsUpdate);
|
||||
this._config
|
||||
.getMessageBus()
|
||||
.subscribe(MessageBusType.TOOL_CALLS_UPDATE, handleToolCallsUpdate);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
@@ -246,17 +248,23 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
toolCallRequests.push(event.value);
|
||||
}
|
||||
|
||||
const translatedEvents = translateEvent(event, this._translationState);
|
||||
const translatedEvents = translateEvent(
|
||||
event,
|
||||
this._translationState,
|
||||
);
|
||||
|
||||
for (const ev of translatedEvents) {
|
||||
if (ev.type === 'tool_request') {
|
||||
const tool = this._config.getToolRegistry().getTool(ev.name);
|
||||
const invocation = tool?.build(ev.args);
|
||||
ev._meta = {
|
||||
displayName: tool?.displayName ?? ev.name,
|
||||
description: invocation?.getDescription() ?? tool?.description ?? '',
|
||||
isOutputMarkdown: tool?.isOutputMarkdown ?? false,
|
||||
kind: tool?.kind,
|
||||
legacyState: {
|
||||
displayName: tool?.displayName ?? ev.name,
|
||||
description:
|
||||
invocation?.getDescription() ?? tool?.description ?? '',
|
||||
isOutputMarkdown: tool?.isOutputMarkdown ?? false,
|
||||
kind: tool?.kind,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -371,7 +379,9 @@ class LegacyAgentProtocol implements AgentProtocol {
|
||||
currentParts = toolResponseParts;
|
||||
}
|
||||
} finally {
|
||||
this._config.getMessageBus().unsubscribe(MessageBusType.TOOL_CALLS_UPDATE, handleToolCallsUpdate);
|
||||
this._config
|
||||
.getMessageBus()
|
||||
.unsubscribe(MessageBusType.TOOL_CALLS_UPDATE, handleToolCallsUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,16 @@ export interface ToolRequest {
|
||||
name: string;
|
||||
/** The arguments for the tool. */
|
||||
args: Record<string, unknown>;
|
||||
/** UI specific metadata */
|
||||
_meta?: {
|
||||
legacyState?: {
|
||||
displayName?: string;
|
||||
isOutputMarkdown?: boolean;
|
||||
description?: string;
|
||||
kind?: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +204,18 @@ export interface ToolUpdate {
|
||||
displayContent?: ContentPart[];
|
||||
content?: ContentPart[];
|
||||
data?: Record<string, unknown>;
|
||||
/** UI specific metadata */
|
||||
_meta?: {
|
||||
legacyState?: {
|
||||
status?: string;
|
||||
progressMessage?: string;
|
||||
progress?: number;
|
||||
progressTotal?: number;
|
||||
pid?: number;
|
||||
description?: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToolResponse {
|
||||
@@ -207,6 +229,13 @@ export interface ToolResponse {
|
||||
data?: Record<string, unknown>;
|
||||
/** When true, the tool call encountered an error that will be sent to the model. */
|
||||
isError?: boolean;
|
||||
/** UI specific metadata */
|
||||
_meta?: {
|
||||
legacyState?: {
|
||||
outputFile?: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export type ElicitationRequest = {
|
||||
|
||||
Reference in New Issue
Block a user