diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts index a4a2f5f0fc..20e41acd1f 100644 --- a/packages/types/src/task.ts +++ b/packages/types/src/task.ts @@ -10,13 +10,26 @@ export interface TaskProviderState { mode?: string } +export interface InitTaskOptions { + mode_slug?: string + enableDiff?: boolean + enableCheckpoints?: boolean + fuzzyMatchThreshold?: number + consecutiveMistakeLimit?: number + experiments?: any +} export interface TaskProviderLike { readonly cwd: string getCurrentCline(): TaskLike | undefined getCurrentTaskStack(): string[] - initClineWithTask(text?: string, images?: string[], parentTask?: TaskLike): Promise + initClineWithTask( + text?: string, + images?: string[], + parentTask?: TaskLike, + options?: InitTaskOptions, + ): Promise cancelTask(): Promise clearTask(): Promise postStateToWebview(): Promise diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index cb6694b7f0..6dfb242109 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -285,15 +285,20 @@ export class Task extends EventEmitter implements TaskLike { } this.taskId = historyItem ? historyItem.id : crypto.randomUUID() - - // Normal use-case is usually retry similar history task with new workspace. - this.workspacePath = parentTask - ? parentTask.workspacePath - : getWorkspacePath(path.join(os.homedir(), "Desktop")) - this.instanceId = crypto.randomUUID().slice(0, 8) this.taskNumber = -1 + // Initialize workspacePath FIRST before any code that uses this.cwd + // This MUST happen before creating RooIgnoreController or RooProtectedController + const defaultPath = path.join(os.homedir(), "Desktop") + const workspaceFromVSCode = getWorkspacePath(defaultPath) + + // Ensure workspacePath is never undefined or empty - use the VSCode workspace or fallback + // Check for both undefined and empty string from parentTask + const parentWorkspace = parentTask?.workspacePath + this.workspacePath = parentWorkspace && parentWorkspace.trim() !== "" ? parentWorkspace : workspaceFromVSCode + + // Now create controllers with properly initialized workspacePath this.rooIgnoreController = new RooIgnoreController(this.cwd) this.rooProtectedController = new RooProtectedController(this.cwd) this.fileContextTracker = new FileContextTracker(provider, this.taskId) @@ -2468,6 +2473,10 @@ export class Task extends EventEmitter implements TaskLike { // Getters public get cwd() { + if (!this.workspacePath) { + // Return a fallback to prevent crashes + return path.join(os.homedir(), "Desktop") + } return this.workspacePath } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index e6817e1825..677fdab543 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -25,6 +25,7 @@ import { type TerminalActionPromptType, type HistoryItem, type CloudUserInfo, + type InitTaskOptions, RooCodeEventName, requestyDefaultModelId, openRouterDefaultModelId, @@ -77,7 +78,7 @@ import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../api/provi import { ContextProxy } from "../config/ContextProxy" import { ProviderSettingsManager } from "../config/ProviderSettingsManager" import { CustomModesManager } from "../config/CustomModesManager" -import { Task, TaskOptions } from "../task/Task" +import { Task } from "../task/Task" import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt" import { webviewMessageHandler } from "./webviewMessageHandler" @@ -698,17 +699,7 @@ export class ClineProvider // 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( - text?: string, - images?: string[], - parentTask?: Task, - options: Partial< - Pick< - TaskOptions, - "enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments" - > - > = {}, - ) { + public async initClineWithTask(text?: string, images?: string[], parentTask?: Task, options: InitTaskOptions = {}) { const { apiConfiguration, organizationAllowList, @@ -724,6 +715,31 @@ export class ClineProvider throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist")) } + // Bridge: If a mode_slug is provided by the cloud extension bridge, honor it before creating the task + // This ensures the task initializes with the correct mode and associated provider profile + try { + const modeSlugFromBridge: string | undefined = (options as any)?.mode_slug + if (typeof modeSlugFromBridge === "string" && modeSlugFromBridge.trim().length > 0) { + const customModes = await this.customModesManager.getCustomModes() + const targetMode = getModeBySlug(modeSlugFromBridge, customModes) + if (targetMode) { + // Switch provider/global mode first so Task reads it during initialization + await this.handleModeSwitch(targetMode.slug) + this.log(`[initClineWithTask] Applied mode from bridge: '${targetMode.slug}'`) + } else { + this.log( + `[initClineWithTask] Ignoring invalid mode_slug from bridge: '${modeSlugFromBridge}'. Falling back to current mode.`, + ) + } + } + } catch (err) { + this.log( + `[initClineWithTask] Failed to apply mode_slug from bridge: ${ + err instanceof Error ? err.message : String(err) + }`, + ) + } + const task = new Task({ provider: this, apiConfiguration, diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index eeae44451d..33e8853deb 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2206,6 +2206,84 @@ describe("ClineProvider", () => { }) }) }) +describe("Bridge mode_slug handling", () => { + let provider: ClineProvider + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockWebviewView: vscode.WebviewView + + beforeEach(() => { + vi.clearAllMocks() + + mockContext = { + extensionPath: "/test/path", + extensionUri: {} as vscode.Uri, + globalState: { + get: vi.fn(), + update: vi.fn(), + keys: vi.fn().mockReturnValue([]), + }, + secrets: { + get: vi.fn(), + store: vi.fn(), + delete: vi.fn(), + }, + subscriptions: [], + extension: { + packageJSON: { version: "1.0.0" }, + }, + globalStorageUri: { + fsPath: "/test/storage/path", + }, + } as unknown as vscode.ExtensionContext + + mockOutputChannel = { + appendLine: vi.fn(), + clear: vi.fn(), + dispose: vi.fn(), + } as unknown as vscode.OutputChannel + + mockWebviewView = { + webview: { + postMessage: vi.fn(), + html: "", + options: {}, + onDidReceiveMessage: vi.fn(), + asWebviewUri: vi.fn(), + cspSource: "vscode-webview://test-csp-source", + }, + visible: true, + onDidDispose: vi.fn(), + onDidChangeVisibility: vi.fn(), + } as unknown as vscode.WebviewView + + provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext)) + }) + + it("applies mode_slug from bridge options when starting task", async () => { + await provider.resolveWebviewView(mockWebviewView) + + // Spy on handleModeSwitch to ensure it's invoked with the bridge-provided mode + const handleModeSwitchSpy = vi.spyOn(provider, "handleModeSwitch").mockResolvedValue(undefined as any) + + // Ensure getModeBySlug returns a valid mode for the provided slug + const { getModeBySlug } = await import("../../../shared/modes") + vi.mocked(getModeBySlug).mockReturnValueOnce({ + slug: "architect", + name: "Architect Mode", + roleDefinition: "You are an architect", + groups: ["read", "edit"] as any, + } as any) + + // Pass mode_slug through the options object (as provided by the bridge package) + await provider.initClineWithTask("Started from bridge", undefined, undefined, { + experiments: {}, + mode_slug: "architect", + } as any) + + expect(handleModeSwitchSpy).toHaveBeenCalledWith("architect") + }) +}) describe("Project MCP Settings", () => { let provider: ClineProvider diff --git a/src/extension/api.ts b/src/extension/api.ts index 49710c32e4..324f2fc476 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -97,11 +97,13 @@ export class API extends EventEmitter implements RooCodeAPI { text, images, newTab, + mode_slug, }: { configuration: RooCodeSettings text?: string images?: string[] newTab?: boolean + mode_slug?: string }) { let provider: ClineProvider @@ -150,6 +152,7 @@ export class API extends EventEmitter implements RooCodeAPI { const cline = await provider.initClineWithTask(text, images, undefined, { consecutiveMistakeLimit: Number.MAX_SAFE_INTEGER, + mode_slug, }) if (!cline) {