mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-22 21:31:08 -05:00
Merge branch 'main' into new_unified
This commit is contained in:
@@ -12,19 +12,25 @@ import { selectImages } from "../../integrations/misc/process-images"
|
||||
import { getTheme } from "../../integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { ApiProvider, ModelInfo } from "../../shared/api"
|
||||
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
|
||||
import { findLast } from "../../shared/array"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import { WebviewMessage, PromptMode } from "../../shared/WebviewMessage"
|
||||
import { defaultPrompts } from "../../shared/modes"
|
||||
import { SYSTEM_PROMPT, addCustomInstructions } from "../prompts/system"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { Cline } from "../Cline"
|
||||
import { openMention } from "../mentions"
|
||||
import { getNonce } from "./getNonce"
|
||||
import { getUri } from "./getUri"
|
||||
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
|
||||
import { checkExistKey } from "../../shared/checkExistApiConfig"
|
||||
import { enhancePrompt } from "../../utils/enhance-prompt"
|
||||
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
||||
import { ConfigManager } from "../config/ConfigManager"
|
||||
import { Mode } from "../prompts/types"
|
||||
import { codeMode, CustomPrompts } from "../../shared/modes"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -85,7 +91,14 @@ type GlobalStateKey =
|
||||
| "mcpEnabled"
|
||||
| "alwaysApproveResubmit"
|
||||
| "requestDelaySeconds"
|
||||
| "experimentalDiffStrategy"
|
||||
| "currentApiConfigName"
|
||||
| "listApiConfigMeta"
|
||||
| "mode"
|
||||
| "modeApiConfigs"
|
||||
| "customPrompts"
|
||||
| "enhancementApiConfigId",
|
||||
| "experimentalDiffStrategy"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
uiMessages: "ui_messages.json",
|
||||
@@ -103,7 +116,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
private cline?: Cline
|
||||
private workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
private latestAnnouncementId = "dec-10-2024" // update to some unique identifier when we add a new announcement
|
||||
private latestAnnouncementId = "jan-13-2025-custom-prompt" // update to some unique identifier when we add a new announcement
|
||||
configManager: ConfigManager
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -113,6 +127,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
ClineProvider.activeInstances.add(this)
|
||||
this.workspaceTracker = new WorkspaceTracker(this)
|
||||
this.mcpHub = new McpHub(this)
|
||||
this.configManager = new ConfigManager(this.context)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -228,20 +243,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.outputChannel.appendLine("Webview view resolved")
|
||||
}
|
||||
|
||||
async initClineWithTask(task?: string, images?: string[]) {
|
||||
public async initClineWithTask(task?: string, images?: string[]) {
|
||||
await this.clearTask()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
customPrompts,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
experimentalDiffStrategy
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
experimentalDiffStrategy
|
||||
} = await this.getState()
|
||||
|
||||
|
||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||
const effectiveInstructions = [globalInstructions, modeInstructions]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
effectiveInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
task,
|
||||
@@ -251,20 +273,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
)
|
||||
}
|
||||
|
||||
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
||||
public async initClineWithHistoryItem(historyItem: HistoryItem) {
|
||||
await this.clearTask()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
customPrompts,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
experimentalDiffStrategy
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
experimentalDiffStrategy
|
||||
} = await this.getState()
|
||||
|
||||
|
||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||
const effectiveInstructions = [globalInstructions, modeInstructions]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
effectiveInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
undefined,
|
||||
@@ -274,8 +303,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
)
|
||||
}
|
||||
|
||||
// Send any JSON serializable data to the react app
|
||||
async postMessageToWebview(message: ExtensionMessage) {
|
||||
public async postMessageToWebview(message: ExtensionMessage) {
|
||||
await this.view?.webview.postMessage(message)
|
||||
}
|
||||
|
||||
@@ -327,15 +355,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
// Use a nonce to only allow a specific script to be run.
|
||||
/*
|
||||
content security policy of your webview to only allow scripts that have a specific nonce
|
||||
create a content security policy meta tag so that only loading scripts with a nonce is allowed
|
||||
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
||||
content security policy of your webview to only allow scripts that have a specific nonce
|
||||
create a content security policy meta tag so that only loading scripts with a nonce is allowed
|
||||
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
|
||||
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
|
||||
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
|
||||
|
||||
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
|
||||
*/
|
||||
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
|
||||
*/
|
||||
const nonce = getNonce()
|
||||
|
||||
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
|
||||
@@ -371,6 +399,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
async (message: WebviewMessage) => {
|
||||
switch (message.type) {
|
||||
case "webviewDidLaunch":
|
||||
|
||||
this.postStateToWebview()
|
||||
this.workspaceTracker?.initializeFilePaths() // don't await
|
||||
getTheme().then((theme) =>
|
||||
@@ -416,6 +445,55 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
this.configManager.ListConfig().then(async (listApiConfig) => {
|
||||
|
||||
if (!listApiConfig) {
|
||||
return
|
||||
}
|
||||
|
||||
if (listApiConfig.length === 1) {
|
||||
// check if first time init then sync with exist config
|
||||
if (!checkExistKey(listApiConfig[0])) {
|
||||
const {
|
||||
apiConfiguration,
|
||||
} = await this.getState()
|
||||
await this.configManager.SaveConfig(listApiConfig[0].name ?? "default", apiConfiguration)
|
||||
listApiConfig[0].apiProvider = apiConfiguration.apiProvider
|
||||
}
|
||||
}
|
||||
|
||||
let currentConfigName = await this.getGlobalState("currentApiConfigName") as string
|
||||
|
||||
if (currentConfigName) {
|
||||
if (!await this.configManager.HasConfig(currentConfigName)) {
|
||||
// current config name not valid, get first config in list
|
||||
await this.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name)
|
||||
if (listApiConfig?.[0]?.name) {
|
||||
const apiConfig = await this.configManager.LoadConfig(listApiConfig?.[0]?.name);
|
||||
|
||||
await Promise.all([
|
||||
this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||
this.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
|
||||
this.updateApiConfiguration(apiConfig),
|
||||
])
|
||||
await this.postStateToWebview()
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||
await this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
|
||||
]
|
||||
)
|
||||
}).catch(console.error);
|
||||
|
||||
break
|
||||
case "newTask":
|
||||
// Code that should run in response to the hello message command
|
||||
@@ -430,70 +508,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
break
|
||||
case "apiConfiguration":
|
||||
if (message.apiConfiguration) {
|
||||
const {
|
||||
apiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
glamaModelId,
|
||||
glamaModelInfo,
|
||||
glamaApiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
azureApiVersion,
|
||||
openAiStreamingEnabled,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
openRouterUseMiddleOutTransform,
|
||||
} = message.apiConfiguration
|
||||
await this.updateGlobalState("apiProvider", apiProvider)
|
||||
await this.updateGlobalState("apiModelId", apiModelId)
|
||||
await this.storeSecret("apiKey", apiKey)
|
||||
await this.updateGlobalState("glamaModelId", glamaModelId)
|
||||
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
|
||||
await this.storeSecret("glamaApiKey", glamaApiKey)
|
||||
await this.storeSecret("openRouterApiKey", openRouterApiKey)
|
||||
await this.storeSecret("awsAccessKey", awsAccessKey)
|
||||
await this.storeSecret("awsSecretKey", awsSecretKey)
|
||||
await this.storeSecret("awsSessionToken", awsSessionToken)
|
||||
await this.updateGlobalState("awsRegion", awsRegion)
|
||||
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
|
||||
await this.updateGlobalState("vertexProjectId", vertexProjectId)
|
||||
await this.updateGlobalState("vertexRegion", vertexRegion)
|
||||
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
|
||||
await this.storeSecret("openAiApiKey", openAiApiKey)
|
||||
await this.updateGlobalState("openAiModelId", openAiModelId)
|
||||
await this.updateGlobalState("ollamaModelId", ollamaModelId)
|
||||
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
|
||||
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
|
||||
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
|
||||
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
|
||||
await this.storeSecret("geminiApiKey", geminiApiKey)
|
||||
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
|
||||
await this.storeSecret("deepSeekApiKey", message.apiConfiguration.deepSeekApiKey)
|
||||
await this.updateGlobalState("azureApiVersion", azureApiVersion)
|
||||
await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
|
||||
await this.updateGlobalState("openRouterModelId", openRouterModelId)
|
||||
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
|
||||
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
|
||||
if (this.cline) {
|
||||
this.cline.api = buildApiHandler(message.apiConfiguration)
|
||||
}
|
||||
await this.updateApiConfiguration(message.apiConfiguration)
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
@@ -572,13 +587,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
if (message?.values?.baseUrl && message?.values?.apiKey) {
|
||||
const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
|
||||
this.postMessageToWebview({ type: "openAiModels", openAiModels })
|
||||
}
|
||||
}
|
||||
break
|
||||
case "openImage":
|
||||
openImage(message.text!)
|
||||
break
|
||||
case "openFile":
|
||||
openFile(message.text!)
|
||||
openFile(message.text!, message.values as { create?: boolean; content?: string })
|
||||
break
|
||||
case "openMention":
|
||||
openMention(message.text)
|
||||
@@ -703,6 +718,90 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.updateGlobalState("terminalOutputLineLimit", message.value)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "mode":
|
||||
const newMode = message.text as Mode
|
||||
await this.updateGlobalState("mode", newMode)
|
||||
|
||||
// Load the saved API config for the new mode if it exists
|
||||
const savedConfigId = await this.configManager.GetModeConfigId(newMode)
|
||||
const listApiConfig = await this.configManager.ListConfig()
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
|
||||
// If this mode has a saved config, use it
|
||||
if (savedConfigId) {
|
||||
const config = listApiConfig?.find(c => c.id === savedConfigId)
|
||||
if (config?.name) {
|
||||
const apiConfig = await this.configManager.LoadConfig(config.name)
|
||||
await Promise.all([
|
||||
this.updateGlobalState("currentApiConfigName", config.name),
|
||||
this.updateApiConfiguration(apiConfig)
|
||||
])
|
||||
}
|
||||
} else {
|
||||
// If no saved config for this mode, save current config as default
|
||||
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
|
||||
if (currentApiConfigName) {
|
||||
const config = listApiConfig?.find(c => c.name === currentApiConfigName)
|
||||
if (config?.id) {
|
||||
await this.configManager.SetModeConfig(newMode, config.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "updateEnhancedPrompt":
|
||||
const existingPrompts = await this.getGlobalState("customPrompts") || {}
|
||||
|
||||
const updatedPrompts = {
|
||||
...existingPrompts,
|
||||
enhance: message.text
|
||||
}
|
||||
|
||||
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||
|
||||
// Get current state and explicitly include customPrompts
|
||||
const currentState = await this.getState()
|
||||
|
||||
const stateWithPrompts = {
|
||||
...currentState,
|
||||
customPrompts: updatedPrompts
|
||||
}
|
||||
|
||||
// Post state with prompts
|
||||
this.view?.webview.postMessage({
|
||||
type: "state",
|
||||
state: stateWithPrompts
|
||||
})
|
||||
break
|
||||
case "updatePrompt":
|
||||
if (message.promptMode && message.customPrompt !== undefined) {
|
||||
const existingPrompts = await this.getGlobalState("customPrompts") || {}
|
||||
|
||||
const updatedPrompts = {
|
||||
...existingPrompts,
|
||||
[message.promptMode]: message.customPrompt
|
||||
}
|
||||
|
||||
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||
|
||||
// Get current state and explicitly include customPrompts
|
||||
const currentState = await this.getState()
|
||||
|
||||
const stateWithPrompts = {
|
||||
...currentState,
|
||||
customPrompts: updatedPrompts
|
||||
}
|
||||
|
||||
// Post state with prompts
|
||||
this.view?.webview.postMessage({
|
||||
type: "state",
|
||||
state: stateWithPrompts
|
||||
})
|
||||
}
|
||||
break
|
||||
case "deleteMessage": {
|
||||
const answer = await vscode.window.showInformationMessage(
|
||||
"What would you like to delete?",
|
||||
@@ -773,16 +872,28 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.updateGlobalState("screenshotQuality", message.value)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "enhancementApiConfigId":
|
||||
await this.updateGlobalState("enhancementApiConfigId", message.text)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "enhancePrompt":
|
||||
if (message.text) {
|
||||
try {
|
||||
const { apiConfiguration } = await this.getState()
|
||||
const enhanceConfig = {
|
||||
...apiConfiguration,
|
||||
apiProvider: "openrouter" as const,
|
||||
openRouterModelId: "gpt-4o",
|
||||
const { apiConfiguration, customPrompts, listApiConfigMeta, enhancementApiConfigId } = await this.getState()
|
||||
|
||||
// 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)
|
||||
if (config?.name) {
|
||||
const loadedConfig = await this.configManager.LoadConfig(config.name)
|
||||
if (loadedConfig.apiProvider) {
|
||||
configToUse = loadedConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
const enhancedPrompt = await enhancePrompt(enhanceConfig, message.text)
|
||||
|
||||
const enhancedPrompt = await enhancePrompt(configToUse, message.text, customPrompts?.enhance)
|
||||
await this.postMessageToWebview({
|
||||
type: "enhancedPrompt",
|
||||
text: enhancedPrompt
|
||||
@@ -790,11 +901,45 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
} catch (error) {
|
||||
console.error("Error enhancing prompt:", error)
|
||||
vscode.window.showErrorMessage("Failed to enhance prompt")
|
||||
await this.postMessageToWebview({
|
||||
type: "enhancedPrompt"
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
case "getSystemPrompt":
|
||||
try {
|
||||
const { apiConfiguration, customPrompts, customInstructions, preferredLanguage, browserViewportSize, mcpEnabled } = await this.getState()
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ''
|
||||
|
||||
const mode = message.mode ?? codeMode
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions, customPrompts, preferredLanguage },
|
||||
cwd,
|
||||
mode
|
||||
)
|
||||
|
||||
const systemPrompt = await SYSTEM_PROMPT(
|
||||
cwd,
|
||||
apiConfiguration.openRouterModelInfo?.supportsComputerUse ?? false,
|
||||
mcpEnabled ? this.mcpHub : undefined,
|
||||
undefined,
|
||||
browserViewportSize ?? "900x600",
|
||||
mode,
|
||||
customPrompts
|
||||
)
|
||||
const fullPrompt = instructions ? `${systemPrompt}${instructions}` : systemPrompt
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "systemPrompt",
|
||||
text: fullPrompt,
|
||||
mode: message.mode
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error getting system prompt:", error)
|
||||
vscode.window.showErrorMessage("Failed to get system prompt")
|
||||
}
|
||||
break
|
||||
case "searchCommits": {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (cwd) {
|
||||
@@ -811,10 +956,123 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "experimentalDiffStrategy":
|
||||
case "upsertApiConfiguration":
|
||||
if (message.text && message.apiConfiguration) {
|
||||
try {
|
||||
await this.configManager.SaveConfig(message.text, message.apiConfiguration);
|
||||
let listApiConfig = await this.configManager.ListConfig();
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
|
||||
|
||||
await Promise.all([
|
||||
this.updateApiConfiguration(message.apiConfiguration),
|
||||
this.updateGlobalState("currentApiConfigName", message.text),
|
||||
])
|
||||
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Error create new api configuration:", error)
|
||||
vscode.window.showErrorMessage("Failed to create api configuration")
|
||||
}
|
||||
}
|
||||
break
|
||||
case "renameApiConfiguration":
|
||||
if (message.values && message.apiConfiguration) {
|
||||
try {
|
||||
const { oldName, newName } = message.values
|
||||
|
||||
await this.configManager.SaveConfig(newName, message.apiConfiguration);
|
||||
await this.configManager.DeleteConfig(oldName)
|
||||
|
||||
let listApiConfig = await this.configManager.ListConfig();
|
||||
const config = listApiConfig?.find(c => c.name === newName);
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
|
||||
|
||||
await Promise.all([
|
||||
this.updateGlobalState("currentApiConfigName", newName),
|
||||
])
|
||||
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Error create new api configuration:", error)
|
||||
vscode.window.showErrorMessage("Failed to create api configuration")
|
||||
}
|
||||
}
|
||||
break
|
||||
case "loadApiConfiguration":
|
||||
if (message.text) {
|
||||
try {
|
||||
const apiConfig = await this.configManager.LoadConfig(message.text);
|
||||
const listApiConfig = await this.configManager.ListConfig();
|
||||
const config = listApiConfig?.find(c => c.name === message.text);
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
|
||||
|
||||
await Promise.all([
|
||||
this.updateGlobalState("currentApiConfigName", message.text),
|
||||
this.updateApiConfiguration(apiConfig),
|
||||
])
|
||||
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Error load api configuration:", error)
|
||||
vscode.window.showErrorMessage("Failed to load api configuration")
|
||||
}
|
||||
}
|
||||
break
|
||||
case "deleteApiConfiguration":
|
||||
if (message.text) {
|
||||
const answer = await vscode.window.showInformationMessage(
|
||||
"Are you sure you want to delete this configuration profile?",
|
||||
{ modal: true },
|
||||
"Yes",
|
||||
)
|
||||
|
||||
if (answer !== "Yes") {
|
||||
break
|
||||
}
|
||||
|
||||
try {
|
||||
await this.configManager.DeleteConfig(message.text);
|
||||
const listApiConfig = await this.configManager.ListConfig();
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
|
||||
|
||||
// If this was the current config, switch to first available
|
||||
let currentApiConfigName = await this.getGlobalState("currentApiConfigName")
|
||||
if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) {
|
||||
const apiConfig = await this.configManager.LoadConfig(listApiConfig[0].name);
|
||||
await Promise.all([
|
||||
this.updateGlobalState("currentApiConfigName", listApiConfig[0].name),
|
||||
this.updateApiConfiguration(apiConfig),
|
||||
])
|
||||
}
|
||||
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Error delete api configuration:", error)
|
||||
vscode.window.showErrorMessage("Failed to delete api configuration")
|
||||
}
|
||||
}
|
||||
break
|
||||
case "getListApiConfiguration":
|
||||
try {
|
||||
let listApiConfig = await this.configManager.ListConfig();
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
|
||||
} catch (error) {
|
||||
console.error("Error get list api configuration:", error)
|
||||
vscode.window.showErrorMessage("Failed to get list api configuration")
|
||||
}
|
||||
break
|
||||
case "experimentalDiffStrategy":
|
||||
await this.updateGlobalState("experimentalDiffStrategy", message.bool ?? false)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
}
|
||||
},
|
||||
null,
|
||||
@@ -822,6 +1080,85 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
)
|
||||
}
|
||||
|
||||
private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
|
||||
// Update mode's default config
|
||||
const { mode } = await this.getState();
|
||||
if (mode) {
|
||||
const currentApiConfigName = await this.getGlobalState("currentApiConfigName");
|
||||
const listApiConfig = await this.configManager.ListConfig();
|
||||
const config = listApiConfig?.find(c => c.name === currentApiConfigName);
|
||||
if (config?.id) {
|
||||
await this.configManager.SetModeConfig(mode, config.id);
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
apiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
glamaModelId,
|
||||
glamaModelInfo,
|
||||
glamaApiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
azureApiVersion,
|
||||
openAiStreamingEnabled,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
openRouterUseMiddleOutTransform,
|
||||
} = apiConfiguration
|
||||
await this.updateGlobalState("apiProvider", apiProvider)
|
||||
await this.updateGlobalState("apiModelId", apiModelId)
|
||||
await this.storeSecret("apiKey", apiKey)
|
||||
await this.updateGlobalState("glamaModelId", glamaModelId)
|
||||
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
|
||||
await this.storeSecret("glamaApiKey", glamaApiKey)
|
||||
await this.storeSecret("openRouterApiKey", openRouterApiKey)
|
||||
await this.storeSecret("awsAccessKey", awsAccessKey)
|
||||
await this.storeSecret("awsSecretKey", awsSecretKey)
|
||||
await this.storeSecret("awsSessionToken", awsSessionToken)
|
||||
await this.updateGlobalState("awsRegion", awsRegion)
|
||||
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
|
||||
await this.updateGlobalState("vertexProjectId", vertexProjectId)
|
||||
await this.updateGlobalState("vertexRegion", vertexRegion)
|
||||
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
|
||||
await this.storeSecret("openAiApiKey", openAiApiKey)
|
||||
await this.updateGlobalState("openAiModelId", openAiModelId)
|
||||
await this.updateGlobalState("ollamaModelId", ollamaModelId)
|
||||
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
|
||||
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
|
||||
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
|
||||
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
|
||||
await this.storeSecret("geminiApiKey", geminiApiKey)
|
||||
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
|
||||
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
|
||||
await this.updateGlobalState("azureApiVersion", azureApiVersion)
|
||||
await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
|
||||
await this.updateGlobalState("openRouterModelId", openRouterModelId)
|
||||
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
|
||||
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
|
||||
if (this.cline) {
|
||||
this.cline.api = buildApiHandler(apiConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
async updateCustomInstructions(instructions?: string) {
|
||||
// User may be clearing the field
|
||||
await this.updateGlobalState("customInstructions", instructions || undefined)
|
||||
@@ -1266,9 +1603,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
mcpEnabled,
|
||||
alwaysApproveResubmit,
|
||||
requestDelaySeconds,
|
||||
experimentalDiffStrategy,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
mode,
|
||||
customPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
} = await this.getState()
|
||||
|
||||
|
||||
const allowedCommands = vscode.workspace
|
||||
.getConfiguration('roo-cline')
|
||||
.get<string[]>('allowedCommands') || []
|
||||
@@ -1285,8 +1627,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
uriScheme: vscode.env.uriScheme,
|
||||
clineMessages: this.cline?.clineMessages || [],
|
||||
taskHistory: (taskHistory || [])
|
||||
.filter((item) => item.ts && item.task)
|
||||
.sort((a, b) => b.ts - a.ts),
|
||||
.filter((item: HistoryItem) => item.ts && item.task)
|
||||
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
|
||||
soundEnabled: soundEnabled ?? false,
|
||||
diffEnabled: diffEnabled ?? true,
|
||||
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
||||
@@ -1301,7 +1643,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
mcpEnabled: mcpEnabled ?? true,
|
||||
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
||||
requestDelaySeconds: requestDelaySeconds ?? 5,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
mode: mode ?? codeMode,
|
||||
customPrompts: customPrompts ?? {},
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,7 +1756,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
mcpEnabled,
|
||||
alwaysApproveResubmit,
|
||||
requestDelaySeconds,
|
||||
experimentalDiffStrategy,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
mode,
|
||||
modeApiConfigs,
|
||||
customPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||
@@ -1462,7 +1815,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
|
||||
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
|
||||
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
|
||||
this.getGlobalState("mode") as Promise<Mode | undefined>,
|
||||
this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | undefined>,
|
||||
this.getGlobalState("customPrompts") as Promise<CustomPrompts | undefined>,
|
||||
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
|
||||
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -1529,6 +1888,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
|
||||
writeDelayMs: writeDelayMs ?? 1000,
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
mode: mode ?? codeMode,
|
||||
preferredLanguage: preferredLanguage ?? (() => {
|
||||
// Get VSCode's locale setting
|
||||
const vscodeLang = vscode.env.language;
|
||||
@@ -1559,7 +1919,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
mcpEnabled: mcpEnabled ?? true,
|
||||
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
||||
requestDelaySeconds: requestDelaySeconds ?? 5,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
modeApiConfigs: modeApiConfigs ?? {} as Record<Mode, string>,
|
||||
customPrompts: customPrompts ?? {},
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ClineProvider } from '../ClineProvider'
|
||||
import * as vscode from 'vscode'
|
||||
import { ExtensionMessage, ExtensionState } from '../../../shared/ExtensionMessage'
|
||||
import { setSoundEnabled } from '../../../utils/sound'
|
||||
import { codeMode } from '../../prompts/system';
|
||||
|
||||
// Mock delay module
|
||||
jest.mock('delay', () => {
|
||||
@@ -61,6 +62,7 @@ jest.mock('vscode', () => ({
|
||||
},
|
||||
window: {
|
||||
showInformationMessage: jest.fn(),
|
||||
showErrorMessage: jest.fn(),
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: jest.fn().mockReturnValue({
|
||||
@@ -112,6 +114,13 @@ jest.mock('../../../api', () => ({
|
||||
buildApiHandler: jest.fn()
|
||||
}))
|
||||
|
||||
// Mock system prompt
|
||||
jest.mock('../../prompts/system', () => ({
|
||||
SYSTEM_PROMPT: jest.fn().mockImplementation(async () => 'mocked system prompt'),
|
||||
codeMode: 'code',
|
||||
addCustomInstructions: jest.fn().mockImplementation(async () => '')
|
||||
}))
|
||||
|
||||
// Mock WorkspaceTracker
|
||||
jest.mock('../../../integrations/workspace/WorkspaceTracker', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
@@ -121,19 +130,25 @@ jest.mock('../../../integrations/workspace/WorkspaceTracker', () => {
|
||||
})
|
||||
|
||||
// Mock Cline
|
||||
jest.mock('../../Cline', () => {
|
||||
return {
|
||||
Cline: jest.fn().mockImplementation(() => ({
|
||||
abortTask: jest.fn(),
|
||||
handleWebviewAskResponse: jest.fn(),
|
||||
clineMessages: [],
|
||||
apiConversationHistory: [],
|
||||
overwriteClineMessages: jest.fn(),
|
||||
overwriteApiConversationHistory: jest.fn(),
|
||||
taskId: 'test-task-id'
|
||||
}))
|
||||
}
|
||||
})
|
||||
jest.mock('../../Cline', () => ({
|
||||
Cline: jest.fn().mockImplementation((
|
||||
provider,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
task,
|
||||
taskId
|
||||
) => ({
|
||||
abortTask: jest.fn(),
|
||||
handleWebviewAskResponse: jest.fn(),
|
||||
clineMessages: [],
|
||||
apiConversationHistory: [],
|
||||
overwriteClineMessages: jest.fn(),
|
||||
overwriteApiConversationHistory: jest.fn(),
|
||||
taskId: taskId || 'test-task-id'
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock extract-text
|
||||
jest.mock('../../../integrations/misc/extract-text', () => ({
|
||||
@@ -171,7 +186,16 @@ describe('ClineProvider', () => {
|
||||
extensionPath: '/test/path',
|
||||
extensionUri: {} as vscode.Uri,
|
||||
globalState: {
|
||||
get: jest.fn(),
|
||||
get: jest.fn().mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'mode':
|
||||
return 'architect'
|
||||
case 'currentApiConfigName':
|
||||
return 'new-config'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}),
|
||||
update: jest.fn(),
|
||||
keys: jest.fn().mockReturnValue([]),
|
||||
},
|
||||
@@ -263,7 +287,8 @@ describe('ClineProvider', () => {
|
||||
browserViewportSize: "900x600",
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
mcpEnabled: true,
|
||||
requestDelaySeconds: 5
|
||||
requestDelaySeconds: 5,
|
||||
mode: codeMode,
|
||||
}
|
||||
|
||||
const message: ExtensionMessage = {
|
||||
@@ -404,6 +429,80 @@ describe('ClineProvider', () => {
|
||||
expect(state.alwaysApproveResubmit).toBe(false)
|
||||
})
|
||||
|
||||
test('loads saved API config when switching modes', async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock ConfigManager methods
|
||||
provider.configManager = {
|
||||
GetModeConfigId: jest.fn().mockResolvedValue('test-id'),
|
||||
ListConfig: jest.fn().mockResolvedValue([
|
||||
{ name: 'test-config', id: 'test-id', apiProvider: 'anthropic' }
|
||||
]),
|
||||
LoadConfig: jest.fn().mockResolvedValue({ apiProvider: 'anthropic' }),
|
||||
SetModeConfig: jest.fn()
|
||||
} as any
|
||||
|
||||
// Switch to architect mode
|
||||
await messageHandler({ type: 'mode', text: 'architect' })
|
||||
|
||||
// Should load the saved config for architect mode
|
||||
expect(provider.configManager.GetModeConfigId).toHaveBeenCalledWith('architect')
|
||||
expect(provider.configManager.LoadConfig).toHaveBeenCalledWith('test-config')
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith('currentApiConfigName', 'test-config')
|
||||
})
|
||||
|
||||
test('saves current config when switching to mode without config', async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock ConfigManager methods
|
||||
provider.configManager = {
|
||||
GetModeConfigId: jest.fn().mockResolvedValue(undefined),
|
||||
ListConfig: jest.fn().mockResolvedValue([
|
||||
{ name: 'current-config', id: 'current-id', apiProvider: 'anthropic' }
|
||||
]),
|
||||
SetModeConfig: jest.fn()
|
||||
} as any
|
||||
|
||||
// Mock current config name
|
||||
(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
|
||||
if (key === 'currentApiConfigName') {
|
||||
return 'current-config'
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
// Switch to architect mode
|
||||
await messageHandler({ type: 'mode', text: 'architect' })
|
||||
|
||||
// Should save current config as default for architect mode
|
||||
expect(provider.configManager.SetModeConfig).toHaveBeenCalledWith('architect', 'current-id')
|
||||
})
|
||||
|
||||
test('saves config as default for current mode when loading config', async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
provider.configManager = {
|
||||
LoadConfig: jest.fn().mockResolvedValue({ apiProvider: 'anthropic', id: 'new-id' }),
|
||||
ListConfig: jest.fn().mockResolvedValue([
|
||||
{ name: 'new-config', id: 'new-id', apiProvider: 'anthropic' }
|
||||
]),
|
||||
SetModeConfig: jest.fn(),
|
||||
GetModeConfigId: jest.fn().mockResolvedValue(undefined)
|
||||
} as any
|
||||
|
||||
// First set the mode
|
||||
await messageHandler({ type: 'mode', text: 'architect' })
|
||||
|
||||
// Then load the config
|
||||
await messageHandler({ type: 'loadApiConfiguration', text: 'new-config' })
|
||||
|
||||
// Should save new config as default for architect mode
|
||||
expect(provider.configManager.SetModeConfig).toHaveBeenCalledWith('architect', 'new-id')
|
||||
})
|
||||
|
||||
test('handles request delay settings messages', async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
@@ -419,6 +518,182 @@ describe('ClineProvider', () => {
|
||||
expect(mockPostMessage).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('handles updatePrompt message correctly', async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock existing prompts
|
||||
const existingPrompts = {
|
||||
code: 'existing code prompt',
|
||||
architect: 'existing architect prompt'
|
||||
}
|
||||
;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
|
||||
if (key === 'customPrompts') {
|
||||
return existingPrompts
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
// Test updating a prompt
|
||||
await messageHandler({
|
||||
type: 'updatePrompt',
|
||||
promptMode: 'code',
|
||||
customPrompt: 'new code prompt'
|
||||
})
|
||||
|
||||
// Verify state was updated correctly
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith(
|
||||
'customPrompts',
|
||||
{
|
||||
...existingPrompts,
|
||||
code: 'new code prompt'
|
||||
}
|
||||
)
|
||||
|
||||
// Verify state was posted to webview
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'state',
|
||||
state: expect.objectContaining({
|
||||
customPrompts: {
|
||||
...existingPrompts,
|
||||
code: 'new code prompt'
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('customPrompts defaults to empty object', async () => {
|
||||
// Mock globalState.get to return undefined for customPrompts
|
||||
(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
|
||||
if (key === 'customPrompts') {
|
||||
return undefined
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const state = await provider.getState()
|
||||
expect(state.customPrompts).toEqual({})
|
||||
})
|
||||
|
||||
test('uses mode-specific custom instructions in Cline initialization', async () => {
|
||||
// Setup mock state
|
||||
const modeCustomInstructions = 'Code mode instructions';
|
||||
const mockApiConfig = {
|
||||
apiProvider: 'openrouter',
|
||||
openRouterModelInfo: { supportsComputerUse: true }
|
||||
};
|
||||
|
||||
jest.spyOn(provider, 'getState').mockResolvedValue({
|
||||
apiConfiguration: mockApiConfig,
|
||||
customPrompts: {
|
||||
code: { customInstructions: modeCustomInstructions }
|
||||
},
|
||||
mode: 'code',
|
||||
diffEnabled: true,
|
||||
fuzzyMatchThreshold: 1.0
|
||||
} as any);
|
||||
|
||||
// Reset Cline mock
|
||||
const { Cline } = require('../../Cline');
|
||||
(Cline as jest.Mock).mockClear();
|
||||
|
||||
// Initialize Cline with a task
|
||||
await provider.initClineWithTask('Test task');
|
||||
|
||||
// Verify Cline was initialized with mode-specific instructions
|
||||
expect(Cline).toHaveBeenCalledWith(
|
||||
provider,
|
||||
mockApiConfig,
|
||||
modeCustomInstructions,
|
||||
true,
|
||||
1.0,
|
||||
'Test task',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
test('handles mode-specific custom instructions updates', async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock existing prompts
|
||||
const existingPrompts = {
|
||||
code: {
|
||||
roleDefinition: 'Code role',
|
||||
customInstructions: 'Old instructions'
|
||||
}
|
||||
}
|
||||
mockContext.globalState.get = jest.fn((key: string) => {
|
||||
if (key === 'customPrompts') {
|
||||
return existingPrompts
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
// Update custom instructions for code mode
|
||||
await messageHandler({
|
||||
type: 'updatePrompt',
|
||||
promptMode: 'code',
|
||||
customPrompt: {
|
||||
roleDefinition: 'Code role',
|
||||
customInstructions: 'New instructions'
|
||||
}
|
||||
})
|
||||
|
||||
// Verify state was updated correctly
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith(
|
||||
'customPrompts',
|
||||
{
|
||||
code: {
|
||||
roleDefinition: 'Code role',
|
||||
customInstructions: 'New instructions'
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('saves mode config when updating API configuration', async () => {
|
||||
// Setup mock context with mode and config name
|
||||
mockContext = {
|
||||
...mockContext,
|
||||
globalState: {
|
||||
...mockContext.globalState,
|
||||
get: jest.fn((key: string) => {
|
||||
if (key === 'mode') {
|
||||
return 'code'
|
||||
} else if (key === 'currentApiConfigName') {
|
||||
return 'test-config'
|
||||
}
|
||||
return undefined
|
||||
}),
|
||||
update: jest.fn(),
|
||||
keys: jest.fn().mockReturnValue([]),
|
||||
}
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
// Create new provider with updated mock context
|
||||
provider = new ClineProvider(mockContext, mockOutputChannel)
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
provider.configManager = {
|
||||
ListConfig: jest.fn().mockResolvedValue([
|
||||
{ name: 'test-config', id: 'test-id', apiProvider: 'anthropic' }
|
||||
]),
|
||||
SetModeConfig: jest.fn()
|
||||
} as any
|
||||
|
||||
// Update API configuration
|
||||
await messageHandler({
|
||||
type: 'apiConfiguration',
|
||||
apiConfiguration: { apiProvider: 'anthropic' }
|
||||
})
|
||||
|
||||
// Should save config as default for current mode
|
||||
expect(provider.configManager.SetModeConfig).toHaveBeenCalledWith('code', 'test-id')
|
||||
})
|
||||
|
||||
test('file content includes line numbers', async () => {
|
||||
const { extractTextFromFile } = require('../../../integrations/misc/extract-text')
|
||||
const result = await extractTextFromFile('test.js')
|
||||
@@ -569,4 +844,165 @@ describe('ClineProvider', () => {
|
||||
expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSystemPrompt', () => {
|
||||
beforeEach(() => {
|
||||
mockPostMessage.mockClear();
|
||||
provider.resolveWebviewView(mockWebviewView);
|
||||
});
|
||||
|
||||
const getMessageHandler = () => {
|
||||
const mockCalls = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls;
|
||||
expect(mockCalls.length).toBeGreaterThan(0);
|
||||
return mockCalls[0][0];
|
||||
};
|
||||
|
||||
test('handles mcpEnabled setting correctly', async () => {
|
||||
// Mock getState to return mcpEnabled: true
|
||||
jest.spyOn(provider, 'getState').mockResolvedValue({
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter' as const,
|
||||
openRouterModelInfo: {
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: false,
|
||||
maxTokens: 4096,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
description: undefined
|
||||
}
|
||||
},
|
||||
mcpEnabled: true,
|
||||
mode: 'code' as const
|
||||
} as any);
|
||||
|
||||
const handler1 = getMessageHandler();
|
||||
expect(typeof handler1).toBe('function');
|
||||
await handler1({ type: 'getSystemPrompt', mode: 'code' });
|
||||
|
||||
// Verify mcpHub is passed when mcpEnabled is true
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'systemPrompt',
|
||||
text: expect.any(String)
|
||||
})
|
||||
);
|
||||
|
||||
// Mock getState to return mcpEnabled: false
|
||||
jest.spyOn(provider, 'getState').mockResolvedValue({
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter' as const,
|
||||
openRouterModelInfo: {
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: false,
|
||||
maxTokens: 4096,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
description: undefined
|
||||
}
|
||||
},
|
||||
mcpEnabled: false,
|
||||
mode: 'code' as const
|
||||
} as any);
|
||||
|
||||
const handler2 = getMessageHandler();
|
||||
await handler2({ type: 'getSystemPrompt', mode: 'code' });
|
||||
|
||||
// Verify mcpHub is not passed when mcpEnabled is false
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'systemPrompt',
|
||||
text: expect.any(String)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('handles errors gracefully', async () => {
|
||||
// Mock SYSTEM_PROMPT to throw an error
|
||||
const systemPrompt = require('../../prompts/system')
|
||||
jest.spyOn(systemPrompt, 'SYSTEM_PROMPT').mockRejectedValueOnce(new Error('Test error'))
|
||||
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
await messageHandler({ type: 'getSystemPrompt', mode: 'code' })
|
||||
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith('Failed to get system prompt')
|
||||
})
|
||||
|
||||
test('uses mode-specific custom instructions in system prompt', async () => {
|
||||
const systemPrompt = require('../../prompts/system')
|
||||
const { addCustomInstructions } = systemPrompt
|
||||
|
||||
// Mock getState to return mode-specific custom instructions
|
||||
jest.spyOn(provider, 'getState').mockResolvedValue({
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter',
|
||||
openRouterModelInfo: { supportsComputerUse: true }
|
||||
},
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Code mode specific instructions' }
|
||||
},
|
||||
mode: 'code',
|
||||
mcpEnabled: false,
|
||||
browserViewportSize: '900x600'
|
||||
} as any)
|
||||
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
await messageHandler({ type: 'getSystemPrompt', mode: 'code' })
|
||||
|
||||
// Verify addCustomInstructions was called with mode-specific instructions
|
||||
expect(addCustomInstructions).toHaveBeenCalledWith(
|
||||
{
|
||||
customInstructions: undefined,
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Code mode specific instructions' }
|
||||
},
|
||||
preferredLanguage: undefined
|
||||
},
|
||||
expect.any(String),
|
||||
'code'
|
||||
)
|
||||
})
|
||||
|
||||
test('uses correct mode-specific instructions when mode is specified', async () => {
|
||||
const systemPrompt = require('../../prompts/system')
|
||||
const { addCustomInstructions } = systemPrompt
|
||||
|
||||
// Mock getState to return instructions for multiple modes
|
||||
jest.spyOn(provider, 'getState').mockResolvedValue({
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter',
|
||||
openRouterModelInfo: { supportsComputerUse: true }
|
||||
},
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Code mode instructions' },
|
||||
architect: { customInstructions: 'Architect mode instructions' }
|
||||
},
|
||||
mode: 'code',
|
||||
mcpEnabled: false,
|
||||
browserViewportSize: '900x600'
|
||||
} as any)
|
||||
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Request architect mode prompt
|
||||
await messageHandler({ type: 'getSystemPrompt', mode: 'architect' })
|
||||
|
||||
// Verify architect mode instructions were used
|
||||
expect(addCustomInstructions).toHaveBeenCalledWith(
|
||||
{
|
||||
customInstructions: undefined,
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Code mode instructions' },
|
||||
architect: { customInstructions: 'Architect mode instructions' }
|
||||
},
|
||||
preferredLanguage: undefined
|
||||
},
|
||||
expect.any(String),
|
||||
'architect'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user