Compare commits

...

31 Commits

Author SHA1 Message Date
Michael Bleigh fbc03b00ad Merge branch 'main' into mb/atui/02-tool-state 2026-05-15 11:14:06 -07:00
Michael Bleigh 04386cd205 Merge branch 'mb/atui/02-tool-state' of github.com:google-gemini/gemini-cli into mb/atui/02-tool-state 2026-05-11 15:16:24 -07:00
Michael Bleigh b6cac32a71 fix: format 2026-05-11 15:16:09 -07:00
Michael Bleigh 5956e3c133 Merge branch 'main' into mb/atui/02-tool-state 2026-05-11 14:59:45 -07:00
Michael Bleigh aa8eca7bfe fix(agent): implement AgentProtocol disposal to prevent memory leaks 2026-05-11 14:56:10 -07:00
Michael Bleigh a7ae31d732 fix: fix type issues from upstream merge 2026-05-11 11:26:48 -07:00
Michael Bleigh d85721541a Merge branch 'main' into mb/atui/02-tool-state 2026-05-11 11:01:36 -07:00
Michael Bleigh 7887e38544 Merge branch 'origin/main' into mb/atui/02-tool-state 2026-05-07 10:38:50 -07:00
Michael Bleigh 93cafc2fce fix(core): use CoreToolCallStatus enum for status mapping in LegacyAgentSession 2026-05-07 10:35:51 -07:00
Michael Bleigh 48e9d80bdb fix(core): filter tool updates by callId to prevent session cross-talk 2026-05-06 12:12:06 -07:00
Michael Bleigh 4a4f54c20d test(core): harden messageBus subscription for legacy-agent-session tests 2026-05-05 12:07:37 -07:00
Michael Bleigh 005e0cfc53 feat(agent): formalize first-class tool lifecycle states and status mapping (#24993) 2026-05-05 11:45:12 -07:00
Michael Bleigh f5e2cf59fd merge: catch up mb/atui/01-ui-rendering with main and resolve conflicts 2026-05-05 10:29:20 -07:00
Michael Bleigh af5dfc445a feat(cli): refine tool display aesthetics for legacy UI parity 2026-04-14 00:06:44 -07:00
Michael Bleigh de9a98c6b5 fix(ui): flatten multiline summaries in compact ToolGroupDisplay and fix populateToolDisplay merge logic 2026-04-13 12:19:41 -07:00
Michael Bleigh ebae07558d Merge branch 'mb/atui/00-display-content' into mb/atui/01-ui-rendering 2026-04-13 12:09:48 -07:00
Michael Bleigh 9802cc7d60 Merge branch 'main' into mb/atui/00-display-content 2026-04-13 10:21:17 -07:00
Michael Bleigh e2b262181c revert(core): restore intentional ReadFolder display behavior (result: null) 2026-04-12 16:45:23 -07:00
Michael Bleigh 46377d2133 fix(core): restore ReadFolder declarative display and add missing test snapshots 2026-04-12 16:44:05 -07:00
Michael Bleigh 8548c6675f test(cli): add unit tests for ToolGroupDisplay and implement tool hiding 2026-04-12 16:40:48 -07:00
Michael Bleigh 9e03476e03 feat(cli): support 'notice' format and refine declarative tool rendering
Key enhancements:
- Updated `UpdateTopicTool` to provide declarative 'notice' display info, using dynamic
  descriptions for high-fidelity output.
- Refined `ToolGroupDisplay` to 'hoist' notice-format tools to the top of the group.
- Implemented conditional boxing in `ToolGroupDisplay`: borders are now suppressed in
  compact mode, matching the standard CLI view.
- Added support for `resultSummary` rendering at the bottom of text results in boxed mode.
- Improved `useAgentStream` to wait for turn completion before pushing tools to history,
  ensuring all notices for a turn are correctly grouped and hoisted together.
- Fixed margin and border logic to handle seamless transitions between notices and tool boxes.
2026-04-12 16:20:27 -07:00
Michael Bleigh 45eababfd8 feat(cli): refactor tool rendering to declarative ToolDisplay system
This change completes the transition of the interactive agent session (`useAgentStream`)
to a declarative-first tool rendering system.

Key changes:
- Reverted experimental `ToolDisplay` logic from legacy UI components (`DenseToolMessage`, etc.)
  to establish a clean baseline.
- Introduced `HistoryItemToolDisplayGroup` and `ToolGroupDisplay` component in CLI.
- Added `display` property to `ToolCallRequestInfo` to carry declarative UI info natively.
- Populated tool request display information at the source (`Turn.ts` and `Scheduler.ts`)
  using dynamic descriptions from tool invocations.
- Updated `useAgentStream` to emit the new history item type, providing a standalone
  rendering path for interactive sessions.
- Ensured tool descriptions are updated when arguments are modified during confirmation.
2026-04-12 11:46:18 -07:00
Michael Bleigh 410e675837 revert: remove invasive ToolDisplay logic from legacy UI components 2026-04-11 19:04:36 -07:00
Michael Bleigh 383cb7d795 wip: HistoryItemToolGroupDisplay 2026-04-11 19:01:57 -07:00
Michael Bleigh 88bebef3a9 fix(ui): hide summary in header when displayed in box 2026-04-11 09:11:03 -07:00
Michael Bleigh 43f93c3cde fix(ui): resolve rebase conflicts and type errors for ToolDisplay 2026-04-10 20:18:51 -07:00
Michael Bleigh fbc87675f0 refactor(cli): consume simplified ToolDisplay property 2026-04-10 20:08:59 -07:00
Michael Bleigh da2fb08069 Merge branch 'main' into mb/atui/00-display-content 2026-04-10 14:03:21 -07:00
Michael Bleigh cbff79328f Merge branch 'main' into mb/atui/00-display-content 2026-04-10 11:44:16 -07:00
Michael Bleigh fae963ff0e fix(core,cli): handle structured tool display properly and prevent metadata overwrite
This addresses PR feedback by:
- Creating a `renderDisplayDiff` utility to render `DisplayDiff` objects.
- Creating a `displayContentToString` utility to safely extract text from any `DisplayContent`.
- Updating non-interactive CLI to use `displayContentToString` to avoid data loss on non-text output.
- Updating `useAgentStream` to use `displayContentToString` to avoid stale UI state for non-text output.
- Shallow merging the `display` object in `useAgentStream` rather than replacing it, preventing loss of display metadata.
2026-04-10 11:42:37 -07:00
Michael Bleigh 7f7e69e6c6 feat(agent): implement tool-controlled display protocol (Steps 2-3) 2026-04-10 11:00:49 -07:00
9 changed files with 165 additions and 12 deletions
@@ -193,6 +193,7 @@ export async function runNonInteractive({
let errorToHandle: unknown | undefined;
let scheduler: Scheduler | undefined;
let session: LegacyAgentSession | undefined;
let abortSession = () => {};
try {
consolePatcher.patch();
@@ -296,7 +297,7 @@ export async function runNonInteractive({
}
// Create LegacyAgentSession — owns the agentic loop
const session = new LegacyAgentSession({
session = new LegacyAgentSession({
client: geminiClient,
scheduler,
config,
@@ -305,7 +306,7 @@ export async function runNonInteractive({
// Wire Ctrl+C to session abort
abortSession = () => {
void session.abort();
void session?.abort();
};
abortController.signal.addEventListener('abort', abortSession);
if (abortController.signal.aborted) {
@@ -640,6 +641,7 @@ export async function runNonInteractive({
cleanupStdinCancellation();
abortController.signal.removeEventListener('abort', abortSession);
session?.dispose();
scheduler?.dispose();
consolePatcher.cleanup();
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
+7
View File
@@ -1180,6 +1180,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
[config, getPreferredEditor],
);
useEffect(
() => () => {
streamAgent?.dispose?.();
},
[streamAgent],
);
const activeStream = streamAgent
? // eslint-disable-next-line react-hooks/rules-of-hooks
useAgentStream({
+16 -8
View File
@@ -224,14 +224,17 @@ export const useAgentStream = ({
if (tc.callId !== event.requestId) return tc;
const legacyState = event._meta?.legacyState;
const evtStatus = legacyState?.status;
let status = tc.status;
if (evtStatus === 'executing')
if (event.status === 'executing')
status = CoreToolCallStatus.Executing;
else if (evtStatus === 'error') status = CoreToolCallStatus.Error;
else if (evtStatus === 'success')
else if (event.status === 'pending_input')
status = CoreToolCallStatus.AwaitingApproval;
else if (event.status === 'errored')
status = CoreToolCallStatus.Error;
else if (event.status === 'succeeded')
status = CoreToolCallStatus.Success;
else if (event.status === 'aborted')
status = CoreToolCallStatus.Cancelled;
const display = event.display?.result;
const liveOutput =
@@ -272,11 +275,16 @@ export const useAgentStream = ({
const resultDisplay =
displayContentToString(display) ?? tc.resultDisplay;
let status = CoreToolCallStatus.Success;
if (event.status === 'errored') status = CoreToolCallStatus.Error;
else if (event.status === 'aborted')
status = CoreToolCallStatus.Cancelled;
else if (event.status === 'succeeded')
status = CoreToolCallStatus.Success;
return {
...tc,
status: event.isError
? CoreToolCallStatus.Error
: CoreToolCallStatus.Success,
status,
display: event.display
? { ...tc.display, ...event.display }
: tc.display,
+4
View File
@@ -34,6 +34,10 @@ export class AgentSession implements AgentProtocol {
return this._protocol.abort();
}
dispose(): void {
this._protocol.dispose?.();
}
get events(): readonly AgentEvent[] {
return this._protocol.events;
}
@@ -235,6 +235,7 @@ export function translateEvent(
makeEvent('tool_request', state, {
requestId: event.value.callId,
name: event.value.name,
status: 'pending',
args: event.value.args,
display: event.value.display,
}),
@@ -257,6 +258,7 @@ export function translateEvent(
makeEvent('tool_response', state, {
requestId: event.value.callId,
name: state.pendingToolNames.get(event.value.callId) ?? 'unknown',
status: event.value.error ? 'errored' : 'succeeded',
content: event.value.error
? [{ type: 'text', text: event.value.error.message }]
: geminiPartsToContentParts(event.value.responseParts),
+110 -1
View File
@@ -13,7 +13,10 @@ import { GeminiEventType } from '../core/turn.js';
import type { Part } from '@google/genai';
import type { GeminiClient } from '../core/client.js';
import type { Config } from '../config/config.js';
import type { ToolCallRequestInfo } from '../scheduler/types.js';
import {
type ToolCallRequestInfo,
CoreToolCallStatus,
} from '../scheduler/types.js';
import { Scheduler } from '../scheduler/scheduler.js';
import { recordToolCallInteractions } from '../code_assist/telemetry.js';
import { ToolErrorType, isFatalToolError } from '../tools/tool-error.js';
@@ -39,7 +42,12 @@ import type {
ContentPart,
StreamEndReason,
Unsubscribe,
ToolEventStatus,
} from './types.js';
import {
MessageBusType,
type ToolCallsUpdateMessage,
} from '../confirmation-bus/types.js';
function isAbortLikeError(err: unknown): boolean {
return err instanceof Error && err.name === 'AbortError';
@@ -63,7 +71,9 @@ export class LegacyAgentProtocol implements AgentProtocol {
private _agentEndEmitted = false;
private _activeStreamId?: string;
private _abortController = new AbortController();
private _disposalController = new AbortController();
private _nextStreamIdOverride?: string;
private _lastToolStatuses = new Map<string, ToolEventStatus>();
private readonly _client: GeminiClient;
private readonly _scheduler: Scheduler;
@@ -92,6 +102,19 @@ export class LegacyAgentProtocol implements AgentProtocol {
}
this._scheduler = scheduler;
}
if (this._config.messageBus) {
this._config.messageBus.subscribe(
MessageBusType.TOOL_CALLS_UPDATE,
this._handleToolCallsUpdate.bind(this),
{ signal: this._disposalController.signal },
);
}
}
dispose(): void {
this._disposalController.abort();
void this.abort();
}
get events(): readonly AgentEvent[] {
@@ -275,6 +298,11 @@ export class LegacyAgentProtocol implements AgentProtocol {
this._makeToolResponseEvent({
requestId: request.callId,
name: request.name,
status: response.error
? 'errored'
: tc.status === CoreToolCallStatus.Cancelled
? 'aborted'
: 'succeeded',
content,
isError: response.error !== undefined,
...(display ? { display } : {}),
@@ -489,6 +517,87 @@ export class LegacyAgentProtocol implements AgentProtocol {
} satisfies AgentEvent<'error'>;
return event;
}
private _handleToolCallsUpdate(msg: ToolCallsUpdateMessage): void {
if (!this._activeStreamId) {
return;
}
const eventsToEmit: AgentEvent[] = [];
for (const tc of msg.toolCalls) {
const callId = tc.request.callId;
if (!this._translationState.pendingToolNames.has(callId)) {
continue;
}
let status: ToolEventStatus = 'pending';
switch (tc.status) {
case CoreToolCallStatus.Validating:
case CoreToolCallStatus.Scheduled:
status = 'pending';
break;
case CoreToolCallStatus.AwaitingApproval:
status = 'pending_input';
break;
case CoreToolCallStatus.Executing:
status = 'executing';
break;
case CoreToolCallStatus.Success:
status = 'succeeded';
break;
case CoreToolCallStatus.Error:
status = 'errored';
break;
case CoreToolCallStatus.Cancelled:
status = 'aborted';
break;
default:
status = 'pending';
break;
}
const lastStatus = this._lastToolStatuses.get(callId);
if (lastStatus !== status) {
this._lastToolStatuses.set(callId, status);
const display = populateToolDisplay({
name: tc.request.name,
invocation: 'invocation' in tc ? tc.invocation : undefined,
displayName: 'tool' in tc ? tc.tool?.displayName : undefined,
display: 'response' in tc ? tc.response?.display : undefined,
});
eventsToEmit.push(
this._makeToolUpdateEvent({
requestId: callId,
status,
...(display ? { display } : {}),
}),
);
}
}
if (eventsToEmit.length > 0) {
this._emit(eventsToEmit);
}
}
private _makeToolUpdateEvent(
payload: Omit<
AgentEvent<'tool_update'>,
'id' | 'timestamp' | 'streamId' | 'type'
>,
): AgentEvent<'tool_update'> {
const event = {
...this._nextEventFields(),
type: 'tool_update',
...payload,
} satisfies AgentEvent<'tool_update'>;
return event;
}
}
export class LegacyAgentSession extends AgentSession {
+19
View File
@@ -37,6 +37,11 @@ export interface AgentProtocol extends Trajectory {
*/
abort(): Promise<void>;
/**
* Disposes of the protocol, cleaning up any long-lived resources.
*/
dispose?(): void;
/**
* AgentProtocol implements the Trajectory interface and can retrieve existing events.
*/
@@ -227,11 +232,21 @@ export interface ToolDisplay {
format?: ToolDisplayFormat;
}
export type ToolEventStatus =
| 'pending'
| 'pending_input'
| 'executing'
| 'succeeded'
| 'errored'
| 'aborted';
export interface ToolRequest {
/** A unique identifier for this tool request to be correlated by the response. */
requestId: string;
/** The name of the tool being requested. */
name: string;
/** The status of the tool execution. */
status: ToolEventStatus;
/** The arguments for the tool. */
/** Tool-controlled display information. */
display?: ToolDisplay;
@@ -255,6 +270,8 @@ export interface ToolRequest {
*/
export interface ToolUpdate {
requestId: string;
/** The status of the tool execution. */
status: ToolEventStatus;
/** Tool-controlled display information. */
display?: ToolDisplay;
content?: ContentPart[];
@@ -276,6 +293,8 @@ export interface ToolUpdate {
export interface ToolResponse {
requestId: string;
name: string;
/** The status of the tool execution. */
status: ToolEventStatus;
/** Tool-controlled display information. */
display?: ToolDisplay;
/** Multi-part content to be sent to the model. */
@@ -277,6 +277,7 @@ class LocalSubagentProtocol implements AgentProtocol {
this._makeEvent('tool_request', {
requestId: callId,
name,
status: 'executing',
args,
}),
];
@@ -292,6 +293,7 @@ class LocalSubagentProtocol implements AgentProtocol {
this._makeEvent('tool_response', {
requestId,
name,
status: 'succeeded',
content: [{ type: 'text', text: output }],
}),
];
+1 -1
View File
@@ -1036,7 +1036,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
return {
llmContent,
display: {
name: 'Shell',
name: this._toolDisplayName,
description: this.getDescription(),
resultSummary: displayResultSummary,
result: