+ )}
+
{selectedProvider === "openrouter" && (
Date: Fri, 28 Feb 2025 10:54:25 +0800
Subject: [PATCH 02/22] fix: Fix the human relay dialog function and optimize
user interaction experience
---
src/api/providers/human-relay.ts | 33 ++++++++++++++-----
src/core/webview/ClineProvider.ts | 2 +-
src/extension.ts | 10 ++++++
src/shared/WebviewMessage.ts | 9 ++---
.../src/components/settings/ApiOptions.tsx | 8 +++--
5 files changed, 45 insertions(+), 17 deletions(-)
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
index 8454a7c9af..85292a27dc 100644
--- a/src/api/providers/human-relay.ts
+++ b/src/api/providers/human-relay.ts
@@ -5,6 +5,7 @@ import { ApiHandler, SingleCompletionHandler } from "../index"
import { ApiStream } from "../transform/stream"
import * as vscode from "vscode"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
+import { getPanel } from "../../activate/registerCommands" // 导入 getPanel 函数
/**
* Human Relay API processor
@@ -114,10 +115,10 @@ function getMessageContent(message: Anthropic.Messages.MessageParam): string {
*/
async function showHumanRelayDialog(promptText: string): Promise {
return new Promise((resolve) => {
- // Create a unique request ID
+ // 创建一个唯一的请求 ID
const requestId = Date.now().toString()
- // Register callback to the global callback map
+ // 注册全局回调函数
vscode.commands.executeCommand(
"roo-code.registerHumanRelayCallback",
requestId,
@@ -126,13 +127,27 @@ async function showHumanRelayDialog(promptText: string): Promise {
+ // 等待面板创建完成后再显示人工中继对话框
+ setTimeout(() => {
+ vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
+ requestId,
+ promptText,
+ })
+ }, 500) // 给面板创建留出一点时间
+ })
+ } else {
+ // 如果 panel 已存在,直接显示对话框
+ vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
+ requestId,
+ promptText,
+ })
+ }
- // Provide a temporary UI in case the WebView fails to load
+ // 提供临时 UI,以防 WebView 加载失败
vscode.window
.showInformationMessage(
"Please paste the copied message to the AI, then copy the response back into the dialog",
@@ -144,7 +159,7 @@ async function showHumanRelayDialog(promptText: string): Promise {
if (selection === "Use Input Box") {
- // Unregister the callback
+ // 注销回调
vscode.commands.executeCommand("roo-code.unregisterHumanRelayCallback", requestId)
vscode.window
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 1c2ffea550..e781a36dbe 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1522,8 +1522,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Switch back to default mode after deletion
await this.updateGlobalState("mode", defaultModeSlug)
await this.postStateToWebview()
- break
}
+ break
case "humanRelayResponse":
if (message.requestId && message.text) {
vscode.commands.executeCommand("roo-code.handleHumanRelayResponse", {
diff --git a/src/extension.ts b/src/extension.ts
index 3b148a41ac..87dc7405a3 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -57,6 +57,16 @@ export function activate(context: vscode.ExtensionContext) {
registerCommands({ context, outputChannel, provider: sidebarProvider })
+ // Register human relay callback registration command
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ "roo-code.registerHumanRelayCallback",
+ (requestId: string, callback: (response: string | undefined) => void) => {
+ registerHumanRelayCallback(requestId, callback)
+ },
+ ),
+ )
+
// Register human relay response processing command
context.subscriptions.push(
vscode.commands.registerCommand(
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 2b0c68f7be..a45a727253 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -94,8 +94,8 @@ export interface WebviewMessage {
| "checkpointRestore"
| "deleteMcpServer"
| "maxOpenTabsContext"
- | "HumanRelayResponseMessage"
- | "HumanRelayCancelMessage"
+ | "humanRelayResponse"
+ | "humanRelayCancel"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
@@ -119,16 +119,17 @@ export interface WebviewMessage {
timeout?: number
payload?: WebViewMessagePayload
source?: "global" | "project"
+ requestId?: string
}
// Human relay related message types
-export interface HumanRelayResponseMessage {
+export interface HumanRelayResponseMessage extends WebviewMessage {
type: "humanRelayResponse"
requestId: string
text: string
}
-export interface HumanRelayCancelMessage {
+export interface HumanRelayCancelMessage extends WebviewMessage {
type: "humanRelayCancel"
requestId: string
}
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index c8598ec101..b125014a62 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -1317,7 +1317,8 @@ const ApiOptions = ({
color: "var(--vscode-descriptionForeground)",
lineHeight: "1.4",
}}>
- 不需要API key,但需要用户协助复制粘贴信息给web的聊天AI。
+ The API key is not required, but the user needs to help copy and paste the information to the
+ web chat AI.
- 在使用过程中,系统会弹出对话框,并自动复制当前消息到剪贴板。您需要将这些内容粘贴给网页版AI(如ChatGPT或Claude),
- 然后将AI的回复复制回对话框中点击确认按钮。
+ During use, a dialog box will pop up and the current message will be copied to the clipboard
+ automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude),Then
+ copy the AI's reply back to the dialog box and click the confirm button.
)}
From 1ae4eaa80ebc3f8bc91f669acda4c74b5fd1520f Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Fri, 28 Feb 2025 11:42:45 +0800
Subject: [PATCH 03/22] fix: Update comments to the human relay
---
src/api/providers/human-relay.ts | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
index 85292a27dc..2911e24eaf 100644
--- a/src/api/providers/human-relay.ts
+++ b/src/api/providers/human-relay.ts
@@ -5,7 +5,7 @@ import { ApiHandler, SingleCompletionHandler } from "../index"
import { ApiStream } from "../transform/stream"
import * as vscode from "vscode"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
-import { getPanel } from "../../activate/registerCommands" // 导入 getPanel 函数
+import { getPanel } from "../../activate/registerCommands" // Import the getPanel function
/**
* Human Relay API processor
@@ -115,10 +115,10 @@ function getMessageContent(message: Anthropic.Messages.MessageParam): string {
*/
async function showHumanRelayDialog(promptText: string): Promise {
return new Promise((resolve) => {
- // 创建一个唯一的请求 ID
+ // Create a unique request ID
const requestId = Date.now().toString()
- // 注册全局回调函数
+ // Register a global callback function
vscode.commands.executeCommand(
"roo-code.registerHumanRelayCallback",
requestId,
@@ -127,27 +127,27 @@ async function showHumanRelayDialog(promptText: string): Promise {
- // 等待面板创建完成后再显示人工中继对话框
+ // Wait for the panel to be created before showing the human relay dialog
setTimeout(() => {
vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
requestId,
promptText,
})
- }, 500) // 给面板创建留出一点时间
+ }, 500) // Allow some time for the panel to be created
})
} else {
- // 如果 panel 已存在,直接显示对话框
+ // If the panel already exists, directly show the dialog
vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
requestId,
promptText,
})
}
- // 提供临时 UI,以防 WebView 加载失败
+ // Provide a temporary UI in case the WebView fails to load
vscode.window
.showInformationMessage(
"Please paste the copied message to the AI, then copy the response back into the dialog",
@@ -159,7 +159,7 @@ async function showHumanRelayDialog(promptText: string): Promise {
if (selection === "Use Input Box") {
- // 注销回调
+ // Unregister the callback
vscode.commands.executeCommand("roo-code.unregisterHumanRelayCallback", requestId)
vscode.window
From 156fe0d9fb74169e2bc9a4367708f4f963917cbf Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Fri, 28 Feb 2025 12:06:39 +0800
Subject: [PATCH 04/22] fix: Optimize panel management, support panel
references for sidebar and tab types
---
src/activate/registerCommands.ts | 37 ++++++++++----
src/api/providers/human-relay.ts | 51 ++-----------------
src/core/webview/ClineProvider.ts | 10 ++++
.../human-relay/HumanRelayDialog.tsx | 12 ++++-
4 files changed, 51 insertions(+), 59 deletions(-)
diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts
index 8cc895f291..b520e0cb8e 100644
--- a/src/activate/registerCommands.ts
+++ b/src/activate/registerCommands.ts
@@ -3,17 +3,32 @@ import delay from "delay"
import { ClineProvider } from "../core/webview/ClineProvider"
-// Add a global variable to store panel references
-let panel: vscode.WebviewPanel | undefined = undefined
+// Store panel references in both modes
+let sidebarPanel: vscode.WebviewView | undefined = undefined
+let tabPanel: vscode.WebviewPanel | undefined = undefined
-// Get the panel function for command access
-export function getPanel(): vscode.WebviewPanel | undefined {
- return panel
+/**
+ * Get the currently active panel
+ * @returns WebviewPanel或WebviewView
+ */
+export function getPanel(): vscode.WebviewPanel | vscode.WebviewView | undefined {
+ return tabPanel || sidebarPanel
}
-// Setting the function of the panel
-export function setPanel(newPanel: vscode.WebviewPanel | undefined): void {
- panel = newPanel
+/**
+ * Set panel references
+ */
+export function setPanel(
+ newPanel: vscode.WebviewPanel | vscode.WebviewView | undefined,
+ type: "sidebar" | "tab",
+): void {
+ if (type === "sidebar") {
+ sidebarPanel = newPanel as vscode.WebviewView
+ tabPanel = undefined
+ } else {
+ tabPanel = newPanel as vscode.WebviewPanel
+ sidebarPanel = undefined
+ }
}
export type RegisterCommandOptions = {
@@ -100,8 +115,8 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit {
- setPanel(undefined)
+ setPanel(undefined, "tab")
})
// Lock the editor group so clicking on files doesn't open them over the panel
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
index 2911e24eaf..618ae847de 100644
--- a/src/api/providers/human-relay.ts
+++ b/src/api/providers/human-relay.ts
@@ -127,51 +127,10 @@ async function showHumanRelayDialog(promptText: string): Promise {
- // Wait for the panel to be created before showing the human relay dialog
- setTimeout(() => {
- vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
- requestId,
- promptText,
- })
- }, 500) // Allow some time for the panel to be created
- })
- } else {
- // If the panel already exists, directly show the dialog
- vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
- requestId,
- promptText,
- })
- }
-
- // Provide a temporary UI in case the WebView fails to load
- vscode.window
- .showInformationMessage(
- "Please paste the copied message to the AI, then copy the response back into the dialog",
- {
- modal: true,
- detail: "The message has been copied to the clipboard. If the dialog does not open, please try using the input box.",
- },
- "Use Input Box",
- )
- .then((selection) => {
- if (selection === "Use Input Box") {
- // Unregister the callback
- vscode.commands.executeCommand("roo-code.unregisterHumanRelayCallback", requestId)
-
- vscode.window
- .showInputBox({
- prompt: "Please paste the AI's response here",
- placeHolder: "Paste the AI's response here...",
- ignoreFocusOut: true,
- })
- .then((input) => {
- resolve(input || undefined)
- })
- }
- })
+ // Open the dialog box directly using the current panel
+ vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
+ requestId,
+ promptText,
+ })
})
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index e781a36dbe..c87406d3d4 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -7,6 +7,7 @@ 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 } from "../../shared/api"
import { findLast } from "../../shared/array"
@@ -233,6 +234,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.outputChannel.appendLine("Resolving webview view")
this.view = webviewView
+ // Set panel reference according to webview type
+ if ("onDidChangeViewState" in webviewView) {
+ // Tag page type
+ setPanel(webviewView, "tab")
+ } else if ("onDidChangeVisibility" in webviewView) {
+ // Sidebar Type
+ setPanel(webviewView, "sidebar")
+ }
+
// Initialize sound enabled state
this.getState().then(({ soundEnabled }) => {
setSoundEnabled(soundEnabled ?? false)
diff --git a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx
index ea306d11d7..61d4cbe213 100644
--- a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx
+++ b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx
@@ -27,12 +27,20 @@ export const HumanRelayDialog: React.FC = ({
onCancel,
}) => {
const [response, setResponse] = React.useState("")
- const { onCopy } = useClipboard(promptText)
+ const { copy } = useClipboard()
const [isCopyClicked, setIsCopyClicked] = React.useState(false)
+ // Listen to isOpen changes, clear the input box when the dialog box is opened
+ React.useEffect(() => {
+ if (isOpen) {
+ setResponse("")
+ setIsCopyClicked(false)
+ }
+ }, [isOpen])
+
// Copy to clipboard and show a success message
const handleCopy = () => {
- onCopy()
+ copy(promptText)
setIsCopyClicked(true)
setTimeout(() => {
setIsCopyClicked(false)
From db65520adb862e52a8e0f30ecb7e003e3e878959 Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Fri, 28 Feb 2025 12:32:06 +0800
Subject: [PATCH 05/22] fix: Fixed human relay dialog message processing,
optimized type use
---
webview-ui/src/App.tsx | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index 5909a3eaef..8c37236adb 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
+import { ShowHumanRelayDialogMessage } from "../../src/shared/ExtensionMessage"
import { vscode } from "./utils/vscode"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
@@ -59,13 +60,13 @@ const App = () => {
switchTab(newTab)
}
}
-
+ const mes: ShowHumanRelayDialogMessage = message as ShowHumanRelayDialogMessage
// Processing displays human relay dialog messages
- if (message.type === "showHumanRelayDialog" && message.requestId && message.promptText) {
+ if (mes.type === "showHumanRelayDialog" && mes.requestId && mes.promptText) {
setHumanRelayDialogState({
isOpen: true,
- requestId: message.requestId,
- promptText: message.promptText,
+ requestId: mes.requestId,
+ promptText: mes.promptText,
})
}
},
From aceeb0b5c5fd20cc2bb90f08e2dd66d12e584b06 Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Fri, 28 Feb 2025 14:53:54 +0800
Subject: [PATCH 06/22] chore: Restore .gitignore, remove unnecessary file
rules
---
.gitignore | 5 -----
1 file changed, 5 deletions(-)
diff --git a/.gitignore b/.gitignore
index bdae7b5b26..211d06aa19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,8 +28,3 @@ docs/_site/
#Logging
logs
-.clinerules-architect
-.clinerules-ask
-.clinerules-code
-MemoryBank
-.github/copilot-instructions.md
From 7a2a08aaa4979e79f945e539b30daa6b49c3fb14 Mon Sep 17 00:00:00 2001
From: Tom X Nguyen
Date: Sun, 2 Mar 2025 19:42:00 +0700
Subject: [PATCH 07/22] style(context-window): add padding to align
---
webview-ui/src/components/chat/TaskHeader.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index 319a9aeccd..c469345fa1 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -415,7 +415,7 @@ const ContextWindowProgress = ({ contextWindow, contextTokens }: { contextWindow
Context Window:
-
+
{formatLargeNumber(contextTokens)}
From ee7650cd0f83a32d6ea733a92112445feaae2923 Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Mon, 3 Mar 2025 12:07:00 +0800
Subject: [PATCH 08/22] Merged temp-branch into origin/human-relay and resolved
conflicts
---
src/api/providers/human-relay.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
index 618ae847de..4700208cda 100644
--- a/src/api/providers/human-relay.ts
+++ b/src/api/providers/human-relay.ts
@@ -17,6 +17,10 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
}
+ async countTokens(content: Array): Promise {
+ // Count the number of tokens in the content blocks
+ return 0
+ }
/**
* Create a message processing flow, display a dialog box to request human assistance
From b47de72cec5891a1b1162ffeefa69fc74ef08ead Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Mon, 3 Mar 2025 12:11:43 +0800
Subject: [PATCH 09/22] Add countTokens method to HumanRelayHandler class in
human-relay.ts
---
src/api/providers/human-relay.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
index 618ae847de..90a82b9bfe 100644
--- a/src/api/providers/human-relay.ts
+++ b/src/api/providers/human-relay.ts
@@ -17,6 +17,9 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
}
+ countTokens(content: Array): Promise {
+ return Promise.resolve(0)
+ }
/**
* Create a message processing flow, display a dialog box to request human assistance
From 381b07849a412333cf927528b2c64e606ff4a65f Mon Sep 17 00:00:00 2001
From: sam hoang
Date: Thu, 27 Feb 2025 14:37:34 +0700
Subject: [PATCH 10/22] Feat ContextProxy to improve state management
- Add ContextProxy class as a wrapper around VSCode's ExtensionContext
- Implement batched state updates for performance optimization
- Update ClineProvider to use ContextProxy instead of direct context access
- Add comprehensive test coverage for ContextProxy
- Extract SECRET_KEYS and GLOBAL_STATE_KEYS constants for better maintainability
---
src/core/__tests__/contextProxy.test.ts | 282 +++++++++
src/core/contextProxy.ts | 123 ++++
src/core/webview/ClineProvider.ts | 577 ++++++------------
.../webview/__tests__/ClineProvider.test.ts | 130 ++++
src/shared/globalState.ts | 91 ++-
5 files changed, 797 insertions(+), 406 deletions(-)
create mode 100644 src/core/__tests__/contextProxy.test.ts
create mode 100644 src/core/contextProxy.ts
diff --git a/src/core/__tests__/contextProxy.test.ts b/src/core/__tests__/contextProxy.test.ts
new file mode 100644
index 0000000000..794cd91497
--- /dev/null
+++ b/src/core/__tests__/contextProxy.test.ts
@@ -0,0 +1,282 @@
+import * as vscode from "vscode"
+import { ContextProxy } from "../contextProxy"
+import { logger } from "../../utils/logging"
+
+// Mock the logger
+jest.mock("../../utils/logging", () => ({
+ logger: {
+ debug: jest.fn(),
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ },
+}))
+
+// Mock VSCode API
+jest.mock("vscode", () => ({
+ Uri: {
+ file: jest.fn((path) => ({ path })),
+ },
+ ExtensionMode: {
+ Development: 1,
+ Production: 2,
+ Test: 3,
+ },
+}))
+
+describe("ContextProxy", () => {
+ let proxy: ContextProxy
+ let mockContext: any
+ let mockGlobalState: any
+ let mockSecrets: any
+
+ beforeEach(() => {
+ // Reset mocks
+ jest.clearAllMocks()
+
+ // Mock globalState
+ mockGlobalState = {
+ get: jest.fn(),
+ update: jest.fn().mockResolvedValue(undefined),
+ }
+
+ // Mock secrets
+ mockSecrets = {
+ get: jest.fn(),
+ store: jest.fn().mockResolvedValue(undefined),
+ delete: jest.fn().mockResolvedValue(undefined),
+ }
+
+ // Mock the extension context
+ mockContext = {
+ globalState: mockGlobalState,
+ secrets: mockSecrets,
+ extensionUri: { path: "/test/extension" },
+ extensionPath: "/test/extension",
+ globalStorageUri: { path: "/test/storage" },
+ logUri: { path: "/test/logs" },
+ extension: { packageJSON: { version: "1.0.0" } },
+ extensionMode: vscode.ExtensionMode.Development,
+ }
+
+ // Create proxy instance
+ proxy = new ContextProxy(mockContext)
+ })
+
+ describe("read-only pass-through properties", () => {
+ it("should return extension properties from the original context", () => {
+ expect(proxy.extensionUri).toBe(mockContext.extensionUri)
+ expect(proxy.extensionPath).toBe(mockContext.extensionPath)
+ expect(proxy.globalStorageUri).toBe(mockContext.globalStorageUri)
+ expect(proxy.logUri).toBe(mockContext.logUri)
+ expect(proxy.extension).toBe(mockContext.extension)
+ expect(proxy.extensionMode).toBe(mockContext.extensionMode)
+ })
+ })
+
+ describe("getGlobalState", () => {
+ it("should return pending change when it exists", async () => {
+ // Set up a pending change
+ await proxy.updateGlobalState("test-key", "new-value")
+
+ // Should return the pending value
+ const result = await proxy.getGlobalState("test-key")
+ expect(result).toBe("new-value")
+
+ // Original context should not be called
+ expect(mockGlobalState.get).not.toHaveBeenCalled()
+ })
+
+ it("should fall back to original context when no pending change exists", async () => {
+ // Set up original context value
+ mockGlobalState.get.mockReturnValue("original-value")
+
+ // Should get from original context
+ const result = await proxy.getGlobalState("test-key")
+ expect(result).toBe("original-value")
+ expect(mockGlobalState.get).toHaveBeenCalledWith("test-key", undefined)
+ })
+
+ it("should handle default values correctly", async () => {
+ // No value in either pending or original
+ mockGlobalState.get.mockImplementation((key: string, defaultValue: any) => defaultValue)
+
+ // Should return the default value
+ const result = await proxy.getGlobalState("test-key", "default-value")
+ expect(result).toBe("default-value")
+ })
+ })
+
+ describe("updateGlobalState", () => {
+ it("should buffer changes without calling original context", async () => {
+ await proxy.updateGlobalState("test-key", "new-value")
+
+ // Should have called logger.debug
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("buffering state update"))
+
+ // Should not have called original context
+ expect(mockGlobalState.update).not.toHaveBeenCalled()
+
+ // Should have stored the value in pendingStateChanges
+ const storedValue = await proxy.getGlobalState("test-key")
+ expect(storedValue).toBe("new-value")
+ })
+
+ it("should throw an error when context is disposed", async () => {
+ await proxy.dispose()
+
+ await expect(proxy.updateGlobalState("test-key", "new-value")).rejects.toThrow(
+ "Cannot update state on disposed context",
+ )
+ })
+ })
+
+ describe("getSecret", () => {
+ it("should return pending secret when it exists", async () => {
+ // Set up a pending secret
+ await proxy.storeSecret("api-key", "secret123")
+
+ // Should return the pending value
+ const result = await proxy.getSecret("api-key")
+ expect(result).toBe("secret123")
+
+ // Original context should not be called
+ expect(mockSecrets.get).not.toHaveBeenCalled()
+ })
+
+ it("should fall back to original context when no pending secret exists", async () => {
+ // Set up original context value
+ mockSecrets.get.mockResolvedValue("original-secret")
+
+ // Should get from original context
+ const result = await proxy.getSecret("api-key")
+ expect(result).toBe("original-secret")
+ expect(mockSecrets.get).toHaveBeenCalledWith("api-key")
+ })
+ })
+
+ describe("storeSecret", () => {
+ it("should buffer secret changes without calling original context", async () => {
+ await proxy.storeSecret("api-key", "new-secret")
+
+ // Should have called logger.debug
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("buffering secret update"))
+
+ // Should not have called original context
+ expect(mockSecrets.store).not.toHaveBeenCalled()
+
+ // Should have stored the value in pendingSecretChanges
+ const storedValue = await proxy.getSecret("api-key")
+ expect(storedValue).toBe("new-secret")
+ })
+
+ it("should handle undefined value for secret deletion", async () => {
+ await proxy.storeSecret("api-key", undefined)
+
+ // Should have stored undefined in pendingSecretChanges
+ const storedValue = await proxy.getSecret("api-key")
+ expect(storedValue).toBeUndefined()
+ })
+
+ it("should throw an error when context is disposed", async () => {
+ await proxy.dispose()
+
+ await expect(proxy.storeSecret("api-key", "new-secret")).rejects.toThrow(
+ "Cannot store secret on disposed context",
+ )
+ })
+ })
+
+ describe("saveChanges", () => {
+ it("should apply state changes to original context", async () => {
+ // Set up pending changes
+ await proxy.updateGlobalState("key1", "value1")
+ await proxy.updateGlobalState("key2", "value2")
+
+ // Save changes
+ await proxy.saveChanges()
+
+ // Should have called update on original context
+ expect(mockGlobalState.update).toHaveBeenCalledTimes(2)
+ expect(mockGlobalState.update).toHaveBeenCalledWith("key1", "value1")
+ expect(mockGlobalState.update).toHaveBeenCalledWith("key2", "value2")
+
+ // Should have cleared pending changes
+ expect(proxy.hasPendingChanges()).toBe(false)
+ })
+
+ it("should apply secret changes to original context", async () => {
+ // Set up pending changes
+ await proxy.storeSecret("secret1", "value1")
+ await proxy.storeSecret("secret2", undefined)
+
+ // Save changes
+ await proxy.saveChanges()
+
+ // Should have called store and delete on original context
+ expect(mockSecrets.store).toHaveBeenCalledTimes(1)
+ expect(mockSecrets.store).toHaveBeenCalledWith("secret1", "value1")
+ expect(mockSecrets.delete).toHaveBeenCalledTimes(1)
+ expect(mockSecrets.delete).toHaveBeenCalledWith("secret2")
+
+ // Should have cleared pending changes
+ expect(proxy.hasPendingChanges()).toBe(false)
+ })
+
+ it("should do nothing when there are no pending changes", async () => {
+ await proxy.saveChanges()
+
+ expect(mockGlobalState.update).not.toHaveBeenCalled()
+ expect(mockSecrets.store).not.toHaveBeenCalled()
+ expect(mockSecrets.delete).not.toHaveBeenCalled()
+ })
+
+ it("should throw an error when context is disposed", async () => {
+ await proxy.dispose()
+
+ await expect(proxy.saveChanges()).rejects.toThrow("Cannot save changes on disposed context")
+ })
+ })
+
+ describe("dispose", () => {
+ it("should save pending changes to original context", async () => {
+ // Set up pending changes
+ await proxy.updateGlobalState("key1", "value1")
+ await proxy.storeSecret("secret1", "value1")
+
+ // Dispose
+ await proxy.dispose()
+
+ // Should have saved changes
+ expect(mockGlobalState.update).toHaveBeenCalledWith("key1", "value1")
+ expect(mockSecrets.store).toHaveBeenCalledWith("secret1", "value1")
+
+ // Should be marked as disposed
+ expect(proxy.hasPendingChanges()).toBe(false)
+ })
+ })
+
+ describe("hasPendingChanges", () => {
+ it("should return false when no changes are pending", () => {
+ expect(proxy.hasPendingChanges()).toBe(false)
+ })
+
+ it("should return true when state changes are pending", async () => {
+ await proxy.updateGlobalState("key", "value")
+ expect(proxy.hasPendingChanges()).toBe(true)
+ })
+
+ it("should return true when secret changes are pending", async () => {
+ await proxy.storeSecret("key", "value")
+ expect(proxy.hasPendingChanges()).toBe(true)
+ })
+
+ it("should return false after changes are saved", async () => {
+ await proxy.updateGlobalState("key", "value")
+ expect(proxy.hasPendingChanges()).toBe(true)
+
+ await proxy.saveChanges()
+ expect(proxy.hasPendingChanges()).toBe(false)
+ })
+ })
+})
diff --git a/src/core/contextProxy.ts b/src/core/contextProxy.ts
new file mode 100644
index 0000000000..e4672ae225
--- /dev/null
+++ b/src/core/contextProxy.ts
@@ -0,0 +1,123 @@
+import * as vscode from "vscode"
+import { logger } from "../utils/logging"
+
+/**
+ * A proxy class for vscode.ExtensionContext that buffers state changes
+ * and only commits them when explicitly requested or during disposal.
+ */
+export class ContextProxy {
+ private readonly originalContext: vscode.ExtensionContext
+ private pendingStateChanges: Map
+ private pendingSecretChanges: Map
+ private disposed: boolean
+
+ constructor(context: vscode.ExtensionContext) {
+ this.originalContext = context
+ this.pendingStateChanges = new Map()
+ this.pendingSecretChanges = new Map()
+ this.disposed = false
+ logger.debug("ContextProxy created")
+ }
+
+ // Read-only pass-through properties
+ get extensionUri(): vscode.Uri {
+ return this.originalContext.extensionUri
+ }
+ get extensionPath(): string {
+ return this.originalContext.extensionPath
+ }
+ get globalStorageUri(): vscode.Uri {
+ return this.originalContext.globalStorageUri
+ }
+ get logUri(): vscode.Uri {
+ return this.originalContext.logUri
+ }
+ get extension(): vscode.Extension | undefined {
+ return this.originalContext.extension
+ }
+ get extensionMode(): vscode.ExtensionMode {
+ return this.originalContext.extensionMode
+ }
+
+ // State management methods
+ async getGlobalState(key: string): Promise
+ async getGlobalState(key: string, defaultValue: T): Promise
+ async getGlobalState(key: string, defaultValue?: T): Promise {
+ // Check pending changes first
+ if (this.pendingStateChanges.has(key)) {
+ const value = this.pendingStateChanges.get(key) as T | undefined
+ return value !== undefined ? value : (defaultValue as T | undefined)
+ }
+ // Fall back to original context
+ return this.originalContext.globalState.get(key, defaultValue as T)
+ }
+
+ async updateGlobalState(key: string, value: T): Promise {
+ if (this.disposed) {
+ throw new Error("Cannot update state on disposed context")
+ }
+ logger.debug(`ContextProxy: buffering state update for key "${key}"`)
+ this.pendingStateChanges.set(key, value)
+ }
+
+ // Secret storage methods
+ async getSecret(key: string): Promise {
+ // Check pending changes first
+ if (this.pendingSecretChanges.has(key)) {
+ return this.pendingSecretChanges.get(key)
+ }
+ // Fall back to original context
+ return this.originalContext.secrets.get(key)
+ }
+
+ async storeSecret(key: string, value?: string): Promise {
+ if (this.disposed) {
+ throw new Error("Cannot store secret on disposed context")
+ }
+ logger.debug(`ContextProxy: buffering secret update for key "${key}"`)
+ this.pendingSecretChanges.set(key, value)
+ }
+
+ // Save pending changes to actual context
+ async saveChanges(): Promise {
+ if (this.disposed) {
+ throw new Error("Cannot save changes on disposed context")
+ }
+
+ // Apply state changes
+ if (this.pendingStateChanges.size > 0) {
+ logger.debug(`ContextProxy: applying ${this.pendingStateChanges.size} buffered state changes`)
+ for (const [key, value] of this.pendingStateChanges.entries()) {
+ await this.originalContext.globalState.update(key, value)
+ }
+ this.pendingStateChanges.clear()
+ }
+
+ // Apply secret changes
+ if (this.pendingSecretChanges.size > 0) {
+ logger.debug(`ContextProxy: applying ${this.pendingSecretChanges.size} buffered secret changes`)
+ for (const [key, value] of this.pendingSecretChanges.entries()) {
+ if (value === undefined) {
+ await this.originalContext.secrets.delete(key)
+ } else {
+ await this.originalContext.secrets.store(key, value)
+ }
+ }
+ this.pendingSecretChanges.clear()
+ }
+ }
+
+ // Called when the provider is disposing
+ async dispose(): Promise {
+ if (!this.disposed) {
+ logger.debug("ContextProxy: disposing and saving pending changes")
+ await this.saveChanges()
+ this.disposed = true
+ }
+ }
+
+ // Method to check if there are pending changes
+ hasPendingChanges(): boolean {
+ return this.pendingStateChanges.size > 0 || this.pendingSecretChanges.size > 0
+ }
+}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index d0e68420b5..d9a1525730 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -12,7 +12,7 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
import { GlobalFileNames } from "../../shared/globalFileNames"
-import type { SecretKey, GlobalStateKey } from "../../shared/globalState"
+import { SecretKey, GlobalStateKey, SECRET_KEYS, GLOBAL_STATE_KEYS } from "../../shared/globalState"
import { HistoryItem } from "../../shared/HistoryItem"
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
@@ -34,6 +34,7 @@ import { getDiffStrategy } from "../diff/DiffStrategy"
import { SYSTEM_PROMPT } from "../prompts/system"
import { ConfigManager } from "../config/ConfigManager"
import { CustomModesManager } from "../config/CustomModesManager"
+import { ContextProxy } from "../contextProxy"
import { buildApiHandler } from "../../api"
import { getOpenRouterModels } from "../../api/providers/openrouter"
import { getGlamaModels } from "../../api/providers/glama"
@@ -65,6 +66,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private workspaceTracker?: WorkspaceTracker
protected mcpHub?: McpHub // Change from private to protected
private latestAnnouncementId = "feb-27-2025-automatic-checkpoints" // update to some unique identifier when we add a new announcement
+ private contextProxy: ContextProxy
configManager: ConfigManager
customModesManager: CustomModesManager
@@ -73,6 +75,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private readonly outputChannel: vscode.OutputChannel,
) {
this.outputChannel.appendLine("ClineProvider instantiated")
+ this.contextProxy = new ContextProxy(context)
ClineProvider.activeInstances.add(this)
this.workspaceTracker = new WorkspaceTracker(this)
this.configManager = new ConfigManager(this.context)
@@ -115,6 +118,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.mcpHub = undefined
this.customModesManager?.dispose()
this.outputChannel.appendLine("Disposed all disposables")
+ // Dispose the context proxy to commit any pending changes
+ await this.contextProxy.dispose()
+ this.outputChannel.appendLine("Disposed context proxy")
ClineProvider.activeInstances.delete(this)
// Unregister from McpServerManager
@@ -241,11 +247,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
webviewView.webview.options = {
// Allow scripts in the webview
enableScripts: true,
- localResourceRoots: [this.context.extensionUri],
+ localResourceRoots: [this.contextProxy.extensionUri],
}
webviewView.webview.html =
- this.context.extensionMode === vscode.ExtensionMode.Development
+ this.contextProxy.extensionMode === vscode.ExtensionMode.Development
? await this.getHMRHtmlContent(webviewView.webview)
: this.getHtmlContent(webviewView.webview)
@@ -389,8 +395,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
const nonce = getNonce()
- const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
- const codiconsUri = getUri(webview, this.context.extensionUri, [
+ const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
+ "webview-ui",
+ "build",
+ "assets",
+ "index.css",
+ ])
+ const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
"node_modules",
"@vscode",
"codicons",
@@ -456,15 +467,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// then convert it to a uri we can use in the webview.
// The CSS file from the React build output
- const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
+ const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
+ "webview-ui",
+ "build",
+ "assets",
+ "index.css",
+ ])
// The JS file from the React build output
- const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.js"])
+ const scriptUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "build", "assets", "index.js"])
// The codicon font from the React build output
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
// we installed this package in the extension so that we can access it how its intended from the extension (the font file is likely bundled in vscode), and we just import the css fileinto our react app we don't have access to it
// don't forget to add font-src ${webview.cspSource};
- const codiconsUri = getUri(webview, this.context.extensionUri, [
+ const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
"node_modules",
"@vscode",
"codicons",
@@ -1249,7 +1265,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Try to get enhancement config first, fall back to current config
let configToUse: ApiConfiguration = apiConfiguration
if (enhancementApiConfigId) {
- const config = listApiConfigMeta?.find((c) => c.id === enhancementApiConfigId)
+ const config = listApiConfigMeta?.find(
+ (c: ApiConfigMeta) => c.id === enhancementApiConfigId,
+ )
if (config?.name) {
const loadedConfig = await this.configManager.loadConfig(config.name)
if (loadedConfig.apiProvider) {
@@ -1628,108 +1646,21 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
- const {
- apiProvider,
- apiModelId,
- apiKey,
- glamaModelId,
- glamaModelInfo,
- glamaApiKey,
- openRouterApiKey,
- awsAccessKey,
- awsSecretKey,
- awsSessionToken,
- awsRegion,
- awsUseCrossRegionInference,
- awsProfile,
- awsUseProfile,
- vertexProjectId,
- vertexRegion,
- openAiBaseUrl,
- openAiApiKey,
- openAiModelId,
- openAiCustomModelInfo,
- openAiUseAzure,
- ollamaModelId,
- ollamaBaseUrl,
- lmStudioModelId,
- lmStudioBaseUrl,
- anthropicBaseUrl,
- geminiApiKey,
- openAiNativeApiKey,
- deepSeekApiKey,
- azureApiVersion,
- openAiStreamingEnabled,
- openRouterModelId,
- openRouterBaseUrl,
- openRouterModelInfo,
- openRouterUseMiddleOutTransform,
- vsCodeLmModelSelector,
- mistralApiKey,
- mistralCodestralUrl,
- unboundApiKey,
- unboundModelId,
- unboundModelInfo,
- requestyApiKey,
- requestyModelId,
- requestyModelInfo,
- modelTemperature,
- modelMaxTokens,
- modelMaxThinkingTokens,
- lmStudioDraftModelId,
- lmStudioSpeculativeDecodingEnabled,
- } = apiConfiguration
- await Promise.all([
- this.updateGlobalState("apiProvider", apiProvider),
- this.updateGlobalState("apiModelId", apiModelId),
- this.storeSecret("apiKey", apiKey),
- this.updateGlobalState("glamaModelId", glamaModelId),
- this.updateGlobalState("glamaModelInfo", glamaModelInfo),
- this.storeSecret("glamaApiKey", glamaApiKey),
- this.storeSecret("openRouterApiKey", openRouterApiKey),
- this.storeSecret("awsAccessKey", awsAccessKey),
- this.storeSecret("awsSecretKey", awsSecretKey),
- this.storeSecret("awsSessionToken", awsSessionToken),
- this.updateGlobalState("awsRegion", awsRegion),
- this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference),
- this.updateGlobalState("awsProfile", awsProfile),
- this.updateGlobalState("awsUseProfile", awsUseProfile),
- this.updateGlobalState("vertexProjectId", vertexProjectId),
- this.updateGlobalState("vertexRegion", vertexRegion),
- this.updateGlobalState("openAiBaseUrl", openAiBaseUrl),
- this.storeSecret("openAiApiKey", openAiApiKey),
- this.updateGlobalState("openAiModelId", openAiModelId),
- this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo),
- this.updateGlobalState("openAiUseAzure", openAiUseAzure),
- this.updateGlobalState("ollamaModelId", ollamaModelId),
- this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl),
- this.updateGlobalState("lmStudioModelId", lmStudioModelId),
- this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
- this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
- this.storeSecret("geminiApiKey", geminiApiKey),
- this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
- this.storeSecret("deepSeekApiKey", deepSeekApiKey),
- this.updateGlobalState("azureApiVersion", azureApiVersion),
- this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled),
- this.updateGlobalState("openRouterModelId", openRouterModelId),
- this.updateGlobalState("openRouterModelInfo", openRouterModelInfo),
- this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl),
- this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform),
- this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector),
- this.storeSecret("mistralApiKey", mistralApiKey),
- this.updateGlobalState("mistralCodestralUrl", mistralCodestralUrl),
- this.storeSecret("unboundApiKey", unboundApiKey),
- this.updateGlobalState("unboundModelId", unboundModelId),
- this.updateGlobalState("unboundModelInfo", unboundModelInfo),
- this.storeSecret("requestyApiKey", requestyApiKey),
- this.updateGlobalState("requestyModelId", requestyModelId),
- this.updateGlobalState("requestyModelInfo", requestyModelInfo),
- this.updateGlobalState("modelTemperature", modelTemperature),
- this.updateGlobalState("modelMaxTokens", modelMaxTokens),
- this.updateGlobalState("anthropicThinking", modelMaxThinkingTokens),
- this.updateGlobalState("lmStudioDraftModelId", lmStudioDraftModelId),
- this.updateGlobalState("lmStudioSpeculativeDecodingEnabled", lmStudioSpeculativeDecodingEnabled),
- ])
+ // Create an array of promises to update state
+ const promises: Promise[] = []
+
+ // For each property in apiConfiguration, update the appropriate state
+ Object.entries(apiConfiguration).forEach(([key, value]) => {
+ // Check if this key is a secret
+ if (SECRET_KEYS.includes(key as SecretKey)) {
+ promises.push(this.storeSecret(key as SecretKey, value))
+ } else {
+ promises.push(this.updateGlobalState(key as GlobalStateKey, value))
+ }
+ })
+
+ await Promise.all(promises)
+
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
}
@@ -1790,13 +1721,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async ensureSettingsDirectoryExists(): Promise {
- const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
+ const settingsDir = path.join(this.contextProxy.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
private async ensureCacheDirectoryExists() {
- const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
+ const cacheDir = path.join(this.contextProxy.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
@@ -1884,7 +1815,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
const historyItem = history.find((item) => item.id === id)
if (historyItem) {
- const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
+ const taskDirPath = path.join(this.contextProxy.globalStorageUri.fsPath, "tasks", id)
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
@@ -2049,7 +1980,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
uriScheme: vscode.env.uriScheme,
currentTaskItem: this.cline?.taskId
- ? (taskHistory || []).find((item) => item.id === this.cline?.taskId)
+ ? (taskHistory || []).find((item: HistoryItem) => item.id === this.cline?.taskId)
: undefined,
clineMessages: this.cline?.clineMessages || [],
taskHistory: (taskHistory || [])
@@ -2140,189 +2071,41 @@ export class ClineProvider implements vscode.WebviewViewProvider {
*/
async getState() {
- const [
- storedApiProvider,
- apiModelId,
- apiKey,
- glamaApiKey,
- glamaModelId,
- glamaModelInfo,
- openRouterApiKey,
- awsAccessKey,
- awsSecretKey,
- awsSessionToken,
- awsRegion,
- awsUseCrossRegionInference,
- awsProfile,
- awsUseProfile,
- vertexProjectId,
- vertexRegion,
- openAiBaseUrl,
- openAiApiKey,
- openAiModelId,
- openAiCustomModelInfo,
- openAiUseAzure,
- ollamaModelId,
- ollamaBaseUrl,
- lmStudioModelId,
- lmStudioBaseUrl,
- anthropicBaseUrl,
- geminiApiKey,
- openAiNativeApiKey,
- deepSeekApiKey,
- mistralApiKey,
- mistralCodestralUrl,
- azureApiVersion,
- openAiStreamingEnabled,
- openRouterModelId,
- openRouterModelInfo,
- openRouterBaseUrl,
- openRouterUseMiddleOutTransform,
- lastShownAnnouncementId,
- customInstructions,
- alwaysAllowReadOnly,
- alwaysAllowWrite,
- alwaysAllowExecute,
- alwaysAllowBrowser,
- alwaysAllowMcp,
- alwaysAllowModeSwitch,
- taskHistory,
- allowedCommands,
- soundEnabled,
- diffEnabled,
- enableCheckpoints,
- soundVolume,
- browserViewportSize,
- fuzzyMatchThreshold,
- preferredLanguage,
- writeDelayMs,
- screenshotQuality,
- terminalOutputLineLimit,
- mcpEnabled,
- enableMcpServerCreation,
- alwaysApproveResubmit,
- requestDelaySeconds,
- rateLimitSeconds,
- currentApiConfigName,
- listApiConfigMeta,
- vsCodeLmModelSelector,
- mode,
- modeApiConfigs,
- customModePrompts,
- customSupportPrompts,
- enhancementApiConfigId,
- autoApprovalEnabled,
- customModes,
- experiments,
- unboundApiKey,
- unboundModelId,
- unboundModelInfo,
- requestyApiKey,
- requestyModelId,
- requestyModelInfo,
- modelTemperature,
- modelMaxTokens,
- modelMaxThinkingTokens,
- maxOpenTabsContext,
- browserToolEnabled,
- lmStudioSpeculativeDecodingEnabled,
- lmStudioDraftModelId,
- ] = await Promise.all([
- this.getGlobalState("apiProvider") as Promise,
- this.getGlobalState("apiModelId") as Promise,
- this.getSecret("apiKey") as Promise,
- this.getSecret("glamaApiKey") as Promise,
- this.getGlobalState("glamaModelId") as Promise,
- this.getGlobalState("glamaModelInfo") as Promise,
- this.getSecret("openRouterApiKey") as Promise,
- this.getSecret("awsAccessKey") as Promise,
- this.getSecret("awsSecretKey") as Promise,
- this.getSecret("awsSessionToken") as Promise,
- this.getGlobalState("awsRegion") as Promise,
- this.getGlobalState("awsUseCrossRegionInference") as Promise,
- this.getGlobalState("awsProfile") as Promise,
- this.getGlobalState("awsUseProfile") as Promise,
- this.getGlobalState("vertexProjectId") as Promise,
- this.getGlobalState("vertexRegion") as Promise,
- this.getGlobalState("openAiBaseUrl") as Promise,
- this.getSecret("openAiApiKey") as Promise,
- this.getGlobalState("openAiModelId") as Promise,
- this.getGlobalState("openAiCustomModelInfo") as Promise,
- this.getGlobalState("openAiUseAzure") as Promise,
- this.getGlobalState("ollamaModelId") as Promise,
- this.getGlobalState("ollamaBaseUrl") as Promise,
- this.getGlobalState("lmStudioModelId") as Promise,
- this.getGlobalState("lmStudioBaseUrl") as Promise,
- this.getGlobalState("anthropicBaseUrl") as Promise,
- this.getSecret("geminiApiKey") as Promise,
- this.getSecret("openAiNativeApiKey") as Promise,
- this.getSecret("deepSeekApiKey") as Promise,
- this.getSecret("mistralApiKey") as Promise,
- this.getGlobalState("mistralCodestralUrl") as Promise,
- this.getGlobalState("azureApiVersion") as Promise,
- this.getGlobalState("openAiStreamingEnabled") as Promise,
- this.getGlobalState("openRouterModelId") as Promise,
- this.getGlobalState("openRouterModelInfo") as Promise,
- this.getGlobalState("openRouterBaseUrl") as Promise,
- this.getGlobalState("openRouterUseMiddleOutTransform") as Promise,
- this.getGlobalState("lastShownAnnouncementId") as Promise,
- this.getGlobalState("customInstructions") as Promise,
- this.getGlobalState("alwaysAllowReadOnly") as Promise,
- this.getGlobalState("alwaysAllowWrite") as Promise,
- this.getGlobalState("alwaysAllowExecute") as Promise,
- this.getGlobalState("alwaysAllowBrowser") as Promise,
- this.getGlobalState("alwaysAllowMcp") as Promise,
- this.getGlobalState("alwaysAllowModeSwitch") as Promise,
- this.getGlobalState("taskHistory") as Promise,
- this.getGlobalState("allowedCommands") as Promise,
- this.getGlobalState("soundEnabled") as Promise,
- this.getGlobalState("diffEnabled") as Promise,
- this.getGlobalState("enableCheckpoints") as Promise,
- this.getGlobalState("soundVolume") as Promise,
- this.getGlobalState("browserViewportSize") as Promise,
- this.getGlobalState("fuzzyMatchThreshold") as Promise,
- this.getGlobalState("preferredLanguage") as Promise,
- this.getGlobalState("writeDelayMs") as Promise,
- this.getGlobalState("screenshotQuality") as Promise,
- this.getGlobalState("terminalOutputLineLimit") as Promise,
- this.getGlobalState("mcpEnabled") as Promise,
- this.getGlobalState("enableMcpServerCreation") as Promise,
- this.getGlobalState("alwaysApproveResubmit") as Promise,
- this.getGlobalState("requestDelaySeconds") as Promise,
- this.getGlobalState("rateLimitSeconds") as Promise,
- this.getGlobalState("currentApiConfigName") as Promise,
- this.getGlobalState("listApiConfigMeta") as Promise,
- this.getGlobalState("vsCodeLmModelSelector") as Promise,
- this.getGlobalState("mode") as Promise,
- this.getGlobalState("modeApiConfigs") as Promise | undefined>,
- this.getGlobalState("customModePrompts") as Promise,
- this.getGlobalState("customSupportPrompts") as Promise,
- this.getGlobalState("enhancementApiConfigId") as Promise,
- this.getGlobalState("autoApprovalEnabled") as Promise,
- this.customModesManager.getCustomModes(),
- this.getGlobalState("experiments") as Promise | undefined>,
- this.getSecret("unboundApiKey") as Promise,
- this.getGlobalState("unboundModelId") as Promise,
- this.getGlobalState("unboundModelInfo") as Promise,
- this.getSecret("requestyApiKey") as Promise,
- this.getGlobalState("requestyModelId") as Promise,
- this.getGlobalState("requestyModelInfo") as Promise,
- this.getGlobalState("modelTemperature") as Promise,
- this.getGlobalState("modelMaxTokens") as Promise,
- this.getGlobalState("anthropicThinking") as Promise,
- this.getGlobalState("maxOpenTabsContext") as Promise,
- this.getGlobalState("browserToolEnabled") as Promise,
- this.getGlobalState("lmStudioSpeculativeDecodingEnabled") as Promise,
- this.getGlobalState("lmStudioDraftModelId") as Promise,
+ // Create an object to store all fetched values
+ const stateValues: Record = {} as Record
+ const secretValues: Record = {} as Record
+
+ // Create promise arrays for global state and secrets
+ const statePromises = GLOBAL_STATE_KEYS.map((key) => this.getGlobalState(key))
+ const secretPromises = SECRET_KEYS.map((key) => this.getSecret(key))
+
+ // Add promise for custom modes which is handled separately
+ const customModesPromise = this.customModesManager.getCustomModes()
+
+ // Wait for all promises to resolve
+ const [stateResults, secretResults, customModes] = await Promise.all([
+ Promise.all(statePromises),
+ Promise.all(secretPromises),
+ customModesPromise,
])
+ // Populate stateValues and secretValues
+ GLOBAL_STATE_KEYS.forEach((key, index) => {
+ stateValues[key] = stateResults[index]
+ })
+
+ SECRET_KEYS.forEach((key, index) => {
+ secretValues[key] = secretResults[index]
+ })
+
+ // Determine apiProvider with the same logic as before
let apiProvider: ApiProvider
- if (storedApiProvider) {
- apiProvider = storedApiProvider
+ if (stateValues.apiProvider) {
+ apiProvider = stateValues.apiProvider
} else {
// Either new user or legacy user that doesn't have the apiProvider stored in state
// (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
- if (apiKey) {
+ if (secretValues.apiKey) {
apiProvider = "anthropic"
} else {
// New users should default to openrouter
@@ -2330,80 +2113,73 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
+ // Build the apiConfiguration object combining state values and secrets
+ const apiConfiguration: ApiConfiguration = {
+ apiProvider,
+ apiModelId: stateValues.apiModelId,
+ glamaModelId: stateValues.glamaModelId,
+ glamaModelInfo: stateValues.glamaModelInfo,
+ awsRegion: stateValues.awsRegion,
+ awsUseCrossRegionInference: stateValues.awsUseCrossRegionInference,
+ awsProfile: stateValues.awsProfile,
+ awsUseProfile: stateValues.awsUseProfile,
+ vertexProjectId: stateValues.vertexProjectId,
+ vertexRegion: stateValues.vertexRegion,
+ openAiBaseUrl: stateValues.openAiBaseUrl,
+ openAiModelId: stateValues.openAiModelId,
+ openAiCustomModelInfo: stateValues.openAiCustomModelInfo,
+ openAiUseAzure: stateValues.openAiUseAzure,
+ ollamaModelId: stateValues.ollamaModelId,
+ ollamaBaseUrl: stateValues.ollamaBaseUrl,
+ lmStudioModelId: stateValues.lmStudioModelId,
+ lmStudioBaseUrl: stateValues.lmStudioBaseUrl,
+ anthropicBaseUrl: stateValues.anthropicBaseUrl,
+ modelMaxThinkingTokens: stateValues.modelMaxThinkingTokens,
+ mistralCodestralUrl: stateValues.mistralCodestralUrl,
+ azureApiVersion: stateValues.azureApiVersion,
+ openAiStreamingEnabled: stateValues.openAiStreamingEnabled,
+ openRouterModelId: stateValues.openRouterModelId,
+ openRouterModelInfo: stateValues.openRouterModelInfo,
+ openRouterBaseUrl: stateValues.openRouterBaseUrl,
+ openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform,
+ vsCodeLmModelSelector: stateValues.vsCodeLmModelSelector,
+ unboundModelId: stateValues.unboundModelId,
+ unboundModelInfo: stateValues.unboundModelInfo,
+ requestyModelId: stateValues.requestyModelId,
+ requestyModelInfo: stateValues.requestyModelInfo,
+ modelTemperature: stateValues.modelTemperature,
+ modelMaxTokens: stateValues.modelMaxTokens,
+ lmStudioSpeculativeDecodingEnabled: stateValues.lmStudioSpeculativeDecodingEnabled,
+ lmStudioDraftModelId: stateValues.lmStudioDraftModelId,
+ // Add all secrets
+ ...secretValues,
+ }
+
+ // Return the same structure as before
return {
- apiConfiguration: {
- apiProvider,
- apiModelId,
- apiKey,
- glamaApiKey,
- glamaModelId,
- glamaModelInfo,
- openRouterApiKey,
- awsAccessKey,
- awsSecretKey,
- awsSessionToken,
- awsRegion,
- awsUseCrossRegionInference,
- awsProfile,
- awsUseProfile,
- vertexProjectId,
- vertexRegion,
- openAiBaseUrl,
- openAiApiKey,
- openAiModelId,
- openAiCustomModelInfo,
- openAiUseAzure,
- ollamaModelId,
- ollamaBaseUrl,
- lmStudioModelId,
- lmStudioBaseUrl,
- anthropicBaseUrl,
- geminiApiKey,
- openAiNativeApiKey,
- deepSeekApiKey,
- mistralApiKey,
- mistralCodestralUrl,
- azureApiVersion,
- openAiStreamingEnabled,
- openRouterModelId,
- openRouterModelInfo,
- openRouterBaseUrl,
- openRouterUseMiddleOutTransform,
- vsCodeLmModelSelector,
- unboundApiKey,
- unboundModelId,
- unboundModelInfo,
- requestyApiKey,
- requestyModelId,
- requestyModelInfo,
- modelTemperature,
- modelMaxTokens,
- modelMaxThinkingTokens,
- lmStudioSpeculativeDecodingEnabled,
- lmStudioDraftModelId,
- },
- lastShownAnnouncementId,
- customInstructions,
- alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
- alwaysAllowWrite: alwaysAllowWrite ?? false,
- alwaysAllowExecute: alwaysAllowExecute ?? false,
- alwaysAllowBrowser: alwaysAllowBrowser ?? false,
- alwaysAllowMcp: alwaysAllowMcp ?? false,
- alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
- taskHistory,
- allowedCommands,
- soundEnabled: soundEnabled ?? false,
- diffEnabled: diffEnabled ?? true,
- enableCheckpoints: enableCheckpoints ?? true,
- soundVolume,
- browserViewportSize: browserViewportSize ?? "900x600",
- screenshotQuality: screenshotQuality ?? 75,
- fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
- writeDelayMs: writeDelayMs ?? 1000,
- terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
- mode: mode ?? defaultModeSlug,
+ apiConfiguration,
+ lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
+ customInstructions: stateValues.customInstructions,
+ alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,
+ alwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,
+ alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
+ alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
+ alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
+ alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
+ taskHistory: stateValues.taskHistory,
+ allowedCommands: stateValues.allowedCommands,
+ soundEnabled: stateValues.soundEnabled ?? false,
+ diffEnabled: stateValues.diffEnabled ?? true,
+ enableCheckpoints: stateValues.enableCheckpoints ?? false,
+ soundVolume: stateValues.soundVolume,
+ browserViewportSize: stateValues.browserViewportSize ?? "900x600",
+ screenshotQuality: stateValues.screenshotQuality ?? 75,
+ fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
+ writeDelayMs: stateValues.writeDelayMs ?? 1000,
+ terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
+ mode: stateValues.mode ?? defaultModeSlug,
preferredLanguage:
- preferredLanguage ??
+ stateValues.preferredLanguage ??
(() => {
// Get VSCode's locale setting
const vscodeLang = vscode.env.language
@@ -2433,23 +2209,23 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Return mapped language or default to English
return langMap[vscodeLang] ?? langMap[vscodeLang.split("-")[0]] ?? "English"
})(),
- mcpEnabled: mcpEnabled ?? true,
- enableMcpServerCreation: enableMcpServerCreation ?? true,
- alwaysApproveResubmit: alwaysApproveResubmit ?? false,
- requestDelaySeconds: Math.max(5, requestDelaySeconds ?? 10),
- rateLimitSeconds: rateLimitSeconds ?? 0,
- currentApiConfigName: currentApiConfigName ?? "default",
- listApiConfigMeta: listApiConfigMeta ?? [],
- modeApiConfigs: modeApiConfigs ?? ({} as Record),
- customModePrompts: customModePrompts ?? {},
- customSupportPrompts: customSupportPrompts ?? {},
- enhancementApiConfigId,
- experiments: experiments ?? experimentDefault,
- autoApprovalEnabled: autoApprovalEnabled ?? false,
+ mcpEnabled: stateValues.mcpEnabled ?? true,
+ enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
+ alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,
+ requestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),
+ rateLimitSeconds: stateValues.rateLimitSeconds ?? 0,
+ currentApiConfigName: stateValues.currentApiConfigName ?? "default",
+ listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
+ modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record),
+ customModePrompts: stateValues.customModePrompts ?? {},
+ customSupportPrompts: stateValues.customSupportPrompts ?? {},
+ enhancementApiConfigId: stateValues.enhancementApiConfigId,
+ experiments: stateValues.experiments ?? experimentDefault,
+ autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
customModes,
- maxOpenTabsContext: maxOpenTabsContext ?? 20,
- openRouterUseMiddleOutTransform: openRouterUseMiddleOutTransform ?? true,
- browserToolEnabled: browserToolEnabled ?? true,
+ maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
+ openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,
+ browserToolEnabled: stateValues.browserToolEnabled ?? true,
}
}
@@ -2469,25 +2245,29 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// global
async updateGlobalState(key: GlobalStateKey, value: any) {
- await this.context.globalState.update(key, value)
+ this.outputChannel.appendLine(`Updating global state: ${key}`)
+ await this.contextProxy.updateGlobalState(key, value)
+
+ // // If we have a lot of pending changes, consider saving them periodically
+ // if (this.contextProxy.hasPendingChanges() && Math.random() < 0.1) { // 10% chance to save changes
+ // this.outputChannel.appendLine("Periodically flushing context state changes")
+ // await this.contextProxy.saveChanges()
+ // }
}
async getGlobalState(key: GlobalStateKey) {
- return await this.context.globalState.get(key)
+ return await this.contextProxy.getGlobalState(key)
}
// secrets
public async storeSecret(key: SecretKey, value?: string) {
- if (value) {
- await this.context.secrets.store(key, value)
- } else {
- await this.context.secrets.delete(key)
- }
+ this.outputChannel.appendLine(`Storing secret: ${key}`)
+ await this.contextProxy.storeSecret(key, value)
}
private async getSecret(key: SecretKey) {
- return await this.context.secrets.get(key)
+ return await this.contextProxy.getSecret(key)
}
// dev
@@ -2504,24 +2284,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
for (const key of this.context.globalState.keys()) {
- await this.context.globalState.update(key, undefined)
+ // Still using original context for listing keys
+ await this.contextProxy.updateGlobalState(key, undefined)
}
- const secretKeys: SecretKey[] = [
- "apiKey",
- "glamaApiKey",
- "openRouterApiKey",
- "awsAccessKey",
- "awsSecretKey",
- "awsSessionToken",
- "openAiApiKey",
- "geminiApiKey",
- "openAiNativeApiKey",
- "deepSeekApiKey",
- "mistralApiKey",
- "unboundApiKey",
- "requestyApiKey",
- ]
- for (const key of secretKeys) {
+
+ for (const key of SECRET_KEYS) {
await this.storeSecret(key, undefined)
}
await this.configManager.resetAllConfigs()
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 154c24bc27..20778b8802 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -5,6 +5,7 @@ import axios from "axios"
import { ClineProvider } from "../ClineProvider"
import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
+import { GlobalStateKey, SecretKey } from "../../../shared/globalState"
import { setSoundEnabled } from "../../../utils/sound"
import { defaultModeSlug } from "../../../shared/modes"
import { experimentDefault } from "../../../shared/experiments"
@@ -12,6 +13,34 @@ import { experimentDefault } from "../../../shared/experiments"
// Mock setup must come before imports
jest.mock("../../prompts/sections/custom-instructions")
+// Mock ContextProxy
+jest.mock("../../contextProxy", () => {
+ return {
+ ContextProxy: jest.fn().mockImplementation((context) => ({
+ originalContext: context,
+ extensionUri: context.extensionUri,
+ extensionPath: context.extensionPath,
+ globalStorageUri: context.globalStorageUri,
+ logUri: context.logUri,
+ extension: context.extension,
+ extensionMode: context.extensionMode,
+ getGlobalState: jest
+ .fn()
+ .mockImplementation((key, defaultValue) => context.globalState.get(key, defaultValue)),
+ updateGlobalState: jest.fn().mockImplementation((key, value) => context.globalState.update(key, value)),
+ getSecret: jest.fn().mockImplementation((key) => context.secrets.get(key)),
+ storeSecret: jest
+ .fn()
+ .mockImplementation((key, value) =>
+ value ? context.secrets.store(key, value) : context.secrets.delete(key),
+ ),
+ saveChanges: jest.fn().mockResolvedValue(undefined),
+ dispose: jest.fn().mockResolvedValue(undefined),
+ hasPendingChanges: jest.fn().mockReturnValue(false),
+ })),
+ }
+})
+
// Mock dependencies
jest.mock("vscode")
jest.mock("delay")
@@ -153,6 +182,16 @@ jest.mock("../../../utils/sound", () => ({
setSoundEnabled: jest.fn(),
}))
+// Mock logger
+jest.mock("../../../utils/logging", () => ({
+ logger: {
+ debug: jest.fn(),
+ error: jest.fn(),
+ warn: jest.fn(),
+ info: jest.fn(),
+ },
+}))
+
// Mock ESM modules
jest.mock("p-wait-for", () => ({
__esModule: true,
@@ -235,6 +274,12 @@ describe("ClineProvider", () => {
let mockOutputChannel: vscode.OutputChannel
let mockWebviewView: vscode.WebviewView
let mockPostMessage: jest.Mock
+ let mockContextProxy: {
+ updateGlobalState: jest.Mock
+ getGlobalState: jest.Mock
+ storeSecret: jest.Mock
+ dispose: jest.Mock
+ }
beforeEach(() => {
// Reset mocks
@@ -307,6 +352,8 @@ describe("ClineProvider", () => {
} as unknown as vscode.WebviewView
provider = new ClineProvider(mockContext, mockOutputChannel)
+ // @ts-ignore - Access private property for testing
+ mockContextProxy = provider.contextProxy
// @ts-ignore - Accessing private property for testing.
provider.customModesManager = mockCustomModesManager
@@ -478,6 +525,7 @@ describe("ClineProvider", () => {
await messageHandler({ type: "writeDelayMs", value: 2000 })
+ expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("writeDelayMs", 2000)
expect(mockContext.globalState.update).toHaveBeenCalledWith("writeDelayMs", 2000)
expect(mockPostMessage).toHaveBeenCalled()
})
@@ -491,6 +539,7 @@ describe("ClineProvider", () => {
// Simulate setting sound to enabled
await messageHandler({ type: "soundEnabled", bool: true })
expect(setSoundEnabled).toHaveBeenCalledWith(true)
+ expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("soundEnabled", true)
expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", true)
expect(mockPostMessage).toHaveBeenCalled()
@@ -613,6 +662,7 @@ describe("ClineProvider", () => {
// Test alwaysApproveResubmit
await messageHandler({ type: "alwaysApproveResubmit", bool: true })
+ expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("alwaysApproveResubmit", true)
expect(mockContext.globalState.update).toHaveBeenCalledWith("alwaysApproveResubmit", true)
expect(mockPostMessage).toHaveBeenCalled()
@@ -1253,6 +1303,17 @@ describe("ClineProvider", () => {
// Verify state was posted to webview
expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
})
+
+ test("disposes the contextProxy when provider is disposed", async () => {
+ // Setup mock Cline instance
+ const mockCline = {
+ abortTask: jest.fn(),
+ }
+ // @ts-ignore - accessing private property for testing
+ provider.cline = mockCline
+ await provider.dispose()
+ expect(mockContextProxy.dispose).toHaveBeenCalled()
+ })
})
describe("updateCustomMode", () => {
@@ -1474,6 +1535,7 @@ describe("ClineProvider", () => {
apiConfiguration: testApiConfig,
})
+ // Reset jest.mock calls tracking
// Verify config was saved
expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
@@ -1481,6 +1543,74 @@ describe("ClineProvider", () => {
expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
])
+ expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("listApiConfigMeta", [
+ { name: "test-config", id: "test-id", apiProvider: "anthropic" },
+ ])
+
+ // Reset jest.mock calls tracking for subsequent tests
+ jest.clearAllMocks()
})
})
})
+
+describe("ContextProxy integration", () => {
+ let provider: ClineProvider
+ let mockContext: vscode.ExtensionContext
+ let mockOutputChannel: vscode.OutputChannel
+ let mockContextProxy: any
+
+ beforeEach(() => {
+ // Reset mocks
+ jest.clearAllMocks()
+
+ // Setup basic mocks
+ mockContext = {
+ globalState: { get: jest.fn(), update: jest.fn(), keys: jest.fn().mockReturnValue([]) },
+ secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
+ extensionUri: {} as vscode.Uri,
+ globalStorageUri: { fsPath: "/test/path" },
+ extension: { packageJSON: { version: "1.0.0" } },
+ } as unknown as vscode.ExtensionContext
+
+ mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel
+ provider = new ClineProvider(mockContext, mockOutputChannel)
+
+ // @ts-ignore - accessing private property for testing
+ mockContextProxy = provider.contextProxy
+ })
+
+ test("updateGlobalState uses contextProxy", async () => {
+ await provider.updateGlobalState("currentApiConfigName" as GlobalStateKey, "testValue")
+ expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("currentApiConfigName", "testValue")
+ })
+
+ test("getGlobalState uses contextProxy", async () => {
+ mockContextProxy.getGlobalState.mockResolvedValueOnce("testValue")
+ const result = await provider.getGlobalState("currentApiConfigName" as GlobalStateKey)
+ expect(mockContextProxy.getGlobalState).toHaveBeenCalledWith("currentApiConfigName")
+ expect(result).toBe("testValue")
+ })
+
+ test("storeSecret uses contextProxy", async () => {
+ await provider.storeSecret("apiKey" as SecretKey, "test-secret")
+ expect(mockContextProxy.storeSecret).toHaveBeenCalledWith("apiKey", "test-secret")
+ })
+
+ test("contextProxy methods are available", () => {
+ // Verify the contextProxy has all the required methods
+ expect(mockContextProxy.getGlobalState).toBeDefined()
+ expect(mockContextProxy.updateGlobalState).toBeDefined()
+ expect(mockContextProxy.storeSecret).toBeDefined()
+ })
+
+ test("contextProxy is properly disposed", async () => {
+ // Setup mock Cline instance
+ const mockCline = {
+ abortTask: jest.fn(),
+ }
+ // @ts-ignore - accessing private property for testing
+ provider.cline = mockCline
+ await provider.dispose()
+ expect(mockContextProxy.dispose).toHaveBeenCalled()
+ })
+})
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 88f9824151..1f36732466 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -13,6 +13,22 @@ export type SecretKey =
| "unboundApiKey"
| "requestyApiKey"
+export const SECRET_KEYS: SecretKey[] = [
+ "apiKey",
+ "glamaApiKey",
+ "openRouterApiKey",
+ "awsAccessKey",
+ "awsSecretKey",
+ "awsSessionToken",
+ "openAiApiKey",
+ "geminiApiKey",
+ "openAiNativeApiKey",
+ "deepSeekApiKey",
+ "mistralApiKey",
+ "unboundApiKey",
+ "requestyApiKey",
+]
+
export type GlobalStateKey =
| "apiProvider"
| "apiModelId"
@@ -83,7 +99,80 @@ export type GlobalStateKey =
| "unboundModelInfo"
| "modelTemperature"
| "modelMaxTokens"
- | "anthropicThinking" // TODO: Rename to `modelMaxThinkingTokens`.
+ | "modelMaxThinkingTokens"
| "mistralCodestralUrl"
| "maxOpenTabsContext"
| "browserToolEnabled" // Setting to enable/disable the browser tool
+
+export const GLOBAL_STATE_KEYS: GlobalStateKey[] = [
+ "apiProvider",
+ "apiModelId",
+ "glamaModelId",
+ "glamaModelInfo",
+ "awsRegion",
+ "awsUseCrossRegionInference",
+ "awsProfile",
+ "awsUseProfile",
+ "vertexProjectId",
+ "vertexRegion",
+ "lastShownAnnouncementId",
+ "customInstructions",
+ "alwaysAllowReadOnly",
+ "alwaysAllowWrite",
+ "alwaysAllowExecute",
+ "alwaysAllowBrowser",
+ "alwaysAllowMcp",
+ "alwaysAllowModeSwitch",
+ "taskHistory",
+ "openAiBaseUrl",
+ "openAiModelId",
+ "openAiCustomModelInfo",
+ "openAiUseAzure",
+ "ollamaModelId",
+ "ollamaBaseUrl",
+ "lmStudioModelId",
+ "lmStudioBaseUrl",
+ "anthropicBaseUrl",
+ "modelMaxThinkingTokens",
+ "azureApiVersion",
+ "openAiStreamingEnabled",
+ "openRouterModelId",
+ "openRouterModelInfo",
+ "openRouterBaseUrl",
+ "openRouterUseMiddleOutTransform",
+ "allowedCommands",
+ "soundEnabled",
+ "soundVolume",
+ "diffEnabled",
+ "enableCheckpoints",
+ "browserViewportSize",
+ "screenshotQuality",
+ "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",
+ "customModes", // Array of custom modes
+ "unboundModelId",
+ "requestyModelId",
+ "requestyModelInfo",
+ "unboundModelInfo",
+ "modelTemperature",
+ "modelMaxTokens",
+ "mistralCodestralUrl",
+ "maxOpenTabsContext",
+]
From 167229fa7365e3f1a0eff638209902d394ae84e6 Mon Sep 17 00:00:00 2001
From: sam hoang
Date: Thu, 27 Feb 2025 15:47:24 +0700
Subject: [PATCH 11/22] Refactor checkExistKey to use centralized SECRET_KEYS
array
---
src/shared/checkExistApiConfig.ts | 35 ++++++++++++++-----------------
1 file changed, 16 insertions(+), 19 deletions(-)
diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts
index 0570f6118a..c141a153d2 100644
--- a/src/shared/checkExistApiConfig.ts
+++ b/src/shared/checkExistApiConfig.ts
@@ -1,23 +1,20 @@
import { ApiConfiguration } from "../shared/api"
+import { SECRET_KEYS } from "./globalState"
export function checkExistKey(config: ApiConfiguration | undefined) {
- return config
- ? [
- config.apiKey,
- config.glamaApiKey,
- config.openRouterApiKey,
- config.awsRegion,
- config.vertexProjectId,
- config.openAiApiKey,
- config.ollamaModelId,
- config.lmStudioModelId,
- config.geminiApiKey,
- config.openAiNativeApiKey,
- config.deepSeekApiKey,
- config.mistralApiKey,
- config.vsCodeLmModelSelector,
- config.requestyApiKey,
- config.unboundApiKey,
- ].some((key) => key !== undefined)
- : false
+ if (!config) return false
+
+ // Check all secret keys from the centralized SECRET_KEYS array
+ const hasSecretKey = SECRET_KEYS.some((key) => config[key as keyof ApiConfiguration] !== undefined)
+
+ // Check additional non-secret configuration properties
+ const hasOtherConfig = [
+ config.awsRegion,
+ config.vertexProjectId,
+ config.ollamaModelId,
+ config.lmStudioModelId,
+ config.vsCodeLmModelSelector,
+ ].some((value) => value !== undefined)
+
+ return hasSecretKey || hasOtherConfig
}
From b4094a628168682df58a185021b139c50293b4a1 Mon Sep 17 00:00:00 2001
From: sam hoang
Date: Tue, 4 Mar 2025 20:26:22 +0700
Subject: [PATCH 12/22] refactor by pr comment
---
src/core/webview/ClineProvider.ts | 46 ++++++-------------------------
src/shared/api.ts | 43 +++++++++++++++++++++++++++++
2 files changed, 52 insertions(+), 37 deletions(-)
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index d9a1525730..ae53e58df9 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -8,7 +8,7 @@ import * as path from "path"
import * as vscode from "vscode"
import simpleGit from "simple-git"
-import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
+import { ApiConfiguration, ApiProvider, ModelInfo, API_CONFIG_KEYS } from "../../shared/api"
import { findLast } from "../../shared/array"
import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
import { GlobalFileNames } from "../../shared/globalFileNames"
@@ -2114,47 +2114,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
// Build the apiConfiguration object combining state values and secrets
+ // Using the dynamic approach with API_CONFIG_KEYS
const apiConfiguration: ApiConfiguration = {
- apiProvider,
- apiModelId: stateValues.apiModelId,
- glamaModelId: stateValues.glamaModelId,
- glamaModelInfo: stateValues.glamaModelInfo,
- awsRegion: stateValues.awsRegion,
- awsUseCrossRegionInference: stateValues.awsUseCrossRegionInference,
- awsProfile: stateValues.awsProfile,
- awsUseProfile: stateValues.awsUseProfile,
- vertexProjectId: stateValues.vertexProjectId,
- vertexRegion: stateValues.vertexRegion,
- openAiBaseUrl: stateValues.openAiBaseUrl,
- openAiModelId: stateValues.openAiModelId,
- openAiCustomModelInfo: stateValues.openAiCustomModelInfo,
- openAiUseAzure: stateValues.openAiUseAzure,
- ollamaModelId: stateValues.ollamaModelId,
- ollamaBaseUrl: stateValues.ollamaBaseUrl,
- lmStudioModelId: stateValues.lmStudioModelId,
- lmStudioBaseUrl: stateValues.lmStudioBaseUrl,
- anthropicBaseUrl: stateValues.anthropicBaseUrl,
- modelMaxThinkingTokens: stateValues.modelMaxThinkingTokens,
- mistralCodestralUrl: stateValues.mistralCodestralUrl,
- azureApiVersion: stateValues.azureApiVersion,
- openAiStreamingEnabled: stateValues.openAiStreamingEnabled,
- openRouterModelId: stateValues.openRouterModelId,
- openRouterModelInfo: stateValues.openRouterModelInfo,
- openRouterBaseUrl: stateValues.openRouterBaseUrl,
- openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform,
- vsCodeLmModelSelector: stateValues.vsCodeLmModelSelector,
- unboundModelId: stateValues.unboundModelId,
- unboundModelInfo: stateValues.unboundModelInfo,
- requestyModelId: stateValues.requestyModelId,
- requestyModelInfo: stateValues.requestyModelInfo,
- modelTemperature: stateValues.modelTemperature,
- modelMaxTokens: stateValues.modelMaxTokens,
- lmStudioSpeculativeDecodingEnabled: stateValues.lmStudioSpeculativeDecodingEnabled,
- lmStudioDraftModelId: stateValues.lmStudioDraftModelId,
+ // Dynamically add all API-related keys from stateValues
+ ...Object.fromEntries(API_CONFIG_KEYS.map((key) => [key, stateValues[key]])),
// Add all secrets
...secretValues,
}
+ // Ensure apiProvider is set properly if not already in state
+ if (!apiConfiguration.apiProvider) {
+ apiConfiguration.apiProvider = apiProvider
+ }
+
// Return the same structure as before
return {
apiConfiguration,
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 2ce7162640..9709ba79fc 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -78,6 +78,49 @@ export type ApiConfiguration = ApiHandlerOptions & {
id?: string // stable unique identifier
}
+// Import GlobalStateKey type from globalState.ts
+import { GlobalStateKey } from "./globalState"
+
+// Define API configuration keys for dynamic object building
+export const API_CONFIG_KEYS: GlobalStateKey[] = [
+ "apiProvider",
+ "apiModelId",
+ "glamaModelId",
+ "glamaModelInfo",
+ "awsRegion",
+ "awsUseCrossRegionInference",
+ "awsProfile",
+ "awsUseProfile",
+ "vertexProjectId",
+ "vertexRegion",
+ "openAiBaseUrl",
+ "openAiModelId",
+ "openAiCustomModelInfo",
+ "openAiUseAzure",
+ "ollamaModelId",
+ "ollamaBaseUrl",
+ "lmStudioModelId",
+ "lmStudioBaseUrl",
+ "anthropicBaseUrl",
+ "modelMaxThinkingTokens",
+ "mistralCodestralUrl",
+ "azureApiVersion",
+ "openAiStreamingEnabled",
+ "openRouterModelId",
+ "openRouterModelInfo",
+ "openRouterBaseUrl",
+ "openRouterUseMiddleOutTransform",
+ "vsCodeLmModelSelector",
+ "unboundModelId",
+ "unboundModelInfo",
+ "requestyModelId",
+ "requestyModelInfo",
+ "modelTemperature",
+ "modelMaxTokens",
+ "lmStudioSpeculativeDecodingEnabled",
+ "lmStudioDraftModelId"
+]
+
// Models
export interface ModelInfo {
From 0a0634488a6e688efe5a9d2ff3c9bdeb65428c5e Mon Sep 17 00:00:00 2001
From: sam hoang
Date: Tue, 4 Mar 2025 22:34:13 +0700
Subject: [PATCH 13/22] update api config key list to match with api key and
global state key
---
src/shared/api.ts | 26 ++++++++++++++------------
1 file changed, 14 insertions(+), 12 deletions(-)
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 9709ba79fc..7c5c65fe90 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -58,7 +58,6 @@ export interface ApiHandlerOptions {
azureApiVersion?: string
openRouterUseMiddleOutTransform?: boolean
openAiStreamingEnabled?: boolean
- setAzureApiVersion?: boolean
deepSeekBaseUrl?: string
deepSeekApiKey?: string
includeMaxTokens?: boolean
@@ -83,12 +82,18 @@ import { GlobalStateKey } from "./globalState"
// Define API configuration keys for dynamic object building
export const API_CONFIG_KEYS: GlobalStateKey[] = [
- "apiProvider",
"apiModelId",
+ "anthropicBaseUrl",
+ "vsCodeLmModelSelector",
"glamaModelId",
"glamaModelInfo",
+ "openRouterModelId",
+ "openRouterModelInfo",
+ "openRouterBaseUrl",
"awsRegion",
"awsUseCrossRegionInference",
+ // "awsUsePromptCache", // NOT exist on GlobalStateKey
+ // "awspromptCacheId", // NOT exist on GlobalStateKey
"awsProfile",
"awsUseProfile",
"vertexProjectId",
@@ -101,24 +106,21 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
"ollamaBaseUrl",
"lmStudioModelId",
"lmStudioBaseUrl",
- "anthropicBaseUrl",
- "modelMaxThinkingTokens",
- "mistralCodestralUrl",
+ "lmStudioDraftModelId",
+ "lmStudioSpeculativeDecodingEnabled",
+ "mistralCodestralUrl", // New option for Codestral URL
"azureApiVersion",
- "openAiStreamingEnabled",
- "openRouterModelId",
- "openRouterModelInfo",
- "openRouterBaseUrl",
"openRouterUseMiddleOutTransform",
- "vsCodeLmModelSelector",
+ "openAiStreamingEnabled",
+ // "deepSeekBaseUrl", // not exist on GlobalStateKey
+ // "includeMaxTokens", // not exist on GlobalStateKey
"unboundModelId",
"unboundModelInfo",
"requestyModelId",
"requestyModelInfo",
"modelTemperature",
"modelMaxTokens",
- "lmStudioSpeculativeDecodingEnabled",
- "lmStudioDraftModelId"
+ "modelMaxThinkingTokens",
]
// Models
From 9bbd902d5d52e078b390085240f3fa2b932ca070 Mon Sep 17 00:00:00 2001
From: sam hoang
Date: Wed, 5 Mar 2025 00:06:30 +0700
Subject: [PATCH 14/22] update new way to manage state
---
src/core/__tests__/contextProxy.test.ts | 217 +++++-------------
src/core/contextProxy.ts | 141 +++++-------
src/core/webview/ClineProvider.ts | 30 ++-
.../webview/__tests__/ClineProvider.test.ts | 22 --
src/shared/api.ts | 2 +-
5 files changed, 125 insertions(+), 287 deletions(-)
diff --git a/src/core/__tests__/contextProxy.test.ts b/src/core/__tests__/contextProxy.test.ts
index 794cd91497..e6f1bfc9ca 100644
--- a/src/core/__tests__/contextProxy.test.ts
+++ b/src/core/__tests__/contextProxy.test.ts
@@ -1,6 +1,7 @@
import * as vscode from "vscode"
import { ContextProxy } from "../contextProxy"
import { logger } from "../../utils/logging"
+import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../../shared/globalState"
// Mock the logger
jest.mock("../../utils/logging", () => ({
@@ -12,6 +13,12 @@ jest.mock("../../utils/logging", () => ({
},
}))
+// Mock shared/globalState
+jest.mock("../../shared/globalState", () => ({
+ GLOBAL_STATE_KEYS: ["apiProvider", "apiModelId", "mode"],
+ SECRET_KEYS: ["apiKey", "openAiApiKey"],
+}))
+
// Mock VSCode API
jest.mock("vscode", () => ({
Uri: {
@@ -42,7 +49,7 @@ describe("ContextProxy", () => {
// Mock secrets
mockSecrets = {
- get: jest.fn(),
+ get: jest.fn().mockResolvedValue("test-secret"),
store: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(undefined),
}
@@ -74,98 +81,80 @@ describe("ContextProxy", () => {
})
})
- describe("getGlobalState", () => {
- it("should return pending change when it exists", async () => {
- // Set up a pending change
- await proxy.updateGlobalState("test-key", "new-value")
-
- // Should return the pending value
- const result = await proxy.getGlobalState("test-key")
- expect(result).toBe("new-value")
-
- // Original context should not be called
- expect(mockGlobalState.get).not.toHaveBeenCalled()
+ describe("constructor", () => {
+ it("should initialize state cache with all global state keys", () => {
+ expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length)
+ for (const key of GLOBAL_STATE_KEYS) {
+ expect(mockGlobalState.get).toHaveBeenCalledWith(key)
+ }
})
- it("should fall back to original context when no pending change exists", async () => {
- // Set up original context value
- mockGlobalState.get.mockReturnValue("original-value")
+ it("should initialize secret cache with all secret keys", () => {
+ expect(mockSecrets.get).toHaveBeenCalledTimes(SECRET_KEYS.length)
+ for (const key of SECRET_KEYS) {
+ expect(mockSecrets.get).toHaveBeenCalledWith(key)
+ }
+ })
+ })
- // Should get from original context
- const result = await proxy.getGlobalState("test-key")
- expect(result).toBe("original-value")
- expect(mockGlobalState.get).toHaveBeenCalledWith("test-key", undefined)
+ 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")
+
+ // Should return the cached value
+ const result = proxy.getGlobalState("test-key")
+ expect(result).toBe("cached-value")
+
+ // Original context should be called once during updateGlobalState
+ expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length) // Only from initialization
})
it("should handle default values correctly", async () => {
- // No value in either pending or original
- mockGlobalState.get.mockImplementation((key: string, defaultValue: any) => defaultValue)
-
- // Should return the default value
- const result = await proxy.getGlobalState("test-key", "default-value")
+ // No value in cache
+ const result = proxy.getGlobalState("unknown-key", "default-value")
expect(result).toBe("default-value")
})
})
describe("updateGlobalState", () => {
- it("should buffer changes without calling original context", async () => {
+ it("should update state directly in original context", async () => {
await proxy.updateGlobalState("test-key", "new-value")
// Should have called logger.debug
- expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("buffering state update"))
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("updating state for key"))
- // Should not have called original context
- expect(mockGlobalState.update).not.toHaveBeenCalled()
+ // Should have called original context
+ expect(mockGlobalState.update).toHaveBeenCalledWith("test-key", "new-value")
- // Should have stored the value in pendingStateChanges
+ // Should have stored the value in cache
const storedValue = await proxy.getGlobalState("test-key")
expect(storedValue).toBe("new-value")
})
-
- it("should throw an error when context is disposed", async () => {
- await proxy.dispose()
-
- await expect(proxy.updateGlobalState("test-key", "new-value")).rejects.toThrow(
- "Cannot update state on disposed context",
- )
- })
})
describe("getSecret", () => {
- it("should return pending secret when it exists", async () => {
- // Set up a pending secret
- await proxy.storeSecret("api-key", "secret123")
+ it("should return value from cache when it exists", async () => {
+ // Manually set a value in the cache
+ await proxy.storeSecret("api-key", "cached-secret")
- // Should return the pending value
- const result = await proxy.getSecret("api-key")
- expect(result).toBe("secret123")
-
- // Original context should not be called
- expect(mockSecrets.get).not.toHaveBeenCalled()
- })
-
- it("should fall back to original context when no pending secret exists", async () => {
- // Set up original context value
- mockSecrets.get.mockResolvedValue("original-secret")
-
- // Should get from original context
- const result = await proxy.getSecret("api-key")
- expect(result).toBe("original-secret")
- expect(mockSecrets.get).toHaveBeenCalledWith("api-key")
+ // Should return the cached value
+ const result = proxy.getSecret("api-key")
+ expect(result).toBe("cached-secret")
})
})
describe("storeSecret", () => {
- it("should buffer secret changes without calling original context", async () => {
+ it("should store secret directly in original context", async () => {
await proxy.storeSecret("api-key", "new-secret")
// Should have called logger.debug
- expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("buffering secret update"))
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("storing secret for key"))
- // Should not have called original context
- expect(mockSecrets.store).not.toHaveBeenCalled()
+ // Should have called original context
+ expect(mockSecrets.store).toHaveBeenCalledWith("api-key", "new-secret")
- // Should have stored the value in pendingSecretChanges
+ // Should have stored the value in cache
const storedValue = await proxy.getSecret("api-key")
expect(storedValue).toBe("new-secret")
})
@@ -173,110 +162,12 @@ describe("ContextProxy", () => {
it("should handle undefined value for secret deletion", async () => {
await proxy.storeSecret("api-key", undefined)
- // Should have stored undefined in pendingSecretChanges
+ // Should have called delete on original context
+ expect(mockSecrets.delete).toHaveBeenCalledWith("api-key")
+
+ // Should have stored undefined in cache
const storedValue = await proxy.getSecret("api-key")
expect(storedValue).toBeUndefined()
})
-
- it("should throw an error when context is disposed", async () => {
- await proxy.dispose()
-
- await expect(proxy.storeSecret("api-key", "new-secret")).rejects.toThrow(
- "Cannot store secret on disposed context",
- )
- })
- })
-
- describe("saveChanges", () => {
- it("should apply state changes to original context", async () => {
- // Set up pending changes
- await proxy.updateGlobalState("key1", "value1")
- await proxy.updateGlobalState("key2", "value2")
-
- // Save changes
- await proxy.saveChanges()
-
- // Should have called update on original context
- expect(mockGlobalState.update).toHaveBeenCalledTimes(2)
- expect(mockGlobalState.update).toHaveBeenCalledWith("key1", "value1")
- expect(mockGlobalState.update).toHaveBeenCalledWith("key2", "value2")
-
- // Should have cleared pending changes
- expect(proxy.hasPendingChanges()).toBe(false)
- })
-
- it("should apply secret changes to original context", async () => {
- // Set up pending changes
- await proxy.storeSecret("secret1", "value1")
- await proxy.storeSecret("secret2", undefined)
-
- // Save changes
- await proxy.saveChanges()
-
- // Should have called store and delete on original context
- expect(mockSecrets.store).toHaveBeenCalledTimes(1)
- expect(mockSecrets.store).toHaveBeenCalledWith("secret1", "value1")
- expect(mockSecrets.delete).toHaveBeenCalledTimes(1)
- expect(mockSecrets.delete).toHaveBeenCalledWith("secret2")
-
- // Should have cleared pending changes
- expect(proxy.hasPendingChanges()).toBe(false)
- })
-
- it("should do nothing when there are no pending changes", async () => {
- await proxy.saveChanges()
-
- expect(mockGlobalState.update).not.toHaveBeenCalled()
- expect(mockSecrets.store).not.toHaveBeenCalled()
- expect(mockSecrets.delete).not.toHaveBeenCalled()
- })
-
- it("should throw an error when context is disposed", async () => {
- await proxy.dispose()
-
- await expect(proxy.saveChanges()).rejects.toThrow("Cannot save changes on disposed context")
- })
- })
-
- describe("dispose", () => {
- it("should save pending changes to original context", async () => {
- // Set up pending changes
- await proxy.updateGlobalState("key1", "value1")
- await proxy.storeSecret("secret1", "value1")
-
- // Dispose
- await proxy.dispose()
-
- // Should have saved changes
- expect(mockGlobalState.update).toHaveBeenCalledWith("key1", "value1")
- expect(mockSecrets.store).toHaveBeenCalledWith("secret1", "value1")
-
- // Should be marked as disposed
- expect(proxy.hasPendingChanges()).toBe(false)
- })
- })
-
- describe("hasPendingChanges", () => {
- it("should return false when no changes are pending", () => {
- expect(proxy.hasPendingChanges()).toBe(false)
- })
-
- it("should return true when state changes are pending", async () => {
- await proxy.updateGlobalState("key", "value")
- expect(proxy.hasPendingChanges()).toBe(true)
- })
-
- it("should return true when secret changes are pending", async () => {
- await proxy.storeSecret("key", "value")
- expect(proxy.hasPendingChanges()).toBe(true)
- })
-
- it("should return false after changes are saved", async () => {
- await proxy.updateGlobalState("key", "value")
- expect(proxy.hasPendingChanges()).toBe(true)
-
- await proxy.saveChanges()
- expect(proxy.hasPendingChanges()).toBe(false)
- })
})
})
diff --git a/src/core/contextProxy.ts b/src/core/contextProxy.ts
index e4672ae225..7c429c86cf 100644
--- a/src/core/contextProxy.ts
+++ b/src/core/contextProxy.ts
@@ -1,25 +1,53 @@
import * as vscode from "vscode"
import { logger } from "../utils/logging"
+import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../shared/globalState"
-/**
- * A proxy class for vscode.ExtensionContext that buffers state changes
- * and only commits them when explicitly requested or during disposal.
- */
export class ContextProxy {
private readonly originalContext: vscode.ExtensionContext
- private pendingStateChanges: Map
- private pendingSecretChanges: Map
- private disposed: boolean
+ private stateCache: Map
+ private secretCache: Map
constructor(context: vscode.ExtensionContext) {
+ // Initialize properties first
this.originalContext = context
- this.pendingStateChanges = new Map()
- this.pendingSecretChanges = new Map()
- this.disposed = false
+ this.stateCache = new Map()
+ this.secretCache = new Map()
+
+ // 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")
}
- // Read-only pass-through properties
+ // Helper method to initialize state cache
+ private initializeStateCache(): void {
+ for (const key of GLOBAL_STATE_KEYS) {
+ try {
+ const value = this.originalContext.globalState.get(key)
+ this.stateCache.set(key, value)
+ } 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)
+ .then((value) => {
+ this.secretCache.set(key, value)
+ })
+ .catch((error: Error) => {
+ logger.error(`Error loading secret ${key}: ${error.message}`)
+ })
+ }
+ }
+
get extensionUri(): vscode.Uri {
return this.originalContext.extensionUri
}
@@ -39,85 +67,30 @@ export class ContextProxy {
return this.originalContext.extensionMode
}
- // State management methods
- async getGlobalState(key: string): Promise
- async getGlobalState(key: string, defaultValue: T): Promise
- async getGlobalState(key: string, defaultValue?: T): Promise {
- // Check pending changes first
- if (this.pendingStateChanges.has(key)) {
- const value = this.pendingStateChanges.get(key) as T | undefined
- return value !== undefined ? value : (defaultValue as T | undefined)
- }
- // Fall back to original context
- return this.originalContext.globalState.get(key, defaultValue as T)
+ getGlobalState(key: string): T | undefined
+ getGlobalState(key: string, defaultValue: T): T
+ getGlobalState(key: string, defaultValue?: T): T | undefined {
+ const value = this.stateCache.get(key) as T | undefined
+ return value !== undefined ? value : (defaultValue as T | undefined)
}
- async updateGlobalState(key: string, value: T): Promise {
- if (this.disposed) {
- throw new Error("Cannot update state on disposed context")
- }
- logger.debug(`ContextProxy: buffering state update for key "${key}"`)
- this.pendingStateChanges.set(key, value)
+ updateGlobalState(key: string, value: T): Thenable {
+ this.stateCache.set(key, value)
+ return this.originalContext.globalState.update(key, value)
}
- // Secret storage methods
- async getSecret(key: string): Promise {
- // Check pending changes first
- if (this.pendingSecretChanges.has(key)) {
- return this.pendingSecretChanges.get(key)
- }
- // Fall back to original context
- return this.originalContext.secrets.get(key)
+ getSecret(key: string): string | undefined {
+ return this.secretCache.get(key)
}
- async storeSecret(key: string, value?: string): Promise {
- if (this.disposed) {
- throw new Error("Cannot store secret on disposed context")
+ storeSecret(key: string, value?: string): Thenable {
+ // 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)
}
- logger.debug(`ContextProxy: buffering secret update for key "${key}"`)
- this.pendingSecretChanges.set(key, value)
- }
-
- // Save pending changes to actual context
- async saveChanges(): Promise {
- if (this.disposed) {
- throw new Error("Cannot save changes on disposed context")
- }
-
- // Apply state changes
- if (this.pendingStateChanges.size > 0) {
- logger.debug(`ContextProxy: applying ${this.pendingStateChanges.size} buffered state changes`)
- for (const [key, value] of this.pendingStateChanges.entries()) {
- await this.originalContext.globalState.update(key, value)
- }
- this.pendingStateChanges.clear()
- }
-
- // Apply secret changes
- if (this.pendingSecretChanges.size > 0) {
- logger.debug(`ContextProxy: applying ${this.pendingSecretChanges.size} buffered secret changes`)
- for (const [key, value] of this.pendingSecretChanges.entries()) {
- if (value === undefined) {
- await this.originalContext.secrets.delete(key)
- } else {
- await this.originalContext.secrets.store(key, value)
- }
- }
- this.pendingSecretChanges.clear()
- }
- }
-
- // Called when the provider is disposing
- async dispose(): Promise {
- if (!this.disposed) {
- logger.debug("ContextProxy: disposing and saving pending changes")
- await this.saveChanges()
- this.disposed = true
- }
- }
-
- // Method to check if there are pending changes
- hasPendingChanges(): boolean {
- return this.pendingStateChanges.size > 0 || this.pendingSecretChanges.size > 0
}
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index ae53e58df9..748ba2525b 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -16,7 +16,7 @@ import { SecretKey, GlobalStateKey, SECRET_KEYS, GLOBAL_STATE_KEYS } from "../..
import { HistoryItem } from "../../shared/HistoryItem"
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
-import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes"
+import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug, ModeConfig } from "../../shared/modes"
import { checkExistKey } from "../../shared/checkExistApiConfig"
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
import { downloadTask } from "../../integrations/misc/export-markdown"
@@ -119,8 +119,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.customModesManager?.dispose()
this.outputChannel.appendLine("Disposed all disposables")
// Dispose the context proxy to commit any pending changes
- await this.contextProxy.dispose()
- this.outputChannel.appendLine("Disposed context proxy")
ClineProvider.activeInstances.delete(this)
// Unregister from McpServerManager
@@ -2082,22 +2080,26 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Add promise for custom modes which is handled separately
const customModesPromise = this.customModesManager.getCustomModes()
- // Wait for all promises to resolve
- const [stateResults, secretResults, customModes] = await Promise.all([
- Promise.all(statePromises),
- Promise.all(secretPromises),
+ let idx = 0
+ const secretValuesArray = await Promise.all([
+ ...statePromises,
+ ...secretPromises,
customModesPromise,
])
// Populate stateValues and secretValues
- GLOBAL_STATE_KEYS.forEach((key, index) => {
- stateValues[key] = stateResults[index]
+ GLOBAL_STATE_KEYS.forEach((key, _) => {
+ stateValues[key] = secretValuesArray[idx]
+ idx = idx + 1
})
SECRET_KEYS.forEach((key, index) => {
- secretValues[key] = secretResults[index]
+ secretValues[key] = secretValuesArray[idx]
+ idx = idx + 1
})
+ let customModes = secretValuesArray[idx] as ModeConfig[] | undefined
+
// Determine apiProvider with the same logic as before
let apiProvider: ApiProvider
if (stateValues.apiProvider) {
@@ -2219,12 +2221,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async updateGlobalState(key: GlobalStateKey, value: any) {
this.outputChannel.appendLine(`Updating global state: ${key}`)
await this.contextProxy.updateGlobalState(key, value)
-
- // // If we have a lot of pending changes, consider saving them periodically
- // if (this.contextProxy.hasPendingChanges() && Math.random() < 0.1) { // 10% chance to save changes
- // this.outputChannel.appendLine("Periodically flushing context state changes")
- // await this.contextProxy.saveChanges()
- // }
}
async getGlobalState(key: GlobalStateKey) {
@@ -2256,13 +2252,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
for (const key of this.context.globalState.keys()) {
- // Still using original context for listing keys
await this.contextProxy.updateGlobalState(key, undefined)
}
for (const key of SECRET_KEYS) {
await this.storeSecret(key, undefined)
}
+
await this.configManager.resetAllConfigs()
await this.customModesManager.resetCustomModes()
if (this.cline) {
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 20778b8802..9463be25b7 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -1303,17 +1303,6 @@ describe("ClineProvider", () => {
// Verify state was posted to webview
expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
})
-
- test("disposes the contextProxy when provider is disposed", async () => {
- // Setup mock Cline instance
- const mockCline = {
- abortTask: jest.fn(),
- }
- // @ts-ignore - accessing private property for testing
- provider.cline = mockCline
- await provider.dispose()
- expect(mockContextProxy.dispose).toHaveBeenCalled()
- })
})
describe("updateCustomMode", () => {
@@ -1602,15 +1591,4 @@ describe("ContextProxy integration", () => {
expect(mockContextProxy.updateGlobalState).toBeDefined()
expect(mockContextProxy.storeSecret).toBeDefined()
})
-
- test("contextProxy is properly disposed", async () => {
- // Setup mock Cline instance
- const mockCline = {
- abortTask: jest.fn(),
- }
- // @ts-ignore - accessing private property for testing
- provider.cline = mockCline
- await provider.dispose()
- expect(mockContextProxy.dispose).toHaveBeenCalled()
- })
})
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 7c5c65fe90..981fcf8d76 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -108,7 +108,7 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
"lmStudioBaseUrl",
"lmStudioDraftModelId",
"lmStudioSpeculativeDecodingEnabled",
- "mistralCodestralUrl", // New option for Codestral URL
+ "mistralCodestralUrl",
"azureApiVersion",
"openRouterUseMiddleOutTransform",
"openAiStreamingEnabled",
From 589387ba65f058575670226ce8e8161f20dde72d Mon Sep 17 00:00:00 2001
From: refactorthis
Date: Sun, 2 Mar 2025 18:08:18 +1100
Subject: [PATCH 15/22] feat: add x-title and http-referer header to all openai
providers
- Provides the ability for Open AI compatible gateways, such as LiteLLM, Open Router, Requesty to determine originating app.
- Uses standard set by Open Router.
---
.changeset/wise-pears-join.md | 5 +++++
src/api/providers/__tests__/openai.test.ts | 14 ++++++++++++++
src/api/providers/openai.ts | 11 ++++++++---
src/api/providers/openrouter.ts | 6 +-----
src/api/providers/requesty.ts | 4 ----
5 files changed, 28 insertions(+), 12 deletions(-)
create mode 100644 .changeset/wise-pears-join.md
diff --git a/.changeset/wise-pears-join.md b/.changeset/wise-pears-join.md
new file mode 100644
index 0000000000..46c019b92e
--- /dev/null
+++ b/.changeset/wise-pears-join.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Improved observability of openai compatible APIs, by sending x-title and http-referer headers, as per Open Router standard.
diff --git a/src/api/providers/__tests__/openai.test.ts b/src/api/providers/__tests__/openai.test.ts
index 5b5da20f51..43634b5862 100644
--- a/src/api/providers/__tests__/openai.test.ts
+++ b/src/api/providers/__tests__/openai.test.ts
@@ -90,6 +90,20 @@ describe("OpenAiHandler", () => {
})
expect(handlerWithCustomUrl).toBeInstanceOf(OpenAiHandler)
})
+
+ it("should set default headers correctly", () => {
+ // Get the mock constructor from the jest mock system
+ const openAiMock = jest.requireMock("openai").default
+
+ expect(openAiMock).toHaveBeenCalledWith({
+ baseURL: expect.any(String),
+ apiKey: expect.any(String),
+ defaultHeaders: {
+ "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
+ "X-Title": "Roo Code",
+ },
+ })
+ })
})
describe("createMessage", () => {
diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts
index 0fa833e82a..9262f3b75a 100644
--- a/src/api/providers/openai.ts
+++ b/src/api/providers/openai.ts
@@ -16,10 +16,14 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { BaseProvider } from "./base-provider"
const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6
-export interface OpenAiHandlerOptions extends ApiHandlerOptions {
- defaultHeaders?: Record
+
+export const defaultHeaders = {
+ "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
+ "X-Title": "Roo Code",
}
+export interface OpenAiHandlerOptions extends ApiHandlerOptions {}
+
export class OpenAiHandler extends BaseProvider implements SingleCompletionHandler {
protected options: OpenAiHandlerOptions
private client: OpenAI
@@ -47,9 +51,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
baseURL,
apiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
+ defaultHeaders,
})
} else {
- this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: this.options.defaultHeaders })
+ this.client = new OpenAI({ baseURL, apiKey, defaultHeaders })
}
}
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index db5c094d02..7d3992caa5 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -13,6 +13,7 @@ import { convertToR1Format } from "../transform/r1-format"
import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants"
import { getModelParams, SingleCompletionHandler } from ".."
import { BaseProvider } from "./base-provider"
+import { defaultHeaders } from "./openai"
// Add custom interface for OpenRouter params.
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
@@ -37,11 +38,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
const apiKey = this.options.openRouterApiKey ?? "not-provided"
- const defaultHeaders = {
- "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
- "X-Title": "Roo Code",
- }
-
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders })
}
diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts
index 5e570ca2a2..27187c5d33 100644
--- a/src/api/providers/requesty.ts
+++ b/src/api/providers/requesty.ts
@@ -16,10 +16,6 @@ export class RequestyHandler extends OpenAiHandler {
openAiModelId: options.requestyModelId ?? requestyDefaultModelId,
openAiBaseUrl: "https://router.requesty.ai/v1",
openAiCustomModelInfo: options.requestyModelInfo ?? requestyModelInfoSaneDefaults,
- defaultHeaders: {
- "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
- "X-Title": "Roo Code",
- },
})
}
From f1de71429f3ca9fefcbf7aaa3351f60399580547 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 5 Mar 2025 09:22:40 -0500
Subject: [PATCH 16/22] PR feedback
---
src/core/__tests__/contextProxy.test.ts | 16 ----------------
src/core/webview/ClineProvider.ts | 15 ++++-----------
2 files changed, 4 insertions(+), 27 deletions(-)
diff --git a/src/core/__tests__/contextProxy.test.ts b/src/core/__tests__/contextProxy.test.ts
index e6f1bfc9ca..9f0c20b0c4 100644
--- a/src/core/__tests__/contextProxy.test.ts
+++ b/src/core/__tests__/contextProxy.test.ts
@@ -3,16 +3,6 @@ import { ContextProxy } from "../contextProxy"
import { logger } from "../../utils/logging"
import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../../shared/globalState"
-// Mock the logger
-jest.mock("../../utils/logging", () => ({
- logger: {
- debug: jest.fn(),
- info: jest.fn(),
- warn: jest.fn(),
- error: jest.fn(),
- },
-}))
-
// Mock shared/globalState
jest.mock("../../shared/globalState", () => ({
GLOBAL_STATE_KEYS: ["apiProvider", "apiModelId", "mode"],
@@ -121,9 +111,6 @@ describe("ContextProxy", () => {
it("should update state directly in original context", async () => {
await proxy.updateGlobalState("test-key", "new-value")
- // Should have called logger.debug
- expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("updating state for key"))
-
// Should have called original context
expect(mockGlobalState.update).toHaveBeenCalledWith("test-key", "new-value")
@@ -148,9 +135,6 @@ describe("ContextProxy", () => {
it("should store secret directly in original context", async () => {
await proxy.storeSecret("api-key", "new-secret")
- // Should have called logger.debug
- expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("storing secret for key"))
-
// Should have called original context
expect(mockSecrets.store).toHaveBeenCalledWith("api-key", "new-secret")
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 748ba2525b..4c3068eac6 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -118,7 +118,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.mcpHub = undefined
this.customModesManager?.dispose()
this.outputChannel.appendLine("Disposed all disposables")
- // Dispose the context proxy to commit any pending changes
ClineProvider.activeInstances.delete(this)
// Unregister from McpServerManager
@@ -2081,24 +2080,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const customModesPromise = this.customModesManager.getCustomModes()
let idx = 0
- const secretValuesArray = await Promise.all([
- ...statePromises,
- ...secretPromises,
- customModesPromise,
- ])
+ const valuePromises = await Promise.all([...statePromises, ...secretPromises, customModesPromise])
// Populate stateValues and secretValues
GLOBAL_STATE_KEYS.forEach((key, _) => {
- stateValues[key] = secretValuesArray[idx]
+ stateValues[key] = valuePromises[idx]
idx = idx + 1
})
SECRET_KEYS.forEach((key, index) => {
- secretValues[key] = secretValuesArray[idx]
+ secretValues[key] = valuePromises[idx]
idx = idx + 1
})
- let customModes = secretValuesArray[idx] as ModeConfig[] | undefined
+ let customModes = valuePromises[idx] as ModeConfig[] | undefined
// Determine apiProvider with the same logic as before
let apiProvider: ApiProvider
@@ -2219,7 +2214,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// global
async updateGlobalState(key: GlobalStateKey, value: any) {
- this.outputChannel.appendLine(`Updating global state: ${key}`)
await this.contextProxy.updateGlobalState(key, value)
}
@@ -2230,7 +2224,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// secrets
public async storeSecret(key: SecretKey, value?: string) {
- this.outputChannel.appendLine(`Storing secret: ${key}`)
await this.contextProxy.storeSecret(key, value)
}
From c3da5b0aa380082eb6c64c3ded039ac3e75d3c38 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 5 Mar 2025 09:43:24 -0500
Subject: [PATCH 17/22] More cleanup
---
.../webview/__tests__/ClineProvider.test.ts | 14 ---
src/shared/globalState.ts | 108 ++----------------
2 files changed, 12 insertions(+), 110 deletions(-)
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 68739e8f06..3ef024afb3 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -182,16 +182,6 @@ jest.mock("../../../utils/sound", () => ({
setSoundEnabled: jest.fn(),
}))
-// Mock logger
-jest.mock("../../../utils/logging", () => ({
- logger: {
- debug: jest.fn(),
- error: jest.fn(),
- warn: jest.fn(),
- info: jest.fn(),
- },
-}))
-
// Mock ESM modules
jest.mock("p-wait-for", () => ({
__esModule: true,
@@ -1527,7 +1517,6 @@ describe("ClineProvider", () => {
apiConfiguration: testApiConfig,
})
- // Reset jest.mock calls tracking
// Verify config was saved
expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
@@ -1538,9 +1527,6 @@ describe("ClineProvider", () => {
expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("listApiConfigMeta", [
{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
])
-
- // Reset jest.mock calls tracking for subsequent tests
- jest.clearAllMocks()
})
})
})
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index bdc263735a..fd7bd1adb9 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -1,19 +1,5 @@
-export type SecretKey =
- | "apiKey"
- | "glamaApiKey"
- | "openRouterApiKey"
- | "awsAccessKey"
- | "awsSecretKey"
- | "awsSessionToken"
- | "openAiApiKey"
- | "geminiApiKey"
- | "openAiNativeApiKey"
- | "deepSeekApiKey"
- | "mistralApiKey"
- | "unboundApiKey"
- | "requestyApiKey"
-
-export const SECRET_KEYS: SecretKey[] = [
+// Define the array first with 'as const' to create a readonly tuple type
+export const SECRET_KEYS = [
"apiKey",
"glamaApiKey",
"openRouterApiKey",
@@ -27,87 +13,13 @@ export const SECRET_KEYS: SecretKey[] = [
"mistralApiKey",
"unboundApiKey",
"requestyApiKey",
-]
+] as const
-export type GlobalStateKey =
- | "apiProvider"
- | "apiModelId"
- | "glamaModelId"
- | "glamaModelInfo"
- | "awsRegion"
- | "awsUseCrossRegionInference"
- | "awsProfile"
- | "awsUseProfile"
- | "vertexProjectId"
- | "vertexRegion"
- | "lastShownAnnouncementId"
- | "customInstructions"
- | "alwaysAllowReadOnly"
- | "alwaysAllowWrite"
- | "alwaysAllowExecute"
- | "alwaysAllowBrowser"
- | "alwaysAllowMcp"
- | "alwaysAllowModeSwitch"
- | "taskHistory"
- | "openAiBaseUrl"
- | "openAiModelId"
- | "openAiCustomModelInfo"
- | "openAiUseAzure"
- | "ollamaModelId"
- | "ollamaBaseUrl"
- | "lmStudioModelId"
- | "lmStudioBaseUrl"
- | "lmStudioDraftModelId"
- | "lmStudioSpeculativeDecodingEnabled"
- | "anthropicBaseUrl"
- | "azureApiVersion"
- | "openAiStreamingEnabled"
- | "openRouterModelId"
- | "openRouterModelInfo"
- | "openRouterBaseUrl"
- | "openRouterUseMiddleOutTransform"
- | "allowedCommands"
- | "soundEnabled"
- | "soundVolume"
- | "diffEnabled"
- | "enableCheckpoints"
- | "checkpointStorage"
- | "browserViewportSize"
- | "screenshotQuality"
- | "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"
- | "customModes" // Array of custom modes
- | "unboundModelId"
- | "requestyModelId"
- | "requestyModelInfo"
- | "unboundModelInfo"
- | "modelTemperature"
- | "modelMaxTokens"
- | "modelMaxThinkingTokens"
- | "mistralCodestralUrl"
- | "maxOpenTabsContext"
- | "browserToolEnabled"
- | "lmStudioSpeculativeDecodingEnabled"
- | "lmStudioDraftModelId"
+// Derive the type from the array - creates a union of string literals
+export type SecretKey = (typeof SECRET_KEYS)[number]
-export const GLOBAL_STATE_KEYS: GlobalStateKey[] = [
+// Define the array first with 'as const' to create a readonly tuple type
+export const GLOBAL_STATE_KEYS = [
"apiProvider",
"apiModelId",
"glamaModelId",
@@ -148,6 +60,7 @@ export const GLOBAL_STATE_KEYS: GlobalStateKey[] = [
"soundVolume",
"diffEnabled",
"enableCheckpoints",
+ "checkpointStorage",
"browserViewportSize",
"screenshotQuality",
"fuzzyMatchThreshold",
@@ -181,4 +94,7 @@ export const GLOBAL_STATE_KEYS: GlobalStateKey[] = [
"browserToolEnabled",
"lmStudioSpeculativeDecodingEnabled",
"lmStudioDraftModelId",
-]
+] as const
+
+// Derive the type from the array - creates a union of string literals
+export type GlobalStateKey = (typeof GLOBAL_STATE_KEYS)[number]
From 86401faa37a56c0b6de706c862823601b39350f7 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 5 Mar 2025 10:04:48 -0500
Subject: [PATCH 18/22] Better encapsulation for API config
---
src/core/__tests__/contextProxy.test.ts | 86 +++++++++++++++++++
src/core/contextProxy.ts | 62 ++++++++++++-
src/core/webview/ClineProvider.ts | 72 ++++------------
.../webview/__tests__/ClineProvider.test.ts | 61 +++++++++++++
4 files changed, 222 insertions(+), 59 deletions(-)
diff --git a/src/core/__tests__/contextProxy.test.ts b/src/core/__tests__/contextProxy.test.ts
index 9f0c20b0c4..ef0c4333e0 100644
--- a/src/core/__tests__/contextProxy.test.ts
+++ b/src/core/__tests__/contextProxy.test.ts
@@ -2,11 +2,20 @@ import * as vscode from "vscode"
import { ContextProxy } from "../contextProxy"
import { logger } from "../../utils/logging"
import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../../shared/globalState"
+import { ApiConfiguration } from "../../shared/api"
// Mock shared/globalState
jest.mock("../../shared/globalState", () => ({
GLOBAL_STATE_KEYS: ["apiProvider", "apiModelId", "mode"],
SECRET_KEYS: ["apiKey", "openAiApiKey"],
+ GlobalStateKey: {},
+ SecretKey: {},
+}))
+
+// Mock shared/api
+jest.mock("../../shared/api", () => ({
+ API_CONFIG_KEYS: ["apiProvider", "apiModelId"],
+ ApiConfiguration: {},
}))
// Mock VSCode API
@@ -153,5 +162,82 @@ describe("ContextProxy", () => {
const storedValue = await proxy.getSecret("api-key")
expect(storedValue).toBeUndefined()
})
+
+ describe("getApiConfiguration", () => {
+ it("should combine global state and secrets into a single ApiConfiguration object", async () => {
+ // Mock data in state cache
+ await proxy.updateGlobalState("apiProvider", "anthropic")
+ await proxy.updateGlobalState("apiModelId", "test-model")
+ // Mock data in secrets cache
+ await proxy.storeSecret("apiKey", "test-api-key")
+
+ const config = proxy.getApiConfiguration()
+
+ // Should contain values from global state
+ expect(config.apiProvider).toBe("anthropic")
+ expect(config.apiModelId).toBe("test-model")
+ // Should contain values from secrets
+ expect(config.apiKey).toBe("test-api-key")
+ })
+
+ it("should handle special case for apiProvider defaulting", async () => {
+ // Clear apiProvider but set apiKey
+ await proxy.updateGlobalState("apiProvider", undefined)
+ await proxy.storeSecret("apiKey", "test-api-key")
+
+ const config = proxy.getApiConfiguration()
+
+ // Should default to anthropic when apiKey exists
+ expect(config.apiProvider).toBe("anthropic")
+
+ // Clear both apiProvider and apiKey
+ await proxy.updateGlobalState("apiProvider", undefined)
+ await proxy.storeSecret("apiKey", undefined)
+
+ const configWithoutKey = proxy.getApiConfiguration()
+
+ // Should default to openrouter when no apiKey exists
+ expect(configWithoutKey.apiProvider).toBe("openrouter")
+ })
+ })
+
+ describe("updateApiConfiguration", () => {
+ it("should update both global state and secrets", async () => {
+ const apiConfig: ApiConfiguration = {
+ apiProvider: "anthropic",
+ apiModelId: "claude-latest",
+ apiKey: "test-api-key",
+ }
+
+ await proxy.updateApiConfiguration(apiConfig)
+
+ // Should update global state
+ expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "anthropic")
+ expect(mockGlobalState.update).toHaveBeenCalledWith("apiModelId", "claude-latest")
+ // Should update secrets
+ expect(mockSecrets.store).toHaveBeenCalledWith("apiKey", "test-api-key")
+
+ // Check that values are in cache
+ expect(proxy.getGlobalState("apiProvider")).toBe("anthropic")
+ expect(proxy.getGlobalState("apiModelId")).toBe("claude-latest")
+ expect(proxy.getSecret("apiKey")).toBe("test-api-key")
+ })
+
+ it("should ignore keys that aren't in either GLOBAL_STATE_KEYS or SECRET_KEYS", async () => {
+ // Use type assertion to add an invalid key
+ const apiConfig = {
+ apiProvider: "anthropic",
+ invalidKey: "should be ignored",
+ } as ApiConfiguration & { invalidKey: string }
+
+ await proxy.updateApiConfiguration(apiConfig)
+
+ // Should update keys in GLOBAL_STATE_KEYS
+ expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "anthropic")
+ // Should not call update/store for invalid keys
+ expect(mockGlobalState.update).not.toHaveBeenCalledWith("invalidKey", expect.anything())
+ expect(mockSecrets.store).not.toHaveBeenCalledWith("invalidKey", expect.anything())
+ })
+ })
})
})
diff --git a/src/core/contextProxy.ts b/src/core/contextProxy.ts
index 7c429c86cf..8d3f9a4b7c 100644
--- a/src/core/contextProxy.ts
+++ b/src/core/contextProxy.ts
@@ -1,6 +1,7 @@
import * as vscode from "vscode"
import { logger } from "../utils/logging"
-import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../shared/globalState"
+import { ApiConfiguration, API_CONFIG_KEYS } from "../shared/api"
+import { GLOBAL_STATE_KEYS, SECRET_KEYS, GlobalStateKey, SecretKey } from "../shared/globalState"
export class ContextProxy {
private readonly originalContext: vscode.ExtensionContext
@@ -82,7 +83,6 @@ export class ContextProxy {
getSecret(key: string): string | undefined {
return this.secretCache.get(key)
}
-
storeSecret(key: string, value?: string): Thenable {
// Update cache
this.secretCache.set(key, value)
@@ -93,4 +93,62 @@ export class ContextProxy {
return this.originalContext.secrets.store(key, value)
}
}
+
+ /**
+ * Gets a complete ApiConfiguration object by fetching values
+ * from both global state and secrets storage
+ */
+ getApiConfiguration(): ApiConfiguration {
+ // Create an empty ApiConfiguration object
+ const config: ApiConfiguration = {}
+
+ // Add all API-related keys from global state
+ for (const key of API_CONFIG_KEYS) {
+ const value = this.getGlobalState(key)
+ if (value !== undefined) {
+ // Use type assertion to avoid TypeScript error
+ ;(config as any)[key] = value
+ }
+ }
+
+ // Add all secret values
+ for (const key of SECRET_KEYS) {
+ const value = this.getSecret(key)
+ if (value !== undefined) {
+ // Use type assertion to avoid TypeScript error
+ ;(config as any)[key] = value
+ }
+ }
+
+ // Handle special case for apiProvider if needed (same logic as current implementation)
+ if (!config.apiProvider) {
+ if (config.apiKey) {
+ config.apiProvider = "anthropic"
+ } else {
+ config.apiProvider = "openrouter"
+ }
+ }
+
+ return config
+ }
+
+ /**
+ * Updates an ApiConfiguration by persisting each property
+ * to the appropriate storage (global state or secrets)
+ */
+ async updateApiConfiguration(apiConfiguration: ApiConfiguration): Promise {
+ const promises: Array> = []
+
+ // For each property, update the appropriate storage
+ Object.entries(apiConfiguration).forEach(([key, value]) => {
+ if (SECRET_KEYS.includes(key as SecretKey)) {
+ promises.push(this.storeSecret(key, value))
+ } else if (API_CONFIG_KEYS.includes(key as GlobalStateKey)) {
+ promises.push(this.updateGlobalState(key, value))
+ }
+ // Ignore keys that aren't in either list
+ })
+
+ await Promise.all(promises)
+ }
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index a09f18d278..121505672d 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1659,20 +1659,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
- // Create an array of promises to update state
- const promises: Promise[] = []
-
- // For each property in apiConfiguration, update the appropriate state
- Object.entries(apiConfiguration).forEach(([key, value]) => {
- // Check if this key is a secret
- if (SECRET_KEYS.includes(key as SecretKey)) {
- promises.push(this.storeSecret(key as SecretKey, value))
- } else {
- promises.push(this.updateGlobalState(key as GlobalStateKey, value))
- }
- })
-
- await Promise.all(promises)
+ // Update all configuration values through the contextProxy
+ await this.contextProxy.updateApiConfiguration(apiConfiguration)
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
@@ -2073,62 +2061,32 @@ export class ClineProvider implements vscode.WebviewViewProvider {
*/
async getState() {
- // Create an object to store all fetched values
- const stateValues: Record = {} as Record
- const secretValues: Record = {} as Record
+ // Get ApiConfiguration directly from contextProxy
+ const apiConfiguration = this.contextProxy.getApiConfiguration()
- // Create promise arrays for global state and secrets
- const statePromises = GLOBAL_STATE_KEYS.map((key) => this.getGlobalState(key))
- const secretPromises = SECRET_KEYS.map((key) => this.getSecret(key))
+ // Create an object to store all fetched values (excluding API config which we already have)
+ const stateValues: Record = {} as Record
+
+ // Create promise arrays for global state
+ const statePromises = GLOBAL_STATE_KEYS
+ // Filter out API config keys since we already have them
+ .filter((key) => !API_CONFIG_KEYS.includes(key))
+ .map((key) => this.getGlobalState(key))
// Add promise for custom modes which is handled separately
const customModesPromise = this.customModesManager.getCustomModes()
let idx = 0
- const valuePromises = await Promise.all([...statePromises, ...secretPromises, customModesPromise])
+ const valuePromises = await Promise.all([...statePromises, customModesPromise])
- // Populate stateValues and secretValues
- GLOBAL_STATE_KEYS.forEach((key, _) => {
+ // Populate stateValues
+ GLOBAL_STATE_KEYS.filter((key) => !API_CONFIG_KEYS.includes(key)).forEach((key) => {
stateValues[key] = valuePromises[idx]
idx = idx + 1
})
- SECRET_KEYS.forEach((key, index) => {
- secretValues[key] = valuePromises[idx]
- idx = idx + 1
- })
-
let customModes = valuePromises[idx] as ModeConfig[] | undefined
- // Determine apiProvider with the same logic as before
- let apiProvider: ApiProvider
- if (stateValues.apiProvider) {
- apiProvider = stateValues.apiProvider
- } else {
- // Either new user or legacy user that doesn't have the apiProvider stored in state
- // (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
- if (secretValues.apiKey) {
- apiProvider = "anthropic"
- } else {
- // New users should default to openrouter
- apiProvider = "openrouter"
- }
- }
-
- // Build the apiConfiguration object combining state values and secrets
- // Using the dynamic approach with API_CONFIG_KEYS
- const apiConfiguration: ApiConfiguration = {
- // Dynamically add all API-related keys from stateValues
- ...Object.fromEntries(API_CONFIG_KEYS.map((key) => [key, stateValues[key]])),
- // Add all secrets
- ...secretValues,
- }
-
- // Ensure apiProvider is set properly if not already in state
- if (!apiConfiguration.apiProvider) {
- apiConfiguration.apiProvider = apiProvider
- }
-
// Return the same structure as before
return {
apiConfiguration,
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 3ef024afb3..ed557c8838 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -34,6 +34,21 @@ jest.mock("../../contextProxy", () => {
.mockImplementation((key, value) =>
value ? context.secrets.store(key, value) : context.secrets.delete(key),
),
+ getApiConfiguration: jest.fn().mockImplementation(() => ({
+ apiProvider: "openrouter",
+ // Add other common properties
+ })),
+ updateApiConfiguration: jest.fn().mockImplementation(async (apiConfiguration) => {
+ // Mock implementation that simulates updating state and secrets
+ for (const [key, value] of Object.entries(apiConfiguration)) {
+ if (key === "apiKey" || key === "openAiApiKey") {
+ context.secrets.store(key, value)
+ } else {
+ context.globalState.update(key, value)
+ }
+ }
+ return Promise.resolve()
+ }),
saveChanges: jest.fn().mockResolvedValue(undefined),
dispose: jest.fn().mockResolvedValue(undefined),
hasPendingChanges: jest.fn().mockReturnValue(false),
@@ -1579,5 +1594,51 @@ describe("ContextProxy integration", () => {
expect(mockContextProxy.getGlobalState).toBeDefined()
expect(mockContextProxy.updateGlobalState).toBeDefined()
expect(mockContextProxy.storeSecret).toBeDefined()
+ expect(mockContextProxy.getApiConfiguration).toBeDefined()
+ expect(mockContextProxy.updateApiConfiguration).toBeDefined()
+ })
+
+ test("getState uses contextProxy.getApiConfiguration", async () => {
+ // Setup mock API configuration
+ const mockApiConfig = {
+ apiProvider: "anthropic",
+ apiModelId: "claude-latest",
+ apiKey: "test-api-key",
+ }
+ mockContextProxy.getApiConfiguration.mockReturnValue(mockApiConfig)
+
+ // Get state
+ const state = await provider.getState()
+
+ // Verify getApiConfiguration was called
+ expect(mockContextProxy.getApiConfiguration).toHaveBeenCalled()
+ // Verify state has the API configuration from contextProxy
+ expect(state.apiConfiguration).toBe(mockApiConfig)
+ })
+
+ test("updateApiConfiguration uses contextProxy.updateApiConfiguration", async () => {
+ // Setup test config
+ const testApiConfig = {
+ apiProvider: "anthropic",
+ apiModelId: "claude-latest",
+ apiKey: "test-api-key",
+ }
+
+ // Mock methods needed for the test
+ provider.configManager = {
+ listConfig: jest.fn().mockResolvedValue([]),
+ setModeConfig: jest.fn(),
+ } as any
+
+ // Mock getState for mode
+ jest.spyOn(provider, "getState").mockResolvedValue({
+ mode: "code",
+ } as any)
+
+ // Call the private method - need to use any to access it
+ await (provider as any).updateApiConfiguration(testApiConfig)
+
+ // Verify contextProxy.updateApiConfiguration was called with the right config
+ expect(mockContextProxy.updateApiConfiguration).toHaveBeenCalledWith(testApiConfig)
})
})
From e4fb0081b1c78943c63dd0a4dc00bf95fb3f937c Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 5 Mar 2025 10:39:36 -0500
Subject: [PATCH 19/22] Update commands to roo-cline to be consistent with
others
---
src/activate/registerCommands.ts | 2 +-
src/api/providers/human-relay.ts | 4 ++--
src/core/webview/ClineProvider.ts | 4 ++--
src/extension.ts | 6 +++---
4 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts
index b520e0cb8e..4aeb87168c 100644
--- a/src/activate/registerCommands.ts
+++ b/src/activate/registerCommands.ts
@@ -47,7 +47,7 @@ export const registerCommands = (options: RegisterCommandOptions) => {
// Human Relay Dialog Command
context.subscriptions.push(
vscode.commands.registerCommand(
- "roo-code.showHumanRelayDialog",
+ "roo-cline.showHumanRelayDialog",
(params: { requestId: string; promptText: string }) => {
if (getPanel()) {
getPanel()?.webview.postMessage({
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
index 90a82b9bfe..b8bd4c2829 100644
--- a/src/api/providers/human-relay.ts
+++ b/src/api/providers/human-relay.ts
@@ -123,7 +123,7 @@ async function showHumanRelayDialog(promptText: string): Promise {
resolve(response)
@@ -131,7 +131,7 @@ async function showHumanRelayDialog(promptText: string): Promise void) => {
registerHumanRelayCallback(requestId, callback)
},
@@ -70,7 +70,7 @@ export function activate(context: vscode.ExtensionContext) {
// Register human relay response processing command
context.subscriptions.push(
vscode.commands.registerCommand(
- "roo-code.handleHumanRelayResponse",
+ "roo-cline.handleHumanRelayResponse",
(response: { requestId: string; text?: string; cancelled?: boolean }) => {
const callback = humanRelayCallbacks.get(response.requestId)
if (callback) {
@@ -86,7 +86,7 @@ export function activate(context: vscode.ExtensionContext) {
)
context.subscriptions.push(
- vscode.commands.registerCommand("roo-code.unregisterHumanRelayCallback", (requestId: string) => {
+ vscode.commands.registerCommand("roo-cline.unregisterHumanRelayCallback", (requestId: string) => {
humanRelayCallbacks.delete(requestId)
}),
)
From f683e4530f8a55c7c9a8a2a8903cbcf9a4c1c87c Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 5 Mar 2025 11:05:05 -0500
Subject: [PATCH 20/22] Revert "Better encapsulation for API config"
This reverts commit 86401faa37a56c0b6de706c862823601b39350f7.
---
src/core/__tests__/contextProxy.test.ts | 86 -------------------
src/core/contextProxy.ts | 62 +------------
src/core/webview/ClineProvider.ts | 72 ++++++++++++----
.../webview/__tests__/ClineProvider.test.ts | 61 -------------
4 files changed, 59 insertions(+), 222 deletions(-)
diff --git a/src/core/__tests__/contextProxy.test.ts b/src/core/__tests__/contextProxy.test.ts
index ef0c4333e0..9f0c20b0c4 100644
--- a/src/core/__tests__/contextProxy.test.ts
+++ b/src/core/__tests__/contextProxy.test.ts
@@ -2,20 +2,11 @@ import * as vscode from "vscode"
import { ContextProxy } from "../contextProxy"
import { logger } from "../../utils/logging"
import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../../shared/globalState"
-import { ApiConfiguration } from "../../shared/api"
// Mock shared/globalState
jest.mock("../../shared/globalState", () => ({
GLOBAL_STATE_KEYS: ["apiProvider", "apiModelId", "mode"],
SECRET_KEYS: ["apiKey", "openAiApiKey"],
- GlobalStateKey: {},
- SecretKey: {},
-}))
-
-// Mock shared/api
-jest.mock("../../shared/api", () => ({
- API_CONFIG_KEYS: ["apiProvider", "apiModelId"],
- ApiConfiguration: {},
}))
// Mock VSCode API
@@ -162,82 +153,5 @@ describe("ContextProxy", () => {
const storedValue = await proxy.getSecret("api-key")
expect(storedValue).toBeUndefined()
})
-
- describe("getApiConfiguration", () => {
- it("should combine global state and secrets into a single ApiConfiguration object", async () => {
- // Mock data in state cache
- await proxy.updateGlobalState("apiProvider", "anthropic")
- await proxy.updateGlobalState("apiModelId", "test-model")
- // Mock data in secrets cache
- await proxy.storeSecret("apiKey", "test-api-key")
-
- const config = proxy.getApiConfiguration()
-
- // Should contain values from global state
- expect(config.apiProvider).toBe("anthropic")
- expect(config.apiModelId).toBe("test-model")
- // Should contain values from secrets
- expect(config.apiKey).toBe("test-api-key")
- })
-
- it("should handle special case for apiProvider defaulting", async () => {
- // Clear apiProvider but set apiKey
- await proxy.updateGlobalState("apiProvider", undefined)
- await proxy.storeSecret("apiKey", "test-api-key")
-
- const config = proxy.getApiConfiguration()
-
- // Should default to anthropic when apiKey exists
- expect(config.apiProvider).toBe("anthropic")
-
- // Clear both apiProvider and apiKey
- await proxy.updateGlobalState("apiProvider", undefined)
- await proxy.storeSecret("apiKey", undefined)
-
- const configWithoutKey = proxy.getApiConfiguration()
-
- // Should default to openrouter when no apiKey exists
- expect(configWithoutKey.apiProvider).toBe("openrouter")
- })
- })
-
- describe("updateApiConfiguration", () => {
- it("should update both global state and secrets", async () => {
- const apiConfig: ApiConfiguration = {
- apiProvider: "anthropic",
- apiModelId: "claude-latest",
- apiKey: "test-api-key",
- }
-
- await proxy.updateApiConfiguration(apiConfig)
-
- // Should update global state
- expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "anthropic")
- expect(mockGlobalState.update).toHaveBeenCalledWith("apiModelId", "claude-latest")
- // Should update secrets
- expect(mockSecrets.store).toHaveBeenCalledWith("apiKey", "test-api-key")
-
- // Check that values are in cache
- expect(proxy.getGlobalState("apiProvider")).toBe("anthropic")
- expect(proxy.getGlobalState("apiModelId")).toBe("claude-latest")
- expect(proxy.getSecret("apiKey")).toBe("test-api-key")
- })
-
- it("should ignore keys that aren't in either GLOBAL_STATE_KEYS or SECRET_KEYS", async () => {
- // Use type assertion to add an invalid key
- const apiConfig = {
- apiProvider: "anthropic",
- invalidKey: "should be ignored",
- } as ApiConfiguration & { invalidKey: string }
-
- await proxy.updateApiConfiguration(apiConfig)
-
- // Should update keys in GLOBAL_STATE_KEYS
- expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "anthropic")
- // Should not call update/store for invalid keys
- expect(mockGlobalState.update).not.toHaveBeenCalledWith("invalidKey", expect.anything())
- expect(mockSecrets.store).not.toHaveBeenCalledWith("invalidKey", expect.anything())
- })
- })
})
})
diff --git a/src/core/contextProxy.ts b/src/core/contextProxy.ts
index 8d3f9a4b7c..7c429c86cf 100644
--- a/src/core/contextProxy.ts
+++ b/src/core/contextProxy.ts
@@ -1,7 +1,6 @@
import * as vscode from "vscode"
import { logger } from "../utils/logging"
-import { ApiConfiguration, API_CONFIG_KEYS } from "../shared/api"
-import { GLOBAL_STATE_KEYS, SECRET_KEYS, GlobalStateKey, SecretKey } from "../shared/globalState"
+import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../shared/globalState"
export class ContextProxy {
private readonly originalContext: vscode.ExtensionContext
@@ -83,6 +82,7 @@ export class ContextProxy {
getSecret(key: string): string | undefined {
return this.secretCache.get(key)
}
+
storeSecret(key: string, value?: string): Thenable {
// Update cache
this.secretCache.set(key, value)
@@ -93,62 +93,4 @@ export class ContextProxy {
return this.originalContext.secrets.store(key, value)
}
}
-
- /**
- * Gets a complete ApiConfiguration object by fetching values
- * from both global state and secrets storage
- */
- getApiConfiguration(): ApiConfiguration {
- // Create an empty ApiConfiguration object
- const config: ApiConfiguration = {}
-
- // Add all API-related keys from global state
- for (const key of API_CONFIG_KEYS) {
- const value = this.getGlobalState(key)
- if (value !== undefined) {
- // Use type assertion to avoid TypeScript error
- ;(config as any)[key] = value
- }
- }
-
- // Add all secret values
- for (const key of SECRET_KEYS) {
- const value = this.getSecret(key)
- if (value !== undefined) {
- // Use type assertion to avoid TypeScript error
- ;(config as any)[key] = value
- }
- }
-
- // Handle special case for apiProvider if needed (same logic as current implementation)
- if (!config.apiProvider) {
- if (config.apiKey) {
- config.apiProvider = "anthropic"
- } else {
- config.apiProvider = "openrouter"
- }
- }
-
- return config
- }
-
- /**
- * Updates an ApiConfiguration by persisting each property
- * to the appropriate storage (global state or secrets)
- */
- async updateApiConfiguration(apiConfiguration: ApiConfiguration): Promise {
- const promises: Array> = []
-
- // For each property, update the appropriate storage
- Object.entries(apiConfiguration).forEach(([key, value]) => {
- if (SECRET_KEYS.includes(key as SecretKey)) {
- promises.push(this.storeSecret(key, value))
- } else if (API_CONFIG_KEYS.includes(key as GlobalStateKey)) {
- promises.push(this.updateGlobalState(key, value))
- }
- // Ignore keys that aren't in either list
- })
-
- await Promise.all(promises)
- }
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 121505672d..a09f18d278 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1659,8 +1659,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
- // Update all configuration values through the contextProxy
- await this.contextProxy.updateApiConfiguration(apiConfiguration)
+ // Create an array of promises to update state
+ const promises: Promise[] = []
+
+ // For each property in apiConfiguration, update the appropriate state
+ Object.entries(apiConfiguration).forEach(([key, value]) => {
+ // Check if this key is a secret
+ if (SECRET_KEYS.includes(key as SecretKey)) {
+ promises.push(this.storeSecret(key as SecretKey, value))
+ } else {
+ promises.push(this.updateGlobalState(key as GlobalStateKey, value))
+ }
+ })
+
+ await Promise.all(promises)
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
@@ -2061,32 +2073,62 @@ export class ClineProvider implements vscode.WebviewViewProvider {
*/
async getState() {
- // Get ApiConfiguration directly from contextProxy
- const apiConfiguration = this.contextProxy.getApiConfiguration()
+ // Create an object to store all fetched values
+ const stateValues: Record = {} as Record
+ const secretValues: Record = {} as Record
- // Create an object to store all fetched values (excluding API config which we already have)
- const stateValues: Record = {} as Record
-
- // Create promise arrays for global state
- const statePromises = GLOBAL_STATE_KEYS
- // Filter out API config keys since we already have them
- .filter((key) => !API_CONFIG_KEYS.includes(key))
- .map((key) => this.getGlobalState(key))
+ // Create promise arrays for global state and secrets
+ const statePromises = GLOBAL_STATE_KEYS.map((key) => this.getGlobalState(key))
+ const secretPromises = SECRET_KEYS.map((key) => this.getSecret(key))
// Add promise for custom modes which is handled separately
const customModesPromise = this.customModesManager.getCustomModes()
let idx = 0
- const valuePromises = await Promise.all([...statePromises, customModesPromise])
+ const valuePromises = await Promise.all([...statePromises, ...secretPromises, customModesPromise])
- // Populate stateValues
- GLOBAL_STATE_KEYS.filter((key) => !API_CONFIG_KEYS.includes(key)).forEach((key) => {
+ // Populate stateValues and secretValues
+ GLOBAL_STATE_KEYS.forEach((key, _) => {
stateValues[key] = valuePromises[idx]
idx = idx + 1
})
+ SECRET_KEYS.forEach((key, index) => {
+ secretValues[key] = valuePromises[idx]
+ idx = idx + 1
+ })
+
let customModes = valuePromises[idx] as ModeConfig[] | undefined
+ // Determine apiProvider with the same logic as before
+ let apiProvider: ApiProvider
+ if (stateValues.apiProvider) {
+ apiProvider = stateValues.apiProvider
+ } else {
+ // Either new user or legacy user that doesn't have the apiProvider stored in state
+ // (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
+ if (secretValues.apiKey) {
+ apiProvider = "anthropic"
+ } else {
+ // New users should default to openrouter
+ apiProvider = "openrouter"
+ }
+ }
+
+ // Build the apiConfiguration object combining state values and secrets
+ // Using the dynamic approach with API_CONFIG_KEYS
+ const apiConfiguration: ApiConfiguration = {
+ // Dynamically add all API-related keys from stateValues
+ ...Object.fromEntries(API_CONFIG_KEYS.map((key) => [key, stateValues[key]])),
+ // Add all secrets
+ ...secretValues,
+ }
+
+ // Ensure apiProvider is set properly if not already in state
+ if (!apiConfiguration.apiProvider) {
+ apiConfiguration.apiProvider = apiProvider
+ }
+
// Return the same structure as before
return {
apiConfiguration,
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index ed557c8838..3ef024afb3 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -34,21 +34,6 @@ jest.mock("../../contextProxy", () => {
.mockImplementation((key, value) =>
value ? context.secrets.store(key, value) : context.secrets.delete(key),
),
- getApiConfiguration: jest.fn().mockImplementation(() => ({
- apiProvider: "openrouter",
- // Add other common properties
- })),
- updateApiConfiguration: jest.fn().mockImplementation(async (apiConfiguration) => {
- // Mock implementation that simulates updating state and secrets
- for (const [key, value] of Object.entries(apiConfiguration)) {
- if (key === "apiKey" || key === "openAiApiKey") {
- context.secrets.store(key, value)
- } else {
- context.globalState.update(key, value)
- }
- }
- return Promise.resolve()
- }),
saveChanges: jest.fn().mockResolvedValue(undefined),
dispose: jest.fn().mockResolvedValue(undefined),
hasPendingChanges: jest.fn().mockReturnValue(false),
@@ -1594,51 +1579,5 @@ describe("ContextProxy integration", () => {
expect(mockContextProxy.getGlobalState).toBeDefined()
expect(mockContextProxy.updateGlobalState).toBeDefined()
expect(mockContextProxy.storeSecret).toBeDefined()
- expect(mockContextProxy.getApiConfiguration).toBeDefined()
- expect(mockContextProxy.updateApiConfiguration).toBeDefined()
- })
-
- test("getState uses contextProxy.getApiConfiguration", async () => {
- // Setup mock API configuration
- const mockApiConfig = {
- apiProvider: "anthropic",
- apiModelId: "claude-latest",
- apiKey: "test-api-key",
- }
- mockContextProxy.getApiConfiguration.mockReturnValue(mockApiConfig)
-
- // Get state
- const state = await provider.getState()
-
- // Verify getApiConfiguration was called
- expect(mockContextProxy.getApiConfiguration).toHaveBeenCalled()
- // Verify state has the API configuration from contextProxy
- expect(state.apiConfiguration).toBe(mockApiConfig)
- })
-
- test("updateApiConfiguration uses contextProxy.updateApiConfiguration", async () => {
- // Setup test config
- const testApiConfig = {
- apiProvider: "anthropic",
- apiModelId: "claude-latest",
- apiKey: "test-api-key",
- }
-
- // Mock methods needed for the test
- provider.configManager = {
- listConfig: jest.fn().mockResolvedValue([]),
- setModeConfig: jest.fn(),
- } as any
-
- // Mock getState for mode
- jest.spyOn(provider, "getState").mockResolvedValue({
- mode: "code",
- } as any)
-
- // Call the private method - need to use any to access it
- await (provider as any).updateApiConfiguration(testApiConfig)
-
- // Verify contextProxy.updateApiConfiguration was called with the right config
- expect(mockContextProxy.updateApiConfiguration).toHaveBeenCalledWith(testApiConfig)
})
})
From f4441e31e18fa50f9f7b1ab747d65d91b38cf37e Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 5 Mar 2025 11:06:54 -0500
Subject: [PATCH 21/22] Update
webview-ui/src/components/settings/ApiOptions.tsx
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
---
webview-ui/src/components/settings/ApiOptions.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index ee21a136a7..d38ef752a6 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -1399,7 +1399,7 @@ const ApiOptions = ({
lineHeight: "1.4",
}}>
During use, a dialog box will pop up and the current message will be copied to the clipboard
- automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude),Then
+ automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude), then
copy the AI's reply back to the dialog box and click the confirm button.
From b1b51f8f145cd4e04b4ac1ce7c81077abdd00fab Mon Sep 17 00:00:00 2001
From: sam hoang
Date: Thu, 6 Mar 2025 00:01:52 +0700
Subject: [PATCH 22/22] feat(contextProxy): add setValue and setValues methods
to simplify state management
- Added new setValue method to ContextProxy to route keys to either secrets or global state
- Added setValues method to process multiple key-value pairs at once
- Updated ClineProvider to use new methods, reducing code duplication
- Added comprehensive test coverage for new methods
This change is part of the larger ClineProvider refactoring effort to improve state management and reduce complexity, as outlined in the refactoring plan documents.
---
src/core/__tests__/contextProxy.test.ts | 101 ++++++++++++++++++++++++
src/core/contextProxy.ts | 36 +++++++++
src/core/webview/ClineProvider.ts | 29 +++----
3 files changed, 148 insertions(+), 18 deletions(-)
diff --git a/src/core/__tests__/contextProxy.test.ts b/src/core/__tests__/contextProxy.test.ts
index 9f0c20b0c4..0ac98bbc8c 100644
--- a/src/core/__tests__/contextProxy.test.ts
+++ b/src/core/__tests__/contextProxy.test.ts
@@ -154,4 +154,105 @@ describe("ContextProxy", () => {
expect(storedValue).toBeUndefined()
})
})
+
+ describe("setValue", () => {
+ it("should route secret keys to storeSecret", async () => {
+ // Spy on storeSecret
+ const storeSecretSpy = jest.spyOn(proxy, "storeSecret")
+
+ // Test with a known secret key
+ await proxy.setValue("openAiApiKey", "test-api-key")
+
+ // Should have called storeSecret
+ expect(storeSecretSpy).toHaveBeenCalledWith("openAiApiKey", "test-api-key")
+
+ // Should have stored the value in secret cache
+ const storedValue = proxy.getSecret("openAiApiKey")
+ expect(storedValue).toBe("test-api-key")
+ })
+
+ it("should route global state keys to updateGlobalState", async () => {
+ // Spy on updateGlobalState
+ const updateGlobalStateSpy = jest.spyOn(proxy, "updateGlobalState")
+
+ // Test with a known global state key
+ await proxy.setValue("apiModelId", "gpt-4")
+
+ // Should have called updateGlobalState
+ expect(updateGlobalStateSpy).toHaveBeenCalledWith("apiModelId", "gpt-4")
+
+ // Should have stored the value in state cache
+ const storedValue = proxy.getGlobalState("apiModelId")
+ expect(storedValue).toBe("gpt-4")
+ })
+
+ it("should handle unknown keys as global state with warning", async () => {
+ // Spy on the logger
+ const warnSpy = jest.spyOn(logger, "warn")
+
+ // Spy on updateGlobalState
+ const updateGlobalStateSpy = jest.spyOn(proxy, "updateGlobalState")
+
+ // Test with an unknown key
+ await proxy.setValue("unknownKey", "some-value")
+
+ // Should have logged a warning
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown key: unknownKey"))
+
+ // Should have called updateGlobalState
+ expect(updateGlobalStateSpy).toHaveBeenCalledWith("unknownKey", "some-value")
+
+ // Should have stored the value in state cache
+ const storedValue = proxy.getGlobalState("unknownKey")
+ expect(storedValue).toBe("some-value")
+ })
+ })
+
+ describe("setValues", () => {
+ it("should process multiple values correctly", async () => {
+ // Spy on setValue
+ const setValueSpy = jest.spyOn(proxy, "setValue")
+
+ // Test with multiple values
+ await proxy.setValues({
+ apiModelId: "gpt-4",
+ apiProvider: "openai",
+ mode: "test-mode",
+ })
+
+ // Should have called setValue for each key
+ expect(setValueSpy).toHaveBeenCalledTimes(3)
+ expect(setValueSpy).toHaveBeenCalledWith("apiModelId", "gpt-4")
+ expect(setValueSpy).toHaveBeenCalledWith("apiProvider", "openai")
+ expect(setValueSpy).toHaveBeenCalledWith("mode", "test-mode")
+
+ // Should have stored all values in state cache
+ expect(proxy.getGlobalState("apiModelId")).toBe("gpt-4")
+ expect(proxy.getGlobalState("apiProvider")).toBe("openai")
+ expect(proxy.getGlobalState("mode")).toBe("test-mode")
+ })
+
+ it("should handle both secret and global state keys", async () => {
+ // Spy on storeSecret and updateGlobalState
+ const storeSecretSpy = jest.spyOn(proxy, "storeSecret")
+ const updateGlobalStateSpy = jest.spyOn(proxy, "updateGlobalState")
+
+ // Test with mixed keys
+ 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")
+ })
+ })
})
diff --git a/src/core/contextProxy.ts b/src/core/contextProxy.ts
index 7c429c86cf..f27cc7f65c 100644
--- a/src/core/contextProxy.ts
+++ b/src/core/contextProxy.ts
@@ -93,4 +93,40 @@ export class ContextProxy {
return this.originalContext.secrets.store(key, value)
}
}
+ /**
+ * Set a value in either secrets or global state based on key type.
+ * If the key is in SECRET_KEYS, it will be stored as a secret.
+ * If the key is in GLOBAL_STATE_KEYS or unknown, it will be stored in global state.
+ * @param key The key to set
+ * @param value The value to set
+ * @returns A promise that resolves when the operation completes
+ */
+ setValue(key: string, value: any): Thenable {
+ if (SECRET_KEYS.includes(key as any)) {
+ return this.storeSecret(key, value)
+ }
+
+ if (GLOBAL_STATE_KEYS.includes(key as any)) {
+ return this.updateGlobalState(key, value)
+ }
+
+ logger.warn(`Unknown key: ${key}. Storing as global state.`)
+ return this.updateGlobalState(key, value)
+ }
+
+ /**
+ * Set multiple values at once. Each key will be routed to either
+ * secrets or global state based on its type.
+ * @param values An object containing key-value pairs to set
+ * @returns A promise that resolves when all operations complete
+ */
+ async setValues(values: Record): Promise {
+ const promises: Thenable[] = []
+
+ for (const [key, value] of Object.entries(values)) {
+ promises.push(this.setValue(key, value))
+ }
+
+ return Promise.all(promises)
+ }
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 8d8732bdd7..017371e558 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1688,20 +1688,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
- // Create an array of promises to update state
- const promises: Promise[] = []
-
- // For each property in apiConfiguration, update the appropriate state
- Object.entries(apiConfiguration).forEach(([key, value]) => {
- // Check if this key is a secret
- if (SECRET_KEYS.includes(key as SecretKey)) {
- promises.push(this.storeSecret(key as SecretKey, value))
- } else {
- promises.push(this.updateGlobalState(key as GlobalStateKey, value))
- }
- })
-
- await Promise.all(promises)
+ // Use the new setValues method to handle routing values to secrets or global state
+ await this.contextProxy.setValues(apiConfiguration)
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
@@ -1805,8 +1793,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
const openrouter: ApiProvider = "openrouter"
- await this.updateGlobalState("apiProvider", openrouter)
- await this.storeSecret("openRouterApiKey", apiKey)
+ await this.contextProxy.setValues({
+ apiProvider: openrouter,
+ openRouterApiKey: apiKey,
+ })
+
await this.postStateToWebview()
if (this.cline) {
this.cline.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
@@ -1833,8 +1824,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
const glama: ApiProvider = "glama"
- await this.updateGlobalState("apiProvider", glama)
- await this.storeSecret("glamaApiKey", apiKey)
+ await this.contextProxy.setValues({
+ apiProvider: glama,
+ glamaApiKey: apiKey,
+ })
await this.postStateToWebview()
if (this.cline) {
this.cline.api = buildApiHandler({