mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-25 12:34:38 -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(() => {
|
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
|
// Only truncate string output if not in alternate buffer mode to ensure
|
||||||
// we can scroll through the full output.
|
// we can scroll through the full output.
|
||||||
if (typeof resultDisplay === 'string' && !isAlternateBuffer) {
|
if (typeof displayContent === 'string' && !isAlternateBuffer) {
|
||||||
let text = resultDisplay;
|
let text = displayContent;
|
||||||
if (text.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
if (text.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
||||||
text = '...' + text.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
text = '...' + text.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
||||||
}
|
}
|
||||||
@@ -101,7 +117,7 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
|||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
return resultDisplay;
|
return displayContent;
|
||||||
}, [resultDisplay, isAlternateBuffer, maxLines]);
|
}, [resultDisplay, isAlternateBuffer, maxLines]);
|
||||||
|
|
||||||
if (!truncatedResultDisplay) return null;
|
if (!truncatedResultDisplay) return null;
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ import type {
|
|||||||
RetrieveUserQuotaResponse,
|
RetrieveUserQuotaResponse,
|
||||||
SkillDefinition,
|
SkillDefinition,
|
||||||
AgentDefinition,
|
AgentDefinition,
|
||||||
|
GrepResult,
|
||||||
|
ListDirectoryResult,
|
||||||
|
ReadManyFilesResult,
|
||||||
|
FileDiff,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { PartListUnion } from '@google/genai';
|
import type { PartListUnion } from '@google/genai';
|
||||||
import { type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
export type { ThoughtSummary, SkillDefinition, ToolResultDisplay };
|
export type {
|
||||||
|
ThoughtSummary,
|
||||||
|
SkillDefinition,
|
||||||
|
ToolResultDisplay,
|
||||||
|
GrepResult,
|
||||||
|
ListDirectoryResult,
|
||||||
|
ReadManyFilesResult,
|
||||||
|
FileDiff,
|
||||||
|
};
|
||||||
|
|
||||||
export enum AuthState {
|
export enum AuthState {
|
||||||
// Attempting to authenticate or re-authenticate
|
// Attempting to authenticate or re-authenticate
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { type FunctionCall } from '@google/genai';
|
import { type FunctionCall } from '@google/genai';
|
||||||
import type {
|
import type {
|
||||||
|
DiffStat,
|
||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
ToolConfirmationPayload,
|
ToolConfirmationPayload,
|
||||||
} from '../tools/tools.js';
|
} from '../tools/tools.js';
|
||||||
@@ -79,6 +80,7 @@ export type SerializableConfirmationDetails =
|
|||||||
fileDiff: string;
|
fileDiff: string;
|
||||||
originalContent: string | null;
|
originalContent: string | null;
|
||||||
newContent: string;
|
newContent: string;
|
||||||
|
diffStat?: DiffStat;
|
||||||
isModifying?: boolean;
|
isModifying?: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -346,6 +346,8 @@ export class SchedulerStateManager {
|
|||||||
response: ToolCallResponseInfo,
|
response: ToolCallResponseInfo,
|
||||||
): ErroredToolCall {
|
): ErroredToolCall {
|
||||||
const startTime = 'startTime' in call ? call.startTime : undefined;
|
const startTime = 'startTime' in call ? call.startTime : undefined;
|
||||||
|
const confirmationDetails =
|
||||||
|
'confirmationDetails' in call ? call.confirmationDetails : undefined;
|
||||||
return {
|
return {
|
||||||
request: call.request,
|
request: call.request,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
@@ -353,6 +355,7 @@ export class SchedulerStateManager {
|
|||||||
response,
|
response,
|
||||||
durationMs: startTime ? Date.now() - startTime : undefined,
|
durationMs: startTime ? Date.now() - startTime : undefined,
|
||||||
outcome: call.outcome,
|
outcome: call.outcome,
|
||||||
|
confirmationDetails,
|
||||||
schedulerId: call.schedulerId,
|
schedulerId: call.schedulerId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -415,6 +418,8 @@ export class SchedulerStateManager {
|
|||||||
private toCancelled(call: ToolCall, reason: string): CancelledToolCall {
|
private toCancelled(call: ToolCall, reason: string): CancelledToolCall {
|
||||||
this.validateHasToolAndInvocation(call, 'cancelled');
|
this.validateHasToolAndInvocation(call, 'cancelled');
|
||||||
const startTime = 'startTime' in call ? call.startTime : undefined;
|
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.
|
// TODO: Refactor this tool-specific logic into the confirmation details payload.
|
||||||
// See: https://github.com/google-gemini/gemini-cli/issues/16716
|
// See: https://github.com/google-gemini/gemini-cli/issues/16716
|
||||||
@@ -463,6 +468,7 @@ export class SchedulerStateManager {
|
|||||||
},
|
},
|
||||||
durationMs: startTime ? Date.now() - startTime : undefined,
|
durationMs: startTime ? Date.now() - startTime : undefined,
|
||||||
outcome: call.outcome,
|
outcome: call.outcome,
|
||||||
|
confirmationDetails,
|
||||||
schedulerId: call.schedulerId,
|
schedulerId: call.schedulerId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ export type ErroredToolCall = {
|
|||||||
tool?: AnyDeclarativeTool;
|
tool?: AnyDeclarativeTool;
|
||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
outcome?: ToolConfirmationOutcome;
|
outcome?: ToolConfirmationOutcome;
|
||||||
|
confirmationDetails?:
|
||||||
|
| ToolCallConfirmationDetails
|
||||||
|
| SerializableConfirmationDetails;
|
||||||
schedulerId?: string;
|
schedulerId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,6 +108,9 @@ export type CancelledToolCall = {
|
|||||||
invocation: AnyToolInvocation;
|
invocation: AnyToolInvocation;
|
||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
outcome?: ToolConfirmationOutcome;
|
outcome?: ToolConfirmationOutcome;
|
||||||
|
confirmationDetails?:
|
||||||
|
| ToolCallConfirmationDetails
|
||||||
|
| SerializableConfirmationDetails;
|
||||||
schedulerId?: string;
|
schedulerId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -687,6 +687,14 @@ class EditToolInvocation
|
|||||||
'Proposed',
|
'Proposed',
|
||||||
DEFAULT_DIFF_OPTIONS,
|
DEFAULT_DIFF_OPTIONS,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const diffStat = getDiffStat(
|
||||||
|
fileName,
|
||||||
|
editData.currentContent ?? '',
|
||||||
|
editData.newContent,
|
||||||
|
this.params.new_string,
|
||||||
|
);
|
||||||
|
|
||||||
const ideClient = await IdeClient.getInstance();
|
const ideClient = await IdeClient.getInstance();
|
||||||
const ideConfirmation =
|
const ideConfirmation =
|
||||||
this.config.getIdeMode() && ideClient.isDiffingEnabled()
|
this.config.getIdeMode() && ideClient.isDiffingEnabled()
|
||||||
@@ -701,6 +709,7 @@ class EditToolInvocation
|
|||||||
fileDiff,
|
fileDiff,
|
||||||
originalContent: editData.currentContent,
|
originalContent: editData.currentContent,
|
||||||
newContent: editData.newContent,
|
newContent: editData.newContent,
|
||||||
|
diffStat,
|
||||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||||
// No need to publish a policy update as the default policy for
|
// No need to publish a policy update as the default policy for
|
||||||
|
|||||||
@@ -286,7 +286,14 @@ class GrepToolInvocation extends BaseToolInvocation<
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
llmContent: llmContent.trim(),
|
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) {
|
} catch (error) {
|
||||||
debugLogger.warn(`Error during GrepLogic execution: ${error}`);
|
debugLogger.warn(`Error during GrepLogic execution: ${error}`);
|
||||||
|
|||||||
@@ -254,7 +254,12 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
llmContent: resultMessage,
|
llmContent: resultMessage,
|
||||||
returnDisplay: displayMessage,
|
returnDisplay: {
|
||||||
|
summary: displayMessage,
|
||||||
|
files: entries.map(
|
||||||
|
(entry) => `${entry.isDirectory ? '[DIR] ' : ''}${entry.name}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = `Error listing directory: ${error instanceof Error ? error.message : String(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 { 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 { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
|
||||||
import { getErrorMessage } from '../utils/errors.js';
|
import { getErrorMessage } from '../utils/errors.js';
|
||||||
import * as fsPromises from 'node:fs/promises';
|
import * as fsPromises from 'node:fs/promises';
|
||||||
@@ -441,9 +445,22 @@ ${finalExclusionPatternsForDescription
|
|||||||
'No files matching the criteria were found or all were skipped.',
|
'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 {
|
return {
|
||||||
llmContent: contentParts,
|
llmContent: contentParts,
|
||||||
returnDisplay: displayMessage.trim(),
|
returnDisplay,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -664,7 +664,40 @@ export interface TodoList {
|
|||||||
todos: Todo[];
|
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';
|
export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
||||||
|
|
||||||
@@ -706,6 +739,7 @@ export interface ToolEditConfirmationDetails {
|
|||||||
fileDiff: string;
|
fileDiff: string;
|
||||||
originalContent: string | null;
|
originalContent: string | null;
|
||||||
newContent: string;
|
newContent: string;
|
||||||
|
diffStat?: DiffStat;
|
||||||
isModifying?: boolean;
|
isModifying?: boolean;
|
||||||
ideConfirmation?: Promise<DiffUpdateResult>;
|
ideConfirmation?: Promise<DiffUpdateResult>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,15 @@ class WriteFileToolInvocation extends BaseToolInvocation<
|
|||||||
DEFAULT_DIFF_OPTIONS,
|
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 ideClient = await IdeClient.getInstance();
|
||||||
const ideConfirmation =
|
const ideConfirmation =
|
||||||
this.config.getIdeMode() && ideClient.isDiffingEnabled()
|
this.config.getIdeMode() && ideClient.isDiffingEnabled()
|
||||||
@@ -226,6 +235,7 @@ class WriteFileToolInvocation extends BaseToolInvocation<
|
|||||||
fileDiff,
|
fileDiff,
|
||||||
originalContent,
|
originalContent,
|
||||||
newContent: correctedContent,
|
newContent: correctedContent,
|
||||||
|
diffStat,
|
||||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||||
// No need to publish a policy update as the default policy for
|
// No need to publish a policy update as the default policy for
|
||||||
|
|||||||
Reference in New Issue
Block a user