Add support for task page event population (#7117)

This commit is contained in:
Chris Estreich 2025-08-14 20:17:05 -10:00 committed by GitHub
parent 0d0bba2eb7
commit 44086e4a86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 276 additions and 167 deletions

View file

@ -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",

View file

@ -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<TaskLike>
createTask(text?: string, images?: string[], parentTask?: TaskLike): Promise<TaskLike>
cancelTask(): Promise<void>
clearTask(): Promise<void>
postStateToWebview(): Promise<void>
getState(): Promise<TaskProviderState>
postStateToWebview(): Promise<void>
postMessageToWebview(message: unknown): Promise<void>
getTelemetryProperties(): Promise<TelemetryProperties>
on<K extends keyof TaskProviderEvents>(
event: K,
listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
@ -36,14 +41,6 @@ export interface TaskProviderLike {
event: K,
listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
): this
context: {
extension?: {
packageJSON?: {
version?: string
}
}
}
}
export type TaskProviderEvents = {

View file

@ -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<typeof staticAppPropertiesSchema>
export const dynamicAppPropertiesSchema = z.object({
language: z.string(),
mode: z.string(),
})
export type DynamicAppProperties = z.infer<typeof dynamicAppPropertiesSchema>
export const cloudAppPropertiesSchema = z.object({
cloudIsAuthenticated: z.boolean().optional(),
})
export type CloudAppProperties = z.infer<typeof cloudAppPropertiesSchema>
export const appPropertiesSchema = z.object({
...staticAppPropertiesSchema.shape,
...dynamicAppPropertiesSchema.shape,
...cloudAppPropertiesSchema.shape,
})
export type AppProperties = z.infer<typeof appPropertiesSchema>
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<typeof taskPropertiesSchema>
export const gitPropertiesSchema = z.object({
repositoryUrl: z.string().optional(),
repositoryName: z.string().optional(),
defaultBranch: z.string().optional(),
})
export type GitProperties = z.infer<typeof gitPropertiesSchema>
export const telemetryPropertiesSchema = z.object({
...appPropertiesSchema.shape,
...taskPropertiesSchema.shape,
@ -112,7 +136,6 @@ export const telemetryPropertiesSchema = z.object({
})
export type TelemetryProperties = z.infer<typeof telemetryPropertiesSchema>
export type GitProperties = z.infer<typeof gitPropertiesSchema>
/**
* TelemetryEvent

18
pnpm-lock.yaml generated
View file

@ -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

View file

@ -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)

View file

@ -1951,7 +1951,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const history = await provider?.getTaskWithId(this.taskId)
if (history) {
await provider?.initClineWithHistoryItem(history.historyItem)
await provider?.createTaskWithHistoryItem(history.historyItem)
}
}
} finally {

View file

@ -22,7 +22,7 @@ const mockAskApproval = vi.fn<AskApproval>()
const mockHandleError = vi.fn<HandleError>()
const mockPushToolResult = vi.fn()
const mockRemoveClosingTag = vi.fn((_name: string, value: string | undefined) => value ?? "")
const mockInitClineWithTask = vi.fn<() => Promise<MockClineInstance>>().mockResolvedValue({ taskId: "mock-subtask-id" })
const mockCreateTask = vi.fn<() => Promise<MockClineInstance>>().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,

View file

@ -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

View file

@ -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<Task, Array<() => 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<TelemetryProperties> {
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<DynamicAppProperties & TaskProperties> {
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<GitProperties> {
if (!this._gitProperties) {
this._gitProperties = await getWorkspaceGitInfo()
}
return this._gitProperties
}
public get gitProperties(): GitProperties | undefined {
return this._gitProperties
}
public async getTelemetryProperties(): Promise<TelemetryProperties> {
return {
...this.getAppProperties(),
...this.getCloudProperties(),
...(await this.getTaskProperties()),
...(await this.getGitProperties()),
}
}

View file

@ -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()
})

View file

@ -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<void>[] = []
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))
})

View file

@ -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({

View file

@ -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

View file

@ -111,9 +111,9 @@ export const webviewMessageHandler = async (
* Handles confirmed message deletion from webview dialog
*/
const handleDeleteMessageConfirm = async (messageTs: number): Promise<void> => {
// 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<void> => {
// 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,

View file

@ -148,7 +148,7 @@ export class API extends EventEmitter<RooCodeEvents> 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<RooCodeEvents> implements RooCodeAPI {
public async resumeTask(taskId: string): Promise<void> {
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" })
}

View file

@ -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:^",

View file

@ -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"

View file

@ -196,7 +196,7 @@ export async function getWorkspaceGitInfo(): Promise<GitRepositoryInfo> {
return {}
}
// Use the first workspace folder
// Use the first workspace folder.
const workspaceRoot = workspaceFolders[0].uri.fsPath
return getGitRepositoryInfo(workspaceRoot)
}