Просмотр исходного кода

Merge pull request #1579 from RooVetGit/cte/context-proxy-fixes

ContextProxy fix - constructor should not be async
Chris Estreich 9 месяцев назад
Родитель
Сommit
ae6903b26c

+ 5 - 0
.changeset/seven-apricots-watch.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+ContextProxy fix - constructor should not be async

+ 2 - 2
.github/workflows/code-qa.yml

@@ -110,9 +110,9 @@ jobs:
           cache: 'npm'
       - name: Install dependencies
         run: npm run install:all
-      - name: Create env.integration file
+      - name: Create .env.local file
         working-directory: e2e
-        run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.integration
+        run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.local
       - name: Run integration tests
         working-directory: e2e
         run: xvfb-run -a npm run ci

+ 3 - 1
.gitignore

@@ -22,7 +22,9 @@ docs/_site/
 
 # Dotenv
 .env
-.env.integration
+.env.*
+!.env.*.sample
+
 
 #Local lint config
 .eslintrc.local.json

+ 0 - 0
e2e/.env.integration.example → e2e/.env.local.sample


+ 11 - 5
e2e/VSCODE_INTEGRATION_TESTS.md

@@ -30,7 +30,7 @@ The test runner (`runTest.ts`) is responsible for:
 
 ### Environment Setup
 
-1. Create a `.env.integration` file in the root directory with required environment variables:
+1. Create a `.env.local` file in the root directory with required environment variables:
 
 ```
 OPENROUTER_API_KEY=sk-or-v1-...
@@ -67,7 +67,7 @@ declare global {
 
 ## Running Tests
 
-1. Ensure you have the required environment variables set in `.env.integration`
+1. Ensure you have the required environment variables set in `.env.local`
 
 2. Run the integration tests:
 
@@ -117,8 +117,10 @@ const interval = 1000
 2. **State Management**: Reset extension state before/after tests:
 
 ```typescript
-await globalThis.provider.updateGlobalState("mode", "Ask")
-await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true)
+await globalThis.api.setConfiguration({
+	mode: "Ask",
+	alwaysAllowModeSwitch: true,
+})
 ```
 
 3. **Assertions**: Use clear assertions with meaningful messages:
@@ -141,8 +143,12 @@ try {
 
 ```typescript
 let startTime = Date.now()
