mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
@@ -8,12 +8,14 @@ import WelcomeView from "./components/welcome/WelcomeView"
|
||||
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
|
||||
import { vscode } from "./utils/vscode"
|
||||
import McpView from "./components/mcp/McpView"
|
||||
import PromptsView from "./components/prompts/PromptsView"
|
||||
|
||||
const AppContent = () => {
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showMcp, setShowMcp] = useState(false)
|
||||
const [showPrompts, setShowPrompts] = useState(false)
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
|
||||
const handleMessage = useCallback((e: MessageEvent) => {
|
||||
@@ -25,21 +27,31 @@ const AppContent = () => {
|
||||
setShowSettings(true)
|
||||
setShowHistory(false)
|
||||
setShowMcp(false)
|
||||
setShowPrompts(false)
|
||||
break
|
||||
case "historyButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(true)
|
||||
setShowMcp(false)
|
||||
setShowPrompts(false)
|
||||
break
|
||||
case "mcpButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
setShowMcp(true)
|
||||
setShowPrompts(false)
|
||||
break
|
||||
case "promptsButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
setShowMcp(false)
|
||||
setShowPrompts(true)
|
||||
break
|
||||
case "chatButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
setShowMcp(false)
|
||||
setShowPrompts(false)
|
||||
break
|
||||
}
|
||||
break
|
||||
@@ -68,14 +80,16 @@ const AppContent = () => {
|
||||
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
|
||||
{showHistory && <HistoryView onDone={() => setShowHistory(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.) */}
|
||||
<ChatView
|
||||
showHistoryView={() => {
|
||||
setShowSettings(false)
|
||||
setShowMcp(false)
|
||||
setShowPrompts(false)
|
||||
setShowHistory(true)
|
||||
}}
|
||||
isHidden={showSettings || showHistory || showMcp}
|
||||
isHidden={showSettings || showHistory || showMcp || showPrompts}
|
||||
showAnnouncement={showAnnouncement}
|
||||
hideAnnouncement={() => {
|
||||
setShowAnnouncement(false)
|
||||
|
||||
@@ -29,100 +29,39 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
style={{ position: "absolute", top: "8px", right: "8px" }}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h2 style={{ margin: "0 0 8px" }}>
|
||||
🎉{" "}Introducing Roo Cline v{minorVersion}
|
||||
</h2>
|
||||
|
||||
<h3 style={{ margin: "0 0 8px" }}>
|
||||
🎉{" "}New in Cline v{minorVersion}
|
||||
Agent Modes Customization
|
||||
</h3>
|
||||
<p style={{ margin: "5px 0px", fontWeight: "bold" }}>Add custom tools to Cline using MCP!</p>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
The Model Context Protocol allows agents like Cline to plug and play custom tools,{" "}
|
||||
<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:
|
||||
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.
|
||||
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
||||
<li>"...fetches Jira tickets": Get ticket ACs and put Cline to work</li>
|
||||
<li>"...manages AWS EC2s": Check server metrics and scale up or down</li>
|
||||
<li>"...pulls PagerDuty incidents": Pulls details to help Cline fix bugs</li>
|
||||
<li>Tailor how Roo Cline behaves in different modes: Code, Architect, and Ask.</li>
|
||||
<li>Preview and verify your changes using the Preview System Prompt button.</li>
|
||||
</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>
|
||||
|
||||
<h3 style={{ margin: "0 0 8px" }}>
|
||||
Prompt Enhancement Configuration
|
||||
</h3>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
Try it yourself by asking Cline to "add a tool that gets the latest npm docs", or
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
||||
see a demo of MCP in action here.
|
||||
</VSCodeLink>
|
||||
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.
|
||||
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
||||
<li>Customize how prompts are enhanced for better results in your workflow.</li>
|
||||
<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>
|
||||
{/*<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
<li>
|
||||
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
|
||||
so I recommend trying them out.
|
||||
<br />
|
||||
{!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
|
||||
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
We're very excited to see what you build with this new feature! Join us at
|
||||
<VSCodeLink href="https://www.reddit.com/r/roocline" style={{ display: "inline" }}>
|
||||
reddit.com/r/roocline
|
||||
</VSCodeLink>
|
||||
for more updates!
|
||||
to discuss and share feedback.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { filePaths, apiConfiguration, currentApiConfigName, listApiConfigMeta } = useExtensionState()
|
||||
const { filePaths, currentApiConfigName, listApiConfigMeta } = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [gitCommits, setGitCommits] = useState<any[]>([])
|
||||
const [showDropdown, setShowDropdown] = useState(false)
|
||||
@@ -69,8 +69,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
useEffect(() => {
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === 'enhancedPrompt' && message.text) {
|
||||
setInputValue(message.text)
|
||||
if (message.type === 'enhancedPrompt') {
|
||||
if (message.text) {
|
||||
setInputValue(message.text)
|
||||
}
|
||||
setIsEnhancingPrompt(false)
|
||||
} else if (message.type === 'commitSearchResults') {
|
||||
const commits = message.commits.map((commit: any) => ({
|
||||
@@ -652,6 +654,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
|
||||
borderColor: "transparent",
|
||||
padding: "9px 9px 25px 9px",
|
||||
marginBottom: "15px",
|
||||
cursor: textAreaDisabled ? "not-allowed" : undefined,
|
||||
flex: 1,
|
||||
zIndex: 1,
|
||||
@@ -766,19 +769,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
</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)", }}>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
{apiConfiguration?.apiProvider === "openrouter" && (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
{isEnhancingPrompt && <span style={{ marginRight: 10, color: "var(--vscode-input-foreground)", opacity: 0.5 }}>Enhancing prompt...</span>}
|
||||
<span
|
||||
role="button"
|
||||
aria-label="enhance prompt"
|
||||
data-testid="enhance-prompt-button"
|
||||
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-sparkle`}
|
||||
onClick={() => !textAreaDisabled && handleEnhancePrompt()}
|
||||
style={{ fontSize: 16.5 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
{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
|
||||
role="button"
|
||||
aria-label="enhance prompt"
|
||||
data-testid="enhance-prompt-button"
|
||||
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-sparkle`}
|
||||
onClick={() => !textAreaDisabled && handleEnhancePrompt()}
|
||||
style={{ fontSize: 16.5 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -3,6 +3,7 @@ import '@testing-library/jest-dom';
|
||||
import ChatTextArea from '../ChatTextArea';
|
||||
import { useExtensionState } from '../../../context/ExtensionStateContext';
|
||||
import { vscode } from '../../../utils/vscode';
|
||||
import { codeMode } from '../../../../../src/shared/modes';
|
||||
|
||||
// Mock modules
|
||||
jest.mock('../../../utils/vscode', () => ({
|
||||
@@ -32,6 +33,8 @@ describe('ChatTextArea', () => {
|
||||
selectedImages: [],
|
||||
setSelectedImages: jest.fn(),
|
||||
onHeightChange: jest.fn(),
|
||||
mode: codeMode,
|
||||
setMode: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -46,37 +49,9 @@ describe('ChatTextArea', () => {
|
||||
});
|
||||
|
||||
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', () => {
|
||||
(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter',
|
||||
},
|
||||
});
|
||||
|
||||
render(<ChatTextArea {...defaultProps} textAreaDisabled={true} />);
|
||||
@@ -137,7 +112,8 @@ describe('ChatTextArea', () => {
|
||||
const enhanceButton = screen.getByRole('button', { name: /enhance prompt/i });
|
||||
fireEvent.click(enhanceButton);
|
||||
|
||||
expect(screen.getByText('Enhancing prompt...')).toBeInTheDocument();
|
||||
const loadingSpinner = screen.getByText('', { selector: '.codicon-loading' });
|
||||
expect(loadingSpinner).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
477
webview-ui/src/components/prompts/PromptsView.tsx
Normal file
477
webview-ui/src/components/prompts/PromptsView.tsx
Normal file
@@ -0,0 +1,477 @@
|
||||
import { VSCodeButton, VSCodeTextArea, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { defaultPrompts, askMode, codeMode, architectMode, Mode, PromptComponent } from "../../../../src/shared/modes"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import React, { useState, useEffect } from "react"
|
||||
|
||||
type PromptsViewProps = {
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
const AGENT_MODES = [
|
||||
{ id: codeMode, label: 'Code' },
|
||||
{ id: architectMode, label: 'Architect' },
|
||||
{ id: askMode, label: 'Ask' },
|
||||
] as const
|
||||
|
||||
const PromptsView = ({ onDone }: PromptsViewProps) => {
|
||||
const {
|
||||
customPrompts,
|
||||
listApiConfigMeta,
|
||||
enhancementApiConfigId,
|
||||
setEnhancementApiConfigId,
|
||||
mode,
|
||||
customInstructions
|
||||
} = 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 AgentMode = typeof codeMode | typeof architectMode | typeof askMode
|
||||
|
||||
const updateAgentPrompt = (mode: AgentMode, promptData: PromptComponent) => {
|
||||
const updatedPrompt = {
|
||||
...customPrompts?.[mode],
|
||||
...promptData
|
||||
}
|
||||
|
||||
// Only include properties that differ from defaults
|
||||
if (updatedPrompt.roleDefinition === defaultPrompts[mode].roleDefinition) {
|
||||
delete updatedPrompt.roleDefinition
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "updatePrompt",
|
||||
promptMode: mode,
|
||||
customPrompt: updatedPrompt
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
updateAgentPrompt(mode, { roleDefinition: value.trim() || undefined })
|
||||
}
|
||||
|
||||
const handleEnhancePromptChange = (e: Event | React.FormEvent<HTMLElement>) => {
|
||||
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
||||
const trimmedValue = value.trim()
|
||||
if (trimmedValue !== defaultPrompts.enhance) {
|
||||
updateEnhancePrompt(trimmedValue || undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAgentReset = (mode: AgentMode) => {
|
||||
updateAgentPrompt(mode, {
|
||||
...customPrompts?.[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
|
||||
}
|
||||
|
||||
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" }}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Custom Instructions for All Modes</div>
|
||||
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>
|
||||
These instructions apply to all modes. They provide a base set of behaviors that can be enhanced by mode-specific instructions below.
|
||||
</div>
|
||||
<VSCodeTextArea
|
||||
value={customInstructions ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
||||
vscode.postMessage({
|
||||
type: "customInstructions",
|
||||
text: value.trim() || undefined
|
||||
})
|
||||
}}
|
||||
rows={4}
|
||||
resize="vertical"
|
||||
style={{ width: "100%" }}
|
||||
data-testid="global-custom-instructions-textarea"
|
||||
/>
|
||||
<div style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", marginTop: "5px" }}>
|
||||
Instructions can also be loaded from <span
|
||||
style={{
|
||||
color: 'var(--vscode-textLink-foreground)',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
onClick={() => vscode.postMessage({
|
||||
type: "openFile",
|
||||
text: "./.clinerules",
|
||||
values: {
|
||||
create: true,
|
||||
content: "",
|
||||
}
|
||||
})}
|
||||
>.clinerules</span> in your workspace.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 20px 0" }}>Mode-Specific Prompts</h3>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
alignItems: 'center',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
{AGENT_MODES.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
data-testid={`${tab.id}-tab`}
|
||||
data-active={activeTab === tab.id ? "true" : "false"}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
background: activeTab === tab.id ? 'var(--vscode-button-background)' : 'none',
|
||||
color: activeTab === tab.id ? 'var(--vscode-button-foreground)' : 'var(--vscode-foreground)',
|
||||
cursor: 'pointer',
|
||||
opacity: activeTab === tab.id ? 1 : 0.8,
|
||||
borderRadius: '3px',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: "4px"
|
||||
}}>
|
||||
<div style={{ fontWeight: "bold" }}>Role Definition</div>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => handleAgentReset(activeTab)}
|
||||
data-testid="reset-prompt-button"
|
||||
title="Revert to default"
|
||||
>
|
||||
<span className="codicon codicon-discard"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>
|
||||
Define Cline's expertise and personality for this mode. This description shapes how Cline presents itself and approaches tasks.
|
||||
</div>
|
||||
</div>
|
||||
<VSCodeTextArea
|
||||
value={getAgentPromptValue(activeTab)}
|
||||
onChange={(e) => handleAgentPromptChange(activeTab, e)}
|
||||
rows={4}
|
||||
resize="vertical"
|
||||
style={{ width: "100%" }}
|
||||
data-testid={`${activeTab}-prompt-textarea`}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Mode-specific Custom Instructions</div>
|
||||
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>
|
||||
Add behavioral guidelines specific to {activeTab} mode. These instructions enhance the base behaviors defined above.
|
||||
</div>
|
||||
<VSCodeTextArea
|
||||
value={customPrompts?.[activeTab]?.customInstructions ?? ''}
|
||||
onChange={(e) => {
|
||||
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
||||
updateAgentPrompt(activeTab, {
|
||||
...customPrompts?.[activeTab],
|
||||
customInstructions: value.trim() || undefined
|
||||
})
|
||||
}}
|
||||
rows={4}
|
||||
resize="vertical"
|
||||
style={{ width: "100%" }}
|
||||
data-testid={`${activeTab}-custom-instructions-textarea`}
|
||||
/>
|
||||
<div style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", marginTop: "5px" }}>
|
||||
Custom instructions specific to {activeTab} mode can also be loaded from <span
|
||||
style={{
|
||||
color: 'var(--vscode-textLink-foreground)',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
onClick={() => {
|
||||
// First create/update the file with current custom instructions
|
||||
const defaultContent = `# ${activeTab} Mode Rules\n\nAdd mode-specific rules and guidelines here.`
|
||||
vscode.postMessage({
|
||||
type: "updatePrompt",
|
||||
promptMode: activeTab,
|
||||
customPrompt: {
|
||||
...customPrompts?.[activeTab],
|
||||
customInstructions: customPrompts?.[activeTab]?.customInstructions || defaultContent
|
||||
}
|
||||
})
|
||||
// Then open the file
|
||||
vscode.postMessage({
|
||||
type: "openFile",
|
||||
text: `./.clinerules-${activeTab}`,
|
||||
values: {
|
||||
create: true,
|
||||
content: "",
|
||||
}
|
||||
})
|
||||
}}
|
||||
>.clinerules-{activeTab}</span> in your workspace.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'flex-start' }}>
|
||||
<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={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "13px",
|
||||
marginBottom: "20px",
|
||||
marginTop: "5px",
|
||||
}}>
|
||||
Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Cline understands your intent and provides the best possible responses.
|
||||
</div>
|
||||
|
||||
<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" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "4px" }}>
|
||||
<div style={{ fontWeight: "bold" }}>Enhancement Prompt</div>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<VSCodeButton appearance="icon" onClick={handleEnhanceReset} title="Revert to default">
|
||||
<span className="codicon codicon-discard"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>
|
||||
This prompt will be used to refine your input when you hit the sparkle icon in chat.
|
||||
</div>
|
||||
</div>
|
||||
<VSCodeTextArea
|
||||
value={getEnhancePromptValue()}
|
||||
onChange={handleEnhancePromptChange}
|
||||
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-start",
|
||||
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',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
flex: 1,
|
||||
padding: '20px',
|
||||
overflowY: 'auto',
|
||||
minHeight: 0
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '20px',
|
||||
right: '20px'
|
||||
}}
|
||||
>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h2 style={{ margin: '0 0 16px' }}>{selectedPromptTitle}</h2>
|
||||
<pre style={{
|
||||
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',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{selectedPromptContent}
|
||||
</pre>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '12px 20px',
|
||||
borderTop: '1px solid var(--vscode-editor-lineHighlightBorder)',
|
||||
backgroundColor: 'var(--vscode-editor-background)'
|
||||
}}>
|
||||
<VSCodeButton onClick={() => setIsDialogOpen(false)}>
|
||||
Close
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptsView
|
||||
134
webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx
Normal file
134
webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
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'
|
||||
|
||||
// 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: { roleDefinition: '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: { roleDefinition: undefined }
|
||||
})
|
||||
})
|
||||
|
||||
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'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -278,24 +278,26 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<VSCodeTextArea
|
||||
value={customInstructions ?? ""}
|
||||
style={{ width: "100%" }}
|
||||
rows={4}
|
||||
placeholder={
|
||||
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
|
||||
}
|
||||
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<span style={{ fontWeight: "500" }}>Custom Instructions</span>
|
||||
</VSCodeTextArea>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
These instructions are added to the end of the system prompt sent with every request. Custom instructions set in .clinerules and .cursorrules in the working directory are also included.
|
||||
</p>
|
||||
<VSCodeTextArea
|
||||
value={customInstructions ?? ""}
|
||||
style={{ width: "100%" }}
|
||||
rows={4}
|
||||
placeholder={
|
||||
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
|
||||
}
|
||||
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
These instructions are added to the end of the system prompt sent with every request. Custom instructions set in .clinerules in the working directory are also included. For mode-specific instructions, use the <span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> Prompts tab in the top menu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<McpEnabledToggle />
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
checkExistKey
|
||||
} from "../../../src/shared/checkExistApiConfig"
|
||||
import { Mode } from "../../../src/core/prompts/types"
|
||||
import { codeMode } from "../../../src/shared/modes"
|
||||
import { codeMode, CustomPrompts, defaultPrompts } from "../../../src/shared/modes"
|
||||
|
||||
export interface ExtensionStateContextType extends ExtensionState {
|
||||
didHydrateState: boolean
|
||||
@@ -60,6 +60,9 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
|
||||
mode: Mode
|
||||
setMode: (value: Mode) => void
|
||||
setCustomPrompts: (value: CustomPrompts) => void
|
||||
enhancementApiConfigId?: string
|
||||
setEnhancementApiConfigId: (value: string) => void
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
@@ -86,6 +89,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
currentApiConfigName: 'default',
|
||||
listApiConfigMeta: [],
|
||||
mode: codeMode,
|
||||
customPrompts: defaultPrompts,
|
||||
enhancementApiConfigId: '',
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
@@ -230,6 +235,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
setListApiConfigMeta,
|
||||
onUpdateApiConfig,
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user