Prettier backfill

This commit is contained in:
Matt Rubens
2025-01-17 14:11:28 -05:00
parent 3bcb4ff8c5
commit 60a0a824b9
174 changed files with 15715 additions and 15428 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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} />}

View File

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

View File

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

View File

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

View File

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