+
 while (Date.now() - startTime < timeout) {
-	if (condition) break
+	if (condition) {
+		break
+	}
+
 	await new Promise((resolve) => setTimeout(resolve, interval))
 }
 ```

+ 5 - 4
e2e/package.json

@@ -3,12 +3,13 @@
 	"version": "0.1.0",
 	"private": true,
 	"scripts": {
-		"build": "cd .. && npm run build",
-		"compile": "tsc -p tsconfig.json",
+		"build": "cd .. && npm run compile && npm run build:webview",
+		"compile": "rm -rf out && tsc -p tsconfig.json",
 		"lint": "eslint src --ext ts",
 		"check-types": "tsc --noEmit",
-		"test": "npm run compile && npx dotenvx run -f .env.integration -- node ./out/runTest.js",
-		"ci": "npm run build && npm run test"
+		"test": "npm run compile && npx dotenvx run -f .env.local -- node ./out/runTest.js",
+		"ci": "npm run build && npm run test",
+		"clean": "rimraf out"
 	},
 	"dependencies": {},
 	"devDependencies": {

+ 19 - 53
e2e/src/suite/index.ts

@@ -1,80 +1,46 @@
 import * as path from "path"
 import Mocha from "mocha"
 import { glob } from "glob"
-import { RooCodeAPI, ClineProvider } from "../../../src/exports/roo-code"
 import * as vscode from "vscode"
 
+import { RooCodeAPI } from "../../../src/exports/roo-code"
+
+import { waitUntilReady } from "./utils"
+
 declare global {
-	var api: RooCodeAPI
-	var provider: ClineProvider
 	var extension: vscode.Extension<RooCodeAPI> | undefined
-	var panel: vscode.WebviewPanel | undefined
+	var api: RooCodeAPI
 }
 
-export async function run(): Promise<void> {
-	const mocha = new Mocha({
-		ui: "tdd",
-		timeout: 600000, // 10 minutes to compensate for time communicating with LLM while running in GHA.
-	})
-
+export async function run() {
+	const mocha = new Mocha({ ui: "tdd", timeout: 300_000 })
 	const testsRoot = path.resolve(__dirname, "..")
 
 	try {
 		// Find all test files.
 		const files = await glob("**/**.test.js", { cwd: testsRoot })
-
-		// Add files to the test suite.
 		files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f)))
 
-		// Set up global extension, api, provider, and panel.
-		globalThis.extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
+		const extension = vscode.extensions.getExtension<RooCodeAPI>("RooVeterinaryInc.roo-cline")
 
-		if (!globalThis.extension) {
+		if (!extension) {
 			throw new Error("Extension not found")
 		}
 
-		globalThis.api = globalThis.extension.isActive
-			? globalThis.extension.exports
-			: await globalThis.extension.activate()
-
-		globalThis.provider = globalThis.api.sidebarProvider
+		const api = extension.isActive ? extension.exports : await extension.activate()
 
-		await globalThis.provider.updateGlobalState("apiProvider", "openrouter")
-		await globalThis.provider.updateGlobalState("openRouterModelId", "anthropic/claude-3.5-sonnet")
-
-		await globalThis.provider.storeSecret(
-			"openRouterApiKey",
-			process.env.OPENROUTER_API_KEY || "sk-or-v1-fake-api-key",
-		)
-
-		globalThis.panel = vscode.window.createWebviewPanel(
-			"roo-cline.SidebarProvider",
-			"Roo Code",
-			vscode.ViewColumn.One,
-			{
-				enableScripts: true,
-				enableCommandUris: true,
-				retainContextWhenHidden: true,
-				localResourceRoots: [globalThis.extension?.extensionUri],
-			},
-		)
-
-		await globalThis.provider.resolveWebviewView(globalThis.panel)
-
-		let startTime = Date.now()
-		const timeout = 60000
-		const interval = 1000
+		await api.setConfiguration({
+			apiProvider: "openrouter",
+			openRouterApiKey: process.env.OPENROUTER_API_KEY!,
+			openRouterModelId: "anthropic/claude-3.5-sonnet",
+		})
 
-		while (Date.now() - startTime < timeout) {
-			if (globalThis.provider.viewLaunched) {
-				break
-			}
+		await waitUntilReady(api)
 
-			await new Promise((resolve) => setTimeout(resolve, interval))
-		}
+		globalThis.api = api
+		globalThis.extension = extension
 
-		// Run the mocha test.
-		return new Promise((resolve, reject) => {
+		return new Promise<void>((resolve, reject) => {
 			try {
 				mocha.run((failures: number) => {
 					if (failures > 0) {

+ 26 - 68
e2e/src/suite/modes.test.ts

@@ -1,102 +1,60 @@
 import * as assert from "assert"
 
+import { waitForMessage } from "./utils"
+
 suite("Roo Code Modes", () => {
 	test("Should handle switching modes correctly", async function () {
-		const timeout = 30000
-		const interval = 1000
+		const timeout = 300_000
+		const api = globalThis.api
 
 		const testPrompt =
-			"For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode, do not start with the current mode, be sure to say 'I AM DONE' after the task is complete"
-
-		if (!globalThis.extension) {
-			assert.fail("Extension not found")
-		}
-
-		let startTime = Date.now()
-
-		// Ensure the webview is launched.
-		while (Date.now() - startTime < timeout) {
-			if (globalThis.provider.viewLaunched) {
-				break
-			}
-
-			await new Promise((resolve) => setTimeout(resolve, interval))
-		}
-
-		await globalThis.provider.updateGlobalState("mode", "Ask")
-		await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true)
-		await globalThis.provider.updateGlobalState("autoApprovalEnabled", true)
-
-		// Start a new task.
-		await globalThis.api.startNewTask(testPrompt)
+			"For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode, do not start with the current mode, be sure to say 'I AM DONE' after the task is complete."
 
-		// Wait for task to appear in history with tokens.
-		startTime = Date.now()
+		await api.setConfiguration({ mode: "Code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true })
+		await api.startNewTask(testPrompt)
 
-		while (Date.now() - startTime < timeout) {
-			const messages = globalThis.provider.messages
+		await waitForMessage(api, { include: "I AM DONE", exclude: "be sure to say", timeout })
 
-			if (
-				messages.some(
-					({ type, text }) =>
-						type === "say" && text?.includes("I AM DONE") && !text?.includes("be sure to say"),
-				)
-			) {
-				break
-			}
-
-			await new Promise((resolve) => setTimeout(resolve, interval))
-		}
-
-		if (globalThis.provider.messages.length === 0) {
+		if (api.getMessages().length === 0) {
 			assert.fail("No messages received")
 		}
 
 		// Log the messages to the console.
-		globalThis.provider.messages.forEach(({ type, text }) => {
+		api.getMessages().forEach(({ type, text }) => {
 			if (type === "say") {
 				console.log(text)
 			}
 		})
 
 		// Start Grading Portion of test to grade the response from 1 to 10.
-		await globalThis.provider.updateGlobalState("mode", "Ask")
-		let output = globalThis.provider.messages.map(({ type, text }) => (type === "say" ? text : "")).join("\n")
-
-		await globalThis.api.startNewTask(
-			`Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output} \n Be sure to say 'I AM DONE GRADING' after the task is complete`,
-		)
-
-		startTime = Date.now()
+		await api.setConfiguration({ mode: "Ask" })
 
-		while (Date.now() - startTime < timeout) {
-			const messages = globalThis.provider.messages
+		let output = api
+			.getMessages()
+			.map(({ type, text }) => (type === "say" ? text : ""))
+			.join("\n")
 
-			if (
-				messages.some(
-					({ type, text }) =>
-						type === "say" && text?.includes("I AM DONE GRADING") && !text?.includes("be sure to say"),
-				)
-			) {
-				break
-			}
+		await api.startNewTask(
+			`Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output}\nBe sure to say 'I AM DONE GRADING' after the task is complete.`,
+		)
 
-			await new Promise((resolve) => setTimeout(resolve, interval))
-		}
+		await waitForMessage(api, { include: "I AM DONE GRADING", exclude: "be sure to say", timeout })
 
-		if (globalThis.provider.messages.length === 0) {
+		if (api.getMessages().length === 0) {
 			assert.fail("No messages received")
 		}
 
-		globalThis.provider.messages.forEach(({ type, text }) => {
+		api.getMessages().forEach(({ type, text }) => {
 			if (type === "say" && text?.includes("Grade:")) {
 				console.log(text)
 			}
 		})
 
-		const gradeMessage = globalThis.provider.messages.find(
-			({ type, text }) => type === "say" && !text?.includes("Grade: (1-10)") && text?.includes("Grade:"),
-		)?.text
+		const gradeMessage = api
+			.getMessages()
+			.find(
+				({ type, text }) => type === "say" && !text?.includes("Grade: (1-10)") && text?.includes("Grade:"),
+			)?.text
 
 		const gradeMatch = gradeMessage?.match(/Grade: (\d+)/)
 		const gradeNum = gradeMatch ? parseInt(gradeMatch[1]) : undefined

+ 58 - 0
e2e/src/suite/subtasks.test.ts

@@ -0,0 +1,58 @@
+import * as assert from "assert"
+
+import { sleep, waitForToolUse, waitForMessage } from "./utils"
+
+suite("Roo Code Subtasks", () => {
+	test.skip("Should handle subtask cancellation and resumption correctly", async function () {
+		const api = globalThis.api
+
+		await api.setConfiguration({
+			mode: "Code",
+			alwaysAllowModeSwitch: true,
+			alwaysAllowSubtasks: true,
+			autoApprovalEnabled: true,
+		})
+
+		// Start a parent task that will create a subtask.
+		await api.startNewTask(
+			"You are the parent task. " +
+				"Create a subtask by using the new_task tool with the message 'You are the subtask'. " +
+				"After creating the subtask, wait for it to complete and then respond with 'Parent task resumed'.",
+		)
+
+		await waitForToolUse(api, "new_task")
+
+		// Cancel the current task (which should be the subtask).
+		await api.cancelTask()
+
+		// Check if the parent task is still waiting (not resumed). We need to
+		// wait a bit to ensure any task resumption would have happened.
+		await sleep(5_000)
+
+		// The parent task should not have resumed yet, so we shouldn't see
+		// "Parent task resumed".
+		assert.ok(
+			!api.getMessages().some(({ type, text }) => type === "say" && text?.includes("Parent task resumed")),
+			"Parent task should not have resumed after subtask cancellation.",
+		)
+
+		// Start a new task with the same message as the subtask.
+		await api.startNewTask("You are the subtask")
+
+		// Wait for the subtask to complete.
+		await waitForMessage(api, { include: "Task complete" })
+
+		// Verify that the parent task is still not resumed. We need to wait a
+		// bit to ensure any task resumption would have happened.
+		await sleep(5_000)
+
+		// The parent task should still not have resumed.
+		assert.ok(
+			!api.getMessages().some(({ type, text }) => type === "say" && text?.includes("Parent task resumed")),
+			"Parent task should not have resumed after subtask completion.",
+		)
+
+		// Clean up - cancel all tasks.
+		await api.cancelTask()
+	})
+})

+ 5 - 156
e2e/src/suite/task.test.ts

@@ -1,161 +1,10 @@
-import * as assert from "assert"
-import * as vscode from "vscode"
+import { waitForMessage } from "./utils"
 
 suite("Roo Code Task", () => {
 	test("Should handle prompt and response correctly", async function () {
-		const timeout = 30000
-		const interval = 1000
-
-		if (!globalThis.extension) {
-			assert.fail("Extension not found")
-		}
-
-		// Ensure the webview is launched.
-		let startTime = Date.now()
-
-		while (Date.now() - startTime < timeout) {
-			if (globalThis.provider.viewLaunched) {
-				break
-			}
-
-			await new Promise((resolve) => setTimeout(resolve, interval))
-		}
-
-		await globalThis.provider.updateGlobalState("mode", "Code")
-		await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true)
-		await globalThis.provider.updateGlobalState("autoApprovalEnabled", true)
-
-		await globalThis.api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'")
-
-		// Wait for task to appear in history with tokens.
-		startTime = Date.now()
-
-		while (Date.now() - startTime < timeout) {
-			const messages = globalThis.provider.messages
-
-			if (messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo"))) {
-				break
-			}
-
-			await new Promise((resolve) => setTimeout(resolve, interval))
-		}
-
-		if (globalThis.provider.messages.length === 0) {
-			assert.fail("No messages received")
-		}
-
-		assert.ok(
-			globalThis.provider.messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo")),
-			"Did not receive expected response containing 'My name is Roo'",
-		)
-	})
-
-	test("Should handle subtask cancellation and resumption correctly", async function () {
-		this.timeout(60000) // Increase timeout for this test
-		const interval = 1000
-
-		if (!globalThis.extension) {
-			assert.fail("Extension not found")
-		}
-
-		// Ensure the webview is launched
-		await ensureWebviewLaunched(30000, interval)
-
-		// Set up required global state
-		await globalThis.provider.updateGlobalState("mode", "Code")
-		await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true)
-		await globalThis.provider.updateGlobalState("alwaysAllowSubtasks", true)
-		await globalThis.provider.updateGlobalState("autoApprovalEnabled", true)
-
-		// 1. Start a parent task that will create a subtask
-		await globalThis.api.startNewTask(
-			"You are the parent task. Create a subtask by using the new_task tool with the message 'You are the subtask'. " +
-				"After creating the subtask, wait for it to complete and then respond with 'Parent task resumed'.",
-		)
-
-		// Wait for the parent task to use the new_task tool
-		await waitForToolUse("new_task", 30000, interval)
-
-		// Wait for the subtask to be created and start responding
-		await waitForMessage("You are the subtask", 10000, interval)
-
-		// 3. Cancel the current task (which should be the subtask)
-		await globalThis.provider.cancelTask()
-
-		// 4. Check if the parent task is still waiting (not resumed)
-		// We need to wait a bit to ensure any task resumption would have happened
-		await new Promise((resolve) => setTimeout(resolve, 5000))
-
-		// The parent task should not have resumed yet, so we shouldn't see "Parent task resumed"
-		assert.ok(
-			!globalThis.provider.messages.some(
-				({ type, text }) => type === "say" && text?.includes("Parent task resumed"),
-			),
-			"Parent task should not have resumed after subtask cancellation",
-		)
-
-		// 5. Start a new task with the same message as the subtask
-		await globalThis.api.startNewTask("You are the subtask")
-
-		// Wait for the subtask to complete
-		await waitForMessage("Task complete", 20000, interval)
-
-		// 6. Verify that the parent task is still not resumed
-		// We need to wait a bit to ensure any task resumption would have happened
-		await new Promise((resolve) => setTimeout(resolve, 5000))
-
-		// The parent task should still not have resumed
-		assert.ok(
-			!globalThis.provider.messages.some(
-				({ type, text }) => type === "say" && text?.includes("Parent task resumed"),
-			),
-			"Parent task should not have resumed after subtask completion",
-		)
-
-		// Clean up - cancel all tasks
-		await globalThis.provider.cancelTask()
+		const api = globalThis.api
+		await api.setConfiguration({ mode: "Ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true })
+		await api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'")
+		await waitForMessage(api, { include: "My name is Roo" })
 	})
 })
-
-// Helper functions
-async function ensureWebviewLaunched(timeout: number, interval: number): Promise<void> {
-	const startTime = Date.now()
-	while (Date.now() - startTime < timeout) {
-		if (globalThis.provider.viewLaunched) {
-			return
-		}
-		await new Promise((resolve) => setTimeout(resolve, interval))
-	}
-	throw new Error("Webview failed to launch within timeout")
-}
-
-async function waitForToolUse(toolName: string, timeout: number, interval: number): Promise<void> {
-	const startTime = Date.now()
-	while (Date.now() - startTime < timeout) {
-		const messages = globalThis.provider.messages
-		if (
-			messages.some(
-				(message) =>
-					message.type === "say" && message.say === "tool" && message.text && message.text.includes(toolName),
-			)
-		) {
-			return
-		}
-		await new Promise((resolve) => setTimeout(resolve, interval))
-	}
-	throw new Error(`Tool ${toolName} was not used within timeout`)
-}
-
-async function waitForMessage(messageContent: string, timeout: number, interval: number): Promise<void> {
-	const startTime = Date.now()
-	while (Date.now() - startTime < timeout) {
-		const messages = globalThis.provider.messages
-		if (
-			messages.some((message) => message.type === "say" && message.text && message.text.includes(messageContent))
-		) {
-			return
-		}
-		await new Promise((resolve) => setTimeout(resolve, interval))
-	}
-	throw new Error(`Message containing "${messageContent}" not found within timeout`)
-}

+ 76 - 0
e2e/src/suite/utils.ts

@@ -0,0 +1,76 @@
+import * as vscode from "vscode"
+
+import { RooCodeAPI } from "../../../src/exports/roo-code"
+
+type WaitForOptions = {
+	timeout?: number
+	interval?: number
+}
+
+export const waitFor = (
+	condition: (() => Promise<boolean>) | (() => boolean),
+	{ timeout = 30_000, interval = 250 }: WaitForOptions = {},
+) => {
+	let timeoutId: NodeJS.Timeout | undefined = undefined
+
+	return Promise.race([
+		new Promise<void>((resolve) => {
+			const check = async () => {
+				const result = condition()
+				const isSatisfied = result instanceof Promise ? await result : result
+
+				if (isSatisfied) {
+					if (timeoutId) {
+						clearTimeout(timeoutId)
+						timeoutId = undefined
+					}
+
+					resolve()
+				} else {
+					setTimeout(check, interval)
+				}
+			}
+
+			check()
+		}),
+		new Promise((_, reject) => {
+			timeoutId = setTimeout(() => {
+				reject(new Error(`Timeout after ${Math.floor(timeout / 1000)}s`))
+			}, timeout)
+		}),
+	])
+}
+
+export const waitUntilReady = async (api: RooCodeAPI, { timeout = 10_000, interval = 250 }: WaitForOptions = {}) => {
+	await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
+	await waitFor(api.isReady, { timeout, interval })
+}
+
+export const waitForToolUse = async (api: RooCodeAPI, toolName: string, options: WaitForOptions = {}) =>
+	waitFor(
+		() =>
+			api
+				.getMessages()
+				.some(({ type, say, text }) => type === "say" && say === "tool" && text && text.includes(toolName)),
+		options,
+	)
+
+export const waitForMessage = async (
+	api: RooCodeAPI,
+	options: WaitForOptions & { include: string; exclude?: string },
+) =>
+	waitFor(
+		() =>
+			api
+				.getMessages()
+				.some(
+					({ type, text }) =>
+						type === "say" &&
+						text &&
+						text.includes(options.include) &&
+						(!options.exclude || !text.includes(options.exclude)),
+				),
+		options,
+	)
+
+export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

+ 2 - 4
knip.json

@@ -13,12 +13,10 @@
 		"dist/**",
 		"out/**",
 		"bin/**",
+		"e2e/**",
 		"src/activate/**",
 		"src/exports/**",
-		"src/extension.ts",
-		"e2e/.vscode-test.mjs",
-		"e2e/src/runTest.ts",
-		"e2e/src/suite/index.ts"
+		"src/extension.ts"
 	],
 	"workspaces": {
 		"webview-ui": {

+ 23 - 18
src/activate/createRooCodeAPI.ts

@@ -3,24 +3,18 @@ import * as vscode from "vscode"
 import { ClineProvider } from "../core/webview/ClineProvider"
 
 import { RooCodeAPI } from "../exports/roo-code"
+import { ConfigurationValues } from "../shared/globalState"
 
-export function createRooCodeAPI(outputChannel: vscode.OutputChannel, sidebarProvider: ClineProvider): RooCodeAPI {
+export function createRooCodeAPI(outputChannel: vscode.OutputChannel, provider: ClineProvider): RooCodeAPI {
 	return {
-		setCustomInstructions: async (value: string) => {
-			await sidebarProvider.updateCustomInstructions(value)
-			outputChannel.appendLine("Custom instructions set")
-		},
-
-		getCustomInstructions: async () => {
-			return (await sidebarProvider.getGlobalState("customInstructions")) as string | undefined
-		},
-
 		startNewTask: async (task?: string, images?: string[]) => {
 			outputChannel.appendLine("Starting new task")
-			await sidebarProvider.removeClineFromStack()
-			await sidebarProvider.postStateToWebview()
-			await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
-			await sidebarProvider.postMessageToWebview({
+
+			await provider.removeClineFromStack()
+			await provider.postStateToWebview()
+			await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
+
+			await provider.postMessageToWebview({
 				type: "invoke",
 				invoke: "sendMessage",
 				text: task,
@@ -32,12 +26,17 @@ export function createRooCodeAPI(outputChannel: vscode.OutputChannel, sidebarPro
 			)
 		},
 
+		cancelTask: async () => {
+			outputChannel.appendLine("Cancelling current task")
+			await provider.cancelTask()
+		},
+
 		sendMessage: async (message?: string, images?: string[]) => {
 			outputChannel.appendLine(
 				`Sending message: ${message ? `"${message}"` : "undefined"} with ${images?.length || 0} image(s)`,
 			)
 
-			await sidebarProvider.postMessageToWebview({
+			await provider.postMessageToWebview({
 				type: "invoke",
 				invoke: "sendMessage",
 				text: message,
@@ -47,14 +46,20 @@ export function createRooCodeAPI(outputChannel: vscode.OutputChannel, sidebarPro
 
 		pressPrimaryButton: async () => {
 			outputChannel.appendLine("Pressing primary button")
-			await sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
+			await provider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
 		},
 
 		pressSecondaryButton: async () => {
 			outputChannel.appendLine("Pressing secondary button")
-			await sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" })
+			await provider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" })
 		},
 
-		sidebarProvider: sidebarProvider,
+		setConfiguration: async (values: Partial<ConfigurationValues>) => {
+			await provider.setValues(values)
+		},
+
+		isReady: () => provider.viewLaunched,
+
+		getMessages: () => provider.messages,
 	}
 }

+ 25 - 32
src/api/providers/openrouter.ts

@@ -1,6 +1,6 @@
 import { Anthropic } from "@anthropic-ai/sdk"
 import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
-import axios from "axios"
+import axios, { AxiosRequestConfig } from "axios"
 import OpenAI from "openai"
 import delay from "delay"
 
@@ -132,62 +132,55 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 			const delta = chunk.choices[0]?.delta
 
 			if ("reasoning" in delta && delta.reasoning) {
-				yield {
-					type: "reasoning",
-					text: delta.reasoning,
-				} as ApiStreamChunk
+				yield { type: "reasoning", text: delta.reasoning } as ApiStreamChunk
 			}
 
 			if (delta?.content) {
 				fullResponseText += delta.content
-				yield {
-					type: "text",
-					text: delta.content,
-				} as ApiStreamChunk
+				yield { type: "text", text: delta.content } as ApiStreamChunk
 			}
+		}
 
-			// if (chunk.usage) {
-			// 	yield {
-			// 		type: "usage",
-			// 		inputTokens: chunk.usage.prompt_tokens || 0,
-			// 		outputTokens: chunk.usage.completion_tokens || 0,
-			// 	}
-			// }
+		const endpoint = `${this.client.baseURL}/generation?id=${genId}`
+
+		const config: AxiosRequestConfig = {
+			headers: { Authorization: `Bearer ${this.options.openRouterApiKey}` },
+			timeout: 3_000,
 		}
 
-		// Retry fetching generation details.
 		let attempt = 0
+		let lastError: Error | undefined
+		const startTime = Date.now()
 
 		while (attempt++ < 10) {
-			await delay(200) // FIXME: necessary delay to ensure generation endpoint is ready
+			await delay(attempt * 100) // Give OpenRouter some time to produce the generation metadata.
 
 			try {
-				const response = await axios.get(`${this.client.baseURL}/generation?id=${genId}`, {
-					headers: {
-						Authorization: `Bearer ${this.options.openRouterApiKey}`,
-					},
-					timeout: 5_000, // this request hangs sometimes
-				})
-
+				const response = await axios.get(endpoint, config)
 				const generation = response.data?.data
 
 				yield {
 					type: "usage",
-					// cacheWriteTokens: 0,
-					// cacheReadTokens: 0,
-					// openrouter generation endpoint fails often
 					inputTokens: generation?.native_tokens_prompt || 0,
 					outputTokens: generation?.native_tokens_completion || 0,
 					totalCost: generation?.total_cost || 0,
 					fullResponseText,
 				} as OpenRouterApiStreamUsageChunk
 
-				return
-			} catch (error) {
-				// ignore if fails
-				console.error("Error fetching OpenRouter generation details:", error)
+				break
+			} catch (error: unknown) {
+				if (error instanceof Error) {
+					lastError = error
+				}
 			}
 		}
+
+		if (lastError) {
+			console.error(
+				`Failed to fetch OpenRouter generation details after ${Date.now() - startTime}ms (${genId})`,
+				lastError,
+			)
+		}
 	}
 
 	override getModel() {

+ 24 - 35
src/core/__tests__/contextProxy.test.ts

@@ -1,15 +1,11 @@
+// npx jest src/core/__tests__/contextProxy.test.ts
+
 import * as vscode from "vscode"
 import { ContextProxy } from "../contextProxy"
-import { logger } from "../../utils/logging"
-import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../../shared/globalState"
 
-// Mock shared/globalState
-jest.mock("../../shared/globalState", () => ({
-	GLOBAL_STATE_KEYS: ["apiProvider", "apiModelId", "mode"],
-	SECRET_KEYS: ["apiKey", "openAiApiKey"],
-}))
+import { logger } from "../../utils/logging"
+import { GLOBAL_STATE_KEYS, SECRET_KEYS, ConfigurationKey, GlobalStateKey } from "../../shared/globalState"
 
-// Mock VSCode API
 jest.mock("vscode", () => ({
 	Uri: {
 		file: jest.fn((path) => ({ path })),
@@ -27,7 +23,7 @@ describe("ContextProxy", () => {
 	let mockGlobalState: any
 	let mockSecrets: any
 
-	beforeEach(() => {
+	beforeEach(async () => {
 		// Reset mocks
 		jest.clearAllMocks()
 
@@ -58,6 +54,7 @@ describe("ContextProxy", () => {
 
 		// Create proxy instance
 		proxy = new ContextProxy(mockContext)
+		await proxy.initialize()
 	})
 
 	describe("read-only pass-through properties", () => {
@@ -90,10 +87,10 @@ describe("ContextProxy", () => {
 	describe("getGlobalState", () => {
 		it("should return value from cache when it exists", async () => {
 			// Manually set a value in the cache
-			await proxy.updateGlobalState("test-key", "cached-value")
+			await proxy.updateGlobalState("apiProvider", "cached-value")
 
 			// Should return the cached value
-			const result = proxy.getGlobalState("test-key")
+			const result = proxy.getGlobalState("apiProvider")
 			expect(result).toBe("cached-value")
 
 			// Original context should be called once during updateGlobalState
@@ -102,20 +99,20 @@ describe("ContextProxy", () => {
 
 		it("should handle default values correctly", async () => {
 			// No value in cache
-			const result = proxy.getGlobalState("unknown-key", "default-value")
+			const result = proxy.getGlobalState("apiProvider", "default-value")
 			expect(result).toBe("default-value")
 		})
 	})
 
 	describe("updateGlobalState", () => {
 		it("should update state directly in original context", async () => {
-			await proxy.updateGlobalState("test-key", "new-value")
+			await proxy.updateGlobalState("apiProvider", "new-value")
 
 			// Should have called original context
-			expect(mockGlobalState.update).toHaveBeenCalledWith("test-key", "new-value")
+			expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "new-value")
 
 			// Should have stored the value in cache
-			const storedValue = await proxy.getGlobalState("test-key")
+			const storedValue = await proxy.getGlobalState("apiProvider")
 			expect(storedValue).toBe("new-value")
 		})
 	})
@@ -123,34 +120,34 @@ describe("ContextProxy", () => {
 	describe("getSecret", () => {
 		it("should return value from cache when it exists", async () => {
 			// Manually set a value in the cache
-			await proxy.storeSecret("api-key", "cached-secret")
+			await proxy.storeSecret("apiKey", "cached-secret")
 
 			// Should return the cached value
-			const result = proxy.getSecret("api-key")
+			const result = proxy.getSecret("apiKey")
 			expect(result).toBe("cached-secret")
 		})
 	})
 
 	describe("storeSecret", () => {
 		it("should store secret directly in original context", async () => {
-			await proxy.storeSecret("api-key", "new-secret")
+			await proxy.storeSecret("apiKey", "new-secret")
 
 			// Should have called original context
-			expect(mockSecrets.store).toHaveBeenCalledWith("api-key", "new-secret")
+			expect(mockSecrets.store).toHaveBeenCalledWith("apiKey", "new-secret")
 
 			// Should have stored the value in cache
-			const storedValue = await proxy.getSecret("api-key")
+			const storedValue = await proxy.getSecret("apiKey")
 			expect(storedValue).toBe("new-secret")
 		})
 
 		it("should handle undefined value for secret deletion", async () => {
-			await proxy.storeSecret("api-key", undefined)
+			await proxy.storeSecret("apiKey", undefined)
 
 			// Should have called delete on original context
-			expect(mockSecrets.delete).toHaveBeenCalledWith("api-key")
+			expect(mockSecrets.delete).toHaveBeenCalledWith("apiKey")
 
 			// Should have stored undefined in cache
-			const storedValue = await proxy.getSecret("api-key")
+			const storedValue = await proxy.getSecret("apiKey")
 			expect(storedValue).toBeUndefined()
 		})
 	})
@@ -194,7 +191,7 @@ describe("ContextProxy", () => {
 			const updateGlobalStateSpy = jest.spyOn(proxy, "updateGlobalState")
 
 			// Test with an unknown key
-			await proxy.setValue("unknownKey", "some-value")
+			await proxy.setValue("unknownKey" as ConfigurationKey, "some-value")
 
 			// Should have logged a warning
 			expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown key: unknownKey"))
@@ -203,7 +200,7 @@ describe("ContextProxy", () => {
 			expect(updateGlobalStateSpy).toHaveBeenCalledWith("unknownKey", "some-value")
 
 			// Should have stored the value in state cache
-			const storedValue = proxy.getGlobalState("unknownKey")
+			const storedValue = proxy.getGlobalState("unknownKey" as GlobalStateKey)
 			expect(storedValue).toBe("some-value")
 		})
 	})
@@ -241,18 +238,15 @@ describe("ContextProxy", () => {
 			await proxy.setValues({
 				apiModelId: "gpt-4", // global state
 				openAiApiKey: "test-api-key", // secret
-				unknownKey: "some-value", // unknown
 			})
 
 			// Should have called appropriate methods
 			expect(storeSecretSpy).toHaveBeenCalledWith("openAiApiKey", "test-api-key")
 			expect(updateGlobalStateSpy).toHaveBeenCalledWith("apiModelId", "gpt-4")
-			expect(updateGlobalStateSpy).toHaveBeenCalledWith("unknownKey", "some-value")
 
 			// Should have stored values in appropriate caches
 			expect(proxy.getSecret("openAiApiKey")).toBe("test-api-key")
 			expect(proxy.getGlobalState("apiModelId")).toBe("gpt-4")
-			expect(proxy.getGlobalState("unknownKey")).toBe("some-value")
 		})
 	})
 
@@ -262,13 +256,11 @@ describe("ContextProxy", () => {
 			await proxy.setValues({
 				apiModelId: "gpt-4", // global state
 				openAiApiKey: "test-api-key", // secret
-				unknownKey: "some-value", // unknown
 			})
 
 			// Verify initial state
 			expect(proxy.getGlobalState("apiModelId")).toBe("gpt-4")
 			expect(proxy.getSecret("openAiApiKey")).toBe("test-api-key")
-			expect(proxy.getGlobalState("unknownKey")).toBe("some-value")
 
 			// Reset all state
 			await proxy.resetAllState()
@@ -277,7 +269,6 @@ describe("ContextProxy", () => {
 			// Since our mock globalState.get returns undefined by default,
 			// the cache should now contain undefined values
 			expect(proxy.getGlobalState("apiModelId")).toBeUndefined()
-			expect(proxy.getGlobalState("unknownKey")).toBeUndefined()
 		})
 
 		it("should update all global state keys to undefined", async () => {
@@ -317,15 +308,13 @@ describe("ContextProxy", () => {
 
 		it("should reinitialize caches after reset", async () => {
 			// Spy on initialization methods
-			const initStateCache = jest.spyOn(proxy as any, "initializeStateCache")
-			const initSecretCache = jest.spyOn(proxy as any, "initializeSecretCache")
+			const initializeSpy = jest.spyOn(proxy as any, "initialize")
 
 			// Reset all state
 			await proxy.resetAllState()
 
 			// Should reinitialize caches
-			expect(initStateCache).toHaveBeenCalledTimes(1)
-			expect(initSecretCache).toHaveBeenCalledTimes(1)
+			expect(initializeSpy).toHaveBeenCalledTimes(1)
 		})
 	})
 })

+ 69 - 64
src/core/contextProxy.ts

@@ -1,97 +1,105 @@
 import * as vscode from "vscode"
+
 import { logger } from "../utils/logging"
-import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../shared/globalState"
+import {
+	GLOBAL_STATE_KEYS,
+	SECRET_KEYS,
+	GlobalStateKey,
+	SecretKey,
+	ConfigurationKey,
+	ConfigurationValues,
+	isSecretKey,
+	isGlobalStateKey,
+} from "../shared/globalState"
 
 export class ContextProxy {
 	private readonly originalContext: vscode.ExtensionContext
-	private stateCache: Map<string, any>
-	private secretCache: Map<string, string | undefined>
+
+	private stateCache: Map<GlobalStateKey, any>
+	private secretCache: Map<SecretKey, string | undefined>
+	private _isInitialized = false
 
 	constructor(context: vscode.ExtensionContext) {
-		// Initialize properties first
 		this.originalContext = context
 		this.stateCache = new Map()
 		this.secretCache = new Map()
+		this._isInitialized = false
+	}
 
-		// Initialize state cache with all defined global state keys
-		this.initializeStateCache()
-
-		// Initialize secret cache with all defined secret keys
-		this.initializeSecretCache()
-
-		logger.debug("ContextProxy created")
+	public get isInitialized() {
+		return this._isInitialized
 	}
 
-	// Helper method to initialize state cache
-	private initializeStateCache(): void {
+	public async initialize() {
 		for (const key of GLOBAL_STATE_KEYS) {
 			try {
-				const value = this.originalContext.globalState.get(key)
-				this.stateCache.set(key, value)
+				this.stateCache.set(key, this.originalContext.globalState.get(key))
 			} catch (error) {
 				logger.error(`Error loading global ${key}: ${error instanceof Error ? error.message : String(error)}`)
 			}
 		}
-	}
 
-	// Helper method to initialize secret cache
-	private initializeSecretCache(): void {
-		for (const key of SECRET_KEYS) {
-			// Get actual value and update cache when promise resolves
-			;(this.originalContext.secrets.get(key) as Promise<string | undefined>)
-				.then((value) => {
-					this.secretCache.set(key, value)
-				})
-				.catch((error: Error) => {
-					logger.error(`Error loading secret ${key}: ${error.message}`)
-				})
-		}
+		const promises = SECRET_KEYS.map(async (key) => {
+			try {
+				this.secretCache.set(key, await this.originalContext.secrets.get(key))
+			} catch (error) {
+				logger.error(`Error loading secret ${key}: ${error instanceof Error ? error.message : String(error)}`)
+			}
+		})
+
+		await Promise.all(promises)
+
+		this._isInitialized = true
 	}
 
-	get extensionUri(): vscode.Uri {
+	get extensionUri() {
 		return this.originalContext.extensionUri
 	}
-	get extensionPath(): string {
+
+	get extensionPath() {
 		return this.originalContext.extensionPath
 	}
-	get globalStorageUri(): vscode.Uri {
+
+	get globalStorageUri() {
 		return this.originalContext.globalStorageUri
 	}
-	get logUri(): vscode.Uri {
+
+	get logUri() {
 		return this.originalContext.logUri
 	}
-	get extension(): vscode.Extension<any> | undefined {
+
+	get extension() {
 		return this.originalContext.extension
 	}
-	get extensionMode(): vscode.ExtensionMode {
+
+	get extensionMode() {
 		return this.originalContext.extensionMode
 	}
 
-	getGlobalState<T>(key: string): T | undefined
-	getGlobalState<T>(key: string, defaultValue: T): T
-	getGlobalState<T>(key: string, defaultValue?: T): T | undefined {
+	getGlobalState<T>(key: GlobalStateKey): T | undefined
+	getGlobalState<T>(key: GlobalStateKey, defaultValue: T): T
+	getGlobalState<T>(key: GlobalStateKey, defaultValue?: T): T | undefined {
 		const value = this.stateCache.get(key) as T | undefined
 		return value !== undefined ? value : (defaultValue as T | undefined)
 	}
 
-	updateGlobalState<T>(key: string, value: T): Thenable<void> {
+	updateGlobalState<T>(key: GlobalStateKey, value: T) {
 		this.stateCache.set(key, value)
 		return this.originalContext.globalState.update(key, value)
 	}
 
-	getSecret(key: string): string | undefined {
+	getSecret(key: SecretKey) {
 		return this.secretCache.get(key)
 	}
 
-	storeSecret(key: string, value?: string): Thenable<void> {
-		// Update cache
+	storeSecret(key: SecretKey, value?: string) {
+		// Update cache.
 		this.secretCache.set(key, value)
-		// Write directly to context
-		if (value === undefined) {
-			return this.originalContext.secrets.delete(key)
-		} else {
-			return this.originalContext.secrets.store(key, value)
-		}
+
+		// Write directly to context.
+		return value === undefined
+			? this.originalContext.secrets.delete(key)
+			: this.originalContext.secrets.store(key, value)
 	}
 	/**
 	 * Set a value in either secrets or global state based on key type.
@@ -101,17 +109,15 @@ export class ContextProxy {
 	 * @param value The value to set
 	 * @returns A promise that resolves when the operation completes
 	 */
-	setValue(key: string, value: any): Thenable<void> {
-		if (SECRET_KEYS.includes(key as any)) {
+	setValue(key: ConfigurationKey, value: any) {
+		if (isSecretKey(key)) {
 			return this.storeSecret(key, value)
-		}
-
-		if (GLOBAL_STATE_KEYS.includes(key as any)) {
+		} else if (isGlobalStateKey(key)) {
+			return this.updateGlobalState(key, value)
+		} else {
+			logger.warn(`Unknown key: ${key}. Storing as global state.`)
 			return this.updateGlobalState(key, value)
 		}
-
-		logger.warn(`Unknown key: ${key}. Storing as global state.`)
-		return this.updateGlobalState(key, value)
 	}
 
 	/**
@@ -120,14 +126,14 @@ export class ContextProxy {
 	 * @param values An object containing key-value pairs to set
 	 * @returns A promise that resolves when all operations complete
 	 */
-	async setValues(values: Record<string, any>): Promise<void[]> {
+	async setValues(values: Partial<ConfigurationValues>) {
 		const promises: Thenable<void>[] = []
 
 		for (const [key, value] of Object.entries(values)) {
-			promises.push(this.setValue(key, value))
+			promises.push(this.setValue(key as ConfigurationKey, value))
 		}
 
-		return Promise.all(promises)
+		await Promise.all(promises)
 	}
 
 	/**
@@ -135,23 +141,22 @@ export class ContextProxy {
 	 * This clears all data from both the in-memory caches and the VSCode storage.
 	 * @returns A promise that resolves when all reset operations are complete
 	 */
-	async resetAllState(): Promise<void> {
+	async resetAllState() {
 		// Clear in-memory caches
 		this.stateCache.clear()
 		this.secretCache.clear()
 
-		// Reset all global state values to undefined
+		// Reset all global state values to undefined.
 		const stateResetPromises = GLOBAL_STATE_KEYS.map((key) =>
 			this.originalContext.globalState.update(key, undefined),
 		)
 
-		// Delete all secrets
+		// Delete all secrets.
 		const secretResetPromises = SECRET_KEYS.map((key) => this.originalContext.secrets.delete(key))
 
-		// Wait for all reset operations to complete
+		// Wait for all reset operations to complete.
 		await Promise.all([...stateResetPromises, ...secretResetPromises])
 
-		this.initializeStateCache()
-		this.initializeSecretCache()
+		this.initialize()
 	}
 }

+ 1 - 1
src/core/prompts/sections/modes.ts

@@ -12,7 +12,7 @@ export async function getModesSection(context: vscode.ExtensionContext): Promise
 	const allModes = await getAllModesWithPrompts(context)
 
 	// Get enableCustomModeCreation setting from extension state
-	const shouldEnableCustomModeCreation = await context.globalState.get<boolean>("enableCustomModeCreation") ?? true
+	const shouldEnableCustomModeCreation = (await context.globalState.get<boolean>("enableCustomModeCreation")) ?? true
 
 	let modesContent = `====
 

