From dbd059f54843592495cbbab5bcbf387de6de4367 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 15 Dec 2025 13:53:34 -0700 Subject: [PATCH] feat: rename smartContextTracking to smartRead and handle @ mentioned files - Rename experiment ID from smartContextTracking to smartRead - Apply smart read logic to @ file mentions (skip files already in context) - Add parseMentions parameters for experiments and apiConversationHistory - Add English translation keys for SMART_READ experiment - Fix TypeScript errors and update tests --- packages/types/src/experiment.ts | 4 +-- src/core/mentions/index.ts | 36 +++++++++++++++++++ .../mentions/processUserContentMentions.ts | 11 ++++++ src/core/task/Task.ts | 14 ++++---- src/core/tools/ReadFileTool.ts | 11 +++--- src/core/tools/simpleReadFileTool.ts | 9 ++--- src/shared/__tests__/experiments.spec.ts | 6 ++-- src/shared/experiments.ts | 4 +-- .../__tests__/ExtensionStateContext.spec.tsx | 4 +-- webview-ui/src/i18n/locales/en/settings.json | 4 +++ 10 files changed, 75 insertions(+), 28 deletions(-) diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index f1c16eb058..71404fb2f5 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -13,7 +13,7 @@ export const experimentIds = [ "imageGeneration", "runSlashCommand", "multipleNativeToolCalls", - "smartContextTracking", + "smartRead", ] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -31,7 +31,7 @@ export const experimentsSchema = z.object({ imageGeneration: z.boolean().optional(), runSlashCommand: z.boolean().optional(), multipleNativeToolCalls: z.boolean().optional(), - smartContextTracking: z.boolean().optional(), + smartRead: z.boolean().optional(), }) export type Experiments = z.infer diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index f038b5b783..04f04752f1 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -15,11 +15,14 @@ import { diagnosticsToProblemsString } from "../../integrations/diagnostics" import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher" import { FileContextTracker } from "../context-tracking/FileContextTracker" +import { checkFileContextStatus, getReReadNotice } from "../context-tracking/FileContextStatusChecker" +import type { ApiMessage } from "../task-persistence/apiMessages" import { RooIgnoreController } from "../ignore/RooIgnoreController" import { getCommand, type Command } from "../../services/command/commands" import { t } from "../../i18n" +import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" function getUrlErrorMessage(error: unknown): string { const errorMessage = error instanceof Error ? error.message : String(error) @@ -81,6 +84,8 @@ export async function parseMentions( includeDiagnosticMessages: boolean = true, maxDiagnosticMessages: number = 50, maxReadFileLine?: number, + experimentsConfig?: Record, + apiConversationHistory?: ApiMessage[], ): Promise { const mentions: Set = new Set() const validCommands: Map = new Map() @@ -182,6 +187,37 @@ export async function parseMentions( } else if (mention.startsWith("/")) { const mentionPath = mention.slice(1) try { + // Check if smart read is enabled and file is already in effective context + const isSmartReadEnabled = experiments.isEnabled(experimentsConfig ?? {}, EXPERIMENT_IDS.SMART_READ) + + if ( + isSmartReadEnabled && + fileContextTracker && + apiConversationHistory && + !mention.endsWith("/") // Only for files, not folders + ) { + const absolutePath = path.resolve(cwd, unescapeSpaces(mentionPath)) + const taskMetadata = await fileContextTracker.getTaskMetadata(fileContextTracker.taskId) + const status = await checkFileContextStatus( + mentionPath, + absolutePath, + taskMetadata, + apiConversationHistory, + ) + + if (!status.shouldReRead) { + // File is already in effective context, return notice instead of content + const notice = getReReadNotice(status.reason) + const noticeText = notice + ? `(This file's content is already in context. ${notice})` + : "(This file's content is already in context and hasn't changed.)" + parsedText += `\n\n\n${noticeText}\n` + // Still track the context with the same source + await fileContextTracker.trackFileContext(mentionPath, "file_mentioned") + continue + } + } + const content = await getFileOrFolderContent( mentionPath, cwd, diff --git a/src/core/mentions/processUserContentMentions.ts b/src/core/mentions/processUserContentMentions.ts index 4bdb422d48..cae71b9ae4 100644 --- a/src/core/mentions/processUserContentMentions.ts +++ b/src/core/mentions/processUserContentMentions.ts @@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { parseMentions } from "./index" import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher" import { FileContextTracker } from "../context-tracking/FileContextTracker" +import type { ApiMessage } from "../task-persistence/apiMessages" /** * Process mentions in user content, specifically within task and feedback tags @@ -16,6 +17,8 @@ export async function processUserContentMentions({ includeDiagnosticMessages = true, maxDiagnosticMessages = 50, maxReadFileLine, + experiments, + apiConversationHistory, }: { userContent: Anthropic.Messages.ContentBlockParam[] cwd: string @@ -26,6 +29,8 @@ export async function processUserContentMentions({ includeDiagnosticMessages?: boolean maxDiagnosticMessages?: number maxReadFileLine?: number + experiments?: Record + apiConversationHistory?: ApiMessage[] }) { // Process userContent array, which contains various block types: // TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam. @@ -59,6 +64,8 @@ export async function processUserContentMentions({ includeDiagnosticMessages, maxDiagnosticMessages, maxReadFileLine, + experiments, + apiConversationHistory, ), } } @@ -79,6 +86,8 @@ export async function processUserContentMentions({ includeDiagnosticMessages, maxDiagnosticMessages, maxReadFileLine, + experiments, + apiConversationHistory, ), } } @@ -100,6 +109,8 @@ export async function processUserContentMentions({ includeDiagnosticMessages, maxDiagnosticMessages, maxReadFileLine, + experiments, + apiConversationHistory, ), } } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index bf94f40a0d..0b94e406e3 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2257,12 +2257,12 @@ export class Task extends EventEmitter implements TaskLike { }), ) - const { - showRooIgnoredFiles = false, - includeDiagnosticMessages = true, - maxDiagnosticMessages = 50, - maxReadFileLine = -1, - } = (await this.providerRef.deref()?.getState()) ?? {} + const providerState = await this.providerRef.deref()?.getState() + const showRooIgnoredFiles = providerState?.showRooIgnoredFiles ?? false + const includeDiagnosticMessages = providerState?.includeDiagnosticMessages ?? true + const maxDiagnosticMessages = providerState?.maxDiagnosticMessages ?? 50 + const maxReadFileLine = providerState?.maxReadFileLine ?? -1 + const experimentsConfig = providerState?.experiments const parsedUserContent = await processUserContentMentions({ userContent: currentUserContent, @@ -2274,6 +2274,8 @@ export class Task extends EventEmitter implements TaskLike { includeDiagnosticMessages, maxDiagnosticMessages, maxReadFileLine, + experiments: experimentsConfig, + apiConversationHistory: this.apiConversationHistory, }) const environmentDetails = await getEnvironmentDetails(this, currentIncludeFileDetails) diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts index fe8ad65189..ea9837612c 100644 --- a/src/core/tools/ReadFileTool.ts +++ b/src/core/tools/ReadFileTool.ts @@ -147,11 +147,11 @@ export class ReadFileTool extends BaseTool<"read_file"> { try { const filesToApprove: FileResult[] = [] - // Check if smart context tracking experiment is enabled + // Check if smart read experiment is enabled const experimentState = await task.providerRef.deref()?.getState() - const isSmartContextTrackingEnabled = experiments.isEnabled( + const isSmartReadEnabled = experiments.isEnabled( experimentState?.experiments ?? {}, - EXPERIMENT_IDS.SMART_CONTEXT_TRACKING, + EXPERIMENT_IDS.SMART_READ, ) for (const fileResult of fileResults) { @@ -205,10 +205,7 @@ export class ReadFileTool extends BaseTool<"read_file"> { // Check if file needs to be re-read based on context status (only if experiment enabled) // Skip this check for line range requests as those always need fresh content - if ( - isSmartContextTrackingEnabled && - (!fileResult.lineRanges || fileResult.lineRanges.length === 0) - ) { + if (isSmartReadEnabled && (!fileResult.lineRanges || fileResult.lineRanges.length === 0)) { const metadata = await task.fileContextTracker.getTaskMetadata(task.taskId) const contextStatus = await checkFileContextStatus( relPath, diff --git a/src/core/tools/simpleReadFileTool.ts b/src/core/tools/simpleReadFileTool.ts index 12e8e83cc1..41db98b4af 100644 --- a/src/core/tools/simpleReadFileTool.ts +++ b/src/core/tools/simpleReadFileTool.ts @@ -91,12 +91,9 @@ export async function simpleReadFileTool( return } - // Check if smart context tracking experiment is enabled + // Check if smart read experiment is enabled const state = await cline.providerRef.deref()?.getState() - const isSmartContextTrackingEnabled = experiments.isEnabled( - state?.experiments ?? {}, - EXPERIMENT_IDS.SMART_CONTEXT_TRACKING, - ) + const isSmartReadEnabled = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.SMART_READ) // Check if file needs to be re-read based on context status (only if experiment enabled) let contextStatus: FileContextStatus = { @@ -105,7 +102,7 @@ export async function simpleReadFileTool( } let reReadNotice: string | undefined = undefined - if (isSmartContextTrackingEnabled) { + if (isSmartReadEnabled) { const metadata = await cline.fileContextTracker.getTaskMetadata(cline.taskId) contextStatus = await checkFileContextStatus(relPath, fullPath, metadata, cline.apiConversationHistory) diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 2f51469c5c..d954ac9bfa 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -32,7 +32,7 @@ describe("experiments", () => { imageGeneration: false, runSlashCommand: false, multipleNativeToolCalls: false, - smartContextTracking: false, + smartRead: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) @@ -45,7 +45,7 @@ describe("experiments", () => { imageGeneration: false, runSlashCommand: false, multipleNativeToolCalls: false, - smartContextTracking: false, + smartRead: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) }) @@ -58,7 +58,7 @@ describe("experiments", () => { imageGeneration: false, runSlashCommand: false, multipleNativeToolCalls: false, - smartContextTracking: false, + smartRead: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index a42ab71c40..25c61024fd 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -7,7 +7,7 @@ export const EXPERIMENT_IDS = { IMAGE_GENERATION: "imageGeneration", RUN_SLASH_COMMAND: "runSlashCommand", MULTIPLE_NATIVE_TOOL_CALLS: "multipleNativeToolCalls", - SMART_CONTEXT_TRACKING: "smartContextTracking", + SMART_READ: "smartRead", } as const satisfies Record type _AssertExperimentIds = AssertEqual>> @@ -25,7 +25,7 @@ export const experimentConfigsMap: Record = { IMAGE_GENERATION: { enabled: false }, RUN_SLASH_COMMAND: { enabled: false }, MULTIPLE_NATIVE_TOOL_CALLS: { enabled: false }, - SMART_CONTEXT_TRACKING: { enabled: false }, + SMART_READ: { enabled: false }, } export const experimentDefault = Object.fromEntries( diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index c3dfe3796e..7fa83beade 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -240,7 +240,7 @@ describe("mergeExtensionState", () => { runSlashCommand: false, nativeToolCalling: false, multipleNativeToolCalls: false, - smartContextTracking: false, + smartRead: false, } as Record, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS + 5, } @@ -264,7 +264,7 @@ describe("mergeExtensionState", () => { runSlashCommand: false, nativeToolCalling: false, multipleNativeToolCalls: false, - smartContextTracking: false, + smartRead: false, }) }) }) diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 56488602b4..1e5174c1c9 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -831,6 +831,10 @@ "MULTIPLE_NATIVE_TOOL_CALLS": { "name": "Parallel tool calls", "description": "When enabled, the native protocol can execute multiple tools in a single assistant message turn." + }, + "SMART_READ": { + "name": "Smart file context tracking", + "description": "When enabled, Roo will skip re-reading files that are already in the conversation context. This reduces token usage and speeds up responses when working with files you've recently viewed." } }, "promptCaching": {