Add a screen for custom prompts

This commit is contained in:
Matt Rubens
2025-01-13 03:16:10 -05:00
parent 4027e1c10c
commit 75e308b033
21 changed files with 1044 additions and 238 deletions

View File

@@ -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)

View File

@@ -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>
)

View File

@@ -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) => ({
@@ -767,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>

View File

@@ -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();
});
});

View 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

View 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'
})
})
})

View File

@@ -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 } 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: {},
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>