mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-08 09:06:48 -07:00
wip: fork agent harness UI and fix engine delegation loop
This commit is contained in:
@@ -95,6 +95,7 @@ import { computeTerminalTitle } from '../utils/windowTitle.js';
|
||||
import { useTextBuffer } from './components/shared/text-buffer.js';
|
||||
import { useLogger } from './hooks/useLogger.js';
|
||||
import { useGeminiStream } from './hooks/useGeminiStream.js';
|
||||
import { useAgentHarness } from './hooks/useAgentHarness.js';
|
||||
import { type BackgroundShell } from './hooks/shellCommandProcessor.js';
|
||||
import { useVim } from './hooks/vim.js';
|
||||
import { type LoadableSettingScope, SettingScope } from '../config/settings.js';
|
||||
@@ -966,26 +967,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
}, [pendingRestorePrompt, inputHistory, historyManager.history]);
|
||||
|
||||
const {
|
||||
streamingState,
|
||||
submitQuery,
|
||||
initError,
|
||||
pendingHistoryItems: pendingGeminiHistoryItems,
|
||||
thought,
|
||||
cancelOngoingRequest,
|
||||
pendingToolCalls,
|
||||
handleApprovalModeChange,
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
toggleBackgroundShell,
|
||||
backgroundCurrentShell,
|
||||
backgroundShells,
|
||||
dismissBackgroundShell,
|
||||
retryStatus,
|
||||
} = useGeminiStream(
|
||||
const isAgentHarnessEnabled = config.isAgentHarnessEnabled();
|
||||
|
||||
const legacyStream = useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
historyManager.history,
|
||||
historyManager.addItem,
|
||||
@@ -1006,6 +990,37 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
embeddedShellFocused,
|
||||
);
|
||||
|
||||
const harnessStream = useAgentHarness(
|
||||
historyManager.addItem,
|
||||
config,
|
||||
onCancelSubmit,
|
||||
);
|
||||
|
||||
const activeStream = isAgentHarnessEnabled ? harnessStream : legacyStream;
|
||||
|
||||
const {
|
||||
streamingState,
|
||||
submitQuery,
|
||||
initError,
|
||||
pendingHistoryItems: pendingGeminiHistoryItems,
|
||||
thought,
|
||||
cancelOngoingRequest,
|
||||
toolCalls: pendingToolCalls,
|
||||
handleApprovalModeChange,
|
||||
activePtyId: rawActivePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
backgroundShellCount,
|
||||
isBackgroundShellVisible,
|
||||
toggleBackgroundShell,
|
||||
backgroundCurrentShell,
|
||||
backgroundShells,
|
||||
dismissBackgroundShell,
|
||||
retryStatus,
|
||||
} = activeStream;
|
||||
|
||||
const activePtyId = rawActivePtyId ?? undefined;
|
||||
|
||||
toggleBackgroundShellRef.current = toggleBackgroundShell;
|
||||
isBackgroundShellVisibleRef.current = isBackgroundShellVisible;
|
||||
backgroundShellsRef.current = backgroundShells;
|
||||
|
||||
@@ -110,7 +110,8 @@ export const Notifications = () => {
|
||||
marginBottom={1}
|
||||
>
|
||||
<Text color={theme.status.error}>
|
||||
Initialization Error: {initError}
|
||||
Initialization Error:{' '}
|
||||
{initError instanceof Error ? initError.message : initError}
|
||||
</Text>
|
||||
<Text color={theme.status.error}>
|
||||
{' '}
|
||||
|
||||
@@ -98,7 +98,7 @@ export interface UIState {
|
||||
permissionConfirmationRequest: PermissionConfirmationRequest | null;
|
||||
geminiMdFileCount: number;
|
||||
streamingState: StreamingState;
|
||||
initError: string | null;
|
||||
initError: string | Error | null;
|
||||
pendingGeminiHistoryItems: HistoryItemWithoutId[];
|
||||
thought: ThoughtSummary | null;
|
||||
shellModeActive: boolean;
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
GeminiEventType as ServerGeminiEventType,
|
||||
debugLogger,
|
||||
AgentFactory,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Config,
|
||||
ServerGeminiStreamEvent as GeminiEvent,
|
||||
ThoughtSummary,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type PartListUnion, type Part } from '@google/genai';
|
||||
import {
|
||||
StreamingState,
|
||||
MessageType,
|
||||
type HistoryItemWithoutId,
|
||||
} from '../types.js';
|
||||
import { useStateAndRef } from './useStateAndRef.js';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { mapToDisplay as mapTrackedToolCallsToDisplay } from './toolMapping.js';
|
||||
import { ROOT_SCHEDULER_ID } from '@google/gemini-cli-core';
|
||||
import type { TrackedToolCall } from './useToolScheduler.js';
|
||||
|
||||
export interface UseAgentHarnessReturn {
|
||||
streamingState: StreamingState;
|
||||
isResponding: boolean;
|
||||
thought: ThoughtSummary | null;
|
||||
streamingContent: string;
|
||||
toolCalls: TrackedToolCall[];
|
||||
submitQuery: (query: PartListUnion) => Promise<void>;
|
||||
cancelOngoingRequest: () => void;
|
||||
reset: () => void;
|
||||
// Legacy compatibility properties
|
||||
initError: Error | null;
|
||||
pendingHistoryItems: HistoryItemWithoutId[];
|
||||
handleApprovalModeChange: (mode: string) => void;
|
||||
activePtyId: number | null;
|
||||
loopDetectionConfirmationRequest: unknown | null;
|
||||
lastOutputTime: number;
|
||||
backgroundShellCount: number;
|
||||
isBackgroundShellVisible: boolean;
|
||||
toggleBackgroundShell: () => void;
|
||||
backgroundCurrentShell: unknown | null;
|
||||
backgroundShells: Map<number, unknown>;
|
||||
dismissBackgroundShell: (pid: number) => void;
|
||||
retryStatus: unknown | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A specialized hook for processing streams from the AgentHarness.
|
||||
* COMPLETELY FORKED from useGeminiStream to ensure zero regressions in legacy mode.
|
||||
*/
|
||||
export const useAgentHarness = (
|
||||
addItem: UseHistoryManagerReturn['addItem'],
|
||||
config: Config,
|
||||
onCancelSubmit: (fullReset: boolean) => void,
|
||||
): UseAgentHarnessReturn => {
|
||||
const [streamingState, setStreamingState] = useState<StreamingState>(
|
||||
StreamingState.Idle,
|
||||
);
|
||||
const [streamingContent, setStreamingContent] = useState('');
|
||||
const [thought, thoughtRef, setThought] = useStateAndRef<ThoughtSummary | null>(null);
|
||||
|
||||
// Tools for the CURRENT turn of the main agent
|
||||
const [toolCalls, setToolCalls] = useState<TrackedToolCall[]>([]);
|
||||
const toolCallsRef = useRef<TrackedToolCall[]>([]);
|
||||
|
||||
// Sync ref with state
|
||||
useEffect(() => {
|
||||
toolCallsRef.current = toolCalls;
|
||||
}, [toolCalls]);
|
||||
|
||||
const pushedToolCallIdsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
// Track subagent activities for hierarchical display
|
||||
// For now, we just log them. In future, we can add a specialized "SubagentBlock" history item.
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setStreamingState(StreamingState.Idle);
|
||||
setStreamingContent('');
|
||||
setThought(null);
|
||||
setToolCalls([]);
|
||||
pushedToolCallIdsRef.current.clear();
|
||||
}, [setThought]);
|
||||
|
||||
const cancelOngoingRequest = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
onCancelSubmit(true);
|
||||
reset();
|
||||
}, [onCancelSubmit, reset]);
|
||||
|
||||
const processEvent = useCallback(
|
||||
(event: GeminiEvent) => {
|
||||
switch (event.type) {
|
||||
case ServerGeminiEventType.Content:
|
||||
setStreamingState(StreamingState.Responding);
|
||||
setStreamingContent((prev) => prev + (event.value || ''));
|
||||
break;
|
||||
|
||||
case ServerGeminiEventType.Thought:
|
||||
setThought(event.value);
|
||||
break;
|
||||
|
||||
case ServerGeminiEventType.ToolCallRequest:
|
||||
setToolCalls((prev) => [
|
||||
...prev,
|
||||
{
|
||||
request: event.value,
|
||||
status: 'validating',
|
||||
schedulerId: event.value.schedulerId || ROOT_SCHEDULER_ID,
|
||||
} as TrackedToolCall,
|
||||
]);
|
||||
break;
|
||||
|
||||
case ServerGeminiEventType.ToolCallResponse:
|
||||
{
|
||||
const response = event.value;
|
||||
setToolCalls((prev) =>
|
||||
prev.map((tc) =>
|
||||
tc.request.callId === response.callId
|
||||
? ({
|
||||
...tc,
|
||||
status: 'success',
|
||||
result: response,
|
||||
} as TrackedToolCall)
|
||||
: tc,
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case ServerGeminiEventType.TurnFinished:
|
||||
// MAIN AGENT turn finished. Flush current state to history.
|
||||
if (thoughtRef.current) {
|
||||
addItem({
|
||||
type: MessageType.THINKING,
|
||||
thought: thoughtRef.current,
|
||||
} as HistoryItemWithoutId);
|
||||
setThought(null);
|
||||
}
|
||||
|
||||
if (toolCallsRef.current.length > 0) {
|
||||
const unpushed = toolCallsRef.current.filter(
|
||||
(tc) => !pushedToolCallIdsRef.current.has(tc.request.callId),
|
||||
);
|
||||
if (unpushed.length > 0) {
|
||||
addItem(
|
||||
mapTrackedToolCallsToDisplay(unpushed as TrackedToolCall[], {
|
||||
borderBottom: true,
|
||||
}),
|
||||
);
|
||||
unpushed.forEach((tc) =>
|
||||
pushedToolCallIdsRef.current.add(tc.request.callId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingContent) {
|
||||
addItem({ type: 'gemini', text: streamingContent });
|
||||
setStreamingContent('');
|
||||
}
|
||||
|
||||
setToolCalls([]);
|
||||
break;
|
||||
|
||||
case ServerGeminiEventType.SubagentActivity:
|
||||
debugLogger.debug(`[HarnessHook] Subagent activity: ${event.value.type} for ${event.value.agentName}`);
|
||||
break;
|
||||
|
||||
case ServerGeminiEventType.Finished:
|
||||
setStreamingState(StreamingState.Idle);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[addItem, setThought, thoughtRef, streamingContent],
|
||||
);
|
||||
|
||||
const submitQuery = useCallback(
|
||||
async (parts: PartListUnion) => {
|
||||
reset();
|
||||
setStreamingState(StreamingState.Responding);
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
const harness = AgentFactory.createHarness(config);
|
||||
|
||||
// Convert parts to Part[] array for harness
|
||||
const requestParts: Part[] = Array.isArray(parts)
|
||||
? (parts as Part[])
|
||||
: [{ text: String(parts) }];
|
||||
|
||||
const stream = harness.run(
|
||||
requestParts,
|
||||
abortControllerRef.current.signal,
|
||||
);
|
||||
|
||||
try {
|
||||
for await (const event of stream) {
|
||||
processEvent(event);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'AbortError') return;
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
addItem({ type: MessageType.ERROR, text: msg });
|
||||
} finally {
|
||||
setStreamingState(StreamingState.Idle);
|
||||
}
|
||||
},
|
||||
[config, reset, processEvent, addItem],
|
||||
);
|
||||
|
||||
return {
|
||||
streamingState,
|
||||
isResponding: streamingState !== StreamingState.Idle,
|
||||
thought,
|
||||
streamingContent,
|
||||
toolCalls,
|
||||
submitQuery,
|
||||
cancelOngoingRequest,
|
||||
reset,
|
||||
initError: null,
|
||||
pendingHistoryItems: [],
|
||||
handleApprovalModeChange: () => {},
|
||||
activePtyId: null,
|
||||
loopDetectionConfirmationRequest: null,
|
||||
lastOutputTime: 0,
|
||||
backgroundShellCount: 0,
|
||||
isBackgroundShellVisible: false,
|
||||
toggleBackgroundShell: () => {},
|
||||
backgroundCurrentShell: null,
|
||||
backgroundShells: new Map(),
|
||||
dismissBackgroundShell: () => {},
|
||||
retryStatus: null,
|
||||
};
|
||||
};
|
||||
@@ -1171,6 +1171,9 @@ export const useGeminiStream = (
|
||||
case ServerGeminiEventType.SubagentActivity:
|
||||
// TODO: UI implementation for subagent activity
|
||||
break;
|
||||
case ServerGeminiEventType.TurnFinished:
|
||||
// No-op for now to satisfy exhaustive switch
|
||||
break;
|
||||
default: {
|
||||
// enforces exhaustive switch-case
|
||||
const unreachable: never = event;
|
||||
@@ -1680,7 +1683,7 @@ export const useGeminiStream = (
|
||||
pendingHistoryItems,
|
||||
thought,
|
||||
cancelOngoingRequest,
|
||||
pendingToolCalls: toolCalls,
|
||||
toolCalls,
|
||||
handleApprovalModeChange,
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
|
||||
@@ -373,6 +373,7 @@ export enum MessageType {
|
||||
AGENTS_LIST = 'agents_list',
|
||||
MCP_STATUS = 'mcp_status',
|
||||
CHAT_LIST = 'chat_list',
|
||||
THINKING = 'thinking',
|
||||
HOOKS_LIST = 'hooks_list',
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user