mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Add a screen for custom prompts
This commit is contained in:
18
package.json
18
package.json
@@ -74,6 +74,11 @@
|
|||||||
"title": "MCP Servers",
|
"title": "MCP Servers",
|
||||||
"icon": "$(server)"
|
"icon": "$(server)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "roo-cline.promptsButtonClicked",
|
||||||
|
"title": "Prompts",
|
||||||
|
"icon": "$(notebook)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "roo-cline.historyButtonClicked",
|
"command": "roo-cline.historyButtonClicked",
|
||||||
"title": "History",
|
"title": "History",
|
||||||
@@ -103,24 +108,29 @@
|
|||||||
"when": "view == roo-cline.SidebarProvider"
|
"when": "view == roo-cline.SidebarProvider"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "roo-cline.mcpButtonClicked",
|
"command": "roo-cline.promptsButtonClicked",
|
||||||
"group": "navigation@2",
|
"group": "navigation@2",
|
||||||
"when": "view == roo-cline.SidebarProvider"
|
"when": "view == roo-cline.SidebarProvider"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "roo-cline.historyButtonClicked",
|
"command": "roo-cline.mcpButtonClicked",
|
||||||
"group": "navigation@3",
|
"group": "navigation@3",
|
||||||
"when": "view == roo-cline.SidebarProvider"
|
"when": "view == roo-cline.SidebarProvider"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "roo-cline.popoutButtonClicked",
|
"command": "roo-cline.historyButtonClicked",
|
||||||
"group": "navigation@4",
|
"group": "navigation@4",
|
||||||
"when": "view == roo-cline.SidebarProvider"
|
"when": "view == roo-cline.SidebarProvider"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "roo-cline.settingsButtonClicked",
|
"command": "roo-cline.popoutButtonClicked",
|
||||||
"group": "navigation@5",
|
"group": "navigation@5",
|
||||||
"when": "view == roo-cline.SidebarProvider"
|
"when": "view == roo-cline.SidebarProvider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "roo-cline.settingsButtonClicked",
|
||||||
|
"group": "navigation@6",
|
||||||
|
"when": "view == roo-cline.SidebarProvider"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -780,14 +780,15 @@ export class Cline {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { browserViewportSize, preferredLanguage, mode } = await this.providerRef.deref()?.getState() ?? {}
|
const { browserViewportSize, preferredLanguage, mode, customPrompts } = await this.providerRef.deref()?.getState() ?? {}
|
||||||
const systemPrompt = await SYSTEM_PROMPT(
|
const systemPrompt = await SYSTEM_PROMPT(
|
||||||
cwd,
|
cwd,
|
||||||
this.api.getModel().info.supportsComputerUse ?? false,
|
this.api.getModel().info.supportsComputerUse ?? false,
|
||||||
mcpHub,
|
mcpHub,
|
||||||
this.diffStrategy,
|
this.diffStrategy,
|
||||||
browserViewportSize,
|
browserViewportSize,
|
||||||
mode
|
mode,
|
||||||
|
customPrompts
|
||||||
) + await addCustomInstructions(this.customInstructions ?? '', cwd, preferredLanguage)
|
) + await addCustomInstructions(this.customInstructions ?? '', cwd, preferredLanguage)
|
||||||
|
|
||||||
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { architectMode } from "./modes"
|
import { architectMode, defaultPrompts } from "../../shared/modes"
|
||||||
import { getToolDescriptionsForMode } from "./tools"
|
import { getToolDescriptionsForMode } from "./tools"
|
||||||
import {
|
import {
|
||||||
getRulesSection,
|
getRulesSection,
|
||||||
@@ -20,7 +20,8 @@ export const ARCHITECT_PROMPT = async (
|
|||||||
mcpHub?: McpHub,
|
mcpHub?: McpHub,
|
||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
browserViewportSize?: string,
|
browserViewportSize?: string,
|
||||||
) => `You are Cline, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements while maintaining a read-only approach to the codebase. Make sure to help the user come up with a solid implementation plan for their project and don't rush to switch to implementing code.
|
customPrompt?: string,
|
||||||
|
) => `${customPrompt || defaultPrompts[architectMode]}
|
||||||
|
|
||||||
${getSharedToolUseSection()}
|
${getSharedToolUseSection()}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Mode, askMode } from "./modes"
|
import { Mode, askMode, defaultPrompts } from "../../shared/modes"
|
||||||
import { getToolDescriptionsForMode } from "./tools"
|
import { getToolDescriptionsForMode } from "./tools"
|
||||||
import {
|
import {
|
||||||
getRulesSection,
|
getRulesSection,
|
||||||
@@ -21,7 +21,8 @@ export const ASK_PROMPT = async (
|
|||||||
mcpHub?: McpHub,
|
mcpHub?: McpHub,
|
||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
browserViewportSize?: string,
|
browserViewportSize?: string,
|
||||||
) => `You are Cline, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources while maintaining a read-only approach to the codebase. Make sure to answer the user's questions and don't rush to switch to implementing code.
|
customPrompt?: string,
|
||||||
|
) => `${customPrompt || defaultPrompts[askMode]}
|
||||||
|
|
||||||
${getSharedToolUseSection()}
|
${getSharedToolUseSection()}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Mode, codeMode } from "./modes"
|
import { Mode, codeMode, defaultPrompts } from "../../shared/modes"
|
||||||
import { getToolDescriptionsForMode } from "./tools"
|
import { getToolDescriptionsForMode } from "./tools"
|
||||||
import {
|
import {
|
||||||
getRulesSection,
|
getRulesSection,
|
||||||
@@ -21,7 +21,8 @@ export const CODE_PROMPT = async (
|
|||||||
mcpHub?: McpHub,
|
mcpHub?: McpHub,
|
||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
browserViewportSize?: string,
|
browserViewportSize?: string,
|
||||||
) => `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
customPrompt?: string,
|
||||||
|
) => `${customPrompt || defaultPrompts[codeMode]}
|
||||||
|
|
||||||
${getSharedToolUseSection()}
|
${getSharedToolUseSection()}
|
||||||
|
|
||||||
|
|||||||
@@ -64,14 +64,15 @@ export const SYSTEM_PROMPT = async (
|
|||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
browserViewportSize?: string,
|
browserViewportSize?: string,
|
||||||
mode: Mode = codeMode,
|
mode: Mode = codeMode,
|
||||||
|
customPrompts?: { ask?: string; code?: string; architect?: string; enhance?: string },
|
||||||
) => {
|
) => {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case architectMode:
|
case architectMode:
|
||||||
return ARCHITECT_PROMPT(cwd, supportsComputerUse, mcpHub, diffStrategy, browserViewportSize)
|
return ARCHITECT_PROMPT(cwd, supportsComputerUse, mcpHub, diffStrategy, browserViewportSize, customPrompts?.architect)
|
||||||
case askMode:
|
case askMode:
|
||||||
return ASK_PROMPT(cwd, supportsComputerUse, mcpHub, diffStrategy, browserViewportSize)
|
return ASK_PROMPT(cwd, supportsComputerUse, mcpHub, diffStrategy, browserViewportSize, customPrompts?.ask)
|
||||||
default:
|
default:
|
||||||
return CODE_PROMPT(cwd, supportsComputerUse, mcpHub, diffStrategy, browserViewportSize)
|
return CODE_PROMPT(cwd, supportsComputerUse, mcpHub, diffStrategy, browserViewportSize, customPrompts?.code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { findLast } from "../../shared/array"
|
|||||||
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
|
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||||
import { HistoryItem } from "../../shared/HistoryItem"
|
import { HistoryItem } from "../../shared/HistoryItem"
|
||||||
import { WebviewMessage } from "../../shared/WebviewMessage"
|
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||||
|
import { defaultPrompts } from "../../shared/modes"
|
||||||
|
import { SYSTEM_PROMPT, addCustomInstructions } from "../prompts/system"
|
||||||
import { fileExistsAtPath } from "../../utils/fs"
|
import { fileExistsAtPath } from "../../utils/fs"
|
||||||
import { Cline } from "../Cline"
|
import { Cline } from "../Cline"
|
||||||
import { openMention } from "../mentions"
|
import { openMention } from "../mentions"
|
||||||
@@ -28,7 +30,7 @@ import { enhancePrompt } from "../../utils/enhance-prompt"
|
|||||||
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
||||||
import { ConfigManager } from "../config/ConfigManager"
|
import { ConfigManager } from "../config/ConfigManager"
|
||||||
import { Mode } from "../prompts/types"
|
import { Mode } from "../prompts/types"
|
||||||
import { codeMode } from "../prompts/system"
|
import { codeMode, CustomPrompts } from "../../shared/modes"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||||
@@ -93,6 +95,8 @@ type GlobalStateKey =
|
|||||||
| "listApiConfigMeta"
|
| "listApiConfigMeta"
|
||||||
| "mode"
|
| "mode"
|
||||||
| "modeApiConfigs"
|
| "modeApiConfigs"
|
||||||
|
| "customPrompts"
|
||||||
|
| "enhancementApiConfigId"
|
||||||
|
|
||||||
export const GlobalFileNames = {
|
export const GlobalFileNames = {
|
||||||
apiConversationHistory: "api_conversation_history.json",
|
apiConversationHistory: "api_conversation_history.json",
|
||||||
@@ -111,7 +115,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
private cline?: Cline
|
private cline?: Cline
|
||||||
private workspaceTracker?: WorkspaceTracker
|
private workspaceTracker?: WorkspaceTracker
|
||||||
mcpHub?: McpHub
|
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
|
configManager: ConfigManager
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -727,6 +731,32 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
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": {
|
case "deleteMessage": {
|
||||||
const answer = await vscode.window.showInformationMessage(
|
const answer = await vscode.window.showInformationMessage(
|
||||||
"What would you like to delete?",
|
"What would you like to delete?",
|
||||||
@@ -797,16 +827,28 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.updateGlobalState("screenshotQuality", message.value)
|
await this.updateGlobalState("screenshotQuality", message.value)
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
|
case "enhancementApiConfigId":
|
||||||
|
await this.updateGlobalState("enhancementApiConfigId", message.text)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
break
|
||||||
case "enhancePrompt":
|
case "enhancePrompt":
|
||||||
if (message.text) {
|
if (message.text) {
|
||||||
try {
|
try {
|
||||||
const { apiConfiguration } = await this.getState()
|
const { apiConfiguration, customPrompts, listApiConfigMeta, enhancementApiConfigId } = await this.getState()
|
||||||
const enhanceConfig = {
|
|
||||||
...apiConfiguration,
|
// Try to get enhancement config first, fall back to current config
|
||||||
apiProvider: "openrouter" as const,
|
let configToUse: ApiConfiguration = apiConfiguration
|
||||||
openRouterModelId: "gpt-4o",
|
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({
|
await this.postMessageToWebview({
|
||||||
type: "enhancedPrompt",
|
type: "enhancedPrompt",
|
||||||
text: enhancedPrompt
|
text: enhancedPrompt
|
||||||
@@ -814,11 +856,37 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error enhancing prompt:", error)
|
console.error("Error enhancing prompt:", error)
|
||||||
vscode.window.showErrorMessage("Failed to enhance prompt")
|
vscode.window.showErrorMessage("Failed to enhance prompt")
|
||||||
|
await this.postMessageToWebview({
|
||||||
|
type: "enhancedPrompt"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
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 fullPrompt = await SYSTEM_PROMPT(
|
||||||
|
cwd,
|
||||||
|
apiConfiguration.openRouterModelInfo?.supportsComputerUse ?? false,
|
||||||
|
mcpEnabled ? this.mcpHub : undefined,
|
||||||
|
undefined,
|
||||||
|
browserViewportSize ?? "900x600",
|
||||||
|
message.mode,
|
||||||
|
customPrompts
|
||||||
|
) + await addCustomInstructions(customInstructions ?? '', cwd, preferredLanguage)
|
||||||
|
|
||||||
|
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": {
|
case "searchCommits": {
|
||||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||||
if (cwd) {
|
if (cwd) {
|
||||||
@@ -1482,6 +1550,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
currentApiConfigName,
|
currentApiConfigName,
|
||||||
listApiConfigMeta,
|
listApiConfigMeta,
|
||||||
mode,
|
mode,
|
||||||
|
customPrompts,
|
||||||
|
enhancementApiConfigId,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
const allowedCommands = vscode.workspace
|
const allowedCommands = vscode.workspace
|
||||||
@@ -1500,11 +1570,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
uriScheme: vscode.env.uriScheme,
|
uriScheme: vscode.env.uriScheme,
|
||||||
clineMessages: this.cline?.clineMessages || [],
|
clineMessages: this.cline?.clineMessages || [],
|
||||||
taskHistory: (taskHistory || [])
|
taskHistory: (taskHistory || [])
|
||||||
.filter((item) => item.ts && item.task)
|
.filter((item: HistoryItem) => item.ts && item.task)
|
||||||
.sort((a, b) => b.ts - a.ts),
|
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
|
||||||
soundEnabled: soundEnabled ?? false,
|
soundEnabled: soundEnabled ?? false,
|
||||||
diffEnabled: diffEnabled ?? true,
|
diffEnabled: diffEnabled ?? true,
|
||||||
shouldShowAnnouncement: false, // lastShownAnnouncementId !== this.latestAnnouncementId,
|
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
||||||
allowedCommands,
|
allowedCommands,
|
||||||
soundVolume: soundVolume ?? 0.5,
|
soundVolume: soundVolume ?? 0.5,
|
||||||
browserViewportSize: browserViewportSize ?? "900x600",
|
browserViewportSize: browserViewportSize ?? "900x600",
|
||||||
@@ -1519,6 +1589,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
currentApiConfigName: currentApiConfigName ?? "default",
|
currentApiConfigName: currentApiConfigName ?? "default",
|
||||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||||
mode: mode ?? codeMode,
|
mode: mode ?? codeMode,
|
||||||
|
customPrompts: customPrompts ?? {},
|
||||||
|
enhancementApiConfigId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1630,6 +1702,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
listApiConfigMeta,
|
listApiConfigMeta,
|
||||||
mode,
|
mode,
|
||||||
modeApiConfigs,
|
modeApiConfigs,
|
||||||
|
customPrompts,
|
||||||
|
enhancementApiConfigId,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||||
@@ -1686,6 +1760,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
|
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
|
||||||
this.getGlobalState("mode") as Promise<Mode | undefined>,
|
this.getGlobalState("mode") as Promise<Mode | undefined>,
|
||||||
this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | undefined>,
|
this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | undefined>,
|
||||||
|
this.getGlobalState("customPrompts") as Promise<CustomPrompts | undefined>,
|
||||||
|
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
|
||||||
])
|
])
|
||||||
|
|
||||||
let apiProvider: ApiProvider
|
let apiProvider: ApiProvider
|
||||||
@@ -1786,6 +1862,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
currentApiConfigName: currentApiConfigName ?? "default",
|
currentApiConfigName: currentApiConfigName ?? "default",
|
||||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||||
modeApiConfigs: modeApiConfigs ?? {} as Record<Mode, string>,
|
modeApiConfigs: modeApiConfigs ?? {} as Record<Mode, string>,
|
||||||
|
customPrompts: customPrompts ?? {},
|
||||||
|
enhancementApiConfigId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ jest.mock('vscode', () => ({
|
|||||||
},
|
},
|
||||||
window: {
|
window: {
|
||||||
showInformationMessage: jest.fn(),
|
showInformationMessage: jest.fn(),
|
||||||
|
showErrorMessage: jest.fn(),
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
getConfiguration: jest.fn().mockReturnValue({
|
getConfiguration: jest.fn().mockReturnValue({
|
||||||
@@ -113,6 +114,13 @@ jest.mock('../../../api', () => ({
|
|||||||
buildApiHandler: jest.fn()
|
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
|
// Mock WorkspaceTracker
|
||||||
jest.mock('../../../integrations/workspace/WorkspaceTracker', () => {
|
jest.mock('../../../integrations/workspace/WorkspaceTracker', () => {
|
||||||
return jest.fn().mockImplementation(() => ({
|
return jest.fn().mockImplementation(() => ({
|
||||||
@@ -504,6 +512,106 @@ describe('ClineProvider', () => {
|
|||||||
expect(mockPostMessage).toHaveBeenCalled()
|
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('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 () => {
|
test('file content includes line numbers', async () => {
|
||||||
const { extractTextFromFile } = require('../../../integrations/misc/extract-text')
|
const { extractTextFromFile } = require('../../../integrations/misc/extract-text')
|
||||||
const result = await extractTextFromFile('test.js')
|
const result = await extractTextFromFile('test.js')
|
||||||
@@ -654,4 +762,103 @@ describe('ClineProvider', () => {
|
|||||||
expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled()
|
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('returns empty prompt for enhance mode', async () => {
|
||||||
|
const enhanceHandler = getMessageHandler();
|
||||||
|
await enhanceHandler({ type: 'getSystemPrompt', mode: 'enhance' })
|
||||||
|
|
||||||
|
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'systemPrompt',
|
||||||
|
text: ''
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("roo-cline.promptsButtonClicked", () => {
|
||||||
|
sidebarProvider.postMessageToWebview({ type: "action", action: "promptsButtonClicked" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const openClineInNewTab = async () => {
|
const openClineInNewTab = async () => {
|
||||||
outputChannel.appendLine("Opening Cline in new tab")
|
outputChannel.appendLine("Opening Cline in new tab")
|
||||||
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
|
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "./api"
|
|||||||
import { HistoryItem } from "./HistoryItem"
|
import { HistoryItem } from "./HistoryItem"
|
||||||
import { McpServer } from "./mcp"
|
import { McpServer } from "./mcp"
|
||||||
import { GitCommit } from "../utils/git"
|
import { GitCommit } from "../utils/git"
|
||||||
import { Mode } from "../core/prompts/types"
|
import { Mode, CustomPrompts } from "./modes"
|
||||||
|
|
||||||
// webview will hold state
|
// webview will hold state
|
||||||
export interface ExtensionMessage {
|
export interface ExtensionMessage {
|
||||||
@@ -25,12 +25,15 @@ export interface ExtensionMessage {
|
|||||||
| "enhancedPrompt"
|
| "enhancedPrompt"
|
||||||
| "commitSearchResults"
|
| "commitSearchResults"
|
||||||
| "listApiConfig"
|
| "listApiConfig"
|
||||||
|
| "updatePrompt"
|
||||||
|
| "systemPrompt"
|
||||||
text?: string
|
text?: string
|
||||||
action?:
|
action?:
|
||||||
| "chatButtonClicked"
|
| "chatButtonClicked"
|
||||||
| "mcpButtonClicked"
|
| "mcpButtonClicked"
|
||||||
| "settingsButtonClicked"
|
| "settingsButtonClicked"
|
||||||
| "historyButtonClicked"
|
| "historyButtonClicked"
|
||||||
|
| "promptsButtonClicked"
|
||||||
| "didBecomeVisible"
|
| "didBecomeVisible"
|
||||||
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||||
state?: ExtensionState
|
state?: ExtensionState
|
||||||
@@ -45,6 +48,7 @@ export interface ExtensionMessage {
|
|||||||
mcpServers?: McpServer[]
|
mcpServers?: McpServer[]
|
||||||
commits?: GitCommit[]
|
commits?: GitCommit[]
|
||||||
listApiConfig?: ApiConfigMeta[]
|
listApiConfig?: ApiConfigMeta[]
|
||||||
|
mode?: Mode | 'enhance'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiConfigMeta {
|
export interface ApiConfigMeta {
|
||||||
@@ -62,6 +66,7 @@ export interface ExtensionState {
|
|||||||
currentApiConfigName?: string
|
currentApiConfigName?: string
|
||||||
listApiConfigMeta?: ApiConfigMeta[]
|
listApiConfigMeta?: ApiConfigMeta[]
|
||||||
customInstructions?: string
|
customInstructions?: string
|
||||||
|
customPrompts?: CustomPrompts
|
||||||
alwaysAllowReadOnly?: boolean
|
alwaysAllowReadOnly?: boolean
|
||||||
alwaysAllowWrite?: boolean
|
alwaysAllowWrite?: boolean
|
||||||
alwaysAllowExecute?: boolean
|
alwaysAllowExecute?: boolean
|
||||||
@@ -82,7 +87,8 @@ export interface ExtensionState {
|
|||||||
terminalOutputLineLimit?: number
|
terminalOutputLineLimit?: number
|
||||||
mcpEnabled: boolean
|
mcpEnabled: boolean
|
||||||
mode: Mode
|
mode: Mode
|
||||||
modeApiConfigs?: Record<Mode, string>;
|
modeApiConfigs?: Record<Mode, string>
|
||||||
|
enhancementApiConfigId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClineMessage {
|
export interface ClineMessage {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { ApiConfiguration, ApiProvider } from "./api"
|
import { ApiConfiguration, ApiProvider } from "./api"
|
||||||
|
import { Mode } from "./modes"
|
||||||
|
|
||||||
|
export type PromptMode = Mode | 'enhance'
|
||||||
|
|
||||||
export type AudioType = "notification" | "celebration" | "progress_loop"
|
export type AudioType = "notification" | "celebration" | "progress_loop"
|
||||||
|
|
||||||
@@ -62,6 +65,10 @@ export interface WebviewMessage {
|
|||||||
| "requestDelaySeconds"
|
| "requestDelaySeconds"
|
||||||
| "setApiConfigPassword"
|
| "setApiConfigPassword"
|
||||||
| "mode"
|
| "mode"
|
||||||
|
| "updatePrompt"
|
||||||
|
| "getSystemPrompt"
|
||||||
|
| "systemPrompt"
|
||||||
|
| "enhancementApiConfigId"
|
||||||
text?: string
|
text?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
askResponse?: ClineAskResponse
|
askResponse?: ClineAskResponse
|
||||||
@@ -74,6 +81,9 @@ export interface WebviewMessage {
|
|||||||
serverName?: string
|
serverName?: string
|
||||||
toolName?: string
|
toolName?: string
|
||||||
alwaysAllow?: boolean
|
alwaysAllow?: boolean
|
||||||
|
mode?: Mode
|
||||||
|
promptMode?: PromptMode
|
||||||
|
customPrompt?: string
|
||||||
dataUrls?: string[]
|
dataUrls?: string[]
|
||||||
values?: Record<string, any>
|
values?: Record<string, any>
|
||||||
query?: string
|
query?: string
|
||||||
|
|||||||
@@ -3,3 +3,17 @@ export const architectMode = 'architect' as const;
|
|||||||
export const askMode = 'ask' as const;
|
export const askMode = 'ask' as const;
|
||||||
|
|
||||||
export type Mode = typeof codeMode | typeof architectMode | typeof askMode;
|
export type Mode = typeof codeMode | typeof architectMode | typeof askMode;
|
||||||
|
|
||||||
|
export type CustomPrompts = {
|
||||||
|
ask?: string;
|
||||||
|
code?: string;
|
||||||
|
architect?: string;
|
||||||
|
enhance?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultPrompts = {
|
||||||
|
[askMode]: "You are Cline, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources while maintaining a read-only approach to the codebase. Make sure to answer the user's questions and don't rush to switch to implementing code.",
|
||||||
|
[codeMode]: "You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
|
||||||
|
[architectMode]: "You are Cline, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements while maintaining a read-only approach to the codebase. Make sure to help the user come up with a solid implementation plan for their project and don't rush to switch to implementing code.",
|
||||||
|
enhance: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):"
|
||||||
|
} as const;
|
||||||
@@ -1,80 +1,126 @@
|
|||||||
import { enhancePrompt } from '../enhance-prompt'
|
import { enhancePrompt } from '../enhance-prompt'
|
||||||
import { buildApiHandler } from '../../api'
|
|
||||||
import { ApiConfiguration } from '../../shared/api'
|
import { ApiConfiguration } from '../../shared/api'
|
||||||
import { OpenRouterHandler } from '../../api/providers/openrouter'
|
import { buildApiHandler, SingleCompletionHandler } from '../../api'
|
||||||
|
import { defaultPrompts } from '../../shared/modes'
|
||||||
|
|
||||||
// Mock the buildApiHandler function
|
// Mock the API handler
|
||||||
jest.mock('../../api', () => ({
|
jest.mock('../../api', () => ({
|
||||||
buildApiHandler: jest.fn()
|
buildApiHandler: jest.fn()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe('enhancePrompt', () => {
|
describe('enhancePrompt', () => {
|
||||||
const mockApiConfig: ApiConfiguration = {
|
const mockApiConfig: ApiConfiguration = {
|
||||||
|
apiProvider: 'openai',
|
||||||
|
openAiApiKey: 'test-key',
|
||||||
|
openAiBaseUrl: 'https://api.openai.com/v1'
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
// Mock the API handler with a completePrompt method
|
||||||
|
;(buildApiHandler as jest.Mock).mockReturnValue({
|
||||||
|
completePrompt: jest.fn().mockResolvedValue('Enhanced prompt'),
|
||||||
|
createMessage: jest.fn(),
|
||||||
|
getModel: jest.fn().mockReturnValue({
|
||||||
|
id: 'test-model',
|
||||||
|
info: {
|
||||||
|
maxTokens: 4096,
|
||||||
|
contextWindow: 8192,
|
||||||
|
supportsPromptCache: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} as unknown as SingleCompletionHandler)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('enhances prompt using default enhancement prompt when no custom prompt provided', async () => {
|
||||||
|
const result = await enhancePrompt(mockApiConfig, 'Test prompt')
|
||||||
|
|
||||||
|
expect(result).toBe('Enhanced prompt')
|
||||||
|
const handler = buildApiHandler(mockApiConfig)
|
||||||
|
expect((handler as any).completePrompt).toHaveBeenCalledWith(
|
||||||
|
`${defaultPrompts.enhance}\n\nTest prompt`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('enhances prompt using custom enhancement prompt when provided', async () => {
|
||||||
|
const customEnhancePrompt = 'You are a custom prompt enhancer'
|
||||||
|
|
||||||
|
const result = await enhancePrompt(mockApiConfig, 'Test prompt', customEnhancePrompt)
|
||||||
|
|
||||||
|
expect(result).toBe('Enhanced prompt')
|
||||||
|
const handler = buildApiHandler(mockApiConfig)
|
||||||
|
expect((handler as any).completePrompt).toHaveBeenCalledWith(
|
||||||
|
`${customEnhancePrompt}\n\nTest prompt`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws error for empty prompt input', async () => {
|
||||||
|
await expect(enhancePrompt(mockApiConfig, '')).rejects.toThrow('No prompt text provided')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws error for missing API configuration', async () => {
|
||||||
|
await expect(enhancePrompt({} as ApiConfiguration, 'Test prompt')).rejects.toThrow('No valid API configuration provided')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws error for API provider that does not support prompt enhancement', async () => {
|
||||||
|
(buildApiHandler as jest.Mock).mockReturnValue({
|
||||||
|
// No completePrompt method
|
||||||
|
createMessage: jest.fn(),
|
||||||
|
getModel: jest.fn().mockReturnValue({
|
||||||
|
id: 'test-model',
|
||||||
|
info: {
|
||||||
|
maxTokens: 4096,
|
||||||
|
contextWindow: 8192,
|
||||||
|
supportsPromptCache: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(enhancePrompt(mockApiConfig, 'Test prompt')).rejects.toThrow('The selected API provider does not support prompt enhancement')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses appropriate model based on provider', async () => {
|
||||||
|
const openRouterConfig: ApiConfiguration = {
|
||||||
apiProvider: 'openrouter',
|
apiProvider: 'openrouter',
|
||||||
apiKey: 'test-key',
|
|
||||||
openRouterApiKey: 'test-key',
|
openRouterApiKey: 'test-key',
|
||||||
openRouterModelId: 'test-model'
|
openRouterModelId: 'test-model'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a mock handler that looks like OpenRouterHandler
|
// Mock successful enhancement
|
||||||
const mockHandler = {
|
;(buildApiHandler as jest.Mock).mockReturnValue({
|
||||||
completePrompt: jest.fn(),
|
completePrompt: jest.fn().mockResolvedValue('Enhanced prompt'),
|
||||||
createMessage: jest.fn(),
|
createMessage: jest.fn(),
|
||||||
getModel: jest.fn()
|
getModel: jest.fn().mockReturnValue({
|
||||||
|
id: 'test-model',
|
||||||
|
info: {
|
||||||
|
maxTokens: 4096,
|
||||||
|
contextWindow: 8192,
|
||||||
|
supportsPromptCache: false
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
} as unknown as SingleCompletionHandler)
|
||||||
|
|
||||||
// Make instanceof check work
|
const result = await enhancePrompt(openRouterConfig, 'Test prompt')
|
||||||
Object.setPrototypeOf(mockHandler, OpenRouterHandler.prototype)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
expect(buildApiHandler).toHaveBeenCalledWith(openRouterConfig)
|
||||||
jest.clearAllMocks()
|
expect(result).toBe('Enhanced prompt')
|
||||||
;(buildApiHandler as jest.Mock).mockReturnValue(mockHandler)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error for non-OpenRouter providers', async () => {
|
it('propagates API errors', async () => {
|
||||||
const nonOpenRouterConfig: ApiConfiguration = {
|
(buildApiHandler as jest.Mock).mockReturnValue({
|
||||||
apiProvider: 'anthropic',
|
completePrompt: jest.fn().mockRejectedValue(new Error('API Error')),
|
||||||
apiKey: 'test-key',
|
createMessage: jest.fn(),
|
||||||
apiModelId: 'claude-3'
|
getModel: jest.fn().mockReturnValue({
|
||||||
|
id: 'test-model',
|
||||||
|
info: {
|
||||||
|
maxTokens: 4096,
|
||||||
|
contextWindow: 8192,
|
||||||
|
supportsPromptCache: false
|
||||||
}
|
}
|
||||||
await expect(enhancePrompt(nonOpenRouterConfig, 'test')).rejects.toThrow('Prompt enhancement is only available with OpenRouter')
|
|
||||||
})
|
})
|
||||||
|
} as unknown as SingleCompletionHandler)
|
||||||
|
|
||||||
it('should enhance a valid prompt', async () => {
|
await expect(enhancePrompt(mockApiConfig, 'Test prompt')).rejects.toThrow('API Error')
|
||||||
const inputPrompt = 'Write a function to sort an array'
|
|
||||||
const enhancedPrompt = 'Write a TypeScript function that implements an efficient sorting algorithm for a generic array, including error handling and type safety'
|
|
||||||
|
|
||||||
mockHandler.completePrompt.mockResolvedValue(enhancedPrompt)
|
|
||||||
|
|
||||||
const result = await enhancePrompt(mockApiConfig, inputPrompt)
|
|
||||||
|
|
||||||
expect(result).toBe(enhancedPrompt)
|
|
||||||
expect(buildApiHandler).toHaveBeenCalledWith(mockApiConfig)
|
|
||||||
expect(mockHandler.completePrompt).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining(inputPrompt)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error when no prompt text is provided', async () => {
|
|
||||||
await expect(enhancePrompt(mockApiConfig, '')).rejects.toThrow('No prompt text provided')
|
|
||||||
expect(mockHandler.completePrompt).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should pass through API errors', async () => {
|
|
||||||
const inputPrompt = 'Test prompt'
|
|
||||||
mockHandler.completePrompt.mockRejectedValue('API error')
|
|
||||||
|
|
||||||
await expect(enhancePrompt(mockApiConfig, inputPrompt)).rejects.toBe('API error')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should pass the correct prompt format to the API', async () => {
|
|
||||||
const inputPrompt = 'Test prompt'
|
|
||||||
mockHandler.completePrompt.mockResolvedValue('Enhanced test prompt')
|
|
||||||
|
|
||||||
await enhancePrompt(mockApiConfig, inputPrompt)
|
|
||||||
|
|
||||||
expect(mockHandler.completePrompt).toHaveBeenCalledWith(
|
|
||||||
'Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):\n\nTest prompt'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,26 +1,27 @@
|
|||||||
import { ApiConfiguration } from "../shared/api"
|
import { ApiConfiguration } from "../shared/api"
|
||||||
import { buildApiHandler } from "../api"
|
import { buildApiHandler, SingleCompletionHandler } from "../api"
|
||||||
import { OpenRouterHandler } from "../api/providers/openrouter"
|
import { defaultPrompts } from "../shared/modes"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhances a prompt using the OpenRouter API without creating a full Cline instance or task history.
|
* Enhances a prompt using the configured API without creating a full Cline instance or task history.
|
||||||
* This is a lightweight alternative that only uses the API's completion functionality.
|
* This is a lightweight alternative that only uses the API's completion functionality.
|
||||||
*/
|
*/
|
||||||
export async function enhancePrompt(apiConfiguration: ApiConfiguration, promptText: string): Promise<string> {
|
export async function enhancePrompt(apiConfiguration: ApiConfiguration, promptText: string, enhancePrompt?: string): Promise<string> {
|
||||||
if (!promptText) {
|
if (!promptText) {
|
||||||
throw new Error("No prompt text provided")
|
throw new Error("No prompt text provided")
|
||||||
}
|
}
|
||||||
if (apiConfiguration.apiProvider !== "openrouter") {
|
if (!apiConfiguration || !apiConfiguration.apiProvider) {
|
||||||
throw new Error("Prompt enhancement is only available with OpenRouter")
|
throw new Error("No valid API configuration provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
const handler = buildApiHandler(apiConfiguration)
|
const handler = buildApiHandler(apiConfiguration)
|
||||||
|
|
||||||
// Type guard to check if handler is OpenRouterHandler
|
// Check if handler supports single completions
|
||||||
if (!(handler instanceof OpenRouterHandler)) {
|
if (!('completePrompt' in handler)) {
|
||||||
throw new Error("Expected OpenRouter handler")
|
throw new Error("The selected API provider does not support prompt enhancement")
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = `Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):\n\n${promptText}`
|
const enhancePromptText = enhancePrompt ?? defaultPrompts.enhance
|
||||||
return handler.completePrompt(prompt)
|
const prompt = `${enhancePromptText}\n\n${promptText}`
|
||||||
|
return (handler as SingleCompletionHandler).completePrompt(prompt)
|
||||||
}
|
}
|
||||||
@@ -8,12 +8,14 @@ import WelcomeView from "./components/welcome/WelcomeView"
|
|||||||
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
|
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
|
||||||
import { vscode } from "./utils/vscode"
|
import { vscode } from "./utils/vscode"
|
||||||
import McpView from "./components/mcp/McpView"
|
import McpView from "./components/mcp/McpView"
|
||||||
|
import PromptsView from "./components/prompts/PromptsView"
|
||||||
|
|
||||||
const AppContent = () => {
|
const AppContent = () => {
|
||||||
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
|
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
const [showSettings, setShowSettings] = useState(false)
|
||||||
const [showHistory, setShowHistory] = useState(false)
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
const [showMcp, setShowMcp] = useState(false)
|
const [showMcp, setShowMcp] = useState(false)
|
||||||
|
const [showPrompts, setShowPrompts] = useState(false)
|
||||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||||
|
|
||||||
const handleMessage = useCallback((e: MessageEvent) => {
|
const handleMessage = useCallback((e: MessageEvent) => {
|
||||||
@@ -25,21 +27,31 @@ const AppContent = () => {
|
|||||||
setShowSettings(true)
|
setShowSettings(true)
|
||||||
setShowHistory(false)
|
setShowHistory(false)
|
||||||
setShowMcp(false)
|
setShowMcp(false)
|
||||||
|
setShowPrompts(false)
|
||||||
break
|
break
|
||||||
case "historyButtonClicked":
|
case "historyButtonClicked":
|
||||||
setShowSettings(false)
|
setShowSettings(false)
|
||||||
setShowHistory(true)
|
setShowHistory(true)
|
||||||
setShowMcp(false)
|
setShowMcp(false)
|
||||||
|
setShowPrompts(false)
|
||||||
break
|
break
|
||||||
case "mcpButtonClicked":
|
case "mcpButtonClicked":
|
||||||
setShowSettings(false)
|
setShowSettings(false)
|
||||||
setShowHistory(false)
|
setShowHistory(false)
|
||||||
setShowMcp(true)
|
setShowMcp(true)
|
||||||
|
setShowPrompts(false)
|
||||||
|
break
|
||||||
|
case "promptsButtonClicked":
|
||||||
|
setShowSettings(false)
|
||||||
|
setShowHistory(false)
|
||||||
|
setShowMcp(false)
|
||||||
|
setShowPrompts(true)
|
||||||
break
|
break
|
||||||
case "chatButtonClicked":
|
case "chatButtonClicked":
|
||||||
setShowSettings(false)
|
setShowSettings(false)
|
||||||
setShowHistory(false)
|
setShowHistory(false)
|
||||||
setShowMcp(false)
|
setShowMcp(false)
|
||||||
|
setShowPrompts(false)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -68,14 +80,16 @@ const AppContent = () => {
|
|||||||
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
|
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
|
||||||
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
|
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
|
||||||
{showMcp && <McpView onDone={() => setShowMcp(false)} />}
|
{showMcp && <McpView onDone={() => setShowMcp(false)} />}
|
||||||
|
{showPrompts && <PromptsView onDone={() => setShowPrompts(false)} />}
|
||||||
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
|
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
|
||||||
<ChatView
|
<ChatView
|
||||||
showHistoryView={() => {
|
showHistoryView={() => {
|
||||||
setShowSettings(false)
|
setShowSettings(false)
|
||||||
setShowMcp(false)
|
setShowMcp(false)
|
||||||
|
setShowPrompts(false)
|
||||||
setShowHistory(true)
|
setShowHistory(true)
|
||||||
}}
|
}}
|
||||||
isHidden={showSettings || showHistory || showMcp}
|
isHidden={showSettings || showHistory || showMcp || showPrompts}
|
||||||
showAnnouncement={showAnnouncement}
|
showAnnouncement={showAnnouncement}
|
||||||
hideAnnouncement={() => {
|
hideAnnouncement={() => {
|
||||||
setShowAnnouncement(false)
|
setShowAnnouncement(false)
|
||||||
|
|||||||
@@ -29,100 +29,39 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
|||||||
style={{ position: "absolute", top: "8px", right: "8px" }}>
|
style={{ position: "absolute", top: "8px", right: "8px" }}>
|
||||||
<span className="codicon codicon-close"></span>
|
<span className="codicon codicon-close"></span>
|
||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
|
<h2 style={{ margin: "0 0 8px" }}>
|
||||||
|
🎉{" "}Introducing Roo Cline v{minorVersion}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<h3 style={{ margin: "0 0 8px" }}>
|
<h3 style={{ margin: "0 0 8px" }}>
|
||||||
🎉{" "}New in Cline v{minorVersion}
|
Agent Modes Customization
|
||||||
</h3>
|
</h3>
|
||||||
<p style={{ margin: "5px 0px", fontWeight: "bold" }}>Add custom tools to Cline using MCP!</p>
|
|
||||||
<p style={{ margin: "5px 0px" }}>
|
<p style={{ margin: "5px 0px" }}>
|
||||||
The Model Context Protocol allows agents like Cline to plug and play custom tools,{" "}
|
Click the new <span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> icon in the menu bar to open the Prompts Settings and customize Agent Modes for new levels of productivity.
|
||||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
|
||||||
e.g. a web-search tool or GitHub tool.
|
|
||||||
</VSCodeLink>
|
|
||||||
</p>
|
|
||||||
<p style={{ margin: "5px 0px" }}>
|
|
||||||
You can add and configure MCP servers by clicking the new{" "}
|
|
||||||
<span className="codicon codicon-server" style={{ fontSize: "10px" }}></span> icon in the menu bar.
|
|
||||||
</p>
|
|
||||||
<p style={{ margin: "5px 0px" }}>
|
|
||||||
To take things a step further, Cline also has the ability to create custom tools for himself. Just say
|
|
||||||
"add a tool that..." and watch as he builds and installs new capabilities specific to{" "}
|
|
||||||
<i>your workflow</i>. For example:
|
|
||||||
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
||||||
<li>"...fetches Jira tickets": Get ticket ACs and put Cline to work</li>
|
<li>Tailor how Roo Cline behaves in different modes: Code, Architect, and Ask.</li>
|
||||||
<li>"...manages AWS EC2s": Check server metrics and scale up or down</li>
|
<li>Preview and verify your changes using the Preview System Prompt button.</li>
|
||||||
<li>"...pulls PagerDuty incidents": Pulls details to help Cline fix bugs</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
Cline handles everything from creating the MCP server to installing it in the extension, ready to use in
|
|
||||||
future tasks. The servers are saved to <code>~/Documents/Cline/MCP</code> so you can easily share them
|
|
||||||
with others too.{" "}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3 style={{ margin: "0 0 8px" }}>
|
||||||
|
Prompt Enhancement Configuration
|
||||||
|
</h3>
|
||||||
<p style={{ margin: "5px 0px" }}>
|
<p style={{ margin: "5px 0px" }}>
|
||||||
Try it yourself by asking Cline to "add a tool that gets the latest npm docs", or
|
Now available for all providers! Access it directly in the chat box by clicking the <span className="codicon codicon-sparkle" style={{ fontSize: "10px" }}></span> sparkle icon next to the input field. From there, you can customize the enhancement logic and provider to best suit your workflow.
|
||||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
||||||
see a demo of MCP in action here.
|
<li>Customize how prompts are enhanced for better results in your workflow.</li>
|
||||||
</VSCodeLink>
|
<li>Use the sparkle icon in the chat box to select a API configuration and provider (e.g., GPT-4) and configure your own enhancement logic.</li>
|
||||||
|
<li>Test your changes instantly with the Preview Prompt Enhancement tool.</li>
|
||||||
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
{/*<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
|
||||||
<li>
|
<p style={{ margin: "5px 0px" }}>
|
||||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
We're very excited to see what you build with this new feature! Join us at
|
||||||
so I recommend trying them out.
|
<VSCodeLink href="https://www.reddit.com/r/roocline" style={{ display: "inline" }}>
|
||||||
<br />
|
reddit.com/r/roocline
|
||||||
{!apiConfiguration?.openRouterApiKey && (
|
|
||||||
<VSCodeButtonLink
|
|
||||||
href={getOpenRouterAuthUrl(vscodeUriScheme)}
|
|
||||||
style={{
|
|
||||||
transform: "scale(0.85)",
|
|
||||||
transformOrigin: "left center",
|
|
||||||
margin: "4px -30px 2px 0",
|
|
||||||
}}>
|
|
||||||
Get OpenRouter API Key
|
|
||||||
</VSCodeButtonLink>
|
|
||||||
)}
|
|
||||||
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
|
|
||||||
<VSCodeButton
|
|
||||||
onClick={() => {
|
|
||||||
vscode.postMessage({
|
|
||||||
type: "apiConfiguration",
|
|
||||||
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
transform: "scale(0.85)",
|
|
||||||
transformOrigin: "left center",
|
|
||||||
margin: "4px -30px 2px 0",
|
|
||||||
}}>
|
|
||||||
Switch to OpenRouter
|
|
||||||
</VSCodeButton>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
|
|
||||||
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
|
|
||||||
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
|
|
||||||
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
When Cline runs commands, you can now type directly in the terminal (+ support for Python
|
|
||||||
environments)
|
|
||||||
</li>
|
|
||||||
</ul>*/}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: "1px",
|
|
||||||
background: "var(--vscode-foreground)",
|
|
||||||
opacity: 0.1,
|
|
||||||
margin: "8px 0",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p style={{ margin: "0" }}>
|
|
||||||
Join
|
|
||||||
<VSCodeLink style={{ display: "inline" }} href="https://discord.gg/cline">
|
|
||||||
discord.gg/cline
|
|
||||||
</VSCodeLink>
|
</VSCodeLink>
|
||||||
for more updates!
|
to discuss and share feedback.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { filePaths, apiConfiguration, currentApiConfigName, listApiConfigMeta } = useExtensionState()
|
const { filePaths, currentApiConfigName, listApiConfigMeta } = useExtensionState()
|
||||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||||
const [gitCommits, setGitCommits] = useState<any[]>([])
|
const [gitCommits, setGitCommits] = useState<any[]>([])
|
||||||
const [showDropdown, setShowDropdown] = useState(false)
|
const [showDropdown, setShowDropdown] = useState(false)
|
||||||
@@ -69,8 +69,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const messageHandler = (event: MessageEvent) => {
|
const messageHandler = (event: MessageEvent) => {
|
||||||
const message = event.data
|
const message = event.data
|
||||||
if (message.type === 'enhancedPrompt' && message.text) {
|
if (message.type === 'enhancedPrompt') {
|
||||||
|
if (message.text) {
|
||||||
setInputValue(message.text)
|
setInputValue(message.text)
|
||||||
|
}
|
||||||
setIsEnhancingPrompt(false)
|
setIsEnhancingPrompt(false)
|
||||||
} else if (message.type === 'commitSearchResults') {
|
} else if (message.type === 'commitSearchResults') {
|
||||||
const commits = message.commits.map((commit: any) => ({
|
const commits = message.commits.map((commit: any) => ({
|
||||||
@@ -767,9 +769,15 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
</div>
|
</div>
|
||||||
<div className="button-row" style={{ position: "absolute", right: 16, display: "flex", alignItems: "center", height: 31, bottom: 11, zIndex: 3, padding: "0 8px", justifyContent: "flex-end", backgroundColor: "var(--vscode-input-background)", }}>
|
<div className="button-row" style={{ position: "absolute", right: 16, display: "flex", alignItems: "center", height: 31, bottom: 11, zIndex: 3, padding: "0 8px", justifyContent: "flex-end", backgroundColor: "var(--vscode-input-background)", }}>
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||||
{apiConfiguration?.apiProvider === "openrouter" && (
|
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
{isEnhancingPrompt && <span style={{ marginRight: 10, color: "var(--vscode-input-foreground)", opacity: 0.5 }}>Enhancing prompt...</span>}
|
{isEnhancingPrompt ? (
|
||||||
|
<span className="codicon codicon-loading codicon-modifier-spin" style={{
|
||||||
|
color: "var(--vscode-input-foreground)",
|
||||||
|
opacity: 0.5,
|
||||||
|
fontSize: 16.5,
|
||||||
|
marginRight: 10
|
||||||
|
}}></span>
|
||||||
|
) : (
|
||||||
<span
|
<span
|
||||||
role="button"
|
role="button"
|
||||||
aria-label="enhance prompt"
|
aria-label="enhance prompt"
|
||||||
@@ -778,8 +786,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
onClick={() => !textAreaDisabled && handleEnhancePrompt()}
|
onClick={() => !textAreaDisabled && handleEnhancePrompt()}
|
||||||
style={{ fontSize: 16.5 }}
|
style={{ fontSize: 16.5 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<span className={`input-icon-button ${shouldDisableImages ? "disabled" : ""} codicon codicon-device-camera`} onClick={() => !shouldDisableImages && onSelectImages()} style={{ fontSize: 16.5 }} />
|
<span className={`input-icon-button ${shouldDisableImages ? "disabled" : ""} codicon codicon-device-camera`} onClick={() => !shouldDisableImages && onSelectImages()} style={{ fontSize: 16.5 }} />
|
||||||
<span className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`} onClick={() => !textAreaDisabled && onSend()} style={{ fontSize: 15 }} />
|
<span className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`} onClick={() => !textAreaDisabled && onSend()} style={{ fontSize: 15 }} />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import '@testing-library/jest-dom';
|
|||||||
import ChatTextArea from '../ChatTextArea';
|
import ChatTextArea from '../ChatTextArea';
|
||||||
import { useExtensionState } from '../../../context/ExtensionStateContext';
|
import { useExtensionState } from '../../../context/ExtensionStateContext';
|
||||||
import { vscode } from '../../../utils/vscode';
|
import { vscode } from '../../../utils/vscode';
|
||||||
|
import { codeMode } from '../../../../../src/shared/modes';
|
||||||
|
|
||||||
// Mock modules
|
// Mock modules
|
||||||
jest.mock('../../../utils/vscode', () => ({
|
jest.mock('../../../utils/vscode', () => ({
|
||||||
@@ -32,6 +33,8 @@ describe('ChatTextArea', () => {
|
|||||||
selectedImages: [],
|
selectedImages: [],
|
||||||
setSelectedImages: jest.fn(),
|
setSelectedImages: jest.fn(),
|
||||||
onHeightChange: jest.fn(),
|
onHeightChange: jest.fn(),
|
||||||
|
mode: codeMode,
|
||||||
|
setMode: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -46,37 +49,9 @@ describe('ChatTextArea', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('enhance prompt button', () => {
|
describe('enhance prompt button', () => {
|
||||||
it('should show enhance prompt button only when apiProvider is openrouter', () => {
|
|
||||||
// Test with non-openrouter provider
|
|
||||||
(useExtensionState as jest.Mock).mockReturnValue({
|
|
||||||
filePaths: [],
|
|
||||||
apiConfiguration: {
|
|
||||||
apiProvider: 'anthropic',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { rerender } = render(<ChatTextArea {...defaultProps} />);
|
|
||||||
expect(screen.queryByTestId('enhance-prompt-button')).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
// Test with openrouter provider
|
|
||||||
(useExtensionState as jest.Mock).mockReturnValue({
|
|
||||||
filePaths: [],
|
|
||||||
apiConfiguration: {
|
|
||||||
apiProvider: 'openrouter',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
rerender(<ChatTextArea {...defaultProps} />);
|
|
||||||
const enhanceButton = screen.getByRole('button', { name: /enhance prompt/i });
|
|
||||||
expect(enhanceButton).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be disabled when textAreaDisabled is true', () => {
|
it('should be disabled when textAreaDisabled is true', () => {
|
||||||
(useExtensionState as jest.Mock).mockReturnValue({
|
(useExtensionState as jest.Mock).mockReturnValue({
|
||||||
filePaths: [],
|
filePaths: [],
|
||||||
apiConfiguration: {
|
|
||||||
apiProvider: 'openrouter',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<ChatTextArea {...defaultProps} textAreaDisabled={true} />);
|
render(<ChatTextArea {...defaultProps} textAreaDisabled={true} />);
|
||||||
@@ -137,7 +112,8 @@ describe('ChatTextArea', () => {
|
|||||||
const enhanceButton = screen.getByRole('button', { name: /enhance prompt/i });
|
const enhanceButton = screen.getByRole('button', { name: /enhance prompt/i });
|
||||||
fireEvent.click(enhanceButton);
|
fireEvent.click(enhanceButton);
|
||||||
|
|
||||||
expect(screen.getByText('Enhancing prompt...')).toBeInTheDocument();
|
const loadingSpinner = screen.getByText('', { selector: '.codicon-loading' });
|
||||||
|
expect(loadingSpinner).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
344
webview-ui/src/components/prompts/PromptsView.tsx
Normal file
344
webview-ui/src/components/prompts/PromptsView.tsx
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import { VSCodeButton, VSCodeTextArea, VSCodeDropdown, VSCodeOption, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"
|
||||||
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
|
import { defaultPrompts, askMode, codeMode, architectMode, Mode } from "../../../../src/shared/modes"
|
||||||
|
import { vscode } from "../../utils/vscode"
|
||||||
|
import React, { useState, useEffect } from "react"
|
||||||
|
|
||||||
|
type PromptsViewProps = {
|
||||||
|
onDone: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PromptsView = ({ onDone }: PromptsViewProps) => {
|
||||||
|
const { customPrompts, listApiConfigMeta, enhancementApiConfigId, setEnhancementApiConfigId, mode } = useExtensionState()
|
||||||
|
const [testPrompt, setTestPrompt] = useState('')
|
||||||
|
const [isEnhancing, setIsEnhancing] = useState(false)
|
||||||
|
const [activeTab, setActiveTab] = useState<Mode>(mode)
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
|
const [selectedPromptContent, setSelectedPromptContent] = useState('')
|
||||||
|
const [selectedPromptTitle, setSelectedPromptTitle] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (event: MessageEvent) => {
|
||||||
|
const message = event.data
|
||||||
|
if (message.type === 'enhancedPrompt') {
|
||||||
|
if (message.text) {
|
||||||
|
setTestPrompt(message.text)
|
||||||
|
}
|
||||||
|
setIsEnhancing(false)
|
||||||
|
} else if (message.type === 'systemPrompt') {
|
||||||
|
if (message.text) {
|
||||||
|
setSelectedPromptContent(message.text)
|
||||||
|
setSelectedPromptTitle(`System Prompt (${message.mode} mode)`)
|
||||||
|
setIsDialogOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handler)
|
||||||
|
return () => window.removeEventListener('message', handler)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
type PromptMode = keyof typeof defaultPrompts
|
||||||
|
|
||||||
|
const updatePromptValue = (promptMode: PromptMode, value: string) => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "updatePrompt",
|
||||||
|
promptMode,
|
||||||
|
customPrompt: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePromptChange = (mode: PromptMode, e: Event | React.FormEvent<HTMLElement>) => {
|
||||||
|
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
||||||
|
updatePromptValue(mode, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = (mode: PromptMode) => {
|
||||||
|
const defaultValue = defaultPrompts[mode]
|
||||||
|
updatePromptValue(mode, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPromptValue = (mode: PromptMode): string => {
|
||||||
|
if (mode === 'enhance') {
|
||||||
|
return customPrompts?.enhance ?? defaultPrompts.enhance
|
||||||
|
}
|
||||||
|
return customPrompts?.[mode] ?? defaultPrompts[mode]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTestEnhancement = () => {
|
||||||
|
if (!testPrompt.trim()) return
|
||||||
|
|
||||||
|
setIsEnhancing(true)
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "enhancePrompt",
|
||||||
|
text: testPrompt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "10px 17px 10px 20px",
|
||||||
|
}}>
|
||||||
|
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Prompts</h3>
|
||||||
|
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
|
||||||
|
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 20px 0" }}>Agent Modes</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: "var(--vscode-foreground)",
|
||||||
|
fontSize: "13px",
|
||||||
|
marginBottom: "20px",
|
||||||
|
marginTop: "5px",
|
||||||
|
}}>
|
||||||
|
Customize Cline's prompt in each mode. The rest of the system prompt will be automatically appended. Click the button to preview the full prompt. Leave empty or click the reset button to use the default.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '12px'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
|
{[
|
||||||
|
{ id: codeMode, label: 'Code' },
|
||||||
|
{ id: architectMode, label: 'Architect' },
|
||||||
|
{ id: askMode, label: 'Ask' },
|
||||||
|
].map((tab, index) => (
|
||||||
|
<React.Fragment key={tab.id}>
|
||||||
|
<button
|
||||||
|
data-testid={`${tab.id}-tab`}
|
||||||
|
data-active={activeTab === tab.id ? "true" : "false"}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
style={{
|
||||||
|
padding: '4px 0',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
color: activeTab === tab.id ? 'var(--vscode-textLink-foreground)' : 'var(--vscode-foreground)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: activeTab === tab.id ? 1 : 0.8,
|
||||||
|
borderBottom: activeTab === tab.id ?
|
||||||
|
'1px solid var(--vscode-textLink-foreground)' :
|
||||||
|
'1px solid var(--vscode-foreground)',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
{index < 2 && (
|
||||||
|
<span style={{ color: 'var(--vscode-foreground)', opacity: 0.4 }}>|</span>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="icon"
|
||||||
|
onClick={() => handleReset(activeTab as any)}
|
||||||
|
data-testid="reset-prompt-button"
|
||||||
|
title="Revert to default"
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-discard"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
{activeTab === codeMode && (
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={getPromptValue(codeMode)}
|
||||||
|
onChange={(e) => handlePromptChange(codeMode, e)}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
data-testid="code-prompt-textarea"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === architectMode && (
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={getPromptValue(architectMode)}
|
||||||
|
onChange={(e) => handlePromptChange(architectMode, e)}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
data-testid="architect-prompt-textarea"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === askMode && (
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={getPromptValue(askMode)}
|
||||||
|
onChange={(e) => handlePromptChange(askMode, e)}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
data-testid="ask-prompt-textarea"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="primary"
|
||||||
|
onClick={() => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "getSystemPrompt",
|
||||||
|
mode: activeTab
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
data-testid="preview-prompt-button"
|
||||||
|
>
|
||||||
|
Preview System Prompt
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style={{ color: "var(--vscode-foreground)", margin: "40px 0 20px 0" }}>Prompt Enhancement</h3>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: "12px" }}>
|
||||||
|
<div style={{ marginBottom: "8px" }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>API Configuration</div>
|
||||||
|
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)" }}>
|
||||||
|
You can select an API configuration to always use for enhancing prompts, or just use whatever is currently selected
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VSCodeDropdown
|
||||||
|
value={enhancementApiConfigId || ''}
|
||||||
|
data-testid="api-config-dropdown"
|
||||||
|
onChange={(e: any) => {
|
||||||
|
const value = e.detail?.target?.value || e.target?.value
|
||||||
|
setEnhancementApiConfigId(value)
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "enhancementApiConfigId",
|
||||||
|
text: value
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
style={{ width: "300px" }}
|
||||||
|
>
|
||||||
|
<VSCodeOption value="">Use currently selected API configuration</VSCodeOption>
|
||||||
|
{(listApiConfigMeta || []).map((config) => (
|
||||||
|
<VSCodeOption key={config.id} value={config.id}>
|
||||||
|
{config.name}
|
||||||
|
</VSCodeOption>
|
||||||
|
))}
|
||||||
|
</VSCodeDropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: "8px", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<div style={{ fontWeight: "bold" }}>Enhancement Prompt</div>
|
||||||
|
<div style={{ display: "flex", gap: "8px" }}>
|
||||||
|
<VSCodeButton appearance="icon" onClick={() => handleReset('enhance')} title="Revert to default">
|
||||||
|
<span className="codicon codicon-discard"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={getPromptValue('enhance')}
|
||||||
|
onChange={(e) => handlePromptChange('enhance', e)}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginTop: "12px" }}>
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={testPrompt}
|
||||||
|
onChange={(e) => setTestPrompt((e.target as HTMLTextAreaElement).value)}
|
||||||
|
placeholder="Enter a prompt to test the enhancement"
|
||||||
|
rows={3}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
data-testid="test-prompt-textarea"
|
||||||
|
/>
|
||||||
|
<div style={{
|
||||||
|
marginTop: "8px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8
|
||||||
|
}}>
|
||||||
|
<VSCodeButton
|
||||||
|
onClick={handleTestEnhancement}
|
||||||
|
disabled={isEnhancing}
|
||||||
|
appearance="primary"
|
||||||
|
>
|
||||||
|
Preview Prompt Enhancement
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom padding */}
|
||||||
|
<div style={{ height: "20px" }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDialogOpen && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
zIndex: 1000
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: 'calc(100vw - 100px)',
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: 'var(--vscode-editor-background)',
|
||||||
|
boxShadow: '-2px 0 5px rgba(0, 0, 0, 0.2)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: '20px',
|
||||||
|
overflowY: 'auto'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '16px'
|
||||||
|
}}>
|
||||||
|
<h2 style={{ margin: 0 }}>{selectedPromptTitle}</h2>
|
||||||
|
<VSCodeButton appearance="icon" onClick={() => setIsDialogOpen(false)}>
|
||||||
|
<span className="codicon codicon-close"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
<VSCodeDivider />
|
||||||
|
<pre style={{
|
||||||
|
margin: '16px 0',
|
||||||
|
padding: '8px',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
fontFamily: 'var(--vscode-editor-font-family)',
|
||||||
|
fontSize: 'var(--vscode-editor-font-size)',
|
||||||
|
color: 'var(--vscode-editor-foreground)',
|
||||||
|
backgroundColor: 'var(--vscode-editor-background)',
|
||||||
|
border: '1px solid var(--vscode-editor-lineHighlightBorder)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto'
|
||||||
|
}}>
|
||||||
|
{selectedPromptContent}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PromptsView
|
||||||
135
webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx
Normal file
135
webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import PromptsView from '../PromptsView'
|
||||||
|
import { ExtensionStateContext } from '../../../context/ExtensionStateContext'
|
||||||
|
import { vscode } from '../../../utils/vscode'
|
||||||
|
import { defaultPrompts } from '../../../../../src/shared/modes'
|
||||||
|
|
||||||
|
// Mock vscode API
|
||||||
|
jest.mock('../../../utils/vscode', () => ({
|
||||||
|
vscode: {
|
||||||
|
postMessage: jest.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockExtensionState = {
|
||||||
|
customPrompts: {},
|
||||||
|
listApiConfigMeta: [
|
||||||
|
{ id: 'config1', name: 'Config 1' },
|
||||||
|
{ id: 'config2', name: 'Config 2' }
|
||||||
|
],
|
||||||
|
enhancementApiConfigId: '',
|
||||||
|
setEnhancementApiConfigId: jest.fn(),
|
||||||
|
mode: 'code'
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPromptsView = (props = {}) => {
|
||||||
|
const mockOnDone = jest.fn()
|
||||||
|
return render(
|
||||||
|
<ExtensionStateContext.Provider value={{ ...mockExtensionState, ...props } as any}>
|
||||||
|
<PromptsView onDone={mockOnDone} />
|
||||||
|
</ExtensionStateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PromptsView', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders all mode tabs', () => {
|
||||||
|
renderPromptsView()
|
||||||
|
expect(screen.getByTestId('code-tab')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('ask-tab')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('architect-tab')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults to current mode as active tab', () => {
|
||||||
|
renderPromptsView({ mode: 'ask' })
|
||||||
|
|
||||||
|
const codeTab = screen.getByTestId('code-tab')
|
||||||
|
const askTab = screen.getByTestId('ask-tab')
|
||||||
|
const architectTab = screen.getByTestId('architect-tab')
|
||||||
|
|
||||||
|
expect(askTab).toHaveAttribute('data-active', 'true')
|
||||||
|
expect(codeTab).toHaveAttribute('data-active', 'false')
|
||||||
|
expect(architectTab).toHaveAttribute('data-active', 'false')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches between tabs correctly', () => {
|
||||||
|
renderPromptsView({ mode: 'code' })
|
||||||
|
|
||||||
|
const codeTab = screen.getByTestId('code-tab')
|
||||||
|
const askTab = screen.getByTestId('ask-tab')
|
||||||
|
const architectTab = screen.getByTestId('architect-tab')
|
||||||
|
|
||||||
|
// Initial state matches current mode (code)
|
||||||
|
expect(codeTab).toHaveAttribute('data-active', 'true')
|
||||||
|
expect(askTab).toHaveAttribute('data-active', 'false')
|
||||||
|
expect(architectTab).toHaveAttribute('data-active', 'false')
|
||||||
|
expect(architectTab).toHaveAttribute('data-active', 'false')
|
||||||
|
|
||||||
|
// Click Ask tab
|
||||||
|
fireEvent.click(askTab)
|
||||||
|
expect(askTab).toHaveAttribute('data-active', 'true')
|
||||||
|
expect(codeTab).toHaveAttribute('data-active', 'false')
|
||||||
|
expect(architectTab).toHaveAttribute('data-active', 'false')
|
||||||
|
|
||||||
|
// Click Architect tab
|
||||||
|
fireEvent.click(architectTab)
|
||||||
|
expect(architectTab).toHaveAttribute('data-active', 'true')
|
||||||
|
expect(askTab).toHaveAttribute('data-active', 'false')
|
||||||
|
expect(codeTab).toHaveAttribute('data-active', 'false')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles prompt changes correctly', () => {
|
||||||
|
renderPromptsView()
|
||||||
|
|
||||||
|
const textarea = screen.getByTestId('code-prompt-textarea')
|
||||||
|
fireEvent(textarea, new CustomEvent('change', {
|
||||||
|
detail: {
|
||||||
|
target: {
|
||||||
|
value: 'New prompt value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'updatePrompt',
|
||||||
|
promptMode: 'code',
|
||||||
|
customPrompt: 'New prompt value'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resets prompt to default value', () => {
|
||||||
|
renderPromptsView()
|
||||||
|
|
||||||
|
const resetButton = screen.getByTestId('reset-prompt-button')
|
||||||
|
fireEvent.click(resetButton)
|
||||||
|
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'updatePrompt',
|
||||||
|
promptMode: 'code',
|
||||||
|
customPrompt: defaultPrompts.code
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles API configuration selection', () => {
|
||||||
|
renderPromptsView()
|
||||||
|
|
||||||
|
const dropdown = screen.getByTestId('api-config-dropdown')
|
||||||
|
fireEvent(dropdown, new CustomEvent('change', {
|
||||||
|
detail: {
|
||||||
|
target: {
|
||||||
|
value: 'config1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
expect(mockExtensionState.setEnhancementApiConfigId).toHaveBeenCalledWith('config1')
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'enhancementApiConfigId',
|
||||||
|
text: 'config1'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
checkExistKey
|
checkExistKey
|
||||||
} from "../../../src/shared/checkExistApiConfig"
|
} from "../../../src/shared/checkExistApiConfig"
|
||||||
import { Mode } from "../../../src/core/prompts/types"
|
import { Mode } from "../../../src/core/prompts/types"
|
||||||
import { codeMode } from "../../../src/shared/modes"
|
import { codeMode, CustomPrompts } from "../../../src/shared/modes"
|
||||||
|
|
||||||
export interface ExtensionStateContextType extends ExtensionState {
|
export interface ExtensionStateContextType extends ExtensionState {
|
||||||
didHydrateState: boolean
|
didHydrateState: boolean
|
||||||
@@ -60,6 +60,9 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||||||
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
|
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
|
||||||
mode: Mode
|
mode: Mode
|
||||||
setMode: (value: Mode) => void
|
setMode: (value: Mode) => void
|
||||||
|
setCustomPrompts: (value: CustomPrompts) => void
|
||||||
|
enhancementApiConfigId?: string
|
||||||
|
setEnhancementApiConfigId: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||||
@@ -86,6 +89,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
currentApiConfigName: 'default',
|
currentApiConfigName: 'default',
|
||||||
listApiConfigMeta: [],
|
listApiConfigMeta: [],
|
||||||
mode: codeMode,
|
mode: codeMode,
|
||||||
|
customPrompts: {},
|
||||||
|
enhancementApiConfigId: '',
|
||||||
})
|
})
|
||||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||||
const [showWelcome, setShowWelcome] = useState(false)
|
const [showWelcome, setShowWelcome] = useState(false)
|
||||||
@@ -230,6 +235,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
setListApiConfigMeta,
|
setListApiConfigMeta,
|
||||||
onUpdateApiConfig,
|
onUpdateApiConfig,
|
||||||
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
|
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
|
||||||
|
setCustomPrompts: (value) => setState((prevState) => ({ ...prevState, customPrompts: value })),
|
||||||
|
setEnhancementApiConfigId: (value) => setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||||
|
|||||||
Reference in New Issue
Block a user