mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 20:31:37 -05:00
Prettier backfill
This commit is contained in:
@@ -33,25 +33,28 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
🎉{" "}Introducing Roo Cline v{minorVersion}
|
||||
</h2>
|
||||
|
||||
<h3 style={{ margin: "0 0 8px" }}>
|
||||
Agent Modes Customization
|
||||
</h3>
|
||||
<h3 style={{ margin: "0 0 8px" }}>Agent Modes Customization</h3>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
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.
|
||||
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>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>
|
||||
</p>
|
||||
|
||||
<h3 style={{ margin: "0 0 8px" }}>
|
||||
Prompt Enhancement Configuration
|
||||
</h3>
|
||||
<h3 style={{ margin: "0 0 8px" }}>Prompt Enhancement Configuration</h3>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
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.
|
||||
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>
|
||||
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>
|
||||
|
||||
@@ -127,7 +127,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
||||
}, [alwaysApproveResubmit, setAlwaysApproveResubmit])
|
||||
|
||||
// Map action IDs to their specific handlers
|
||||
const actionHandlers: Record<AutoApproveAction['id'], () => void> = {
|
||||
const actionHandlers: Record<AutoApproveAction["id"], () => void> = {
|
||||
readFiles: handleReadOnlyChange,
|
||||
editFiles: handleWriteChange,
|
||||
executeCommands: handleExecuteChange,
|
||||
@@ -166,25 +166,30 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
flex: 1,
|
||||
minWidth: 0
|
||||
}}>
|
||||
<span style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
flexShrink: 0
|
||||
}}>Auto-approve:</span>
|
||||
<span style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flex: 1,
|
||||
minWidth: 0
|
||||
minWidth: 0,
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
Auto-approve:
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
}}>
|
||||
{enabledActionsList || "None"}
|
||||
</span>
|
||||
<span
|
||||
@@ -210,9 +215,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
||||
{actions.map((action) => (
|
||||
<div key={action.id} style={{ margin: "6px 0" }}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<VSCodeCheckbox
|
||||
checked={action.enabled}
|
||||
onChange={actionHandlers[action.id]}>
|
||||
<VSCodeCheckbox checked={action.enabled} onChange={actionHandlers[action.id]}>
|
||||
{action.label}
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
|
||||
@@ -31,8 +31,8 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
|
||||
const { browserViewportSize = "900x600" } = useExtensionState()
|
||||
const [viewportWidth, viewportHeight] = browserViewportSize.split("x").map(Number)
|
||||
const aspectRatio = (viewportHeight / viewportWidth * 100).toFixed(2)
|
||||
const defaultMousePosition = `${Math.round(viewportWidth/2)},${Math.round(viewportHeight/2)}`
|
||||
const aspectRatio = ((viewportHeight / viewportWidth) * 100).toFixed(2)
|
||||
const defaultMousePosition = `${Math.round(viewportWidth / 2)},${Math.round(viewportHeight / 2)}`
|
||||
|
||||
const isLastApiReqInterrupted = useMemo(() => {
|
||||
// Check if last api_req_started is cancelled
|
||||
@@ -171,7 +171,8 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
const displayState = isLastPage
|
||||
? {
|
||||
url: currentPage?.currentState.url || latestState.url || initialUrl,
|
||||
mousePosition: currentPage?.currentState.mousePosition || latestState.mousePosition || defaultMousePosition,
|
||||
mousePosition:
|
||||
currentPage?.currentState.mousePosition || latestState.mousePosition || defaultMousePosition,
|
||||
consoleLogs: currentPage?.currentState.consoleLogs,
|
||||
screenshot: currentPage?.currentState.screenshot || latestState.screenshot,
|
||||
}
|
||||
@@ -226,7 +227,9 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
}, [isBrowsing, currentPage?.nextAction?.messages])
|
||||
|
||||
// Use latest click position while browsing, otherwise use display state
|
||||
const mousePosition = isBrowsing ? latestClickPosition || displayState.mousePosition : displayState.mousePosition || defaultMousePosition
|
||||
const mousePosition = isBrowsing
|
||||
? latestClickPosition || displayState.mousePosition
|
||||
: displayState.mousePosition || defaultMousePosition
|
||||
|
||||
const [browserSessionRow, { height: rowHeight }] = useSize(
|
||||
<div style={{ padding: "10px 6px 10px 15px", marginBottom: -10 }}>
|
||||
|
||||
@@ -565,7 +565,13 @@ export const ChatRowContent = ({
|
||||
whiteSpace: "pre-line",
|
||||
wordWrap: "break-word",
|
||||
}}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: "10px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
gap: "10px",
|
||||
}}>
|
||||
<span style={{ display: "block", flexGrow: 1 }}>{highlightMentions(message.text)}</span>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
@@ -574,17 +580,16 @@ export const ChatRowContent = ({
|
||||
flexShrink: 0,
|
||||
height: "24px",
|
||||
marginTop: "-6px",
|
||||
marginRight: "-6px"
|
||||
marginRight: "-6px",
|
||||
}}
|
||||
disabled={isStreaming}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation()
|
||||
vscode.postMessage({
|
||||
type: "deleteMessage",
|
||||
value: message.ts
|
||||
});
|
||||
}}
|
||||
>
|
||||
value: message.ts,
|
||||
})
|
||||
}}>
|
||||
<span className="codicon codicon-trash"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
@@ -835,10 +840,13 @@ export const ChatRowContent = ({
|
||||
tool={{
|
||||
name: useMcpServer.toolName || "",
|
||||
description:
|
||||
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
|
||||
?.description || "",
|
||||
alwaysAllow: server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
|
||||
?.alwaysAllow || false,
|
||||
server?.tools?.find(
|
||||
(tool) => tool.name === useMcpServer.toolName,
|
||||
)?.description || "",
|
||||
alwaysAllow:
|
||||
server?.tools?.find(
|
||||
(tool) => tool.name === useMcpServer.toolName,
|
||||
)?.alwaysAllow || false,
|
||||
}}
|
||||
serverName={useMcpServer.serverName}
|
||||
/>
|
||||
@@ -919,14 +927,13 @@ export const ProgressIndicator = () => (
|
||||
)
|
||||
|
||||
const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [isHovering, setIsHovering] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
style={{ position: "relative" }}>
|
||||
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
|
||||
<MarkdownBlock markdown={markdown} />
|
||||
</div>
|
||||
@@ -938,9 +945,8 @@ const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boo
|
||||
right: "8px",
|
||||
opacity: 0,
|
||||
animation: "fadeIn 0.2s ease-in-out forwards",
|
||||
borderRadius: "4px"
|
||||
}}
|
||||
>
|
||||
borderRadius: "4px",
|
||||
}}>
|
||||
<style>
|
||||
{`
|
||||
@keyframes fadeIn {
|
||||
@@ -956,21 +962,20 @@ const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boo
|
||||
height: "24px",
|
||||
border: "none",
|
||||
background: "var(--vscode-editor-background)",
|
||||
transition: "background 0.2s ease-in-out"
|
||||
transition: "background 0.2s ease-in-out",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(markdown);
|
||||
navigator.clipboard.writeText(markdown)
|
||||
// Flash the button background briefly to indicate success
|
||||
const button = document.activeElement as HTMLElement;
|
||||
const button = document.activeElement as HTMLElement
|
||||
if (button) {
|
||||
button.style.background = "var(--vscode-button-background)";
|
||||
button.style.background = "var(--vscode-button-background)"
|
||||
setTimeout(() => {
|
||||
button.style.background = "";
|
||||
}, 200);
|
||||
button.style.background = ""
|
||||
}, 200)
|
||||
}
|
||||
}}
|
||||
title="Copy as markdown"
|
||||
>
|
||||
title="Copy as markdown">
|
||||
<span className="codicon codicon-copy"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
@@ -69,24 +69,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
useEffect(() => {
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === 'enhancedPrompt') {
|
||||
if (message.type === "enhancedPrompt") {
|
||||
if (message.text) {
|
||||
setInputValue(message.text)
|
||||
}
|
||||
setIsEnhancingPrompt(false)
|
||||
} else if (message.type === 'commitSearchResults') {
|
||||
} else if (message.type === "commitSearchResults") {
|
||||
const commits = message.commits.map((commit: any) => ({
|
||||
type: ContextMenuOptionType.Git,
|
||||
value: commit.hash,
|
||||
label: commit.subject,
|
||||
description: `${commit.shortHash} by ${commit.author} on ${commit.date}`,
|
||||
icon: "$(git-commit)"
|
||||
icon: "$(git-commit)",
|
||||
}))
|
||||
setGitCommits(commits)
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', messageHandler)
|
||||
return () => window.removeEventListener('message', messageHandler)
|
||||
window.addEventListener("message", messageHandler)
|
||||
return () => window.removeEventListener("message", messageHandler)
|
||||
}, [setInputValue])
|
||||
|
||||
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
|
||||
@@ -109,12 +109,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
|
||||
const message: WebviewMessage = {
|
||||
type: "searchCommits",
|
||||
query: searchQuery || ""
|
||||
query: searchQuery || "",
|
||||
} as const
|
||||
vscode.postMessage(message)
|
||||
}
|
||||
}, [selectedType, searchQuery])
|
||||
|
||||
|
||||
const handleEnhancePrompt = useCallback(() => {
|
||||
if (!textAreaDisabled) {
|
||||
const trimmedInput = inputValue.trim()
|
||||
@@ -126,7 +126,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}
|
||||
vscode.postMessage(message)
|
||||
} else {
|
||||
const promptDescription = "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works."
|
||||
const promptDescription =
|
||||
"The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works."
|
||||
setInputValue(promptDescription)
|
||||
}
|
||||
}
|
||||
@@ -170,9 +171,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
return
|
||||
}
|
||||
|
||||
if (type === ContextMenuOptionType.File ||
|
||||
if (
|
||||
type === ContextMenuOptionType.File ||
|
||||
type === ContextMenuOptionType.Folder ||
|
||||
type === ContextMenuOptionType.Git) {
|
||||
type === ContextMenuOptionType.Git
|
||||
) {
|
||||
if (!value) {
|
||||
setSelectedType(type)
|
||||
setSearchQuery("")
|
||||
@@ -505,7 +508,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
paddingRight: "6px",
|
||||
WebkitAppearance: "none" as const,
|
||||
MozAppearance: "none" as const,
|
||||
appearance: "none" as const
|
||||
appearance: "none" as const,
|
||||
}
|
||||
|
||||
const caretContainerStyle = {
|
||||
@@ -514,11 +517,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
top: "50%",
|
||||
transform: "translateY(-45%)",
|
||||
pointerEvents: "none" as const,
|
||||
opacity: textAreaDisabled ? 0.5 : 0.8
|
||||
opacity: textAreaDisabled ? 0.5 : 0.8,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className="chat-text-area"
|
||||
style={{
|
||||
opacity: textAreaDisabled ? 0.5 : 1,
|
||||
@@ -528,15 +531,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
gap: "8px",
|
||||
backgroundColor: "var(--vscode-input-background)",
|
||||
margin: "10px 15px",
|
||||
padding: "8px"
|
||||
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)
|
||||
const newValue = inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition)
|
||||
setInputValue(newValue)
|
||||
const newCursorPosition = cursorPosition + text.length
|
||||
setCursorPosition(newCursorPosition)
|
||||
@@ -567,11 +569,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
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') {
|
||||
setSelectedImages((prevImages) =>
|
||||
[...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE),
|
||||
)
|
||||
if (typeof vscode !== "undefined") {
|
||||
vscode.postMessage({
|
||||
type: 'draggedImages',
|
||||
dataUrls: dataUrls
|
||||
type: "draggedImages",
|
||||
dataUrls: dataUrls,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -581,8 +585,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{showContextMenu && (
|
||||
<div ref={contextMenuContainerRef}>
|
||||
<ContextMenu
|
||||
@@ -596,15 +599,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
position: "relative",
|
||||
flex: "1 1 auto",
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
minHeight: 0,
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
flex: "1 1 auto",
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div
|
||||
ref={highlightLayerRef}
|
||||
style={{
|
||||
@@ -620,7 +624,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
lineHeight: "var(--vscode-editor-line-height)",
|
||||
padding: "8px",
|
||||
marginBottom: thumbnailsHeight > 0 ? `${thumbnailsHeight + 16}px` : 0,
|
||||
zIndex: 1
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
<DynamicTextArea
|
||||
@@ -671,7 +675,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
marginBottom: thumbnailsHeight > 0 ? `${thumbnailsHeight + 16}px` : 0,
|
||||
cursor: textAreaDisabled ? "not-allowed" : undefined,
|
||||
flex: "0 1 auto",
|
||||
zIndex: 2
|
||||
zIndex: 2,
|
||||
}}
|
||||
onScroll={() => updateHighlights()}
|
||||
/>
|
||||
@@ -687,22 +691,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
bottom: "36px",
|
||||
left: "16px",
|
||||
zIndex: 2,
|
||||
marginBottom: "8px"
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: "auto",
|
||||
paddingTop: "8px"
|
||||
}}>
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center"
|
||||
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}
|
||||
@@ -712,24 +718,22 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
setMode(newMode)
|
||||
vscode.postMessage({
|
||||
type: "mode",
|
||||
text: newMode
|
||||
text: newMode,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
...selectStyle,
|
||||
minWidth: "70px",
|
||||
flex: "0 0 auto"
|
||||
}}
|
||||
>
|
||||
{modes.map(mode => (
|
||||
flex: "0 0 auto",
|
||||
}}>
|
||||
{modes.map((mode) => (
|
||||
<option
|
||||
key={mode.slug}
|
||||
value={mode.slug}
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
color: "var(--vscode-dropdown-foreground)"
|
||||
}}
|
||||
>
|
||||
color: "var(--vscode-dropdown-foreground)",
|
||||
}}>
|
||||
{mode.name}
|
||||
</option>
|
||||
))}
|
||||
@@ -739,36 +743,37 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: "relative",
|
||||
display: "inline-block",
|
||||
flex: "1 1 auto",
|
||||
minWidth: 0,
|
||||
maxWidth: "150px",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<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
|
||||
})}
|
||||
onChange={(e) =>
|
||||
vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: e.target.value,
|
||||
})
|
||||
}
|
||||
style={{
|
||||
...selectStyle,
|
||||
width: "100%",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{(listApiConfigMeta || [])?.map((config) => (
|
||||
<option
|
||||
key={config.name}
|
||||
value={config.name}
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
color: "var(--vscode-dropdown-foreground)"
|
||||
}}
|
||||
>
|
||||
color: "var(--vscode-dropdown-foreground)",
|
||||
}}>
|
||||
{config.name}
|
||||
</option>
|
||||
))}
|
||||
@@ -779,19 +784,23 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px"
|
||||
}}>
|
||||
<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
|
||||
className="codicon codicon-loading codicon-modifier-spin"
|
||||
style={{
|
||||
color: "var(--vscode-input-foreground)",
|
||||
opacity: 0.5,
|
||||
fontSize: 16.5,
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
role="button"
|
||||
@@ -803,12 +812,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
<span
|
||||
className={`input-icon-button ${shouldDisableImages ? "disabled" : ""} codicon codicon-device-camera`}
|
||||
onClick={() => !shouldDisableImages && onSelectImages()}
|
||||
style={{ fontSize: 16.5 }}
|
||||
/>
|
||||
<span
|
||||
<span
|
||||
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
|
||||
onClick={() => !textAreaDisabled && onSend()}
|
||||
style={{ fontSize: 15 }}
|
||||
|
||||
@@ -39,7 +39,23 @@ 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, mode, setMode, autoApprovalEnabled } = useExtensionState()
|
||||
const {
|
||||
version,
|
||||
clineMessages: messages,
|
||||
taskHistory,
|
||||
apiConfiguration,
|
||||
mcpServers,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowMcp,
|
||||
allowedCommands,
|
||||
writeDelayMs,
|
||||
mode,
|
||||
setMode,
|
||||
autoApprovalEnabled,
|
||||
} = 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)
|
||||
@@ -178,7 +194,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Resume Task")
|
||||
setSecondaryButtonText("Terminate")
|
||||
setDidClickCancel(false) // special case where we reset the cancel button state
|
||||
setDidClickCancel(false) // special case where we reset the cancel button state
|
||||
break
|
||||
case "resume_completed_task":
|
||||
setTextAreaDisabled(false)
|
||||
@@ -490,7 +506,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
return true
|
||||
}
|
||||
const tool = JSON.parse(message.text)
|
||||
return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool)
|
||||
return [
|
||||
"readFile",
|
||||
"listFiles",
|
||||
"listFilesTopLevel",
|
||||
"listFilesRecursive",
|
||||
"listCodeDefinitionNames",
|
||||
"searchFiles",
|
||||
].includes(tool.tool)
|
||||
}
|
||||
return false
|
||||
}, [])
|
||||
@@ -506,26 +529,32 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
return false
|
||||
}, [])
|
||||
|
||||
const isMcpToolAlwaysAllowed = useCallback((message: ClineMessage | undefined) => {
|
||||
if (message?.type === "ask" && message.ask === "use_mcp_server") {
|
||||
if (!message.text) {
|
||||
return true
|
||||
const isMcpToolAlwaysAllowed = useCallback(
|
||||
(message: ClineMessage | undefined) => {
|
||||
if (message?.type === "ask" && message.ask === "use_mcp_server") {
|
||||
if (!message.text) {
|
||||
return true
|
||||
}
|
||||
const mcpServerUse = JSON.parse(message.text) as { type: string; serverName: string; toolName: string }
|
||||
if (mcpServerUse.type === "use_mcp_tool") {
|
||||
const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
|
||||
const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
|
||||
return tool?.alwaysAllow || false
|
||||
}
|
||||
}
|
||||
const mcpServerUse = JSON.parse(message.text) as { type: string; serverName: string; toolName: string }
|
||||
if (mcpServerUse.type === "use_mcp_tool") {
|
||||
const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
|
||||
const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
|
||||
return tool?.alwaysAllow || false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, [mcpServers])
|
||||
return false
|
||||
},
|
||||
[mcpServers],
|
||||
)
|
||||
|
||||
// Check if a command message is allowed
|
||||
const isAllowedCommand = useCallback((message: ClineMessage | undefined): boolean => {
|
||||
if (message?.type !== "ask") return false
|
||||
return validateCommand(message.text || '', allowedCommands || [])
|
||||
}, [allowedCommands])
|
||||
const isAllowedCommand = useCallback(
|
||||
(message: ClineMessage | undefined): boolean => {
|
||||
if (message?.type !== "ask") return false
|
||||
return validateCommand(message.text || "", allowedCommands || [])
|
||||
},
|
||||
[allowedCommands],
|
||||
)
|
||||
|
||||
const isAutoApproved = useCallback(
|
||||
(message: ClineMessage | undefined) => {
|
||||
@@ -539,7 +568,18 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
|
||||
)
|
||||
},
|
||||
[autoApprovalEnabled, alwaysAllowBrowser, alwaysAllowReadOnly, isReadOnlyToolAction, alwaysAllowWrite, isWriteToolAction, alwaysAllowExecute, isAllowedCommand, alwaysAllowMcp, isMcpToolAlwaysAllowed]
|
||||
[
|
||||
autoApprovalEnabled,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowReadOnly,
|
||||
isReadOnlyToolAction,
|
||||
alwaysAllowWrite,
|
||||
isWriteToolAction,
|
||||
alwaysAllowExecute,
|
||||
isAllowedCommand,
|
||||
alwaysAllowMcp,
|
||||
isMcpToolAlwaysAllowed,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -812,7 +852,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
/>
|
||||
)
|
||||
},
|
||||
[expandedRows, modifiedMessages, groupedMessages.length, handleRowHeightChange, isStreaming, toggleRowExpansion],
|
||||
[
|
||||
expandedRows,
|
||||
modifiedMessages,
|
||||
groupedMessages.length,
|
||||
handleRowHeightChange,
|
||||
isStreaming,
|
||||
toggleRowExpansion,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -823,13 +870,29 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
if (isAutoApproved(lastMessage)) {
|
||||
// Add delay for write operations
|
||||
if (lastMessage?.ask === "tool" && isWriteToolAction(lastMessage)) {
|
||||
await new Promise(resolve => setTimeout(resolve, writeDelayMs))
|
||||
await new Promise((resolve) => setTimeout(resolve, writeDelayMs))
|
||||
}
|
||||
handlePrimaryButtonClick()
|
||||
}
|
||||
}
|
||||
autoApprove()
|
||||
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers, isAutoApproved, lastMessage, writeDelayMs, isWriteToolAction])
|
||||
}, [
|
||||
clineAsk,
|
||||
enableButtons,
|
||||
handlePrimaryButtonClick,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowMcp,
|
||||
messages,
|
||||
allowedCommands,
|
||||
mcpServers,
|
||||
isAutoApproved,
|
||||
lastMessage,
|
||||
writeDelayMs,
|
||||
isWriteToolAction,
|
||||
])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -868,11 +931,11 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
<div style={{ padding: "0 20px", flexShrink: 0 }}>
|
||||
<h2>What can I do for you?</h2>
|
||||
<p>
|
||||
Thanks to the latest breakthroughs in agentic coding capabilities,
|
||||
I can handle complex software development tasks step-by-step. With tools that let me create
|
||||
& edit files, explore complex projects, use the browser, and execute terminal commands
|
||||
(after you grant permission), I can assist you in ways that go beyond code completion or
|
||||
tech support. I can even use MCP to create new tools and extend my own capabilities.
|
||||
Thanks to the latest breakthroughs in agentic coding capabilities, I can handle complex
|
||||
software development tasks step-by-step. With tools that let me create & edit files, explore
|
||||
complex projects, use the browser, and execute terminal commands (after you grant
|
||||
permission), I can assist you in ways that go beyond code completion or tech support. I can
|
||||
even use MCP to create new tools and extend my own capabilities.
|
||||
</p>
|
||||
</div>
|
||||
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
|
||||
|
||||
@@ -55,16 +55,17 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
case ContextMenuOptionType.Git:
|
||||
if (option.value) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<span style={{ lineHeight: '1.2' }}>{option.label}</span>
|
||||
<span style={{
|
||||
fontSize: '0.85em',
|
||||
opacity: 0.7,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: '1.2'
|
||||
}}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||||
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
opacity: 0.7,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
lineHeight: "1.2",
|
||||
}}>
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
@@ -168,33 +169,33 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
paddingTop: 0
|
||||
paddingTop: 0,
|
||||
}}>
|
||||
<i
|
||||
className={`codicon codicon-${getIconForOption(option)}`}
|
||||
style={{
|
||||
style={{
|
||||
marginRight: "6px",
|
||||
flexShrink: 0,
|
||||
fontSize: "14px",
|
||||
marginTop: 0
|
||||
marginTop: 0,
|
||||
}}
|
||||
/>
|
||||
{renderOptionContent(option)}
|
||||
</div>
|
||||
{((option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder ||
|
||||
option.type === ContextMenuOptionType.Git) &&
|
||||
!option.value) && (
|
||||
<i
|
||||
className="codicon codicon-chevron-right"
|
||||
style={{ fontSize: "14px", flexShrink: 0, marginLeft: 8 }}
|
||||
/>
|
||||
)}
|
||||
{(option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder ||
|
||||
option.type === ContextMenuOptionType.Git) &&
|
||||
!option.value && (
|
||||
<i
|
||||
className="codicon codicon-chevron-right"
|
||||
style={{ fontSize: "14px", flexShrink: 0, marginLeft: 8 }}
|
||||
/>
|
||||
)}
|
||||
{(option.type === ContextMenuOptionType.Problems ||
|
||||
((option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder ||
|
||||
option.type === ContextMenuOptionType.Git) &&
|
||||
option.value)) && (
|
||||
((option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder ||
|
||||
option.type === ContextMenuOptionType.Git) &&
|
||||
option.value)) && (
|
||||
<i
|
||||
className="codicon codicon-add"
|
||||
style={{ fontSize: "14px", flexShrink: 0, marginLeft: 8 }}
|
||||
|
||||
@@ -9,190 +9,190 @@ jest.mock("../../../context/ExtensionStateContext")
|
||||
const mockUseExtensionState = useExtensionState as jest.MockedFunction<typeof useExtensionState>
|
||||
|
||||
describe("AutoApproveMenu", () => {
|
||||
const defaultMockState = {
|
||||
// Required state properties
|
||||
version: "1.0.0",
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
soundEnabled: false,
|
||||
soundVolume: 0.5,
|
||||
diffEnabled: false,
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
preferredLanguage: "English",
|
||||
writeDelayMs: 1000,
|
||||
browserViewportSize: "900x600",
|
||||
screenshotQuality: 75,
|
||||
terminalOutputLineLimit: 500,
|
||||
mcpEnabled: true,
|
||||
requestDelaySeconds: 5,
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [],
|
||||
mode: codeMode,
|
||||
customPrompts: defaultPrompts,
|
||||
enhancementApiConfigId: "",
|
||||
didHydrateState: true,
|
||||
showWelcome: false,
|
||||
theme: {},
|
||||
glamaModels: {},
|
||||
openRouterModels: {},
|
||||
openAiModels: [],
|
||||
mcpServers: [],
|
||||
filePaths: [],
|
||||
const defaultMockState = {
|
||||
// Required state properties
|
||||
version: "1.0.0",
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
soundEnabled: false,
|
||||
soundVolume: 0.5,
|
||||
diffEnabled: false,
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
preferredLanguage: "English",
|
||||
writeDelayMs: 1000,
|
||||
browserViewportSize: "900x600",
|
||||
screenshotQuality: 75,
|
||||
terminalOutputLineLimit: 500,
|
||||
mcpEnabled: true,
|
||||
requestDelaySeconds: 5,
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [],
|
||||
mode: codeMode,
|
||||
customPrompts: defaultPrompts,
|
||||
enhancementApiConfigId: "",
|
||||
didHydrateState: true,
|
||||
showWelcome: false,
|
||||
theme: {},
|
||||
glamaModels: {},
|
||||
openRouterModels: {},
|
||||
openAiModels: [],
|
||||
mcpServers: [],
|
||||
filePaths: [],
|
||||
|
||||
// Auto-approve specific properties
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowMcp: false,
|
||||
alwaysApproveResubmit: false,
|
||||
autoApprovalEnabled: false,
|
||||
// Auto-approve specific properties
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowMcp: false,
|
||||
alwaysApproveResubmit: false,
|
||||
autoApprovalEnabled: false,
|
||||
|
||||
// Required setter functions
|
||||
setApiConfiguration: jest.fn(),
|
||||
setCustomInstructions: jest.fn(),
|
||||
setAlwaysAllowReadOnly: jest.fn(),
|
||||
setAlwaysAllowWrite: jest.fn(),
|
||||
setAlwaysAllowExecute: jest.fn(),
|
||||
setAlwaysAllowBrowser: jest.fn(),
|
||||
setAlwaysAllowMcp: jest.fn(),
|
||||
setShowAnnouncement: jest.fn(),
|
||||
setAllowedCommands: jest.fn(),
|
||||
setSoundEnabled: jest.fn(),
|
||||
setSoundVolume: jest.fn(),
|
||||
setDiffEnabled: jest.fn(),
|
||||
setBrowserViewportSize: jest.fn(),
|
||||
setFuzzyMatchThreshold: jest.fn(),
|
||||
setPreferredLanguage: jest.fn(),
|
||||
setWriteDelayMs: jest.fn(),
|
||||
setScreenshotQuality: jest.fn(),
|
||||
setTerminalOutputLineLimit: jest.fn(),
|
||||
setMcpEnabled: jest.fn(),
|
||||
setAlwaysApproveResubmit: jest.fn(),
|
||||
setRequestDelaySeconds: jest.fn(),
|
||||
setCurrentApiConfigName: jest.fn(),
|
||||
setListApiConfigMeta: jest.fn(),
|
||||
onUpdateApiConfig: jest.fn(),
|
||||
setMode: jest.fn(),
|
||||
setCustomPrompts: jest.fn(),
|
||||
setEnhancementApiConfigId: jest.fn(),
|
||||
setAutoApprovalEnabled: jest.fn(),
|
||||
}
|
||||
// Required setter functions
|
||||
setApiConfiguration: jest.fn(),
|
||||
setCustomInstructions: jest.fn(),
|
||||
setAlwaysAllowReadOnly: jest.fn(),
|
||||
setAlwaysAllowWrite: jest.fn(),
|
||||
setAlwaysAllowExecute: jest.fn(),
|
||||
setAlwaysAllowBrowser: jest.fn(),
|
||||
setAlwaysAllowMcp: jest.fn(),
|
||||
setShowAnnouncement: jest.fn(),
|
||||
setAllowedCommands: jest.fn(),
|
||||
setSoundEnabled: jest.fn(),
|
||||
setSoundVolume: jest.fn(),
|
||||
setDiffEnabled: jest.fn(),
|
||||
setBrowserViewportSize: jest.fn(),
|
||||
setFuzzyMatchThreshold: jest.fn(),
|
||||
setPreferredLanguage: jest.fn(),
|
||||
setWriteDelayMs: jest.fn(),
|
||||
setScreenshotQuality: jest.fn(),
|
||||
setTerminalOutputLineLimit: jest.fn(),
|
||||
setMcpEnabled: jest.fn(),
|
||||
setAlwaysApproveResubmit: jest.fn(),
|
||||
setRequestDelaySeconds: jest.fn(),
|
||||
setCurrentApiConfigName: jest.fn(),
|
||||
setListApiConfigMeta: jest.fn(),
|
||||
onUpdateApiConfig: jest.fn(),
|
||||
setMode: jest.fn(),
|
||||
setCustomPrompts: jest.fn(),
|
||||
setEnhancementApiConfigId: jest.fn(),
|
||||
setAutoApprovalEnabled: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseExtensionState.mockReturnValue(defaultMockState)
|
||||
})
|
||||
beforeEach(() => {
|
||||
mockUseExtensionState.mockReturnValue(defaultMockState)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders with initial collapsed state", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Check for main checkbox and label
|
||||
expect(screen.getByText("Auto-approve:")).toBeInTheDocument()
|
||||
expect(screen.getByText("None")).toBeInTheDocument()
|
||||
|
||||
// Verify the menu is collapsed (actions not visible)
|
||||
expect(screen.queryByText("Read files and directories")).not.toBeInTheDocument()
|
||||
})
|
||||
it("renders with initial collapsed state", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
it("expands menu when clicked", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Click to expand
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Verify menu items are visible
|
||||
expect(screen.getByText("Read files and directories")).toBeInTheDocument()
|
||||
expect(screen.getByText("Edit files")).toBeInTheDocument()
|
||||
expect(screen.getByText("Execute approved commands")).toBeInTheDocument()
|
||||
expect(screen.getByText("Use the browser")).toBeInTheDocument()
|
||||
expect(screen.getByText("Use MCP servers")).toBeInTheDocument()
|
||||
expect(screen.getByText("Retry failed requests")).toBeInTheDocument()
|
||||
})
|
||||
// Check for main checkbox and label
|
||||
expect(screen.getByText("Auto-approve:")).toBeInTheDocument()
|
||||
expect(screen.getByText("None")).toBeInTheDocument()
|
||||
|
||||
it("toggles main auto-approval checkbox", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
const mainCheckbox = screen.getByRole("checkbox")
|
||||
fireEvent.click(mainCheckbox)
|
||||
|
||||
expect(defaultMockState.setAutoApprovalEnabled).toHaveBeenCalledWith(true)
|
||||
})
|
||||
// Verify the menu is collapsed (actions not visible)
|
||||
expect(screen.queryByText("Read files and directories")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("toggles individual permissions", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand menu
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Click read files checkbox
|
||||
fireEvent.click(screen.getByText("Read files and directories"))
|
||||
expect(defaultMockState.setAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
|
||||
|
||||
// Click edit files checkbox
|
||||
fireEvent.click(screen.getByText("Edit files"))
|
||||
expect(defaultMockState.setAlwaysAllowWrite).toHaveBeenCalledWith(true)
|
||||
|
||||
// Click execute commands checkbox
|
||||
fireEvent.click(screen.getByText("Execute approved commands"))
|
||||
expect(defaultMockState.setAlwaysAllowExecute).toHaveBeenCalledWith(true)
|
||||
})
|
||||
it("expands menu when clicked", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
it("displays enabled actions in summary", () => {
|
||||
mockUseExtensionState.mockReturnValue({
|
||||
...defaultMockState,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
autoApprovalEnabled: true,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Check that enabled actions are shown in summary
|
||||
expect(screen.getByText("Read, Edit")).toBeInTheDocument()
|
||||
})
|
||||
// Click to expand
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
it("preserves checkbox states", () => {
|
||||
// Mock state with some permissions enabled
|
||||
const mockState = {
|
||||
...defaultMockState,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
}
|
||||
|
||||
// Update mock to return our state
|
||||
mockUseExtensionState.mockReturnValue(mockState)
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand menu
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Verify read and edit checkboxes are checked
|
||||
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
||||
|
||||
// Verify the setters haven't been called yet
|
||||
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
||||
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
||||
|
||||
// Collapse menu
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Expand again
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Verify checkboxes are still present
|
||||
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
||||
|
||||
// Verify the setters still haven't been called
|
||||
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
||||
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
// Verify menu items are visible
|
||||
expect(screen.getByText("Read files and directories")).toBeInTheDocument()
|
||||
expect(screen.getByText("Edit files")).toBeInTheDocument()
|
||||
expect(screen.getByText("Execute approved commands")).toBeInTheDocument()
|
||||
expect(screen.getByText("Use the browser")).toBeInTheDocument()
|
||||
expect(screen.getByText("Use MCP servers")).toBeInTheDocument()
|
||||
expect(screen.getByText("Retry failed requests")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("toggles main auto-approval checkbox", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
const mainCheckbox = screen.getByRole("checkbox")
|
||||
fireEvent.click(mainCheckbox)
|
||||
|
||||
expect(defaultMockState.setAutoApprovalEnabled).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("toggles individual permissions", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand menu
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Click read files checkbox
|
||||
fireEvent.click(screen.getByText("Read files and directories"))
|
||||
expect(defaultMockState.setAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
|
||||
|
||||
// Click edit files checkbox
|
||||
fireEvent.click(screen.getByText("Edit files"))
|
||||
expect(defaultMockState.setAlwaysAllowWrite).toHaveBeenCalledWith(true)
|
||||
|
||||
// Click execute commands checkbox
|
||||
fireEvent.click(screen.getByText("Execute approved commands"))
|
||||
expect(defaultMockState.setAlwaysAllowExecute).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("displays enabled actions in summary", () => {
|
||||
mockUseExtensionState.mockReturnValue({
|
||||
...defaultMockState,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
autoApprovalEnabled: true,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Check that enabled actions are shown in summary
|
||||
expect(screen.getByText("Read, Edit")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("preserves checkbox states", () => {
|
||||
// Mock state with some permissions enabled
|
||||
const mockState = {
|
||||
...defaultMockState,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
}
|
||||
|
||||
// Update mock to return our state
|
||||
mockUseExtensionState.mockReturnValue(mockState)
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand menu
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Verify read and edit checkboxes are checked
|
||||
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
||||
|
||||
// Verify the setters haven't been called yet
|
||||
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
||||
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
||||
|
||||
// Collapse menu
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Expand again
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Verify checkboxes are still present
|
||||
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
||||
|
||||
// Verify the setters still haven't been called
|
||||
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
||||
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,159 +1,159 @@
|
||||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ChatTextArea from '../ChatTextArea';
|
||||
import { useExtensionState } from '../../../context/ExtensionStateContext';
|
||||
import { vscode } from '../../../utils/vscode';
|
||||
import { defaultModeSlug } from '../../../../../src/shared/modes';
|
||||
import { render, fireEvent, screen } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import ChatTextArea from "../ChatTextArea"
|
||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../../utils/vscode"
|
||||
import { defaultModeSlug } from "../../../../../src/shared/modes"
|
||||
|
||||
// Mock modules
|
||||
jest.mock('../../../utils/vscode', () => ({
|
||||
vscode: {
|
||||
postMessage: jest.fn()
|
||||
}
|
||||
}));
|
||||
jest.mock('../../../components/common/CodeBlock');
|
||||
jest.mock('../../../components/common/MarkdownBlock');
|
||||
jest.mock("../../../utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: jest.fn(),
|
||||
},
|
||||
}))
|
||||
jest.mock("../../../components/common/CodeBlock")
|
||||
jest.mock("../../../components/common/MarkdownBlock")
|
||||
|
||||
// Get the mocked postMessage function
|
||||
const mockPostMessage = vscode.postMessage as jest.Mock;
|
||||
const mockPostMessage = vscode.postMessage as jest.Mock
|
||||
/* eslint-enable import/first */
|
||||
|
||||
// Mock ExtensionStateContext
|
||||
jest.mock('../../../context/ExtensionStateContext');
|
||||
jest.mock("../../../context/ExtensionStateContext")
|
||||
|
||||
describe('ChatTextArea', () => {
|
||||
const defaultProps = {
|
||||
inputValue: '',
|
||||
setInputValue: jest.fn(),
|
||||
onSend: jest.fn(),
|
||||
textAreaDisabled: false,
|
||||
onSelectImages: jest.fn(),
|
||||
shouldDisableImages: false,
|
||||
placeholderText: 'Type a message...',
|
||||
selectedImages: [],
|
||||
setSelectedImages: jest.fn(),
|
||||
onHeightChange: jest.fn(),
|
||||
mode: defaultModeSlug,
|
||||
setMode: jest.fn(),
|
||||
};
|
||||
describe("ChatTextArea", () => {
|
||||
const defaultProps = {
|
||||
inputValue: "",
|
||||
setInputValue: jest.fn(),
|
||||
onSend: jest.fn(),
|
||||
textAreaDisabled: false,
|
||||
onSelectImages: jest.fn(),
|
||||
shouldDisableImages: false,
|
||||
placeholderText: "Type a message...",
|
||||
selectedImages: [],
|
||||
setSelectedImages: jest.fn(),
|
||||
onHeightChange: jest.fn(),
|
||||
mode: defaultModeSlug,
|
||||
setMode: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Default mock implementation for useExtensionState
|
||||
(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: 'anthropic',
|
||||
},
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
// Default mock implementation for useExtensionState
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: "anthropic",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('enhance prompt button', () => {
|
||||
it('should be disabled when textAreaDisabled is true', () => {
|
||||
(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
});
|
||||
describe("enhance prompt button", () => {
|
||||
it("should be disabled when textAreaDisabled is true", () => {
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
})
|
||||
|
||||
render(<ChatTextArea {...defaultProps} textAreaDisabled={true} />);
|
||||
const enhanceButton = screen.getByRole('button', { name: /enhance prompt/i });
|
||||
expect(enhanceButton).toHaveClass('disabled');
|
||||
});
|
||||
});
|
||||
render(<ChatTextArea {...defaultProps} textAreaDisabled={true} />)
|
||||
const enhanceButton = screen.getByRole("button", { name: /enhance prompt/i })
|
||||
expect(enhanceButton).toHaveClass("disabled")
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleEnhancePrompt', () => {
|
||||
it('should send message with correct configuration when clicked', () => {
|
||||
const apiConfiguration = {
|
||||
apiProvider: 'openrouter',
|
||||
apiKey: 'test-key',
|
||||
};
|
||||
describe("handleEnhancePrompt", () => {
|
||||
it("should send message with correct configuration when clicked", () => {
|
||||
const apiConfiguration = {
|
||||
apiProvider: "openrouter",
|
||||
apiKey: "test-key",
|
||||
}
|
||||
|
||||
(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration,
|
||||
});
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration,
|
||||
})
|
||||
|
||||
render(<ChatTextArea {...defaultProps} inputValue="Test prompt" />);
|
||||
|
||||
const enhanceButton = screen.getByRole('button', { name: /enhance prompt/i });
|
||||
fireEvent.click(enhanceButton);
|
||||
render(<ChatTextArea {...defaultProps} inputValue="Test prompt" />)
|
||||
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: 'enhancePrompt',
|
||||
text: 'Test prompt',
|
||||
});
|
||||
});
|
||||
const enhanceButton = screen.getByRole("button", { name: /enhance prompt/i })
|
||||
fireEvent.click(enhanceButton)
|
||||
|
||||
it('should not send message when input is empty', () => {
|
||||
(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter',
|
||||
},
|
||||
});
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
type: "enhancePrompt",
|
||||
text: "Test prompt",
|
||||
})
|
||||
})
|
||||
|
||||
render(<ChatTextArea {...defaultProps} inputValue="" />);
|
||||
|
||||
const enhanceButton = screen.getByRole('button', { name: /enhance prompt/i });
|
||||
fireEvent.click(enhanceButton);
|
||||
it("should not send message when input is empty", () => {
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: "openrouter",
|
||||
},
|
||||
})
|
||||
|
||||
expect(mockPostMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
render(<ChatTextArea {...defaultProps} inputValue="" />)
|
||||
|
||||
it('should show loading state while enhancing', () => {
|
||||
(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter',
|
||||
},
|
||||
});
|
||||
const enhanceButton = screen.getByRole("button", { name: /enhance prompt/i })
|
||||
fireEvent.click(enhanceButton)
|
||||
|
||||
render(<ChatTextArea {...defaultProps} inputValue="Test prompt" />);
|
||||
|
||||
const enhanceButton = screen.getByRole('button', { name: /enhance prompt/i });
|
||||
fireEvent.click(enhanceButton);
|
||||
expect(mockPostMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
const loadingSpinner = screen.getByText('', { selector: '.codicon-loading' });
|
||||
expect(loadingSpinner).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it("should show loading state while enhancing", () => {
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: "openrouter",
|
||||
},
|
||||
})
|
||||
|
||||
describe('effect dependencies', () => {
|
||||
it('should update when apiConfiguration changes', () => {
|
||||
const { rerender } = render(<ChatTextArea {...defaultProps} />);
|
||||
render(<ChatTextArea {...defaultProps} inputValue="Test prompt" />)
|
||||
|
||||
// Update apiConfiguration
|
||||
(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: 'openrouter',
|
||||
newSetting: 'test',
|
||||
},
|
||||
});
|
||||
const enhanceButton = screen.getByRole("button", { name: /enhance prompt/i })
|
||||
fireEvent.click(enhanceButton)
|
||||
|
||||
rerender(<ChatTextArea {...defaultProps} />);
|
||||
|
||||
// Verify the enhance button appears after apiConfiguration changes
|
||||
expect(screen.getByRole('button', { name: /enhance prompt/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
const loadingSpinner = screen.getByText("", { selector: ".codicon-loading" })
|
||||
expect(loadingSpinner).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('enhanced prompt response', () => {
|
||||
it('should update input value when receiving enhanced prompt', () => {
|
||||
const setInputValue = jest.fn();
|
||||
|
||||
render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} />);
|
||||
describe("effect dependencies", () => {
|
||||
it("should update when apiConfiguration changes", () => {
|
||||
const { rerender } = render(<ChatTextArea {...defaultProps} />)
|
||||
|
||||
// Simulate receiving enhanced prompt message
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data: {
|
||||
type: 'enhancedPrompt',
|
||||
text: 'Enhanced test prompt',
|
||||
},
|
||||
})
|
||||
);
|
||||
// Update apiConfiguration
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: "openrouter",
|
||||
newSetting: "test",
|
||||
},
|
||||
})
|
||||
|
||||
expect(setInputValue).toHaveBeenCalledWith('Enhanced test prompt');
|
||||
});
|
||||
});
|
||||
});
|
||||
rerender(<ChatTextArea {...defaultProps} />)
|
||||
|
||||
// Verify the enhance button appears after apiConfiguration changes
|
||||
expect(screen.getByRole("button", { name: /enhance prompt/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("enhanced prompt response", () => {
|
||||
it("should update input value when receiving enhanced prompt", () => {
|
||||
const setInputValue = jest.fn()
|
||||
|
||||
render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} />)
|
||||
|
||||
// Simulate receiving enhanced prompt message
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
type: "enhancedPrompt",
|
||||
text: "Enhanced test prompt",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(setInputValue).toHaveBeenCalledWith("Enhanced test prompt")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,313 +1,316 @@
|
||||
import React from 'react'
|
||||
import { render, waitFor } from '@testing-library/react'
|
||||
import ChatView from '../ChatView'
|
||||
import { ExtensionStateContextProvider } from '../../../context/ExtensionStateContext'
|
||||
import { vscode } from '../../../utils/vscode'
|
||||
import React from "react"
|
||||
import { render, waitFor } from "@testing-library/react"
|
||||
import ChatView from "../ChatView"
|
||||
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../../utils/vscode"
|
||||
|
||||
// Mock vscode API
|
||||
jest.mock('../../../utils/vscode', () => ({
|
||||
vscode: {
|
||||
postMessage: jest.fn(),
|
||||
},
|
||||
jest.mock("../../../utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock all problematic dependencies
|
||||
jest.mock('rehype-highlight', () => ({
|
||||
__esModule: true,
|
||||
default: () => () => {},
|
||||
jest.mock("rehype-highlight", () => ({
|
||||
__esModule: true,
|
||||
default: () => () => {},
|
||||
}))
|
||||
|
||||
jest.mock('hast-util-to-text', () => ({
|
||||
__esModule: true,
|
||||
default: () => '',
|
||||
jest.mock("hast-util-to-text", () => ({
|
||||
__esModule: true,
|
||||
default: () => "",
|
||||
}))
|
||||
|
||||
// Mock components that use ESM dependencies
|
||||
jest.mock('../BrowserSessionRow', () => ({
|
||||
__esModule: true,
|
||||
default: function MockBrowserSessionRow({ messages }: { messages: any[] }) {
|
||||
return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
|
||||
}
|
||||
jest.mock("../BrowserSessionRow", () => ({
|
||||
__esModule: true,
|
||||
default: function MockBrowserSessionRow({ messages }: { messages: any[] }) {
|
||||
return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('../ChatRow', () => ({
|
||||
__esModule: true,
|
||||
default: function MockChatRow({ message }: { message: any }) {
|
||||
return <div data-testid="chat-row">{JSON.stringify(message)}</div>
|
||||
}
|
||||
jest.mock("../ChatRow", () => ({
|
||||
__esModule: true,
|
||||
default: function MockChatRow({ message }: { message: any }) {
|
||||
return <div data-testid="chat-row">{JSON.stringify(message)}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('../TaskHeader', () => ({
|
||||
__esModule: true,
|
||||
default: function MockTaskHeader({ task }: { task: any }) {
|
||||
return <div data-testid="task-header">{JSON.stringify(task)}</div>
|
||||
}
|
||||
jest.mock("../TaskHeader", () => ({
|
||||
__esModule: true,
|
||||
default: function MockTaskHeader({ task }: { task: any }) {
|
||||
return <div data-testid="task-header">{JSON.stringify(task)}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('../AutoApproveMenu', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
jest.mock("../AutoApproveMenu", () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
jest.mock('../../common/CodeBlock', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
CODE_BLOCK_BG_COLOR: 'rgb(30, 30, 30)',
|
||||
jest.mock("../../common/CodeBlock", () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
CODE_BLOCK_BG_COLOR: "rgb(30, 30, 30)",
|
||||
}))
|
||||
|
||||
jest.mock('../../common/CodeAccordian', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
jest.mock("../../common/CodeAccordian", () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
jest.mock('../ContextMenu', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
jest.mock("../ContextMenu", () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
// Mock window.postMessage to trigger state hydration
|
||||
const mockPostMessage = (state: any) => {
|
||||
window.postMessage({
|
||||
type: 'state',
|
||||
state: {
|
||||
version: '1.0.0',
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
alwaysAllowExecute: false,
|
||||
autoApprovalEnabled: true,
|
||||
...state
|
||||
}
|
||||
}, '*')
|
||||
window.postMessage(
|
||||
{
|
||||
type: "state",
|
||||
state: {
|
||||
version: "1.0.0",
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
alwaysAllowExecute: false,
|
||||
autoApprovalEnabled: true,
|
||||
...state,
|
||||
},
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}
|
||||
|
||||
describe('ChatView - Auto Approval Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
describe("ChatView - Auto Approval Tests", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('auto-approves read operations when enabled', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
it("auto-approves read operations when enabled", async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
autoApprovalEnabled: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
autoApprovalEnabled: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then send the read tool ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
autoApprovalEnabled: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: 'readFile', path: 'test.txt' }),
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
// Then send the read tool ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
autoApprovalEnabled: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: "readFile", path: "test.txt" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('does not auto-approve when autoApprovalEnabled is false', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
it("does not auto-approve when autoApprovalEnabled is false", async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
autoApprovalEnabled: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
autoApprovalEnabled: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then send the read tool ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
autoApprovalEnabled: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: 'readFile', path: 'test.txt' }),
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
// Then send the read tool ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowReadOnly: true,
|
||||
autoApprovalEnabled: false,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: "readFile", path: "test.txt" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Verify no auto-approval message was sent
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
// Verify no auto-approval message was sent
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-approves write operations when enabled', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
it("auto-approves write operations when enabled", async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowWrite: true,
|
||||
autoApprovalEnabled: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowWrite: true,
|
||||
autoApprovalEnabled: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then send the write tool ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowWrite: true,
|
||||
autoApprovalEnabled: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'tool',
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: 'editedExistingFile', path: 'test.txt' }),
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
// Then send the write tool ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowWrite: true,
|
||||
autoApprovalEnabled: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: "editedExistingFile", path: "test.txt" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-approves browser actions when enabled', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
it("auto-approves browser actions when enabled", async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowBrowser: true,
|
||||
autoApprovalEnabled: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
alwaysAllowBrowser: true,
|
||||
autoApprovalEnabled: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Then send the browser action ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowBrowser: true,
|
||||
autoApprovalEnabled: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: 'browser_action_launch',
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ action: 'launch', url: 'http://example.com' }),
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
// Then send the browser action ask message
|
||||
mockPostMessage({
|
||||
alwaysAllowBrowser: true,
|
||||
autoApprovalEnabled: true,
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "browser_action_launch",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ action: "launch", url: "http://example.com" }),
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user