+ 23 - 6
src/core/webview/ClineProvider.ts

@@ -6,7 +6,6 @@ import os from "os"
 import pWaitFor from "p-wait-for"
 import * as path from "path"
 import * as vscode from "vscode"
-import simpleGit from "simple-git"
 
 import { setPanel } from "../../activate/registerCommands"
 import { ApiConfiguration, ApiProvider, ModelInfo, API_CONFIG_KEYS } from "../../shared/api"
@@ -14,7 +13,13 @@ import { CheckpointStorage } from "../../shared/checkpoints"
 import { findLast } from "../../shared/array"
 import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
 import { GlobalFileNames } from "../../shared/globalFileNames"
-import { SecretKey, GlobalStateKey, SECRET_KEYS, GLOBAL_STATE_KEYS } from "../../shared/globalState"
+import {
+	SecretKey,
+	GlobalStateKey,
+	SECRET_KEYS,
+	GLOBAL_STATE_KEYS,
+	ConfigurationValues,
+} from "../../shared/globalState"
 import { HistoryItem } from "../../shared/HistoryItem"
 import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
 import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
@@ -387,6 +392,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 	async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
 		this.outputChannel.appendLine("Resolving webview view")
+
+		if (!this.contextProxy.isInitialized) {
+			await this.contextProxy.initialize()
+		}
+
 		this.view = webviewView
 
 		// Set panel reference according to webview type
