mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-13 07:30:52 -07:00
feat: support structured tool results and preserve terminal state context
Establishes core data contracts and state management for enhanced tool outputs. - Introduces GrepResult, ListDirectoryResult, and ReadManyFilesResult types. - Updates tools to return structured data instead of flat strings. - Preserves confirmation details and diff stats in terminal tool states. - Ensures backward compatibility in standard CLI views via safe type guards.
This commit is contained in:
@@ -82,10 +82,26 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
);
|
||||
|
||||
const truncatedResultDisplay = React.useMemo(() => {
|
||||
// Helper to handle structured objects that should be displayed as strings
|
||||
// in the main view (grep, ls, read-many-files).
|
||||
let displayContent: string | object | undefined = resultDisplay;
|
||||
|
||||
if (typeof resultDisplay === 'object' && resultDisplay !== null) {
|
||||
// Use a type-safe check for structured results that contain a summary
|
||||
// (e.g. GrepResult, ListDirectoryResult, ReadManyFilesResult)
|
||||
const hasSummary = (obj: object): obj is { summary: string } =>
|
||||
'summary' in obj &&
|
||||
typeof (obj as { summary?: unknown }).summary === 'string';
|
||||
|
||||
if (hasSummary(resultDisplay)) {
|
||||
displayContent = resultDisplay.summary;
|
||||
}
|
||||
}
|
||||
|
||||
// Only truncate string output if not in alternate buffer mode to ensure
|
||||
// we can scroll through the full output.
|
||||
if (typeof resultDisplay === 'string' && !isAlternateBuffer) {
|
||||
let text = resultDisplay;
|
||||
if (typeof displayContent === 'string' && !isAlternateBuffer) {
|
||||
let text = displayContent;
|
||||
if (text.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
||||
text = '...' + text.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
||||
}
|
||||
@@ -101,7 +117,7 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return resultDisplay;
|
||||
return displayContent;
|
||||
}, [resultDisplay, isAlternateBuffer, maxLines]);
|
||||
|
||||
if (!truncatedResultDisplay) return null;
|
||||
|
||||
@@ -15,11 +15,23 @@ import type {
|
||||
RetrieveUserQuotaResponse,
|
||||
SkillDefinition,
|
||||
AgentDefinition,
|
||||
GrepResult,
|
||||
ListDirectoryResult,
|
||||
ReadManyFilesResult,
|
||||
FileDiff,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { PartListUnion } from '@google/genai';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
export type { ThoughtSummary, SkillDefinition, ToolResultDisplay };
|
||||
export type {
|
||||
ThoughtSummary,
|
||||
SkillDefinition,
|
||||
ToolResultDisplay,
|
||||
GrepResult,
|
||||
ListDirectoryResult,
|
||||
ReadManyFilesResult,
|
||||
FileDiff,
|
||||
};
|
||||
|
||||
export enum AuthState {
|
||||
// Attempting to authenticate or re-authenticate
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { type FunctionCall } from '@google/genai';
|
||||
import type {
|
||||
DiffStat,
|
||||
ToolConfirmationOutcome,
|
||||
ToolConfirmationPayload,
|
||||
} from '../tools/tools.js';
|
||||
@@ -79,6 +80,7 @@ export type SerializableConfirmationDetails =
|
||||
fileDiff: string;
|
||||
originalContent: string | null;
|
||||
newContent: string;
|
||||
diffStat?: DiffStat;
|
||||
isModifying?: boolean;
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -346,6 +346,8 @@ export class SchedulerStateManager {
|
||||
response: ToolCallResponseInfo,
|
||||
): ErroredToolCall {
|
||||
const startTime = 'startTime' in call ? call.startTime : undefined;
|
||||
const confirmationDetails =
|
||||
'confirmationDetails' in call ? call.confirmationDetails : undefined;
|
||||
return {
|
||||
request: call.request,
|
||||
status: 'error',
|
||||
@@ -353,6 +355,7 @@ export class SchedulerStateManager {
|
||||
response,
|
||||
durationMs: startTime ? Date.now() - startTime : undefined,
|
||||
outcome: call.outcome,
|
||||
confirmationDetails,
|
||||
schedulerId: call.schedulerId,
|
||||
};
|
||||
}
|
||||
@@ -415,6 +418,8 @@ export class SchedulerStateManager {
|
||||
private toCancelled(call: ToolCall, reason: string): CancelledToolCall {
|
||||
this.validateHasToolAndInvocation(call, 'cancelled');
|
||||
const startTime = 'startTime' in call ? call.startTime : undefined;
|
||||
const confirmationDetails =
|
||||
'confirmationDetails' in call ? call.confirmationDetails : undefined;
|
||||
|
||||
// TODO: Refactor this tool-specific logic into the confirmation details payload.
|
||||
// See: https://github.com/google-gemini/gemini-cli/issues/16716
|
||||
@@ -463,6 +468,7 @@ export class SchedulerStateManager {
|
||||
},
|
||||
durationMs: startTime ? Date.now() - startTime : undefined,
|
||||
outcome: call.outcome,
|
||||
confirmationDetails,
|
||||
schedulerId: call.schedulerId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ export type ErroredToolCall = {
|
||||
tool?: AnyDeclarativeTool;
|
||||
durationMs?: number;
|
||||
outcome?: ToolConfirmationOutcome;
|
||||
confirmationDetails?:
|
||||
| ToolCallConfirmationDetails
|
||||
| SerializableConfirmationDetails;
|
||||
schedulerId?: string;
|
||||
};
|
||||
|
||||
@@ -105,6 +108,9 @@ export type CancelledToolCall = {
|
||||
invocation: AnyToolInvocation;
|
||||
durationMs?: number;
|
||||
outcome?: ToolConfirmationOutcome;
|
||||
confirmationDetails?:
|
||||
| ToolCallConfirmationDetails
|
||||
| SerializableConfirmationDetails;
|
||||
schedulerId?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -687,6 +687,14 @@ class EditToolInvocation
|
||||
'Proposed',
|
||||
DEFAULT_DIFF_OPTIONS,
|
||||
);
|
||||
|
||||
const diffStat = getDiffStat(
|
||||
fileName,
|
||||
editData.currentContent ?? '',
|
||||
editData.newContent,
|
||||
this.params.new_string,
|
||||
);
|
||||
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
const ideConfirmation =
|
||||
this.config.getIdeMode() && ideClient.isDiffingEnabled()
|
||||
@@ -701,6 +709,7 @@ class EditToolInvocation
|
||||
fileDiff,
|
||||
originalContent: editData.currentContent,
|
||||
newContent: editData.newContent,
|
||||
diffStat,
|
||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||
// No need to publish a policy update as the default policy for
|
||||
|
||||
@@ -286,7 +286,14 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
|
||||
return {
|
||||
llmContent: llmContent.trim(),
|
||||
returnDisplay: `Found ${matchCount} ${matchTerm}${wasTruncated ? ' (limited)' : ''}`,
|
||||
returnDisplay: {
|
||||
summary: `Found ${matchCount} ${matchTerm}${wasTruncated ? ' (limited)' : ''}`,
|
||||
matches: allMatches.map((m) => ({
|
||||
filePath: m.filePath,
|
||||
lineNumber: m.lineNumber,
|
||||
line: m.line,
|
||||
})),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
debugLogger.warn(`Error during GrepLogic execution: ${error}`);
|
||||
|
||||
@@ -254,7 +254,12 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
|
||||
|
||||
return {
|
||||
llmContent: resultMessage,
|
||||
returnDisplay: displayMessage,
|
||||
returnDisplay: {
|
||||
summary: displayMessage,
|
||||
files: entries.map(
|
||||
(entry) => `${entry.isDirectory ? '[DIR] ' : ''}${entry.name}`,
|
||||
),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMsg = `Error listing directory: ${error instanceof Error ? error.message : String(error)}`;
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
*/
|
||||
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import type { ToolInvocation, ToolResult } from './tools.js';
|
||||
import type {
|
||||
ToolInvocation,
|
||||
ToolResult,
|
||||
ReadManyFilesResult,
|
||||
} from './tools.js';
|
||||
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
|
||||
import { getErrorMessage } from '../utils/errors.js';
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
@@ -441,9 +445,22 @@ ${finalExclusionPatternsForDescription
|
||||
'No files matching the criteria were found or all were skipped.',
|
||||
);
|
||||
}
|
||||
|
||||
const returnDisplay: ReadManyFilesResult = {
|
||||
summary:
|
||||
processedFilesRelativePaths.length > 0
|
||||
? `Read ${processedFilesRelativePaths.length} file(s)`
|
||||
: 'No files read',
|
||||
files: processedFilesRelativePaths,
|
||||
skipped: skippedFiles,
|
||||
include: this.params.include,
|
||||
excludes: effectiveExcludes,
|
||||
targetDir: this.config.getTargetDir(),
|
||||
};
|
||||
|
||||
return {
|
||||
llmContent: contentParts,
|
||||
returnDisplay: displayMessage.trim(),
|
||||
returnDisplay,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,7 +664,40 @@ export interface TodoList {
|
||||
todos: Todo[];
|
||||
}
|
||||
|
||||
export type ToolResultDisplay = string | FileDiff | AnsiOutput | TodoList;
|
||||
export interface GrepResult {
|
||||
summary: string;
|
||||
matches: Array<{
|
||||
filePath: string;
|
||||
lineNumber: number;
|
||||
line: string;
|
||||
}>;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface ListDirectoryResult {
|
||||
summary: string;
|
||||
files: string[];
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface ReadManyFilesResult {
|
||||
summary: string;
|
||||
files: string[];
|
||||
skipped?: Array<{ path: string; reason: string }>;
|
||||
include?: string[];
|
||||
excludes?: string[];
|
||||
targetDir?: string;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export type ToolResultDisplay =
|
||||
| string
|
||||
| FileDiff
|
||||
| AnsiOutput
|
||||
| TodoList
|
||||
| GrepResult
|
||||
| ListDirectoryResult
|
||||
| ReadManyFilesResult;
|
||||
|
||||
export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
||||
|
||||
@@ -706,6 +739,7 @@ export interface ToolEditConfirmationDetails {
|
||||
fileDiff: string;
|
||||
originalContent: string | null;
|
||||
newContent: string;
|
||||
diffStat?: DiffStat;
|
||||
isModifying?: boolean;
|
||||
ideConfirmation?: Promise<DiffUpdateResult>;
|
||||
}
|
||||
|
||||
@@ -212,6 +212,15 @@ class WriteFileToolInvocation extends BaseToolInvocation<
|
||||
DEFAULT_DIFF_OPTIONS,
|
||||
);
|
||||
|
||||
const originallyProposedContent =
|
||||
this.params.ai_proposed_content || this.params.content;
|
||||
const diffStat = getDiffStat(
|
||||
fileName,
|
||||
originalContent,
|
||||
originallyProposedContent,
|
||||
this.params.content,
|
||||
);
|
||||
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
const ideConfirmation =
|
||||
this.config.getIdeMode() && ideClient.isDiffingEnabled()
|
||||
@@ -226,6 +235,7 @@ class WriteFileToolInvocation extends BaseToolInvocation<
|
||||
fileDiff,
|
||||
originalContent,
|
||||
newContent: correctedContent,
|
||||
diffStat,
|
||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||
// No need to publish a policy update as the default policy for
|
||||
|
||||
Reference in New Issue
Block a user