first attempt

This commit is contained in:
Will Li 2025-08-14 17:19:44 -07:00
parent f9e85a5e84
commit 6ac68a765b
5 changed files with 138 additions and 19 deletions

View file

@ -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<TaskLike>
initClineWithTask(
text?: string,
images?: string[],
parentTask?: TaskLike,
options?: InitTaskOptions,
): Promise<TaskLike>
cancelTask(): Promise<void>
clearTask(): Promise<void>
postStateToWebview(): Promise<void>

View file

@ -285,15 +285,20 @@ export class Task extends EventEmitter<TaskEvents> 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<TaskEvents> implements TaskLike {
// Getters
public get cwd() {
if (!this.workspacePath) {
// Return a fallback to prevent crashes
return path.join(os.homedir(), "Desktop")
}
return this.workspacePath
}
}

View file

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

View file

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

View file

@ -97,11 +97,13 @@ export class API extends EventEmitter<RooCodeEvents> 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<RooCodeEvents> implements RooCodeAPI {
const cline = await provider.initClineWithTask(text, images, undefined, {
consecutiveMistakeLimit: Number.MAX_SAFE_INTEGER,
mode_slug,
})
if (!cline) {