@@ -2013,18 +2023,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	}
 
 	private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
-		// Update mode's default config
+		// Update mode's default config.
 		const { mode } = await this.getState()
+
 		if (mode) {
 			const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
 			const listApiConfig = await this.configManager.listConfig()
 			const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
+
 			if (config?.id) {
 				await this.configManager.setModeConfig(mode, config.id)
 			}
 		}
 
-		// Use the new setValues method to handle routing values to secrets or global state
 		await this.contextProxy.setValues(apiConfiguration)
 
 		if (this.getCurrentCline()) {
@@ -2620,11 +2631,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 	// global
 
-	async updateGlobalState(key: GlobalStateKey, value: any) {
+	public async updateGlobalState(key: GlobalStateKey, value: any) {
 		await this.contextProxy.updateGlobalState(key, value)
 	}
 
-	async getGlobalState(key: GlobalStateKey) {
+	public async getGlobalState(key: GlobalStateKey) {
 		return await this.contextProxy.getGlobalState(key)
 	}
 
@@ -2638,6 +2649,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		return await this.contextProxy.getSecret(key)
 	}
 
+	// global + secret
+
+	public async setValues(values: Partial<ConfigurationValues>) {
+		await this.contextProxy.setValues(values)
+	}
+
 	// dev
 
 	async resetState() {

+ 2 - 0
src/core/webview/__tests__/ClineProvider.test.ts

@@ -19,6 +19,8 @@ jest.mock("../../contextProxy", () => {
 	return {
 		ContextProxy: jest.fn().mockImplementation((context) => ({
 			originalContext: context,
+			isInitialized: true,
+			initialize: jest.fn(),
 			extensionUri: context.extensionUri,
 			extensionPath: context.extensionPath,
 			globalStorageUri: context.globalStorageUri,

+ 0 - 7
src/exports/README.md

@@ -19,13 +19,6 @@ if (!api) {
 	throw new Error("API is not available")
 }
 
-// Set custom instructions.
-await api.setCustomInstructions("Talk like a pirate")
-
-// Get custom instructions.
-const instructions = await api.getCustomInstructions()
-console.log("Current custom instructions:", instructions)
-
 // Start a new task with an initial message.
 await api.startNewTask("Hello, Roo Code API! Let's make a new project...")
 

+ 121 - 95
src/exports/roo-code.d.ts

@@ -1,16 +1,4 @@
 export interface RooCodeAPI {
-	/**
-	 * Sets the custom instructions in the global storage.
-	 * @param value The custom instructions to be saved.
-	 */
-	setCustomInstructions(value: string): Promise<void>
-
-	/**
-	 * Retrieves the custom instructions from the global storage.
-	 * @returns The saved custom instructions, or undefined if not set.
-	 */
-	getCustomInstructions(): Promise<string | undefined>
-
 	/**
 	 * Starts a new task with an optional initial message and images.
 	 * @param task Optional initial task message.
@@ -18,6 +6,11 @@ export interface RooCodeAPI {
 	 */
 	startNewTask(task?: string, images?: string[]): Promise<void>
 
+	/**
+	 * Cancels the current task.
+	 */
+	cancelTask(): Promise<void>
+
 	/**
 	 * Sends a message to the current task.
 	 * @param message Optional message to send.
@@ -36,9 +29,20 @@ export interface RooCodeAPI {
 	pressSecondaryButton(): Promise<void>
 
 	/**
-	 * The sidebar provider instance.
+	 * Sets the configuration for the current task.
+	 * @param values An object containing key-value pairs to set.
+	 */
+	setConfiguration(values: Partial<ConfigurationValues>): Promise<void>
+
+	/**
+	 * Returns true if the API is ready to use.
+	 */
+	isReady(): boolean
+
+	/**
+	 * Returns the messages from the current task.
 	 */
-	sidebarProvider: ClineProvider
+	getMessages(): ClineMessage[]
 }
 
 export type ClineAsk =
@@ -95,84 +99,106 @@ export interface ClineMessage {
 	progressStatus?: ToolProgressStatus
 }
 
-export interface ClineProvider {
-	readonly context: vscode.ExtensionContext
-	readonly viewLaunched: boolean
-	readonly messages: ClineMessage[]
-
-	/**
-	 * Resolves the webview view for the provider
-	 * @param webviewView The webview view or panel to resolve
-	 */
-	resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel): Promise<void>
-
-	/**
-	 * Initializes Cline with a task
-	 */
-	initClineWithTask(task?: string, images?: string[]): Promise<void>
-
-	/**
-	 * Initializes Cline with a history item
-	 */
-	initClineWithHistoryItem(historyItem: HistoryItem): Promise<void>
-
-	/**
-	 * Posts a message to the webview
-	 */
-	postMessageToWebview(message: ExtensionMessage): Promise<void>
-
-	/**
-	 * Handles mode switching
-	 */
-	handleModeSwitch(newMode: Mode): Promise<void>
-
-	/**
-	 * Updates custom instructions
-	 */
-	updateCustomInstructions(instructions?: string): Promise<void>
-
-	/**
-	 * Cancels the current task
-	 */
-	cancelTask(): Promise<void>
-
-	/**
-	 * Gets the current state
-	 */
-	getState(): Promise<any>
-
-	/**
-	 * Updates a value in the global state
-	 * @param key The key to update
-	 * @param value The value to set
-	 */
-	updateGlobalState(key: GlobalStateKey, value: any): Promise<void>
-
-	/**
-	 * Gets a value from the global state
-	 * @param key The key to get
-	 */
-	getGlobalState(key: GlobalStateKey): Promise<any>
-
-	/**
-	 * Stores a secret value in secure storage
-	 * @param key The key to store the secret under
-	 * @param value The secret value to store, or undefined to remove the secret
-	 */
-	storeSecret(key: SecretKey, value?: string): Promise<void>
-
-	/**
-	 * Resets the state
-	 */
-	resetState(): Promise<void>
-
-	/**
-	 * Logs a message
-	 */
-	log(message: string): void
-
-	/**
-	 * Disposes of the provider
-	 */
-	dispose(): Promise<void>
-}
+export type SecretKey =
+	| "apiKey"
+	| "glamaApiKey"
+	| "openRouterApiKey"
+	| "awsAccessKey"
+	| "awsSecretKey"
+	| "awsSessionToken"
+	| "openAiApiKey"
+	| "geminiApiKey"
+	| "openAiNativeApiKey"
+	| "deepSeekApiKey"
+	| "mistralApiKey"
+	| "unboundApiKey"
+	| "requestyApiKey"
+
+export type GlobalStateKey =
+	| "apiProvider"
+	| "apiModelId"
+	| "glamaModelId"
+	| "glamaModelInfo"
+	| "awsRegion"
+	| "awsUseCrossRegionInference"
+	| "awsProfile"
+	| "awsUseProfile"
+	| "awsCustomArn"
+	| "vertexKeyFile"
+	| "vertexJsonCredentials"
+	| "vertexProjectId"
+	| "vertexRegion"
+	| "lastShownAnnouncementId"
+	| "customInstructions"
+	| "alwaysAllowReadOnly"
+	| "alwaysAllowWrite"
+	| "alwaysAllowExecute"
+	| "alwaysAllowBrowser"
+	| "alwaysAllowMcp"
+	| "alwaysAllowModeSwitch"
+	| "alwaysAllowSubtasks"
+	| "taskHistory"
+	| "openAiBaseUrl"
+	| "openAiModelId"
+	| "openAiCustomModelInfo"
+	| "openAiUseAzure"
+	| "ollamaModelId"
+	| "ollamaBaseUrl"
+	| "lmStudioModelId"
+	| "lmStudioBaseUrl"
+	| "anthropicBaseUrl"
+	| "modelMaxThinkingTokens"
+	| "azureApiVersion"
+	| "openAiStreamingEnabled"
+	| "openRouterModelId"
+	| "openRouterModelInfo"
+	| "openRouterBaseUrl"
+	| "openRouterUseMiddleOutTransform"
+	| "allowedCommands"
+	| "soundEnabled"
+	| "soundVolume"
+	| "diffEnabled"
+	| "enableCheckpoints"
+	| "checkpointStorage"
+	| "browserViewportSize"
+	| "screenshotQuality"
+	| "remoteBrowserHost"
+	| "fuzzyMatchThreshold"
+	| "preferredLanguage" // Language setting for Cline's communication
+	| "writeDelayMs"
+	| "terminalOutputLineLimit"
+	| "mcpEnabled"
+	| "enableMcpServerCreation"
+	| "alwaysApproveResubmit"
+	| "requestDelaySeconds"
+	| "rateLimitSeconds"
+	| "currentApiConfigName"
+	| "listApiConfigMeta"
+	| "vsCodeLmModelSelector"
+	| "mode"
+	| "modeApiConfigs"
+	| "customModePrompts"
+	| "customSupportPrompts"
+	| "enhancementApiConfigId"
+	| "experiments" // Map of experiment IDs to their enabled state
+	| "autoApprovalEnabled"
+	| "enableCustomModeCreation" // Enable the ability for Roo to create custom modes
+	| "customModes" // Array of custom modes
+	| "unboundModelId"
+	| "requestyModelId"
+	| "requestyModelInfo"
+	| "unboundModelInfo"
+	| "modelTemperature"
+	| "modelMaxTokens"
+	| "mistralCodestralUrl"
+	| "maxOpenTabsContext"
+	| "browserToolEnabled"
+	| "lmStudioSpeculativeDecodingEnabled"
+	| "lmStudioDraftModelId"
+	| "telemetrySetting"
+	| "showRooIgnoredFiles"
+	| "remoteBrowserEnabled"
+
+export type ConfigurationKey = GlobalStateKey | SecretKey
+
+export type ConfigurationValues = Record<ConfigurationKey, any>

+ 1 - 1
src/shared/ExtensionMessage.ts

@@ -7,7 +7,7 @@ import { CustomSupportPrompts } from "./support-prompt"
 import { ExperimentId } from "./experiments"
 import { CheckpointStorage } from "./checkpoints"
 import { TelemetrySetting } from "./TelemetrySetting"
-import { ClineMessage, ClineAsk, ClineSay } from "../exports/roo-code"
+import type { ClineMessage, ClineAsk, ClineSay } from "../exports/roo-code"
 
 export interface LanguageModelChatSelector {
 	vendor?: string

+ 30 - 10
src/shared/globalState.ts

@@ -1,4 +1,17 @@
-// Define the array first with 'as const' to create a readonly tuple type
+import type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues } from "../exports/roo-code"
+
+export type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues }
+
+/**
+ * For convenience we'd like the `RooCodeAPI` to define `SecretKey` and `GlobalStateKey`,
+ * but since it is a type definition file we can't export constants without some
+ * annoyances. In order to achieve proper type safety without using constants as
+ * in the type definition we use this clever Check<>Exhaustiveness pattern.
+ * If you extend the `SecretKey` or `GlobalStateKey` types, you will need to
+ * update the `SECRET_KEYS` and `GLOBAL_STATE_KEYS` arrays to include the new
+ * keys or a type error will be thrown.
+ */
+
 export const SECRET_KEYS = [
 	"apiKey",
 	"glamaApiKey",
@@ -15,10 +28,10 @@ export const SECRET_KEYS = [
 	"requestyApiKey",
 ] as const
 
-// Derive the type from the array - creates a union of string literals
-export type SecretKey = (typeof SECRET_KEYS)[number]
+type CheckSecretKeysExhaustiveness = Exclude<SecretKey, (typeof SECRET_KEYS)[number]> extends never ? true : false
+
+const _checkSecretKeysExhaustiveness: CheckSecretKeysExhaustiveness = true
 
-// Define the array first with 'as const' to create a readonly tuple type
 export const GLOBAL_STATE_KEYS = [
 	"apiProvider",
 	"apiModelId",
@@ -69,7 +82,7 @@ export const GLOBAL_STATE_KEYS = [
 	"screenshotQuality",
 	"remoteBrowserHost",
 	"fuzzyMatchThreshold",
-	"preferredLanguage", // Language setting for Cline's communication
+	"preferredLanguage", // Language setting for Cline's communication.
 	"writeDelayMs",
 	"terminalOutputLineLimit",
 	"mcpEnabled",
@@ -85,10 +98,10 @@ export const GLOBAL_STATE_KEYS = [
 	"customModePrompts",
 	"customSupportPrompts",
 	"enhancementApiConfigId",
-	"experiments", // Map of experiment IDs to their enabled state
+	"experiments", // Map of experiment IDs to their enabled state.
 	"autoApprovalEnabled",
-	"enableCustomModeCreation", // Enable the ability for Roo to create custom modes
-	"customModes", // Array of custom modes
+	"enableCustomModeCreation", // Enable the ability for Roo to create custom modes.
+	"customModes", // Array of custom modes.
 	"unboundModelId",
 	"requestyModelId",
 	"requestyModelInfo",
@@ -105,5 +118,12 @@ export const GLOBAL_STATE_KEYS = [
 	"remoteBrowserEnabled",
 ] as const
 
-// Derive the type from the array - creates a union of string literals
-export type GlobalStateKey = (typeof GLOBAL_STATE_KEYS)[number]
+type CheckGlobalStateKeysExhaustiveness =
+	Exclude<GlobalStateKey, (typeof GLOBAL_STATE_KEYS)[number]> extends never ? true : false
+
+const _checkGlobalStateKeysExhaustiveness: CheckGlobalStateKeysExhaustiveness = true
+
+export const isSecretKey = (key: string): key is SecretKey => SECRET_KEYS.includes(key as SecretKey)
+
+export const isGlobalStateKey = (key: string): key is GlobalStateKey =>
+	GLOBAL_STATE_KEYS.includes(key as GlobalStateKey)