Skip to content
13 changes: 11 additions & 2 deletions packages/types/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 Experiments } from "./experiment.js"
import type { StaticAppProperties, GitProperties, TelemetryProperties } from "./telemetry.js"

/**
Expand All @@ -13,6 +14,14 @@ export interface TaskProviderState {
mode?: string
}

export interface CreateTaskOptions {
modeSlug?: string
enableDiff?: boolean
enableCheckpoints?: boolean
fuzzyMatchThreshold?: number
consecutiveMistakeLimit?: number
experiments?: Experiments
}
export interface TaskProviderLike {
readonly cwd: string
readonly appProperties: StaticAppProperties
Expand All @@ -22,7 +31,7 @@ export interface TaskProviderLike {
getCurrentTaskStack(): string[]
getRecentTasks(): string[]

createTask(text?: string, images?: string[], parentTask?: TaskLike): Promise<TaskLike>
createTask(text?: string, images?: string[], parentTask?: TaskLike, options?: CreateTaskOptions): Promise<TaskLike>
cancelTask(): Promise<void>
clearTask(): Promise<void>

Expand Down Expand Up @@ -78,7 +87,7 @@ export interface TaskLike {
off<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this

setMessageResponse(text: string, images?: string[]): void
submitUserMessage(text: string, images?: string[]): void
submitUserMessage(text: string, images?: string[], modeSlug?: string): void
}

export type TaskEvents = {
Expand Down
60 changes: 48 additions & 12 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { t } from "../../i18n"
import { ClineApiReqCancelReason, ClineApiReqInfo } from "../../shared/ExtensionMessage"
import { getApiMetrics } from "../../shared/getApiMetrics"
import { ClineAskResponse } from "../../shared/WebviewMessage"
import { defaultModeSlug } from "../../shared/modes"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
import { DiffStrategy } from "../../shared/tools"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { getModelMaxOutputTokens } from "../../shared/api"
Expand Down Expand Up @@ -303,6 +303,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
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)
Expand Down Expand Up @@ -757,27 +768,52 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.askResponseImages = images
}

public submitUserMessage(text: string, images?: string[]): void {
public submitUserMessage(text: string, images?: string[], modeSlug?: string): void {
try {
const trimmed = (text ?? "").trim()
const imgs = images ?? []

if (!trimmed && imgs.length === 0) {
return
}

const provider = this.providerRef.deref()
if (!provider) {
console.error("[Task#submitUserMessage] Provider reference lost")
return
}

void provider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: trimmed,
images: imgs,
})
// Run asynchronously to allow awaiting mode switch before sending the message
void (async () => {
// If a mode slug is provided, handle the mode switch first (same behavior as createTask)
try {
const modeSlugValue = (modeSlug ?? "").trim()
if (modeSlugValue.length > 0) {
const customModes = await provider.customModesManager.getCustomModes()
const targetMode = getModeBySlug(modeSlugValue, customModes)
if (targetMode) {
await provider.handleModeSwitch(targetMode.slug)
provider.log(`[Task#submitUserMessage] Applied mode from modeSlug: '${targetMode.slug}'`)
} else {
provider.log(`[Task#submitUserMessage] Ignoring invalid modeSlug: '${modeSlugValue}'.`)
}
}
} catch (err) {
provider.log(
`[Task#submitUserMessage] Failed to apply modeSlug: ${
err instanceof Error ? err.message : String(err)
}`,
)
}

// If there's no content to send, exit after potential mode switch
if (!trimmed && imgs.length === 0) {
return
}

await provider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: trimmed,
images: imgs,
})
})()
} catch (error) {
console.error("[Task#submitUserMessage] Failed to submit user message:", error)
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/tools/codebaseSearchTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function codebaseSearchTool(
removeClosingTag: RemoveClosingTag,
) {
const toolName = "codebase_search"
const workspacePath = (cline.cwd && cline.cwd.trim() !== '') ? cline.cwd : getWorkspacePath()
const workspacePath = cline.cwd && cline.cwd.trim() !== "" ? cline.cwd : getWorkspacePath()

if (!workspacePath) {
// This case should ideally not happen if Cline is initialized correctly
Expand Down
40 changes: 28 additions & 12 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
type TerminalActionId,
type TerminalActionPromptType,
type HistoryItem,
type CreateTaskOptions,
RooCodeEventName,
requestyDefaultModelId,
openRouterDefaultModelId,
Expand Down Expand Up @@ -80,7 +81,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"
Expand Down Expand Up @@ -736,17 +737,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 createTask(
text?: string,
images?: string[],
parentTask?: Task,
options: Partial<
Pick<
TaskOptions,
"enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments"
>
> = {},
) {
public async createTask(text?: string, images?: string[], parentTask?: Task, options: CreateTaskOptions = {}) {
const {
apiConfiguration,
organizationAllowList,
Expand All @@ -762,6 +753,31 @@ export class ClineProvider
throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
}

// Bridge: If a modeSlug 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)?.modeSlug
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(`[createTask] Applied mode from bridge: '${targetMode.slug}'`)
} else {
this.log(
`[createTask] Ignoring invalid modeSlug from bridge: '${modeSlugFromBridge}'. Falling back to current mode.`,
)
}
}
} catch (err) {
this.log(
`[createTask] Failed to apply modeSlug from bridge: ${
err instanceof Error ? err.message : String(err)
}`,
)
}

const task = new Task({
provider: this,
apiConfiguration,
Expand Down
78 changes: 78 additions & 0 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2208,6 +2208,84 @@ describe("ClineProvider", () => {
})
})
})
describe("Bridge modeSlug 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 modeSlug 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 modeSlug through the options object (as provided by the bridge package)
await provider.createTask("Started from bridge", undefined, undefined, {
experiments: {},
modeSlug: "architect",
} as any)

expect(handleModeSwitchSpy).toHaveBeenCalledWith("architect")
})
})

describe("Project MCP Settings", () => {
let provider: ClineProvider
Expand Down
3 changes: 3 additions & 0 deletions src/extension/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
text,
images,
newTab,
modeSlug,
}: {
configuration: RooCodeSettings
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't RooCodeSettings allow you to set the mode? Or am I misremembering?

text?: string
images?: string[]
newTab?: boolean
modeSlug?: string
}) {
let provider: ClineProvider

Expand Down Expand Up @@ -150,6 +152,7 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {

const cline = await provider.createTask(text, images, undefined, {
consecutiveMistakeLimit: Number.MAX_SAFE_INTEGER,
modeSlug,
})

if (!cline) {
Expand Down
Loading