diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index 391d2acbaa..e33d5530af 100644 --- a/packages/types/npm/package.metadata.json +++ b/packages/types/npm/package.metadata.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.48.0", + "version": "1.49.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts index 4fe74c6cd4..1a5a5039bb 100644 --- a/packages/types/src/task.ts +++ b/packages/types/src/task.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { RooCodeEventName } from "./events.js" import { type ClineMessage, type BlockingAsk, type TokenUsage } from "./message.js" import { type ToolUsage, type ToolName } from "./tool.js" +import type { StaticAppProperties, GitProperties, TelemetryProperties } from "./telemetry.js" /** * TaskProviderLike @@ -14,19 +15,23 @@ export interface TaskProviderState { export interface TaskProviderLike { readonly cwd: string + readonly appProperties: StaticAppProperties + readonly gitProperties: GitProperties | undefined - getCurrentCline(): TaskLike | undefined + getCurrentTask(): TaskLike | undefined getCurrentTaskStack(): string[] + getRecentTasks(): string[] - initClineWithTask(text?: string, images?: string[], parentTask?: TaskLike): Promise + createTask(text?: string, images?: string[], parentTask?: TaskLike): Promise cancelTask(): Promise clearTask(): Promise - postStateToWebview(): Promise getState(): Promise - + postStateToWebview(): Promise postMessageToWebview(message: unknown): Promise + getTelemetryProperties(): Promise + on( event: K, listener: (...args: TaskProviderEvents[K]) => void | Promise, @@ -36,14 +41,6 @@ export interface TaskProviderLike { event: K, listener: (...args: TaskProviderEvents[K]) => void | Promise, ): this - - context: { - extension?: { - packageJSON?: { - version?: string - } - } - } } export type TaskProviderEvents = { diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index 223c39484c..d872329b93 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -72,17 +72,37 @@ export enum TelemetryEventName { * TelemetryProperties */ -export const appPropertiesSchema = z.object({ +export const staticAppPropertiesSchema = z.object({ appName: z.string(), appVersion: z.string(), vscodeVersion: z.string(), platform: z.string(), editorName: z.string(), +}) + +export type StaticAppProperties = z.infer + +export const dynamicAppPropertiesSchema = z.object({ language: z.string(), mode: z.string(), +}) + +export type DynamicAppProperties = z.infer + +export const cloudAppPropertiesSchema = z.object({ cloudIsAuthenticated: z.boolean().optional(), }) +export type CloudAppProperties = z.infer + +export const appPropertiesSchema = z.object({ + ...staticAppPropertiesSchema.shape, + ...dynamicAppPropertiesSchema.shape, + ...cloudAppPropertiesSchema.shape, +}) + +export type AppProperties = z.infer + export const taskPropertiesSchema = z.object({ taskId: z.string().optional(), apiProvider: z.enum(providerNames).optional(), @@ -99,12 +119,16 @@ export const taskPropertiesSchema = z.object({ .optional(), }) +export type TaskProperties = z.infer + export const gitPropertiesSchema = z.object({ repositoryUrl: z.string().optional(), repositoryName: z.string().optional(), defaultBranch: z.string().optional(), }) +export type GitProperties = z.infer + export const telemetryPropertiesSchema = z.object({ ...appPropertiesSchema.shape, ...taskPropertiesSchema.shape, @@ -112,7 +136,6 @@ export const telemetryPropertiesSchema = z.object({ }) export type TelemetryProperties = z.infer -export type GitProperties = z.infer /** * TelemetryEvent diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bcc01f194..d0bdcf9ab8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,8 +584,8 @@ importers: specifier: ^1.14.0 version: 1.14.0(typescript@5.8.3) '@roo-code/cloud': - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.15.0 + version: 0.15.0 '@roo-code/ipc': specifier: workspace:^ version: link:../packages/ipc @@ -3103,11 +3103,11 @@ packages: cpu: [x64] os: [win32] - '@roo-code/cloud@0.14.0': - resolution: {integrity: sha512-EKFw7sLSJcOpXQDjCMFL4EnE7OH3RhO00joteaysNYNpNe+js6kMFrBZY61rj1tZAXV/943RWeQptG4sSAfPFQ==} + '@roo-code/cloud@0.15.0': + resolution: {integrity: sha512-0DivOP5uUS9U6UKSxzoxZ4NxMCYUxA7wG72y3PBP91JhGzHaTqxi9WrvF61bo134dCnCktU9oTKIAD9AvFigzg==} - '@roo-code/types@1.48.0': - resolution: {integrity: sha512-DJkA/310sLcs4vM4YLhJhc5A1JFYmdcOt+hRQMUPq+wF02yW9rc7ZZzHNDOOUZL+kk5naUd2nBRxLiJoh6o0Ng==} + '@roo-code/types@1.49.0': + resolution: {integrity: sha512-h7gbjfIxBN+fgFecQiZs3W+vjdUhZOvtjh4OqcpPGG1w8B1DlXPDV4L/+ARUeNzZxSOK5S5rNPy57jC35EaD/w==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -12305,9 +12305,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true - '@roo-code/cloud@0.14.0': + '@roo-code/cloud@0.15.0': dependencies: - '@roo-code/types': 1.48.0 + '@roo-code/types': 1.49.0 ioredis: 5.6.1 p-wait-for: 5.0.2 socket.io-client: 4.8.1 @@ -12317,7 +12317,7 @@ snapshots: - supports-color - utf-8-validate - '@roo-code/types@1.48.0': + '@roo-code/types@1.49.0': dependencies: zod: 3.25.76 diff --git a/scripts/link-packages.ts b/scripts/link-packages.ts index 4d7e4a7c4c..97aeebd6b5 100644 --- a/scripts/link-packages.ts +++ b/scripts/link-packages.ts @@ -212,7 +212,7 @@ function startWatch(pkg: PackageConfig): WatcherResult { throw new Error(`Invalid watch command for ${pkg.name}`) } - console.log(`Watching for changes to ${pkg.sourcePath} with ${cmd} ${args.join(" ")}`) + console.log(`👀 Watching for changes to ${pkg.sourcePath} with ${cmd} ${args.join(" ")}`) const child = spawn(cmd, args, { cwd: path.resolve(__dirname, "..", pkg.sourcePath), @@ -251,7 +251,7 @@ function startWatch(pkg: PackageConfig): WatcherResult { debounceTimer = setTimeout(() => { linkPackage(pkg) - console.log(`📋 Copied ${pkg.name} to ${pkg.targetPaths.length} paths\n`) + console.log(`♻️ Copied ${pkg.name} to ${pkg.targetPaths.length} paths\n`) debounceTimer = null }, DEBOUNCE_DELAY) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 899ec41401..204348d028 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1951,7 +1951,7 @@ export class Task extends EventEmitter implements TaskLike { const history = await provider?.getTaskWithId(this.taskId) if (history) { - await provider?.initClineWithHistoryItem(history.historyItem) + await provider?.createTaskWithHistoryItem(history.historyItem) } } } finally { diff --git a/src/core/tools/__tests__/newTaskTool.spec.ts b/src/core/tools/__tests__/newTaskTool.spec.ts index 1dd79d6e98..8680a60035 100644 --- a/src/core/tools/__tests__/newTaskTool.spec.ts +++ b/src/core/tools/__tests__/newTaskTool.spec.ts @@ -22,7 +22,7 @@ const mockAskApproval = vi.fn() const mockHandleError = vi.fn() const mockPushToolResult = vi.fn() const mockRemoveClosingTag = vi.fn((_name: string, value: string | undefined) => value ?? "") -const mockInitClineWithTask = vi.fn<() => Promise>().mockResolvedValue({ taskId: "mock-subtask-id" }) +const mockCreateTask = vi.fn<() => Promise>().mockResolvedValue({ taskId: "mock-subtask-id" }) const mockEmit = vi.fn() const mockRecordToolError = vi.fn() const mockSayAndCreateMissingParamError = vi.fn() @@ -40,7 +40,7 @@ const mockCline = { deref: vi.fn(() => ({ getState: vi.fn(() => ({ customModes: [], mode: "ask" })), handleModeSwitch: vi.fn(), - initClineWithTask: mockInitClineWithTask, + createTask: mockCreateTask, })), }, } @@ -88,8 +88,8 @@ describe("newTaskTool", () => { // Verify askApproval was called expect(mockAskApproval).toHaveBeenCalled() - // Verify the message passed to initClineWithTask reflects the code's behavior in unit tests - expect(mockInitClineWithTask).toHaveBeenCalledWith( + // Verify the message passed to createTask reflects the code's behavior in unit tests + expect(mockCreateTask).toHaveBeenCalledWith( "Review this: \\@file1.txt and also \\\\\\@file2.txt", // Unit Test Expectation: \\@ -> \@, \\\\@ -> \\\\@ undefined, mockCline, @@ -122,7 +122,7 @@ describe("newTaskTool", () => { mockRemoveClosingTag, ) - expect(mockInitClineWithTask).toHaveBeenCalledWith( + expect(mockCreateTask).toHaveBeenCalledWith( "This is already unescaped: \\@file1.txt", // Expected: \@ remains \@ undefined, mockCline, @@ -149,7 +149,7 @@ describe("newTaskTool", () => { mockRemoveClosingTag, ) - expect(mockInitClineWithTask).toHaveBeenCalledWith( + expect(mockCreateTask).toHaveBeenCalledWith( "A normal mention @file1.txt", // Expected: @ remains @ undefined, mockCline, @@ -176,7 +176,7 @@ describe("newTaskTool", () => { mockRemoveClosingTag, ) - expect(mockInitClineWithTask).toHaveBeenCalledWith( + expect(mockCreateTask).toHaveBeenCalledWith( "Mix: @file0.txt, \\@file1.txt, \\@file2.txt, \\\\\\@file3.txt", // Unit Test Expectation: @->@, \@->\@, \\@->\@, \\\\@->\\\\@ undefined, mockCline, diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index 46a1fe5d9b..dbe08cbae8 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -83,7 +83,8 @@ export async function newTaskTool( cline.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug // Create new task instance first (this preserves parent's current mode in its history) - const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline) + const newCline = await provider.createTask(unescapedMessage, undefined, cline) + if (!newCline) { pushToolResult(t("tools:newTask.errors.policy_restriction")) return diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6622b29891..2270d5aee5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -17,6 +17,11 @@ import { type ProviderSettings, type RooCodeSettings, type ProviderSettingsEntry, + type StaticAppProperties, + type DynamicAppProperties, + type CloudAppProperties, + type TaskProperties, + type GitProperties, type TelemetryProperties, type TelemetryPropertiesProvider, type CodeActionId, @@ -104,15 +109,14 @@ export class ClineProvider private codeIndexStatusSubscription?: vscode.Disposable private currentWorkspaceManager?: CodeIndexManager private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class - public get workspaceTracker(): WorkspaceTracker | undefined { - return this._workspaceTracker - } protected mcpHub?: McpHub // Change from private to protected private marketplaceManager: MarketplaceManager private mdmService?: MdmService private taskCreationCallback: (task: Task) => void private taskEventListeners: WeakMap void>> = new WeakMap() + private recentTasksCache?: string[] + public isViewLaunched = false public settingsImportedAt?: number public readonly latestAnnouncementId = "jul-29-2025-3-25-0" // Update for v3.25.0 announcement @@ -366,7 +370,7 @@ export class ClineProvider // returns the current cline object in the stack (the top one) // if the stack is empty, returns undefined - getCurrentCline(): Task | undefined { + getCurrentTask(): Task | undefined { if (this.clineStack.length === 0) { return undefined } @@ -374,7 +378,7 @@ export class ClineProvider } // returns the current clineStack length (how many cline objects are in the stack) - getClineStackSize(): number { + getTaskStackSize(): number { return this.clineStack.length } @@ -390,7 +394,7 @@ export class ClineProvider // remove the last cline instance from the stack (this is the finished sub task) await this.removeClineFromStack() // resume the last cline instance in the stack (if it exists - this is the 'parent' calling task) - await this.getCurrentCline()?.resumePausedTask(lastMessage) + await this.getCurrentTask()?.resumePausedTask(lastMessage) } // Clear the current task without treating it as a subtask @@ -399,6 +403,51 @@ export class ClineProvider await this.removeClineFromStack() } + getRecentTasks(): string[] { + if (this.recentTasksCache) { + return this.recentTasksCache + } + + const history = this.getGlobalState("taskHistory") ?? [] + const workspaceTasks: HistoryItem[] = [] + + for (const item of history) { + if (!item.ts || !item.task || item.workspace !== this.cwd) { + continue + } + + workspaceTasks.push(item) + } + + if (workspaceTasks.length === 0) { + this.recentTasksCache = [] + return this.recentTasksCache + } + + workspaceTasks.sort((a, b) => b.ts - a.ts) + let recentTaskIds: string[] = [] + + if (workspaceTasks.length >= 100) { + // If we have at least 100 tasks, return tasks from the last 7 days. + const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000 + + for (const item of workspaceTasks) { + // Stop when we hit tasks older than 7 days. + if (item.ts < sevenDaysAgo) { + break + } + + recentTaskIds.push(item.id) + } + } else { + // Otherwise, return the most recent 100 tasks (or all if less than 100). + recentTaskIds = workspaceTasks.slice(0, Math.min(100, workspaceTasks.length)).map((item) => item.id) + } + + this.recentTasksCache = recentTaskIds + return this.recentTasksCache + } + /* VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc. - https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/ @@ -489,7 +538,7 @@ export class ClineProvider } // Check if there is a cline instance in the stack (if this provider has an active task) - if (visibleProvider.getCurrentCline()) { + if (visibleProvider.getCurrentTask()) { return true } @@ -520,7 +569,7 @@ export class ClineProvider return } - await visibleProvider.initClineWithTask(prompt) + await visibleProvider.createTask(prompt) } public static async handleTerminalAction( @@ -545,7 +594,7 @@ export class ClineProvider } try { - await visibleProvider.initClineWithTask(prompt) + await visibleProvider.createTask(prompt) } catch (error) { if (error instanceof OrganizationAllowListViolationError) { // Errors from terminal commands seem to get swallowed / ignored. @@ -681,17 +730,13 @@ export class ClineProvider this.log("Webview view resolved") } - public async initClineWithSubTask(parent: Task, task?: string, images?: string[]) { - return this.initClineWithTask(task, images, parent) - } - // When initializing a new task, (not from history but from a tool command // new_task) there is no need to remove the previous task since the new // task is a subtask of the previous one, and when it finishes it is removed // from the stack and the caller is resumed in this way we can have a chain // of tasks, each one being a sub task of the previous one until the main // task is finished. - public async initClineWithTask( + public async createTask( text?: string, images?: string[], parentTask?: Task, @@ -744,7 +789,7 @@ export class ClineProvider return task } - public async initClineWithHistoryItem(historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task }) { + public async createTaskWithHistoryItem(historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task }) { await this.removeClineFromStack() // If the history item has a saved mode, restore it and its associated API configuration @@ -1020,7 +1065,7 @@ export class ClineProvider * @param newMode The mode to switch to */ public async handleModeSwitch(newMode: Mode) { - const cline = this.getCurrentCline() + const cline = this.getCurrentTask() if (cline) { TelemetryService.instance.captureModeSwitch(cline.taskId, newMode) @@ -1133,7 +1178,7 @@ export class ClineProvider // Change the provider for the current task. // TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`). - const task = this.getCurrentCline() + const task = this.getCurrentTask() if (task) { task.api = buildApiHandler(providerSettings) @@ -1194,7 +1239,7 @@ export class ClineProvider } // Change the provider for the current task. - const task = this.getCurrentCline() + const task = this.getCurrentTask() if (task) { task.api = buildApiHandler(providerSettings) @@ -1206,7 +1251,7 @@ export class ClineProvider // Task Management async cancelTask() { - const cline = this.getCurrentCline() + const cline = this.getCurrentTask() if (!cline) { return @@ -1223,13 +1268,13 @@ export class ClineProvider await pWaitFor( () => - this.getCurrentCline()! === undefined || - this.getCurrentCline()!.isStreaming === false || - this.getCurrentCline()!.didFinishAbortingStream || + this.getCurrentTask()! === undefined || + this.getCurrentTask()!.isStreaming === false || + this.getCurrentTask()!.didFinishAbortingStream || // If only the first chunk is processed, then there's no // need to wait for graceful abort (closes edits, browser, // etc). - this.getCurrentCline()!.isWaitingForFirstChunk, + this.getCurrentTask()!.isWaitingForFirstChunk, { timeout: 3_000, }, @@ -1237,15 +1282,15 @@ export class ClineProvider console.error("Failed to abort task") }) - if (this.getCurrentCline()) { + if (this.getCurrentTask()) { // 'abandoned' will prevent this Cline instance from affecting // future Cline instances. This may happen if its hanging on a // streaming request. - this.getCurrentCline()!.abandoned = true + this.getCurrentTask()!.abandoned = true } // Clears task again, so we need to abortTask manually above. - await this.initClineWithHistoryItem({ ...historyItem, rootTask, parentTask }) + await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask }) } async updateCustomInstructions(instructions?: string) { @@ -1403,10 +1448,10 @@ export class ClineProvider } async showTaskWithId(id: string) { - if (id !== this.getCurrentCline()?.taskId) { + if (id !== this.getCurrentTask()?.taskId) { // Non-current task. const { historyItem } = await this.getTaskWithId(id) - await this.initClineWithHistoryItem(historyItem) // Clears existing task. + await this.createTaskWithHistoryItem(historyItem) // Clears existing task. } await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) @@ -1440,7 +1485,7 @@ export class ClineProvider const { taskDirPath } = await this.getTaskWithId(id) // remove task from stack if it's the current task - if (id === this.getCurrentCline()?.taskId) { + if (id === this.getCurrentTask()?.taskId) { // if we found the taskid to delete - call finish to abort this task and allow a new task to be started, // if we are deleting a subtask and parent task is still waiting for subtask to finish - it allows the parent to resume (this case should neve exist) await this.finishSubTask(t("common:tasks.deleted")) @@ -1485,6 +1530,7 @@ export class ClineProvider const taskHistory = this.getGlobalState("taskHistory") ?? [] const updatedTaskHistory = taskHistory.filter((task) => task.id !== id) await this.updateGlobalState("taskHistory", updatedTaskHistory) + this.recentTasksCache = undefined await this.postStateToWebview() } @@ -1726,10 +1772,10 @@ export class ClineProvider autoCondenseContext: autoCondenseContext ?? true, autoCondenseContextPercent: autoCondenseContextPercent ?? 100, uriScheme: vscode.env.uriScheme, - currentTaskItem: this.getCurrentCline()?.taskId - ? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId) + currentTaskItem: this.getCurrentTask()?.taskId + ? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentTask()?.taskId) : undefined, - clineMessages: this.getCurrentCline()?.clineMessages || [], + clineMessages: this.getCurrentTask()?.clineMessages || [], taskHistory: (taskHistory || []) .filter((item: HistoryItem) => item.ts && item.task) .sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts), @@ -2025,6 +2071,8 @@ export class ClineProvider } await this.updateGlobalState("taskHistory", history) + this.recentTasksCache = undefined + return history } @@ -2090,17 +2138,20 @@ export class ClineProvider console.log(message) } - // integration tests + // getters + + public get workspaceTracker(): WorkspaceTracker | undefined { + return this._workspaceTracker + } get viewLaunched() { return this.isViewLaunched } get messages() { - return this.getCurrentCline()?.clineMessages || [] + return this.getCurrentTask()?.clineMessages || [] } - // Add public getter public getMcpHub(): McpHub | undefined { return this.mcpHub } @@ -2143,7 +2194,7 @@ export class ClineProvider ) if (isRemoteControlEnabled(userInfo, enabled)) { - const currentTask = this.getCurrentCline() + const currentTask = this.getCurrentTask() if (currentTask && !currentTask.bridgeService) { try { @@ -2176,18 +2227,29 @@ export class ClineProvider } } - /** - * Returns properties to be included in every telemetry event - * This method is called by the telemetry service to get context information - * like the current mode, API provider, git repository information, etc. - */ - public async getTelemetryProperties(): Promise { - const { mode, apiConfiguration, language } = await this.getState() - const task = this.getCurrentCline() + private _appProperties?: StaticAppProperties - const packageJSON = this.context.extension?.packageJSON + private getAppProperties(): StaticAppProperties { + if (!this._appProperties) { + const packageJSON = this.context.extension?.packageJSON - // Get Roo Code Cloud authentication state + this._appProperties = { + appName: packageJSON?.name ?? Package.name, + appVersion: packageJSON?.version ?? Package.version, + vscodeVersion: vscode.version, + platform: process.platform, + editorName: vscode.env.appName, + } + } + + return this._appProperties + } + + public get appProperties(): StaticAppProperties { + return this._appProperties ?? this.getAppProperties() + } + + private getCloudProperties(): CloudAppProperties { let cloudIsAuthenticated: boolean | undefined try { @@ -2195,14 +2257,19 @@ export class ClineProvider cloudIsAuthenticated = CloudService.instance.isAuthenticated() } } catch (error) { - // Silently handle errors to avoid breaking telemetry collection + // Silently handle errors to avoid breaking telemetry collection. this.log(`[getTelemetryProperties] Failed to get cloud auth state: ${error}`) } - // Get git repository information - const gitInfo = await getWorkspaceGitInfo() + return { + cloudIsAuthenticated, + } + } - // Calculate todo list statistics + private async getTaskProperties(): Promise { + const { language, mode, apiConfiguration } = await this.getState() + + const task = this.getCurrentTask() const todoList = task?.todoList let todos: { total: number; completed: number; inProgress: number; pending: number } | undefined @@ -2215,22 +2282,38 @@ export class ClineProvider } } - // Return all properties including git info - clients will filter as needed return { - appName: packageJSON?.name ?? Package.name, - appVersion: packageJSON?.version ?? Package.version, - vscodeVersion: vscode.version, - platform: process.platform, - editorName: vscode.env.appName, language, mode, + taskId: task?.taskId, apiProvider: apiConfiguration?.apiProvider, modelId: task?.api?.getModel().id, diffStrategy: task?.diffStrategy?.getName(), isSubtask: task ? !!task.parentTask : undefined, - cloudIsAuthenticated, ...(todos && { todos }), - ...gitInfo, + } + } + + private _gitProperties?: GitProperties + + private async getGitProperties(): Promise { + if (!this._gitProperties) { + this._gitProperties = await getWorkspaceGitInfo() + } + + return this._gitProperties + } + + public get gitProperties(): GitProperties | undefined { + return this._gitProperties + } + + public async getTelemetryProperties(): Promise { + return { + ...this.getAppProperties(), + ...this.getCloudProperties(), + ...(await this.getTaskProperties()), + ...(await this.getGitProperties()), } } diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 29df1e4a2e..80c2f537a2 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -580,13 +580,13 @@ describe("ClineProvider", () => { await provider.addClineToStack(mockCline) // get the stack size before the abort call - const stackSizeBeforeAbort = provider.getClineStackSize() + const stackSizeBeforeAbort = provider.getTaskStackSize() // call the removeClineFromStack method so it will call the current cline abort and remove it from the stack await provider.removeClineFromStack() // get the stack size after the abort call - const stackSizeAfterAbort = provider.getClineStackSize() + const stackSizeAfterAbort = provider.getTaskStackSize() // check if the abort method was called expect(mockCline.abortTask).toHaveBeenCalled() @@ -692,7 +692,7 @@ describe("ClineProvider", () => { await provider.addClineToStack(mockCline) // Verify stack size is 1 - expect(provider.getClineStackSize()).toBe(1) + expect(provider.getTaskStackSize()).toBe(1) // Get the message handler const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] @@ -718,10 +718,10 @@ describe("ClineProvider", () => { await provider.addClineToStack(mockCline2) // verify cline instances were added to the stack - expect(provider.getClineStackSize()).toBe(2) + expect(provider.getTaskStackSize()).toBe(2) // verify current cline instance is the last one added - expect(provider.getCurrentCline()).toBe(mockCline2) + expect(provider.getCurrentTask()).toBe(mockCline2) }) test("getState returns correct initial state", async () => { @@ -1205,8 +1205,8 @@ describe("ClineProvider", () => { historyItem: { id: "test-task-id" }, }) - // Mock initClineWithHistoryItem - ;(provider as any).initClineWithHistoryItem = vi.fn() + // Mock createTaskWithHistoryItem + ;(provider as any).createTaskWithHistoryItem = vi.fn() // Trigger message deletion const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] @@ -1230,8 +1230,8 @@ describe("ClineProvider", () => { mockApiHistory[1], ]) - // Verify initClineWithHistoryItem was called - expect((provider as any).initClineWithHistoryItem).toHaveBeenCalledWith({ id: "test-task-id" }) + // Verify createTaskWithHistoryItem was called + expect((provider as any).createTaskWithHistoryItem).toHaveBeenCalledWith({ id: "test-task-id" }) }) test("handles case when no current task exists", async () => { @@ -1660,7 +1660,7 @@ describe("ClineProvider", () => { }) }) - describe("initClineWithHistoryItem mode validation", () => { + describe("createTaskWithHistoryItem mode validation", () => { test("validates and falls back to default mode when restored mode no longer exists", async () => { await provider.resolveWebviewView(mockWebviewView) @@ -1711,7 +1711,7 @@ describe("ClineProvider", () => { } // Initialize with history item - await provider.initClineWithHistoryItem(historyItem) + await provider.createTaskWithHistoryItem(historyItem) // Verify mode validation occurred expect(mockCustomModesManager.getCustomModes).toHaveBeenCalled() @@ -1780,7 +1780,7 @@ describe("ClineProvider", () => { } // Initialize with history item - await provider.initClineWithHistoryItem(historyItem) + await provider.createTaskWithHistoryItem(historyItem) // Verify mode validation occurred expect(mockCustomModesManager.getCustomModes).toHaveBeenCalled() @@ -1832,7 +1832,7 @@ describe("ClineProvider", () => { } // Initialize with history item - await provider.initClineWithHistoryItem(historyItem) + await provider.createTaskWithHistoryItem(historyItem) // Verify mode was preserved expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect") @@ -1863,7 +1863,7 @@ describe("ClineProvider", () => { } // Initialize with history item - await provider.initClineWithHistoryItem(historyItem) + await provider.createTaskWithHistoryItem(historyItem) // Verify no mode validation occurred (mode update not called) expect(mockContext.globalState.update).not.toHaveBeenCalledWith("mode", expect.any(String)) @@ -1913,7 +1913,7 @@ describe("ClineProvider", () => { } // Initialize with history item - should not throw - await expect(provider.initClineWithHistoryItem(historyItem)).resolves.not.toThrow() + await expect(provider.createTaskWithHistoryItem(historyItem)).resolves.not.toThrow() // Verify error was logged but task restoration continued expect(logSpy).toHaveBeenCalledWith( @@ -3235,7 +3235,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { test("handles edit permission failures", async () => { // Mock no current cline (simulating permission failure) - vi.spyOn(provider, "getCurrentCline").mockReturnValue(undefined) + vi.spyOn(provider, "getCurrentTask").mockReturnValue(undefined) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] @@ -3654,7 +3654,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ historyItem: { id: "test-task-id" }, }) - ;(provider as any).initClineWithHistoryItem = vi.fn() + ;(provider as any).createTaskWithHistoryItem = vi.fn() const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] @@ -3671,7 +3671,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { // Verify successful operation completed expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - expect(provider.initClineWithHistoryItem).toHaveBeenCalled() + expect(provider.createTaskWithHistoryItem).toHaveBeenCalled() expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() }) diff --git a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts index 99285bb68e..0ae7b38b81 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts @@ -415,7 +415,7 @@ describe("ClineProvider - Sticky Mode", () => { }) }) - describe("initClineWithHistoryItem", () => { + describe("createTaskWithHistoryItem", () => { it("should restore mode from history item when reopening task", async () => { await provider.resolveWebviewView(mockWebviewView) @@ -437,7 +437,7 @@ describe("ClineProvider - Sticky Mode", () => { const updateGlobalStateSpy = vi.spyOn(provider as any, "updateGlobalState").mockResolvedValue(undefined) // Initialize task with history item - await provider.initClineWithHistoryItem(historyItem) + await provider.createTaskWithHistoryItem(historyItem) // Verify mode was restored via updateGlobalState expect(updateGlobalStateSpy).toHaveBeenCalledWith("mode", "architect") @@ -479,7 +479,7 @@ describe("ClineProvider - Sticky Mode", () => { const handleModeSwitchSpy = vi.spyOn(provider, "handleModeSwitch").mockResolvedValue() // Initialize task with history item - await provider.initClineWithHistoryItem(historyItem) + await provider.createTaskWithHistoryItem(historyItem) // Verify mode was not changed (should use current mode) expect(handleModeSwitchSpy).not.toHaveBeenCalled() @@ -606,15 +606,15 @@ describe("ClineProvider - Sticky Mode", () => { // Initialize subtask with parent's mode taskModes[subtaskId] = "architect" - // Mock getCurrentCline to return the parent task initially - const getCurrentClineMock = vi.spyOn(provider, "getCurrentCline") - getCurrentClineMock.mockReturnValue(parentTask as any) + // Mock getCurrentTask to return the parent task initially + const getCurrentTaskMock = vi.spyOn(provider, "getCurrentTask") + getCurrentTaskMock.mockReturnValue(parentTask as any) // Add subtask to stack await provider.addClineToStack(subtask) - // Now mock getCurrentCline to return the subtask (simulating stack behavior) - getCurrentClineMock.mockReturnValue(subtask as any) + // Now mock getCurrentTask to return the subtask (simulating stack behavior) + getCurrentTaskMock.mockReturnValue(subtask as any) // Switch subtask to code mode - this should only affect the subtask await provider.handleModeSwitch("code") @@ -678,7 +678,7 @@ describe("ClineProvider - Sticky Mode", () => { const handleModeSwitchSpy = vi.spyOn(provider, "handleModeSwitch").mockResolvedValue() // Initialize task with history item - should not throw - await expect(provider.initClineWithHistoryItem(historyItem)).resolves.not.toThrow() + await expect(provider.createTaskWithHistoryItem(historyItem)).resolves.not.toThrow() // Verify mode switch was not called with null expect(handleModeSwitchSpy).not.toHaveBeenCalledWith(null) @@ -719,7 +719,7 @@ describe("ClineProvider - Sticky Mode", () => { } // Restore the task from history - await provider.initClineWithHistoryItem(historyItem) + await provider.createTaskWithHistoryItem(historyItem) // Verify that the mode was restored const state = await provider.getState() @@ -764,7 +764,7 @@ describe("ClineProvider - Sticky Mode", () => { const handleModeSwitchSpy = vi.spyOn(provider, "handleModeSwitch").mockResolvedValue() // Initialize task with history item - should not throw - await expect(provider.initClineWithHistoryItem(historyItem)).resolves.not.toThrow() + await expect(provider.createTaskWithHistoryItem(historyItem)).resolves.not.toThrow() // Verify mode switch was not called with deleted mode expect(handleModeSwitchSpy).not.toHaveBeenCalledWith("deleted-mode") @@ -1105,17 +1105,17 @@ describe("ClineProvider - Sticky Mode", () => { .spyOn(provider, "updateTaskHistory") .mockImplementation(() => Promise.resolve([])) - // Mock getCurrentCline to return different tasks - const getCurrentClineSpy = vi.spyOn(provider, "getCurrentCline") + // Mock getCurrentTask to return different tasks + const getCurrentTaskSpy = vi.spyOn(provider, "getCurrentTask") // Simulate simultaneous mode switches for different tasks - getCurrentClineSpy.mockReturnValue(task1 as any) + getCurrentTaskSpy.mockReturnValue(task1 as any) const switch1 = provider.handleModeSwitch("architect") - getCurrentClineSpy.mockReturnValue(task2 as any) + getCurrentTaskSpy.mockReturnValue(task2 as any) const switch2 = provider.handleModeSwitch("debug") - getCurrentClineSpy.mockReturnValue(task3 as any) + getCurrentTaskSpy.mockReturnValue(task3 as any) const switch3 = provider.handleModeSwitch("code") await Promise.all([switch1, switch2, switch3]) @@ -1166,7 +1166,7 @@ describe("ClineProvider - Sticky Mode", () => { vi.clearAllMocks() // Start initialization - const initPromise = provider.initClineWithHistoryItem(historyItem) + const initPromise = provider.createTaskWithHistoryItem(historyItem) // Try to switch mode during initialization await provider.handleModeSwitch("code") @@ -1201,13 +1201,13 @@ describe("ClineProvider - Sticky Mode", () => { await provider.addClineToStack(task as any) } - // Mock getCurrentCline - const getCurrentClineSpy = vi.spyOn(provider, "getCurrentCline") + // Mock getCurrentTask + const getCurrentTaskSpy = vi.spyOn(provider, "getCurrentTask") // Rapidly switch between tasks and modes const switches: Promise[] = [] tasks.forEach((task, index) => { - getCurrentClineSpy.mockReturnValue(task as any) + getCurrentTaskSpy.mockReturnValue(task as any) const mode = ["architect", "debug", "code"][index % 3] switches.push(provider.handleModeSwitch(mode as any)) }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 7ba7128bb9..6f76974d89 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -32,9 +32,9 @@ const mockClineProvider = { }, log: vi.fn(), postStateToWebview: vi.fn(), - getCurrentCline: vi.fn(), + getCurrentTask: vi.fn(), getTaskWithId: vi.fn(), - initClineWithHistoryItem: vi.fn(), + createTaskWithHistoryItem: vi.fn(), } as unknown as ClineProvider import { t } from "../../../i18n" @@ -533,7 +533,7 @@ describe("webviewMessageHandler - message dialog preferences", () => { beforeEach(() => { vi.clearAllMocks() // Mock a current Cline instance - vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({ + vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue({ taskId: "test-task-id", apiConversationHistory: [], clineMessages: [], @@ -544,7 +544,7 @@ describe("webviewMessageHandler - message dialog preferences", () => { describe("deleteMessage", () => { it("should always show dialog for delete confirmation", async () => { - vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists + vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue({} as any) await webviewMessageHandler(mockClineProvider, { type: "deleteMessage", @@ -560,12 +560,12 @@ describe("webviewMessageHandler - message dialog preferences", () => { describe("submitEditedMessage", () => { it("should always show dialog for edit confirmation", async () => { - vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists + vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue({} as any) await webviewMessageHandler(mockClineProvider, { type: "submitEditedMessage", - value: 123456789, // messageTs as number - editedMessageContent: "edited content", // text content in editedMessageContent field + value: 123456789, + editedMessageContent: "edited content", }) expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ diff --git a/src/core/webview/generateSystemPrompt.ts b/src/core/webview/generateSystemPrompt.ts index b8e87a1d4a..9b96dbedbb 100644 --- a/src/core/webview/generateSystemPrompt.ts +++ b/src/core/webview/generateSystemPrompt.ts @@ -42,7 +42,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web const mode = message.mode ?? defaultModeSlug const customModes = await provider.customModesManager.getCustomModes() - const rooIgnoreInstructions = provider.getCurrentCline()?.rooIgnoreController?.getInstructions() + const rooIgnoreInstructions = provider.getCurrentTask()?.rooIgnoreController?.getInstructions() // Determine if browser tools can be used based on model support, mode, and user settings let modelSupportsComputerUse = false diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 7e25ae14dc..d16ed06132 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -111,9 +111,9 @@ export const webviewMessageHandler = async ( * Handles confirmed message deletion from webview dialog */ const handleDeleteMessageConfirm = async (messageTs: number): Promise => { - // Only proceed if we have a current cline - if (provider.getCurrentCline()) { - const currentCline = provider.getCurrentCline()! + // Only proceed if we have a current task. + if (provider.getCurrentTask()) { + const currentCline = provider.getCurrentTask()! const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline) if (messageIndex !== -1) { @@ -124,7 +124,7 @@ export const webviewMessageHandler = async ( await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex) // Initialize with history item after deletion - await provider.initClineWithHistoryItem(historyItem) + await provider.createTaskWithHistoryItem(historyItem) } catch (error) { console.error("Error in delete message:", error) vscode.window.showErrorMessage( @@ -156,9 +156,9 @@ export const webviewMessageHandler = async ( editedContent: string, images?: string[], ): Promise => { - // Only proceed if we have a current cline - if (provider.getCurrentCline()) { - const currentCline = provider.getCurrentCline()! + // Only proceed if we have a current task. + if (provider.getCurrentTask()) { + const currentCline = provider.getCurrentTask()! // Use findMessageIndices to find messages based on timestamp const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline) @@ -288,7 +288,7 @@ export const webviewMessageHandler = async ( // Initializing new instance of Cline will make sure that any // agentically running promises in old instance don't affect our new // task. This essentially creates a fresh slate for the new task. - await provider.initClineWithTask(message.text, message.images) + await provider.createTask(message.text, message.images) break case "customInstructions": await provider.updateCustomInstructions(message.text) @@ -346,7 +346,7 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() break case "askResponse": - provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) + provider.getCurrentTask()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) break case "autoCondenseContext": await updateGlobalState("autoCondenseContext", message.bool) @@ -358,19 +358,23 @@ export const webviewMessageHandler = async ( break case "terminalOperation": if (message.terminalOperation) { - provider.getCurrentCline()?.handleTerminalOperation(message.terminalOperation) + provider.getCurrentTask()?.handleTerminalOperation(message.terminalOperation) } break case "clearTask": - // clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed - // Check if the current task actually has a parent task - const currentTask = provider.getCurrentCline() + // Clear task resets the current session and allows for a new task + // to be started, if this session is a subtask - it allows the + // parent task to be resumed. + // Check if the current task actually has a parent task. + const currentTask = provider.getCurrentTask() + if (currentTask && currentTask.parentTask) { await provider.finishSubTask(t("common:tasks.canceled")) } else { // Regular task - just clear it await provider.clearTask() } + await provider.postStateToWebview() break case "didShowAnnouncement": @@ -387,14 +391,15 @@ export const webviewMessageHandler = async ( }) break case "exportCurrentTask": - const currentTaskId = provider.getCurrentCline()?.taskId + const currentTaskId = provider.getCurrentTask()?.taskId if (currentTaskId) { provider.exportTaskWithId(currentTaskId) } break case "shareCurrentTask": - const shareTaskId = provider.getCurrentCline()?.taskId - const clineMessages = provider.getCurrentCline()?.clineMessages + const shareTaskId = provider.getCurrentTask()?.taskId + const clineMessages = provider.getCurrentTask()?.clineMessages + if (!shareTaskId) { vscode.window.showErrorMessage(t("common:errors.share_no_active_task")) break @@ -725,7 +730,7 @@ export const webviewMessageHandler = async ( const result = checkoutDiffPayloadSchema.safeParse(message.payload) if (result.success) { - await provider.getCurrentCline()?.checkpointDiff(result.data) + await provider.getCurrentTask()?.checkpointDiff(result.data) } break @@ -736,13 +741,13 @@ export const webviewMessageHandler = async ( await provider.cancelTask() try { - await pWaitFor(() => provider.getCurrentCline()?.isInitialized === true, { timeout: 3_000 }) + await pWaitFor(() => provider.getCurrentTask()?.isInitialized === true, { timeout: 3_000 }) } catch (error) { vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout")) } try { - await provider.getCurrentCline()?.checkpointRestore(result.data) + await provider.getCurrentTask()?.checkpointRestore(result.data) } catch (error) { vscode.window.showErrorMessage(t("common:errors.checkpoint_failed")) } @@ -1227,14 +1232,14 @@ export const webviewMessageHandler = async ( } break case "deleteMessage": { - if (provider.getCurrentCline() && typeof message.value === "number" && message.value) { + if (provider.getCurrentTask() && typeof message.value === "number" && message.value) { await handleMessageModificationsOperation(message.value, "delete") } break } case "submitEditedMessage": { if ( - provider.getCurrentCline() && + provider.getCurrentTask() && typeof message.value === "number" && message.value && message.editedMessageContent @@ -1366,6 +1371,7 @@ export const webviewMessageHandler = async ( if (message.text) { try { const state = await provider.getState() + const { apiConfiguration, customSupportPrompts, @@ -1374,7 +1380,8 @@ export const webviewMessageHandler = async ( includeTaskHistoryInEnhance, } = state - const currentCline = provider.getCurrentCline() + const currentCline = provider.getCurrentTask() + const result = await MessageEnhancer.enhanceMessage({ text: message.text, apiConfiguration, diff --git a/src/extension/api.ts b/src/extension/api.ts index 49710c32e4..f419613b8e 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -148,7 +148,7 @@ export class API extends EventEmitter implements RooCodeAPI { await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images }) - const cline = await provider.initClineWithTask(text, images, undefined, { + const cline = await provider.createTask(text, images, undefined, { consecutiveMistakeLimit: Number.MAX_SAFE_INTEGER, }) @@ -161,7 +161,7 @@ export class API extends EventEmitter implements RooCodeAPI { public async resumeTask(taskId: string): Promise { const { historyItem } = await this.sidebarProvider.getTaskWithId(taskId) - await this.sidebarProvider.initClineWithHistoryItem(historyItem) + await this.sidebarProvider.createTaskWithHistoryItem(historyItem) await this.sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) } diff --git a/src/package.json b/src/package.json index 084e42e08e..296e54abc7 100644 --- a/src/package.json +++ b/src/package.json @@ -427,7 +427,7 @@ "@mistralai/mistralai": "^1.3.6", "@modelcontextprotocol/sdk": "^1.9.0", "@qdrant/js-client-rest": "^1.14.0", - "@roo-code/cloud": "^0.14.0", + "@roo-code/cloud": "^0.15.0", "@roo-code/ipc": "workspace:^", "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", diff --git a/src/utils/__tests__/git.spec.ts b/src/utils/__tests__/git.spec.ts index f87ae5667b..16b404f9e0 100644 --- a/src/utils/__tests__/git.spec.ts +++ b/src/utils/__tests__/git.spec.ts @@ -1,5 +1,4 @@ import { ExecException } from "child_process" -import * as vscode from "vscode" import * as fs from "fs" import * as path from "path" @@ -12,7 +11,6 @@ import { sanitizeGitUrl, extractRepositoryName, getWorkspaceGitInfo, - GitRepositoryInfo, convertGitUrlToHttps, } from "../git" import { truncateOutput } from "../../integrations/misc/extract-text" diff --git a/src/utils/git.ts b/src/utils/git.ts index 42d069416e..3bb562bf43 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -196,7 +196,7 @@ export async function getWorkspaceGitInfo(): Promise { return {} } - // Use the first workspace folder + // Use the first workspace folder. const workspaceRoot = workspaceFolders[0].uri.fsPath return getGitRepositoryInfo(workspaceRoot) }