mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Merge remote-tracking branch 'origin/main' into vscode-lm-provider
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { memo, useEffect, useMemo, useRef } from "react"
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useSize } from "react-use"
|
||||
import {
|
||||
ClineApiReqInfo,
|
||||
@@ -154,6 +154,8 @@ export const ChatRowContent = ({
|
||||
style={{ color: successColor, marginBottom: "-1.5px" }}></span>,
|
||||
<span style={{ color: successColor, fontWeight: "bold" }}>Task Completed</span>,
|
||||
]
|
||||
case "api_req_retry_delayed":
|
||||
return []
|
||||
case "api_req_started":
|
||||
const getIconSpan = (iconName: string, color: string) => (
|
||||
<div
|
||||
@@ -211,15 +213,7 @@ export const ChatRowContent = ({
|
||||
default:
|
||||
return [null, null]
|
||||
}
|
||||
}, [
|
||||
type,
|
||||
cost,
|
||||
apiRequestFailedMessage,
|
||||
isCommandExecuting,
|
||||
apiReqCancelReason,
|
||||
isMcpServerResponding,
|
||||
message.text,
|
||||
])
|
||||
}, [type, isCommandExecuting, message, isMcpServerResponding, apiReqCancelReason, cost, apiRequestFailedMessage])
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
@@ -557,7 +551,7 @@ export const ChatRowContent = ({
|
||||
case "text":
|
||||
return (
|
||||
<div>
|
||||
<Markdown markdown={message.text} />
|
||||
<Markdown markdown={message.text} partial={message.partial} />
|
||||
</div>
|
||||
)
|
||||
case "user_feedback":
|
||||
@@ -709,7 +703,7 @@ export const ChatRowContent = ({
|
||||
</div>
|
||||
)}
|
||||
<div style={{ paddingTop: 10 }}>
|
||||
<Markdown markdown={message.text} />
|
||||
<Markdown markdown={message.text} partial={message.partial} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@@ -882,7 +876,7 @@ export const ChatRowContent = ({
|
||||
{title}
|
||||
</div>
|
||||
<div style={{ color: "var(--vscode-charts-green)", paddingTop: 10 }}>
|
||||
<Markdown markdown={message.text} />
|
||||
<Markdown markdown={message.text} partial={message.partial} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -924,10 +918,63 @@ export const ProgressIndicator = () => (
|
||||
</div>
|
||||
)
|
||||
|
||||
const Markdown = memo(({ markdown }: { markdown?: string }) => {
|
||||
const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
return (
|
||||
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
|
||||
<MarkdownBlock markdown={markdown} />
|
||||
<div
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
|
||||
<MarkdownBlock markdown={markdown} />
|
||||
</div>
|
||||
{markdown && !partial && isHovering && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "-4px",
|
||||
right: "8px",
|
||||
opacity: 0,
|
||||
animation: "fadeIn 0.2s ease-in-out forwards",
|
||||
borderRadius: "4px"
|
||||
}}
|
||||
>
|
||||
<style>
|
||||
{`
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1.0; }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<VSCodeButton
|
||||
className="copy-button"
|
||||
appearance="icon"
|
||||
style={{
|
||||
height: "24px",
|
||||
border: "none",
|
||||
background: "var(--vscode-editor-background)",
|
||||
transition: "background 0.2s ease-in-out"
|
||||
}}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(markdown);
|
||||
// Flash the button background briefly to indicate success
|
||||
const button = document.activeElement as HTMLElement;
|
||||
if (button) {
|
||||
button.style.background = "var(--vscode-button-background)";
|
||||
setTimeout(() => {
|
||||
button.style.background = "";
|
||||
}, 200);
|
||||
}
|
||||
}}
|
||||
title="Copy as markdown"
|
||||
>
|
||||
<span className="codicon codicon-copy"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -14,6 +14,8 @@ import ContextMenu from "./ContextMenu"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
||||
import { Mode } from "../../../../src/core/prompts/types"
|
||||
import { CaretIcon } from "../common/CaretIcon"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
@@ -26,6 +28,8 @@ interface ChatTextAreaProps {
|
||||
onSelectImages: () => void
|
||||
shouldDisableImages: boolean
|
||||
onHeightChange?: (height: number) => void
|
||||
mode: Mode
|
||||
setMode: (value: Mode) => void
|
||||
}
|
||||
|
||||
const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
@@ -41,11 +45,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
onSelectImages,
|
||||
shouldDisableImages,
|
||||
onHeightChange,
|
||||
mode,
|
||||
setMode,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { filePaths, apiConfiguration, currentApiConfigName, listApiConfigMeta } = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const { filePaths, currentApiConfigName, listApiConfigMeta } = useExtensionState()
|
||||
const [gitCommits, setGitCommits] = useState<any[]>([])
|
||||
const [showDropdown, setShowDropdown] = useState(false)
|
||||
|
||||
@@ -64,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) => ({
|
||||
@@ -369,7 +376,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
if (!isMouseDownOnMenu) {
|
||||
setShowContextMenu(false)
|
||||
}
|
||||
setIsTextAreaFocused(false)
|
||||
}, [isMouseDownOnMenu])
|
||||
|
||||
const handlePaste = useCallback(
|
||||
@@ -487,65 +493,97 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
[updateCursorPosition],
|
||||
)
|
||||
|
||||
const selectStyle = {
|
||||
fontSize: "11px",
|
||||
cursor: textAreaDisabled ? "not-allowed" : "pointer",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "var(--vscode-foreground)",
|
||||
opacity: textAreaDisabled ? 0.5 : 0.8,
|
||||
outline: "none",
|
||||
paddingLeft: "20px",
|
||||
paddingRight: "6px",
|
||||
WebkitAppearance: "none" as const,
|
||||
MozAppearance: "none" as const,
|
||||
appearance: "none" as const
|
||||
}
|
||||
|
||||
const caretContainerStyle = {
|
||||
position: "absolute" as const,
|
||||
left: 6,
|
||||
top: "50%",
|
||||
transform: "translateY(-45%)",
|
||||
pointerEvents: "none" as const,
|
||||
opacity: textAreaDisabled ? 0.5 : 0.8
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: "10px 15px",
|
||||
opacity: textAreaDisabled ? 0.5 : 1,
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
}}
|
||||
onDrop={async (e) => {
|
||||
e.preventDefault()
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const text = e.dataTransfer.getData("text")
|
||||
if (text) {
|
||||
const newValue =
|
||||
inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition)
|
||||
setInputValue(newValue)
|
||||
const newCursorPosition = cursorPosition + text.length
|
||||
setCursorPosition(newCursorPosition)
|
||||
setIntendedCursorPosition(newCursorPosition)
|
||||
return
|
||||
}
|
||||
const acceptedTypes = ["png", "jpeg", "webp"]
|
||||
const imageFiles = files.filter((file) => {
|
||||
const [type, subtype] = file.type.split("/")
|
||||
return type === "image" && acceptedTypes.includes(subtype)
|
||||
})
|
||||
if (!shouldDisableImages && imageFiles.length > 0) {
|
||||
const imagePromises = imageFiles.map((file) => {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
if (reader.error) {
|
||||
console.error("Error reading file:", reader.error)
|
||||
resolve(null)
|
||||
} else {
|
||||
const result = reader.result
|
||||
resolve(typeof result === "string" ? result : null)
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
})
|
||||
const imageDataArray = await Promise.all(imagePromises)
|
||||
const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
|
||||
if (dataUrls.length > 0) {
|
||||
setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE))
|
||||
if (typeof vscode !== 'undefined') {
|
||||
vscode.postMessage({
|
||||
type: 'draggedImages',
|
||||
dataUrls: dataUrls
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.warn("No valid images were processed")
|
||||
<div
|
||||
className="chat-text-area"
|
||||
style={{
|
||||
opacity: textAreaDisabled ? 0.5 : 1,
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
backgroundColor: "var(--vscode-input-background)",
|
||||
minHeight: "100px",
|
||||
margin: "10px 15px",
|
||||
padding: "8px"
|
||||
}}
|
||||
onDrop={async (e) => {
|
||||
e.preventDefault()
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const text = e.dataTransfer.getData("text")
|
||||
if (text) {
|
||||
const newValue =
|
||||
inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition)
|
||||
setInputValue(newValue)
|
||||
const newCursorPosition = cursorPosition + text.length
|
||||
setCursorPosition(newCursorPosition)
|
||||
setIntendedCursorPosition(newCursorPosition)
|
||||
return
|
||||
}
|
||||
}
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
}}>
|
||||
const acceptedTypes = ["png", "jpeg", "webp"]
|
||||
const imageFiles = files.filter((file) => {
|
||||
const [type, subtype] = file.type.split("/")
|
||||
return type === "image" && acceptedTypes.includes(subtype)
|
||||
})
|
||||
if (!shouldDisableImages && imageFiles.length > 0) {
|
||||
const imagePromises = imageFiles.map((file) => {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
if (reader.error) {
|
||||
console.error("Error reading file:", reader.error)
|
||||
resolve(null)
|
||||
} else {
|
||||
const result = reader.result
|
||||
resolve(typeof result === "string" ? result : null)
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
})
|
||||
const imageDataArray = await Promise.all(imagePromises)
|
||||
const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
|
||||
if (dataUrls.length > 0) {
|
||||
setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE))
|
||||
if (typeof vscode !== 'undefined') {
|
||||
vscode.postMessage({
|
||||
type: 'draggedImages',
|
||||
dataUrls: dataUrls
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.warn("No valid images were processed")
|
||||
}
|
||||
}
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
{showContextMenu && (
|
||||
<div ref={contextMenuContainerRef}>
|
||||
<ContextMenu
|
||||
@@ -559,100 +597,87 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isTextAreaFocused && (
|
||||
|
||||
<div style={{
|
||||
position: "relative",
|
||||
flex: "1 1 auto",
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
minHeight: 0,
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<div
|
||||
ref={highlightLayerRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: "10px 15px",
|
||||
border: "1px solid var(--vscode-input-border)",
|
||||
borderRadius: 2,
|
||||
inset: 0,
|
||||
pointerEvents: "none",
|
||||
zIndex: 5,
|
||||
whiteSpace: "pre-wrap",
|
||||
wordWrap: "break-word",
|
||||
color: "transparent",
|
||||
overflow: "hidden",
|
||||
fontFamily: "var(--vscode-font-family)",
|
||||
fontSize: "var(--vscode-editor-font-size)",
|
||||
lineHeight: "var(--vscode-editor-line-height)",
|
||||
padding: "8px",
|
||||
marginBottom: thumbnailsHeight > 0 ? `${thumbnailsHeight + 16}px` : 0,
|
||||
zIndex: 1
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={highlightLayerRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 15,
|
||||
right: 15,
|
||||
bottom: 10,
|
||||
pointerEvents: "none",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordWrap: "break-word",
|
||||
color: "transparent",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "var(--vscode-input-background)",
|
||||
fontFamily: "var(--vscode-font-family)",
|
||||
fontSize: "var(--vscode-editor-font-size)",
|
||||
lineHeight: "var(--vscode-editor-line-height)",
|
||||
borderRadius: 2,
|
||||
borderLeft: 0,
|
||||
borderRight: 0,
|
||||
borderTop: 0,
|
||||
borderColor: "transparent",
|
||||
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
|
||||
padding: "9px 9px 25px 9px",
|
||||
}}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
ref={(el) => {
|
||||
if (typeof ref === "function") {
|
||||
ref(el)
|
||||
} else if (ref) {
|
||||
ref.current = el
|
||||
}
|
||||
textAreaRef.current = el
|
||||
}}
|
||||
value={inputValue}
|
||||
disabled={textAreaDisabled}
|
||||
onChange={(e) => {
|
||||
handleInputChange(e)
|
||||
updateHighlights()
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onFocus={() => setIsTextAreaFocused(true)}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
onSelect={updateCursorPosition}
|
||||
onMouseUp={updateCursorPosition}
|
||||
onHeightChange={(height) => {
|
||||
if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
|
||||
setTextAreaBaseHeight(height)
|
||||
}
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={placeholderText}
|
||||
minRows={2}
|
||||
maxRows={20}
|
||||
autoFocus={true}
|
||||
style={{
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--vscode-input-foreground)",
|
||||
borderRadius: 2,
|
||||
fontFamily: "var(--vscode-font-family)",
|
||||
fontSize: "var(--vscode-editor-font-size)",
|
||||
lineHeight: "var(--vscode-editor-line-height)",
|
||||
resize: "none",
|
||||
overflowX: "hidden",
|
||||
overflowY: "scroll",
|
||||
borderLeft: 0,
|
||||
borderRight: 0,
|
||||
borderTop: 0,
|
||||
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
|
||||
borderColor: "transparent",
|
||||
padding: "9px 9px 25px 9px",
|
||||
cursor: textAreaDisabled ? "not-allowed" : undefined,
|
||||
flex: 1,
|
||||
zIndex: 1,
|
||||
}}
|
||||
onScroll={() => updateHighlights()}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
ref={(el) => {
|
||||
if (typeof ref === "function") {
|
||||
ref(el)
|
||||
} else if (ref) {
|
||||
ref.current = el
|
||||
}
|
||||
textAreaRef.current = el
|
||||
}}
|
||||
value={inputValue}
|
||||
disabled={textAreaDisabled}
|
||||
onChange={(e) => {
|
||||
handleInputChange(e)
|
||||
updateHighlights()
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
onSelect={updateCursorPosition}
|
||||
onMouseUp={updateCursorPosition}
|
||||
onHeightChange={(height) => {
|
||||
if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
|
||||
setTextAreaBaseHeight(height)
|
||||
}
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={placeholderText}
|
||||
minRows={4}
|
||||
maxRows={20}
|
||||
autoFocus={true}
|
||||
style={{
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--vscode-input-foreground)",
|
||||
borderRadius: 2,
|
||||
fontFamily: "var(--vscode-font-family)",
|
||||
fontSize: "var(--vscode-editor-font-size)",
|
||||
lineHeight: "var(--vscode-editor-line-height)",
|
||||
resize: "none",
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
border: "none",
|
||||
padding: "8px",
|
||||
marginBottom: thumbnailsHeight > 0 ? `${thumbnailsHeight + 16}px` : 0,
|
||||
cursor: textAreaDisabled ? "not-allowed" : undefined,
|
||||
flex: "0 1 auto",
|
||||
zIndex: 2
|
||||
}}
|
||||
onScroll={() => updateHighlights()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedImages.length > 0 && (
|
||||
<Thumbnails
|
||||
images={selectedImages}
|
||||
@@ -660,81 +685,136 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
onHeightChange={handleThumbnailsHeightChange}
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingTop: 4,
|
||||
bottom: 32,
|
||||
left: 22,
|
||||
right: 67,
|
||||
bottom: "36px",
|
||||
left: "16px",
|
||||
zIndex: 2,
|
||||
marginBottom: "8px"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(listApiConfigMeta || []).length > 1 && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 25,
|
||||
bottom: 14,
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
<select
|
||||
value={currentApiConfigName}
|
||||
disabled={textAreaDisabled}
|
||||
onChange={(e) => vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: e.target.value
|
||||
})}
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
cursor: textAreaDisabled ? "not-allowed" : "pointer",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "var(--vscode-input-foreground)",
|
||||
opacity: textAreaDisabled ? 0.5 : 0.6,
|
||||
outline: "none",
|
||||
paddingLeft: 14,
|
||||
WebkitAppearance: "none",
|
||||
MozAppearance: "none",
|
||||
appearance: "none",
|
||||
backgroundImage: "url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\")",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "left 0px center",
|
||||
backgroundSize: "10px"
|
||||
}}
|
||||
>
|
||||
{(listApiConfigMeta || [])?.map((config) => (
|
||||
<option
|
||||
key={config.name}
|
||||
value={config.name}
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
color: "var(--vscode-dropdown-foreground)"
|
||||
}}
|
||||
>
|
||||
{config.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: "auto",
|
||||
paddingTop: "8px"
|
||||
}}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ position: "relative", display: "inline-block" }}>
|
||||
<select
|
||||
value={mode}
|
||||
disabled={textAreaDisabled}
|
||||
onChange={(e) => {
|
||||
const newMode = e.target.value as Mode
|
||||
setMode(newMode)
|
||||
vscode.postMessage({
|
||||
type: "mode",
|
||||
text: newMode
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
...selectStyle,
|
||||
minWidth: "70px",
|
||||
flex: "0 0 auto"
|
||||
}}
|
||||
>
|
||||
<option value="code" style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
color: "var(--vscode-dropdown-foreground)"
|
||||
}}>Code</option>
|
||||
<option value="architect" style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
color: "var(--vscode-dropdown-foreground)"
|
||||
}}>Architect</option>
|
||||
<option value="ask" style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
color: "var(--vscode-dropdown-foreground)"
|
||||
}}>Ask</option>
|
||||
</select>
|
||||
<div style={caretContainerStyle}>
|
||||
<CaretIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: "relative",
|
||||
display: "inline-block",
|
||||
flex: "1 1 auto",
|
||||
minWidth: 0,
|
||||
maxWidth: "150px",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<select
|
||||
value={currentApiConfigName}
|
||||
disabled={textAreaDisabled}
|
||||
onChange={(e) => vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: e.target.value
|
||||
})}
|
||||
style={{
|
||||
...selectStyle,
|
||||
width: "100%",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
{(listApiConfigMeta || [])?.map((config) => (
|
||||
<option
|
||||
key={config.name}
|
||||
value={config.name}
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
color: "var(--vscode-dropdown-foreground)"
|
||||
}}
|
||||
>
|
||||
{config.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div style={caretContainerStyle}>
|
||||
<CaretIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="button-row" style={{ position: "absolute", right: 20, display: "flex", alignItems: "center", height: 31, bottom: 8, zIndex: 2, justifyContent: "flex-end" }}>
|
||||
<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 style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px"
|
||||
}}>
|
||||
<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
|
||||
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 }}
|
||||
/>
|
||||
</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>
|
||||
<span
|
||||
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
|
||||
onClick={() => !textAreaDisabled && onSend()}
|
||||
style={{ fontSize: 15 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ interface ChatViewProps {
|
||||
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||
|
||||
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
|
||||
const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands, writeDelayMs } = useExtensionState()
|
||||
const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands, writeDelayMs, mode, setMode } = useExtensionState()
|
||||
|
||||
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
|
||||
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
|
||||
@@ -192,6 +192,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "say":
|
||||
// don't want to reset since there could be a "say" after an "ask" while ask is waiting for response
|
||||
switch (lastMessage.say) {
|
||||
case "api_req_retry_delayed":
|
||||
setTextAreaDisabled(true)
|
||||
break
|
||||
case "api_req_started":
|
||||
if (secondLastMessage?.ask === "command_output") {
|
||||
// if the last ask is a command_output, and we receive an api_req_started, then that means the command has finished and we don't need input from the user anymore (in every other case, the user has to interact with input field or buttons to continue, which does the following automatically)
|
||||
@@ -294,11 +297,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
// there is no other case that a textfield should be enabled
|
||||
}
|
||||
}
|
||||
// Only reset message-specific state, preserving mode
|
||||
setInputValue("")
|
||||
setTextAreaDisabled(true)
|
||||
setSelectedImages([])
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// Do not reset mode here as it should persist
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
@@ -335,8 +340,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
}, [clineAsk, startNewTask])
|
||||
|
||||
@@ -364,8 +367,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
}, [clineAsk, startNewTask, isStreaming])
|
||||
|
||||
@@ -466,6 +467,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "api_req_finished": // combineApiRequests removes this from modifiedMessages anyways
|
||||
case "api_req_retried": // this message is used to update the latest api_req_started that the request was retried
|
||||
return false
|
||||
case "api_req_retry_delayed":
|
||||
// Only show the retry message if it's the last message
|
||||
return message === modifiedMessages.at(-1)
|
||||
case "text":
|
||||
// Sometimes cline returns an empty text message, we don't want to render these. (We also use a say text for user messages, so in case they just sent images we still render that)
|
||||
if ((message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
|
||||
@@ -773,9 +777,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance
|
||||
|
||||
const placeholderText = useMemo(() => {
|
||||
const text = task ? "Type a message...\n(@ to add context, hold shift to drag in images)" : "Type your task here...\n(@ to add context, hold shift to drag in images)"
|
||||
return text
|
||||
}, [task])
|
||||
const baseText = task ? "Type a message..." : "Type your task here..."
|
||||
const contextText = "(@ to add context"
|
||||
const imageText = shouldDisableImages ? "" : ", hold shift to drag in images"
|
||||
const helpText = imageText ? `\n${contextText}${imageText})` : `\n${contextText})`
|
||||
return baseText + helpText
|
||||
}, [task, shouldDisableImages])
|
||||
|
||||
const itemContent = useCallback(
|
||||
(index: number, messageOrGroup: ClineMessage | ClineMessage[]) => {
|
||||
@@ -977,6 +984,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
scrollToBottomAuto()
|
||||
}
|
||||
}}
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -263,6 +263,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowWrite: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
@@ -276,6 +277,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
// Then send the write tool ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowWrite: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
|
||||
16
webview-ui/src/components/common/CaretIcon.tsx
Normal file
16
webview-ui/src/components/common/CaretIcon.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
export const CaretIcon = () => (
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
)
|
||||
479
webview-ui/src/components/prompts/PromptsView.tsx
Normal file
479
webview-ui/src/components/prompts/PromptsView.tsx
Normal file
@@ -0,0 +1,479 @@
|
||||
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,
|
||||
setCustomInstructions
|
||||
} = 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
|
||||
setCustomInstructions(value || undefined)
|
||||
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
|
||||
160
webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx
Normal file
160
webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
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',
|
||||
customInstructions: 'Initial instructions',
|
||||
setCustomInstructions: jest.fn()
|
||||
}
|
||||
|
||||
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'
|
||||
})
|
||||
})
|
||||
|
||||
it('handles clearing custom instructions correctly', async () => {
|
||||
const setCustomInstructions = jest.fn()
|
||||
renderPromptsView({
|
||||
...mockExtensionState,
|
||||
customInstructions: 'Initial instructions',
|
||||
setCustomInstructions
|
||||
})
|
||||
|
||||
const textarea = screen.getByTestId('global-custom-instructions-textarea')
|
||||
const changeEvent = new CustomEvent('change', {
|
||||
detail: { target: { value: '' } }
|
||||
})
|
||||
Object.defineProperty(changeEvent, 'target', {
|
||||
value: { value: '' }
|
||||
})
|
||||
await fireEvent(textarea, changeEvent)
|
||||
|
||||
expect(setCustomInstructions).toHaveBeenCalledWith(undefined)
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'customInstructions',
|
||||
text: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,7 @@ import { vscode } from "../../utils/vscode"
|
||||
import ApiOptions from "./ApiOptions"
|
||||
import McpEnabledToggle from "../mcp/McpEnabledToggle"
|
||||
import ApiConfigManager from "./ApiConfigManager"
|
||||
import { Mode } from "../../../../src/shared/modes"
|
||||
|
||||
const IS_DEV = false // FIXME: use flags when packaging
|
||||
|
||||
@@ -58,6 +59,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
setRequestDelaySeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
mode,
|
||||
setMode,
|
||||
} = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
@@ -99,7 +102,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
text: currentApiConfigName,
|
||||
apiConfiguration
|
||||
})
|
||||
|
||||
vscode.postMessage({ type: "mode", text: mode })
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
@@ -203,6 +206,37 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Agent Settings</h3>
|
||||
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Agent Mode</label>
|
||||
<select
|
||||
value={mode}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value as Mode
|
||||
setMode(value)
|
||||
vscode.postMessage({ type: "mode", text: value })
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "4px 8px",
|
||||
backgroundColor: "var(--vscode-input-background)",
|
||||
color: "var(--vscode-input-foreground)",
|
||||
border: "1px solid var(--vscode-input-border)",
|
||||
borderRadius: "2px",
|
||||
height: "28px"
|
||||
}}>
|
||||
<option value="code">Code</option>
|
||||
<option value="architect">Architect</option>
|
||||
<option value="ask">Ask</option>
|
||||
</select>
|
||||
<p style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Select the mode that best fits your needs. Code mode focuses on implementation details, Architect mode on high-level design, and Ask mode on asking questions about the codebase.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Preferred Language</label>
|
||||
<select
|
||||
value={preferredLanguage}
|
||||
@@ -244,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>
|
||||
@@ -520,7 +556,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
minWidth: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
color: 'var(--vscode-button-foreground)',
|
||||
}}
|
||||
onClick={() => {
|
||||
const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index)
|
||||
@@ -673,7 +710,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
|
||||
If you have any questions or feedback, feel free to open an issue at{" "}
|
||||
<VSCodeLink href="https://github.com/RooVetGit/Roo-Cline" style={{ display: "inline" }}>
|
||||
https://github.com/RooVetGit/Roo-Cline
|
||||
github.com/RooVetGit/Roo-Cline
|
||||
</VSCodeLink> or join {" "}
|
||||
<VSCodeLink href="https://www.reddit.com/r/roocline/" style={{ display: "inline" }}>
|
||||
reddit.com/r/roocline
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
|
||||
|
||||
@@ -16,6 +16,8 @@ import { McpServer } from "../../../src/shared/mcp"
|
||||
import {
|
||||
checkExistKey
|
||||
} from "../../../src/shared/checkExistApiConfig"
|
||||
import { Mode } from "../../../src/core/prompts/types"
|
||||
import { codeMode, CustomPrompts, defaultPrompts } from "../../../src/shared/modes"
|
||||
|
||||
export interface ExtensionStateContextType extends ExtensionState {
|
||||
didHydrateState: boolean
|
||||
@@ -56,6 +58,11 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
setCurrentApiConfigName: (value: string) => void
|
||||
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
|
||||
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)
|
||||
@@ -81,6 +88,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
requestDelaySeconds: 5,
|
||||
currentApiConfigName: 'default',
|
||||
listApiConfigMeta: [],
|
||||
mode: codeMode,
|
||||
customPrompts: defaultPrompts,
|
||||
enhancementApiConfigId: '',
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
@@ -111,7 +121,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "state": {
|
||||
setState(message.state!)
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
...message.state!
|
||||
}))
|
||||
const config = message.state?.apiConfiguration
|
||||
const hasKey = checkExistKey(config)
|
||||
setShowWelcome(!hasKey)
|
||||
@@ -220,7 +233,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
|
||||
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
|
||||
setListApiConfigMeta,
|
||||
onUpdateApiConfig
|
||||
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