refactor(cli,core): foundational layout, identity management, and type safety (#23286)

This commit is contained in:
Jarrod Whelan
2026-03-23 18:49:51 -07:00
committed by GitHub
parent 57a66f5f0d
commit 89ca78837e
31 changed files with 477 additions and 182 deletions
+36
View File
@@ -76,3 +76,39 @@ export function getDiffStat(
user_removed_chars: userStats.removedChars,
};
}
/**
* Extracts line and character stats from a unified diff patch string.
* This is useful for reconstructing stats for rejected or errored operations
* where the full strings may no longer be easily accessible.
*/
export function getDiffStatFromPatch(patch: string): DiffStat {
let addedLines = 0;
let removedLines = 0;
let addedChars = 0;
let removedChars = 0;
const lines = patch.split('\n');
for (const line of lines) {
// Only count lines that are additions or removals,
// excluding the diff headers (--- and +++) and metadata (\)
if (line.startsWith('+') && !line.startsWith('+++')) {
addedLines++;
addedChars += line.length - 1;
} else if (line.startsWith('-') && !line.startsWith('---')) {
removedLines++;
removedChars += line.length - 1;
}
}
return {
model_added_lines: addedLines,
model_removed_lines: removedLines,
model_added_chars: addedChars,
model_removed_chars: removedChars,
user_added_lines: 0,
user_removed_lines: 0,
user_added_chars: 0,
user_removed_chars: 0,
};
}
+26 -1
View File
@@ -900,11 +900,36 @@ class EditToolInvocation
DEFAULT_DIFF_OPTIONS,
);
// Determine the full content as originally proposed by the AI to ensure accurate diff stats.
let fullAiProposedContent = editData.newContent;
if (
this.params.modified_by_user &&
this.params.ai_proposed_content !== undefined
) {
try {
const aiReplacement = await calculateReplacement(this.config, {
params: {
...this.params,
new_string: this.params.ai_proposed_content,
},
currentContent: editData.currentContent ?? '',
abortSignal: signal,
});
fullAiProposedContent = aiReplacement.newContent;
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : String(error);
debugLogger.log(`AI replacement fallback: ${errorMsg}`);
// Fallback to newContent if speculative calculation fails
fullAiProposedContent = editData.newContent;
}
}
const diffStat = getDiffStat(
fileName,
editData.currentContent ?? '',
fullAiProposedContent,
editData.newContent,
this.params.new_string,
);
displayResult = {
fileDiff,
+5 -2
View File
@@ -150,8 +150,6 @@ export {
SKILL_PARAM_NAME,
};
export const LS_TOOL_NAME_LEGACY = 'list_directory'; // Just to be safe if anything used the old exported name directly
export const EDIT_TOOL_NAMES = new Set([EDIT_TOOL_NAME, WRITE_FILE_TOOL_NAME]);
/**
@@ -182,6 +180,11 @@ export const EDIT_DISPLAY_NAME = 'Edit';
export const ASK_USER_DISPLAY_NAME = 'Ask User';
export const READ_FILE_DISPLAY_NAME = 'ReadFile';
export const GLOB_DISPLAY_NAME = 'FindFiles';
export const LS_DISPLAY_NAME = 'ReadFolder';
export const GREP_DISPLAY_NAME = 'SearchText';
export const WEB_SEARCH_DISPLAY_NAME = 'GoogleSearch';
export const WEB_FETCH_DISPLAY_NAME = 'WebFetch';
export const READ_MANY_FILES_DISPLAY_NAME = 'ReadManyFiles';
/**
* Mapping of legacy tool names to their current names.
+54
View File
@@ -6,6 +6,7 @@
import type { FunctionDeclaration, PartListUnion } from '@google/genai';
import { ToolErrorType } from './tool-error.js';
import type { GrepMatch } from './grep-utils.js';
import type { DiffUpdateResult } from '../ide/ide-client.js';
import type { ShellExecutionConfig } from '../services/shellExecutionService.js';
import { SchemaValidator } from '../utils/schemaValidator.js';
@@ -859,6 +860,51 @@ export interface TodoList {
export type ToolLiveOutput = string | AnsiOutput | SubagentProgress;
export interface StructuredToolResult {
summary: string;
}
export function isStructuredToolResult(
obj: unknown,
): obj is StructuredToolResult {
return (
typeof obj === 'object' &&
obj !== null &&
'summary' in obj &&
typeof obj.summary === 'string'
);
}
export const hasSummary = (res: unknown): res is { summary: string } =>
isStructuredToolResult(res);
export interface GrepResult extends StructuredToolResult {
matches: GrepMatch[];
payload?: string;
}
export interface ListDirectoryResult extends StructuredToolResult {
files: string[];
payload?: string;
}
export interface ReadManyFilesResult extends StructuredToolResult {
files: string[];
skipped?: Array<{ path: string; reason: string }>;
include?: string[];
excludes?: string[];
targetDir?: string;
payload?: string;
}
export const isGrepResult = (res: unknown): res is GrepResult =>
isStructuredToolResult(res) && 'matches' in res && Array.isArray(res.matches);
export const isListResult = (
res: unknown,
): res is ListDirectoryResult | ReadManyFilesResult =>
isStructuredToolResult(res) && 'files' in res && Array.isArray(res.files);
export type ToolResultDisplay =
| string
| FileDiff
@@ -888,6 +934,13 @@ export interface FileDiff {
isNewFile?: boolean;
}
export const isFileDiff = (res: unknown): res is FileDiff =>
typeof res === 'object' &&
res !== null &&
'fileDiff' in res &&
'fileName' in res &&
'filePath' in res;
export interface DiffStat {
model_added_lines: number;
model_removed_lines: number;
@@ -913,6 +966,7 @@ export interface ToolEditConfirmationDetails {
originalContent: string | null;
newContent: string;
isModifying?: boolean;
diffStat?: DiffStat;
ideConfirmation?: Promise<DiffUpdateResult>;
}
+2 -2
View File
@@ -28,7 +28,7 @@ import {
NetworkRetryAttemptEvent,
} from '../telemetry/index.js';
import { LlmRole } from '../telemetry/llmRole.js';
import { WEB_FETCH_TOOL_NAME } from './tool-names.js';
import { WEB_FETCH_TOOL_NAME, WEB_FETCH_DISPLAY_NAME } from './tool-names.js';
import { debugLogger } from '../utils/debugLogger.js';
import { coreEvents } from '../utils/events.js';
import { retryWithBackoff, getRetryErrorType } from '../utils/retry.js';
@@ -883,7 +883,7 @@ export class WebFetchTool extends BaseDeclarativeTool<
) {
super(
WebFetchTool.Name,
'WebFetch',
WEB_FETCH_DISPLAY_NAME,
WEB_FETCH_DEFINITION.base.description!,
Kind.Fetch,
WEB_FETCH_DEFINITION.base.parametersJsonSchema,
+2 -2
View File
@@ -5,7 +5,7 @@
*/
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { WEB_SEARCH_TOOL_NAME } from './tool-names.js';
import { WEB_SEARCH_TOOL_NAME, WEB_SEARCH_DISPLAY_NAME } from './tool-names.js';
import type { GroundingMetadata } from '@google/genai';
import {
BaseDeclarativeTool,
@@ -212,7 +212,7 @@ export class WebSearchTool extends BaseDeclarativeTool<
) {
super(
WebSearchTool.Name,
'GoogleSearch',
WEB_SEARCH_DISPLAY_NAME,
WEB_SEARCH_DEFINITION.base.description!,
Kind.Search,
WEB_SEARCH_DEFINITION.base.parametersJsonSchema,