Refactor to support more sections in the future

This commit is contained in:
Matt Rubens
2025-01-14 07:55:16 -05:00
parent 75e308b033
commit 092a121a37
12 changed files with 116 additions and 95 deletions

View File

@@ -1,4 +1,4 @@
import { architectMode, defaultPrompts } from "../../shared/modes" import { architectMode, defaultPrompts, PromptComponent } from "../../shared/modes"
import { getToolDescriptionsForMode } from "./tools" import { getToolDescriptionsForMode } from "./tools"
import { import {
getRulesSection, getRulesSection,
@@ -20,8 +20,8 @@ export const ARCHITECT_PROMPT = async (
mcpHub?: McpHub, mcpHub?: McpHub,
diffStrategy?: DiffStrategy, diffStrategy?: DiffStrategy,
browserViewportSize?: string, browserViewportSize?: string,
customPrompt?: string, customPrompt?: PromptComponent,
) => `${customPrompt || defaultPrompts[architectMode]} ) => `${customPrompt?.roleDefinition || defaultPrompts[architectMode].roleDefinition}
${getSharedToolUseSection()} ${getSharedToolUseSection()}

View File

@@ -1,4 +1,4 @@
import { Mode, askMode, defaultPrompts } from "../../shared/modes" import { Mode, askMode, defaultPrompts, PromptComponent } from "../../shared/modes"
import { getToolDescriptionsForMode } from "./tools" import { getToolDescriptionsForMode } from "./tools"
import { import {
getRulesSection, getRulesSection,
@@ -21,8 +21,8 @@ export const ASK_PROMPT = async (
mcpHub?: McpHub, mcpHub?: McpHub,
diffStrategy?: DiffStrategy, diffStrategy?: DiffStrategy,
browserViewportSize?: string, browserViewportSize?: string,
customPrompt?: string, customPrompt?: PromptComponent,
) => `${customPrompt || defaultPrompts[askMode]} ) => `${customPrompt?.roleDefinition || defaultPrompts[askMode].roleDefinition}
${getSharedToolUseSection()} ${getSharedToolUseSection()}

View File

@@ -1,4 +1,4 @@
import { Mode, codeMode, defaultPrompts } from "../../shared/modes" import { Mode, codeMode, defaultPrompts, PromptComponent } from "../../shared/modes"
import { getToolDescriptionsForMode } from "./tools" import { getToolDescriptionsForMode } from "./tools"
import { import {
getRulesSection, getRulesSection,
@@ -21,8 +21,8 @@ export const CODE_PROMPT = async (
mcpHub?: McpHub, mcpHub?: McpHub,
diffStrategy?: DiffStrategy, diffStrategy?: DiffStrategy,
browserViewportSize?: string, browserViewportSize?: string,
customPrompt?: string, customPrompt?: PromptComponent,
) => `${customPrompt || defaultPrompts[codeMode]} ) => `${customPrompt?.roleDefinition || defaultPrompts[codeMode].roleDefinition}
${getSharedToolUseSection()} ${getSharedToolUseSection()}

View File

@@ -4,6 +4,7 @@ import { CODE_PROMPT } from "./code"
import { ARCHITECT_PROMPT } from "./architect" import { ARCHITECT_PROMPT } from "./architect"
import { ASK_PROMPT } from "./ask" import { ASK_PROMPT } from "./ask"
import { Mode, codeMode, architectMode, askMode } from "./modes" import { Mode, codeMode, architectMode, askMode } from "./modes"
import { CustomPrompts } from "../../shared/modes"
import fs from 'fs/promises' import fs from 'fs/promises'
import path from 'path' import path from 'path'
@@ -64,7 +65,7 @@ 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 }, customPrompts?: CustomPrompts,
) => { ) => {
switch (mode) { switch (mode) {
case architectMode: case architectMode:

View File

@@ -16,7 +16,7 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array" 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, PromptMode } from "../../shared/WebviewMessage"
import { defaultPrompts } from "../../shared/modes" import { defaultPrompts } from "../../shared/modes"
import { SYSTEM_PROMPT, addCustomInstructions } from "../prompts/system" import { SYSTEM_PROMPT, addCustomInstructions } from "../prompts/system"
import { fileExistsAtPath } from "../../utils/fs" import { fileExistsAtPath } from "../../utils/fs"
@@ -731,6 +731,32 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview() await this.postStateToWebview()
break break
case "updateEnhancedPrompt":
if (message.text !== undefined) {
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": case "updatePrompt":
if (message.promptMode && message.customPrompt !== undefined) { if (message.promptMode && message.customPrompt !== undefined) {
const existingPrompts = await this.getGlobalState("customPrompts") || {} const existingPrompts = await this.getGlobalState("customPrompts") || {}

View File

@@ -838,18 +838,6 @@ describe('ClineProvider', () => {
); );
}); });
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 () => { test('handles errors gracefully', async () => {
// Mock SYSTEM_PROMPT to throw an error // Mock SYSTEM_PROMPT to throw an error
const systemPrompt = require('../../prompts/system') const systemPrompt = require('../../prompts/system')

View File

@@ -48,7 +48,7 @@ export interface ExtensionMessage {
mcpServers?: McpServer[] mcpServers?: McpServer[]
commits?: GitCommit[] commits?: GitCommit[]
listApiConfig?: ApiConfigMeta[] listApiConfig?: ApiConfigMeta[]
mode?: Mode | 'enhance' mode?: Mode
} }
export interface ApiConfigMeta { export interface ApiConfigMeta {

View File

@@ -1,5 +1,5 @@
import { ApiConfiguration, ApiProvider } from "./api" import { ApiConfiguration, ApiProvider } from "./api"
import { Mode } from "./modes" import { Mode, PromptComponent } from "./modes"
export type PromptMode = Mode | 'enhance' export type PromptMode = Mode | 'enhance'
@@ -66,6 +66,7 @@ export interface WebviewMessage {
| "setApiConfigPassword" | "setApiConfigPassword"
| "mode" | "mode"
| "updatePrompt" | "updatePrompt"
| "updateEnhancedPrompt"
| "getSystemPrompt" | "getSystemPrompt"
| "systemPrompt" | "systemPrompt"
| "enhancementApiConfigId" | "enhancementApiConfigId"
@@ -83,7 +84,7 @@ export interface WebviewMessage {
alwaysAllow?: boolean alwaysAllow?: boolean
mode?: Mode mode?: Mode
promptMode?: PromptMode promptMode?: PromptMode
customPrompt?: string customPrompt?: PromptComponent
dataUrls?: string[] dataUrls?: string[]
values?: Record<string, any> values?: Record<string, any>
query?: string query?: string

View File

@@ -4,16 +4,26 @@ 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 PromptComponent = {
roleDefinition?: string;
}
export type CustomPrompts = { export type CustomPrompts = {
ask?: string; ask?: PromptComponent;
code?: string; code?: PromptComponent;
architect?: string; architect?: PromptComponent;
enhance?: string; enhance?: string;
} }
export const defaultPrompts = { 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.", [askMode]: {
[codeMode]: "You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.", roleDefinition: "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.",
[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.", },
[codeMode]: {
roleDefinition: "You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
},
[architectMode]: {
roleDefinition: "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):" 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; } as const;

View File

@@ -1,6 +1,6 @@
import { VSCodeButton, VSCodeTextArea, VSCodeDropdown, VSCodeOption, VSCodeDivider } from "@vscode/webview-ui-toolkit/react" import { VSCodeButton, VSCodeTextArea, VSCodeDropdown, VSCodeOption, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "../../context/ExtensionStateContext" import { useExtensionState } from "../../context/ExtensionStateContext"
import { defaultPrompts, askMode, codeMode, architectMode, Mode } from "../../../../src/shared/modes" import { defaultPrompts, askMode, codeMode, architectMode, Mode, PromptComponent } from "../../../../src/shared/modes"
import { vscode } from "../../utils/vscode" import { vscode } from "../../utils/vscode"
import React, { useState, useEffect } from "react" import React, { useState, useEffect } from "react"
@@ -8,6 +8,12 @@ type PromptsViewProps = {
onDone: () => void onDone: () => void
} }
const AGENT_MODES = [
{ id: codeMode, label: 'Code' },
{ id: architectMode, label: 'Architect' },
{ id: askMode, label: 'Ask' },
] as const
const PromptsView = ({ onDone }: PromptsViewProps) => { const PromptsView = ({ onDone }: PromptsViewProps) => {
const { customPrompts, listApiConfigMeta, enhancementApiConfigId, setEnhancementApiConfigId, mode } = useExtensionState() const { customPrompts, listApiConfigMeta, enhancementApiConfigId, setEnhancementApiConfigId, mode } = useExtensionState()
const [testPrompt, setTestPrompt] = useState('') const [testPrompt, setTestPrompt] = useState('')
@@ -38,32 +44,48 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
return () => window.removeEventListener('message', handler) return () => window.removeEventListener('message', handler)
}, []) }, [])
type PromptMode = keyof typeof defaultPrompts type AgentMode = typeof codeMode | typeof architectMode | typeof askMode
const updatePromptValue = (promptMode: PromptMode, value: string) => { const updateAgentPrompt = (mode: AgentMode, promptData: PromptComponent) => {
vscode.postMessage({ vscode.postMessage({
type: "updatePrompt", type: "updatePrompt",
promptMode, promptMode: mode,
customPrompt: value customPrompt: promptData
}) })
} }
const handlePromptChange = (mode: PromptMode, e: Event | React.FormEvent<HTMLElement>) => { const updateEnhancePrompt = (value: string | undefined) => {
vscode.postMessage({
type: "updateEnhancedPrompt",
text: value
})
}
const handleAgentPromptChange = (mode: AgentMode, e: Event | React.FormEvent<HTMLElement>) => {
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
updatePromptValue(mode, value) updateAgentPrompt(mode, { roleDefinition: value.trim() || undefined })
} }
const handleReset = (mode: PromptMode) => { const handleEnhancePromptChange = (e: Event | React.FormEvent<HTMLElement>) => {
const defaultValue = defaultPrompts[mode] const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
updatePromptValue(mode, defaultValue) updateEnhancePrompt(value.trim() || undefined)
} }
const getPromptValue = (mode: PromptMode): string => { const handleAgentReset = (mode: AgentMode) => {
if (mode === 'enhance') { updateAgentPrompt(mode, { roleDefinition: undefined })
}
const handleEnhanceReset = () => {
updateEnhancePrompt(undefined)
}
const getAgentPromptValue = (mode: AgentMode): string => {
return customPrompts?.[mode]?.roleDefinition ?? defaultPrompts[mode].roleDefinition
}
const getEnhancePromptValue = (): string => {
return customPrompts?.enhance ?? defaultPrompts.enhance return customPrompts?.enhance ?? defaultPrompts.enhance
} }
return customPrompts?.[mode] ?? defaultPrompts[mode]
}
const handleTestEnhancement = () => { const handleTestEnhancement = () => {
if (!testPrompt.trim()) return if (!testPrompt.trim()) return
@@ -117,11 +139,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
marginBottom: '12px' marginBottom: '12px'
}}> }}>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}> <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{[ {AGENT_MODES.map((tab, index) => (
{ id: codeMode, label: 'Code' },
{ id: architectMode, label: 'Architect' },
{ id: askMode, label: 'Ask' },
].map((tab, index) => (
<React.Fragment key={tab.id}> <React.Fragment key={tab.id}>
<button <button
data-testid={`${tab.id}-tab`} data-testid={`${tab.id}-tab`}
@@ -142,7 +160,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
> >
{tab.label} {tab.label}
</button> </button>
{index < 2 && ( {index < AGENT_MODES.length - 1 && (
<span style={{ color: 'var(--vscode-foreground)', opacity: 0.4 }}>|</span> <span style={{ color: 'var(--vscode-foreground)', opacity: 0.4 }}>|</span>
)} )}
</React.Fragment> </React.Fragment>
@@ -150,7 +168,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div> </div>
<VSCodeButton <VSCodeButton
appearance="icon" appearance="icon"
onClick={() => handleReset(activeTab as any)} onClick={() => handleAgentReset(activeTab)}
data-testid="reset-prompt-button" data-testid="reset-prompt-button"
title="Revert to default" title="Revert to default"
> >
@@ -159,38 +177,16 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div> </div>
<div style={{ marginBottom: '8px' }}> <div style={{ marginBottom: '8px' }}>
{activeTab === codeMode && (
<VSCodeTextArea <VSCodeTextArea
value={getPromptValue(codeMode)} value={getAgentPromptValue(activeTab)}
onChange={(e) => handlePromptChange(codeMode, e)} onChange={(e) => handleAgentPromptChange(activeTab, e)}
rows={4} rows={4}
resize="vertical" resize="vertical"
style={{ width: "100%" }} style={{ width: "100%" }}
data-testid="code-prompt-textarea" data-testid={`${activeTab}-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>
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'flex-end' }}> <div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'flex-start' }}>
<VSCodeButton <VSCodeButton
appearance="primary" appearance="primary"
onClick={() => { onClick={() => {
@@ -241,14 +237,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div style={{ marginBottom: "8px", display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ marginBottom: "8px", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ fontWeight: "bold" }}>Enhancement Prompt</div> <div style={{ fontWeight: "bold" }}>Enhancement Prompt</div>
<div style={{ display: "flex", gap: "8px" }}> <div style={{ display: "flex", gap: "8px" }}>
<VSCodeButton appearance="icon" onClick={() => handleReset('enhance')} title="Revert to default"> <VSCodeButton appearance="icon" onClick={handleEnhanceReset} title="Revert to default">
<span className="codicon codicon-discard"></span> <span className="codicon codicon-discard"></span>
</VSCodeButton> </VSCodeButton>
</div> </div>
</div> </div>
<VSCodeTextArea <VSCodeTextArea
value={getPromptValue('enhance')} value={getEnhancePromptValue()}
onChange={(e) => handlePromptChange('enhance', e)} onChange={handleEnhancePromptChange}
rows={4} rows={4}
resize="vertical" resize="vertical"
style={{ width: "100%" }} style={{ width: "100%" }}
@@ -267,7 +263,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div style={{ <div style={{
marginTop: "8px", marginTop: "8px",
display: "flex", display: "flex",
justifyContent: "flex-end", justifyContent: "flex-start",
alignItems: "center", alignItems: "center",
gap: 8 gap: 8
}}> }}>

View File

@@ -3,7 +3,6 @@ import '@testing-library/jest-dom'
import PromptsView from '../PromptsView' import PromptsView from '../PromptsView'
import { ExtensionStateContext } from '../../../context/ExtensionStateContext' import { ExtensionStateContext } from '../../../context/ExtensionStateContext'
import { vscode } from '../../../utils/vscode' import { vscode } from '../../../utils/vscode'
import { defaultPrompts } from '../../../../../src/shared/modes'
// Mock vscode API // Mock vscode API
jest.mock('../../../utils/vscode', () => ({ jest.mock('../../../utils/vscode', () => ({
@@ -97,7 +96,7 @@ describe('PromptsView', () => {
expect(vscode.postMessage).toHaveBeenCalledWith({ expect(vscode.postMessage).toHaveBeenCalledWith({
type: 'updatePrompt', type: 'updatePrompt',
promptMode: 'code', promptMode: 'code',
customPrompt: 'New prompt value' customPrompt: { roleDefinition: 'New prompt value' }
}) })
}) })
@@ -110,7 +109,7 @@ describe('PromptsView', () => {
expect(vscode.postMessage).toHaveBeenCalledWith({ expect(vscode.postMessage).toHaveBeenCalledWith({
type: 'updatePrompt', type: 'updatePrompt',
promptMode: 'code', promptMode: 'code',
customPrompt: defaultPrompts.code customPrompt: { roleDefinition: undefined }
}) })
}) })

View File

@@ -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, CustomPrompts } from "../../../src/shared/modes" import { codeMode, CustomPrompts, defaultPrompts } from "../../../src/shared/modes"
export interface ExtensionStateContextType extends ExtensionState { export interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean didHydrateState: boolean
@@ -89,7 +89,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
currentApiConfigName: 'default', currentApiConfigName: 'default',
listApiConfigMeta: [], listApiConfigMeta: [],
mode: codeMode, mode: codeMode,
customPrompts: {}, customPrompts: defaultPrompts,
enhancementApiConfigId: '', enhancementApiConfigId: '',
}) })
const [didHydrateState, setDidHydrateState] = useState(false) const [didHydrateState, setDidHydrateState] = useState(false)