mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Merge remote-tracking branch 'origin/main' into new_unified
This commit is contained in:
@@ -15,7 +15,7 @@ module.exports.jest = function(config) {
|
||||
|
||||
// Configure transform ignore patterns for ES modules
|
||||
config.transformIgnorePatterns = [
|
||||
'/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6)/)'
|
||||
'/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6|vscrui)/)'
|
||||
];
|
||||
|
||||
return config;
|
||||
|
||||
15
webview-ui/package-lock.json
generated
15
webview-ui/package-lock.json
generated
@@ -31,6 +31,7 @@
|
||||
"shell-quote": "^1.8.2",
|
||||
"styled-components": "^6.1.13",
|
||||
"typescript": "^4.9.5",
|
||||
"vscrui": "^0.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -15155,6 +15156,20 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vscrui": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vscrui/-/vscrui-0.2.0.tgz",
|
||||
"integrity": "sha512-fvxZM/uIYOMN3fUbE2In+R1VrNj8PKcfAdh+Us2bJaPGuG9ySkR6xkV2aJVqXxWDX77U3v/UQGc5e7URrB52Gw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/estruyf"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-hr-time": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"shell-quote": "^1.8.2",
|
||||
"styled-components": "^6.1.13",
|
||||
"typescript": "^4.9.5",
|
||||
"vscrui": "^0.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
235
webview-ui/src/components/chat/AutoApproveMenu.tsx
Normal file
235
webview-ui/src/components/chat/AutoApproveMenu.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
interface AutoApproveAction {
|
||||
id: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
shortName: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface AutoApproveMenuProps {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const {
|
||||
alwaysAllowReadOnly,
|
||||
setAlwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
setAlwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
setAlwaysAllowExecute,
|
||||
alwaysAllowBrowser,
|
||||
setAlwaysAllowBrowser,
|
||||
alwaysAllowMcp,
|
||||
setAlwaysAllowMcp,
|
||||
alwaysApproveResubmit,
|
||||
setAlwaysApproveResubmit,
|
||||
autoApprovalEnabled,
|
||||
setAutoApprovalEnabled,
|
||||
} = useExtensionState()
|
||||
|
||||
const actions: AutoApproveAction[] = [
|
||||
{
|
||||
id: "readFiles",
|
||||
label: "Read files and directories",
|
||||
shortName: "Read",
|
||||
enabled: alwaysAllowReadOnly ?? false,
|
||||
description: "Allows access to read any file on your computer.",
|
||||
},
|
||||
{
|
||||
id: "editFiles",
|
||||
label: "Edit files",
|
||||
shortName: "Edit",
|
||||
enabled: alwaysAllowWrite ?? false,
|
||||
description: "Allows modification of any files on your computer.",
|
||||
},
|
||||
{
|
||||
id: "executeCommands",
|
||||
label: "Execute approved commands",
|
||||
shortName: "Commands",
|
||||
enabled: alwaysAllowExecute ?? false,
|
||||
description:
|
||||
"Allows execution of approved terminal commands. You can configure this in the settings panel.",
|
||||
},
|
||||
{
|
||||
id: "useBrowser",
|
||||
label: "Use the browser",
|
||||
shortName: "Browser",
|
||||
enabled: alwaysAllowBrowser ?? false,
|
||||
description: "Allows ability to launch and interact with any website in a headless browser.",
|
||||
},
|
||||
{
|
||||
id: "useMcp",
|
||||
label: "Use MCP servers",
|
||||
shortName: "MCP",
|
||||
enabled: alwaysAllowMcp ?? false,
|
||||
description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.",
|
||||
},
|
||||
{
|
||||
id: "retryRequests",
|
||||
label: "Retry failed requests",
|
||||
shortName: "Retries",
|
||||
enabled: alwaysApproveResubmit ?? false,
|
||||
description: "Automatically retry failed API requests when the provider returns an error response.",
|
||||
},
|
||||
]
|
||||
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setIsExpanded((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
const enabledActionsList = actions
|
||||
.filter((action) => action.enabled)
|
||||
.map((action) => action.shortName)
|
||||
.join(", ")
|
||||
|
||||
// Individual checkbox handlers - each one only updates its own state
|
||||
const handleReadOnlyChange = useCallback(() => {
|
||||
const newValue = !(alwaysAllowReadOnly ?? false)
|
||||
setAlwaysAllowReadOnly(newValue)
|
||||
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: newValue })
|
||||
}, [alwaysAllowReadOnly, setAlwaysAllowReadOnly])
|
||||
|
||||
const handleWriteChange = useCallback(() => {
|
||||
const newValue = !(alwaysAllowWrite ?? false)
|
||||
setAlwaysAllowWrite(newValue)
|
||||
vscode.postMessage({ type: "alwaysAllowWrite", bool: newValue })
|
||||
}, [alwaysAllowWrite, setAlwaysAllowWrite])
|
||||
|
||||
const handleExecuteChange = useCallback(() => {
|
||||
const newValue = !(alwaysAllowExecute ?? false)
|
||||
setAlwaysAllowExecute(newValue)
|
||||
vscode.postMessage({ type: "alwaysAllowExecute", bool: newValue })
|
||||
}, [alwaysAllowExecute, setAlwaysAllowExecute])
|
||||
|
||||
const handleBrowserChange = useCallback(() => {
|
||||
const newValue = !(alwaysAllowBrowser ?? false)
|
||||
setAlwaysAllowBrowser(newValue)
|
||||
vscode.postMessage({ type: "alwaysAllowBrowser", bool: newValue })
|
||||
}, [alwaysAllowBrowser, setAlwaysAllowBrowser])
|
||||
|
||||
const handleMcpChange = useCallback(() => {
|
||||
const newValue = !(alwaysAllowMcp ?? false)
|
||||
setAlwaysAllowMcp(newValue)
|
||||
vscode.postMessage({ type: "alwaysAllowMcp", bool: newValue })
|
||||
}, [alwaysAllowMcp, setAlwaysAllowMcp])
|
||||
|
||||
const handleRetryChange = useCallback(() => {
|
||||
const newValue = !(alwaysApproveResubmit ?? false)
|
||||
setAlwaysApproveResubmit(newValue)
|
||||
vscode.postMessage({ type: "alwaysApproveResubmit", bool: newValue })
|
||||
}, [alwaysApproveResubmit, setAlwaysApproveResubmit])
|
||||
|
||||
// Map action IDs to their specific handlers
|
||||
const actionHandlers: Record<AutoApproveAction['id'], () => void> = {
|
||||
readFiles: handleReadOnlyChange,
|
||||
editFiles: handleWriteChange,
|
||||
executeCommands: handleExecuteChange,
|
||||
useBrowser: handleBrowserChange,
|
||||
useMcp: handleMcpChange,
|
||||
retryRequests: handleRetryChange,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "0 15px",
|
||||
userSelect: "none",
|
||||
borderTop: isExpanded
|
||||
? `0.5px solid color-mix(in srgb, var(--vscode-titleBar-inactiveForeground) 20%, transparent)`
|
||||
: "none",
|
||||
overflowY: "auto",
|
||||
...style,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
padding: isExpanded ? "8px 0" : "8px 0 0 0",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={toggleExpanded}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<VSCodeCheckbox
|
||||
checked={autoApprovalEnabled ?? false}
|
||||
onChange={() => {
|
||||
const newValue = !(autoApprovalEnabled ?? false)
|
||||
setAutoApprovalEnabled(newValue)
|
||||
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
|
||||
}}
|
||||
/>
|
||||
</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",
|
||||
flex: 1,
|
||||
minWidth: 0
|
||||
}}>
|
||||
{enabledActionsList || "None"}
|
||||
</span>
|
||||
<span
|
||||
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
marginLeft: isExpanded ? "2px" : "-2px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div style={{ padding: "0" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "10px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
Auto-approve allows Cline to perform actions without asking for permission. Only enable for
|
||||
actions you fully trust.
|
||||
</div>
|
||||
{actions.map((action) => (
|
||||
<div key={action.id} style={{ margin: "6px 0" }}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<VSCodeCheckbox
|
||||
checked={action.enabled}
|
||||
onChange={actionHandlers[action.id]}>
|
||||
{action.label}
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginLeft: "28px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
{action.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AutoApproveMenu
|
||||
@@ -527,7 +527,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
backgroundColor: "var(--vscode-input-background)",
|
||||
minHeight: "100px",
|
||||
margin: "10px 15px",
|
||||
padding: "8px"
|
||||
}}
|
||||
@@ -652,7 +651,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={placeholderText}
|
||||
minRows={4}
|
||||
minRows={2}
|
||||
maxRows={20}
|
||||
autoFocus={true}
|
||||
style={{
|
||||
|
||||
@@ -25,6 +25,7 @@ import BrowserSessionRow from "./BrowserSessionRow"
|
||||
import ChatRow from "./ChatRow"
|
||||
import ChatTextArea from "./ChatTextArea"
|
||||
import TaskHeader from "./TaskHeader"
|
||||
import AutoApproveMenu from "./AutoApproveMenu"
|
||||
import { AudioType } from "../../../../src/shared/WebviewMessage"
|
||||
import { validateCommand } from "../../utils/command-validation"
|
||||
|
||||
@@ -38,7 +39,7 @@ interface ChatViewProps {
|
||||
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||
|
||||
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
|
||||
const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands, writeDelayMs, mode, setMode } = 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)
|
||||
@@ -528,7 +529,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
|
||||
const isAutoApproved = useCallback(
|
||||
(message: ClineMessage | undefined) => {
|
||||
if (!message || message.type !== "ask") return false
|
||||
if (!autoApprovalEnabled || !message || message.type !== "ask") return false
|
||||
|
||||
return (
|
||||
(alwaysAllowBrowser && message.ask === "browser_action_launch") ||
|
||||
@@ -538,17 +539,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
|
||||
)
|
||||
},
|
||||
[
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowMcp,
|
||||
isReadOnlyToolAction,
|
||||
isWriteToolAction,
|
||||
isAllowedCommand,
|
||||
isMcpToolAlwaysAllowed
|
||||
]
|
||||
[autoApprovalEnabled, alwaysAllowBrowser, alwaysAllowReadOnly, isReadOnlyToolAction, alwaysAllowWrite, isWriteToolAction, alwaysAllowExecute, isAllowedCommand, alwaysAllowMcp, isMcpToolAlwaysAllowed]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -866,10 +857,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flex: "1 1 0", // flex-grow: 1, flex-shrink: 1, flex-basis: 0
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
paddingBottom: "10px",
|
||||
}}>
|
||||
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
|
||||
<div style={{ padding: "0 20px", flexShrink: 0 }}>
|
||||
@@ -885,6 +878,32 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*
|
||||
// Flex layout explanation:
|
||||
// 1. Content div above uses flex: "1 1 0" to:
|
||||
// - Grow to fill available space (flex-grow: 1)
|
||||
// - Shrink when AutoApproveMenu needs space (flex-shrink: 1)
|
||||
// - Start from zero size (flex-basis: 0) to ensure proper distribution
|
||||
// minHeight: 0 allows it to shrink below its content height
|
||||
//
|
||||
// 2. AutoApproveMenu uses flex: "0 1 auto" to:
|
||||
// - Not grow beyond its content (flex-grow: 0)
|
||||
// - Shrink when viewport is small (flex-shrink: 1)
|
||||
// - Use its content size as basis (flex-basis: auto)
|
||||
// This ensures it takes its natural height when there's space
|
||||
// but becomes scrollable when the viewport is too small
|
||||
*/}
|
||||
{!task && (
|
||||
<AutoApproveMenu
|
||||
style={{
|
||||
marginBottom: -2,
|
||||
flex: "0 1 auto", // flex-grow: 0, flex-shrink: 1, flex-basis: auto
|
||||
minHeight: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{task && (
|
||||
<>
|
||||
<div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}>
|
||||
@@ -914,6 +933,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
initialTopMostItemIndex={groupedMessages.length - 1}
|
||||
/>
|
||||
</div>
|
||||
<AutoApproveMenu />
|
||||
{showScrollToBottom ? (
|
||||
<div
|
||||
style={{
|
||||
@@ -938,7 +958,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
: 0.5
|
||||
: 0,
|
||||
display: "flex",
|
||||
padding: "10px 15px 0px 15px",
|
||||
padding: `${primaryButtonText || secondaryButtonText || isStreaming ? "10" : "0"}px 15px 0px 15px`,
|
||||
}}>
|
||||
{primaryButtonText && !isStreaming && (
|
||||
<VSCodeButton
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { render, fireEvent, screen } from "@testing-library/react"
|
||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
import AutoApproveMenu from "../AutoApproveMenu"
|
||||
import { codeMode, defaultPrompts } from "../../../../../src/shared/modes"
|
||||
|
||||
// Mock the ExtensionStateContext hook
|
||||
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: [],
|
||||
|
||||
// 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(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseExtensionState.mockReturnValue(defaultMockState)
|
||||
})
|
||||
|
||||
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("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()
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,313 @@
|
||||
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(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock all problematic dependencies
|
||||
jest.mock('rehype-highlight', () => ({
|
||||
__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('../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('../AutoApproveMenu', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
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('../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
|
||||
}
|
||||
}, '*')
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 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>
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 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>
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 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>
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Wait for the auto-approval message
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -46,6 +46,11 @@ jest.mock('../ChatRow', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
jest.mock('../AutoApproveMenu', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
onSend: (value: string) => void;
|
||||
inputValue?: string;
|
||||
@@ -139,6 +144,89 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('does not auto-approve any actions when autoApprovalEnabled is false', () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ChatView
|
||||
isHidden={false}
|
||||
showAnnouncement={false}
|
||||
hideAnnouncement={() => {}}
|
||||
showHistoryView={() => {}}
|
||||
/>
|
||||
</ExtensionStateContextProvider>
|
||||
)
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: false,
|
||||
alwaysAllowBrowser: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Test various types of actions that should not be auto-approved
|
||||
const testCases = [
|
||||
{
|
||||
ask: 'browser_action_launch',
|
||||
text: JSON.stringify({ action: 'launch', url: 'http://example.com' })
|
||||
},
|
||||
{
|
||||
ask: 'tool',
|
||||
text: JSON.stringify({ tool: 'readFile', path: 'test.txt' })
|
||||
},
|
||||
{
|
||||
ask: 'tool',
|
||||
text: JSON.stringify({ tool: 'editedExistingFile', path: 'test.txt' })
|
||||
},
|
||||
{
|
||||
ask: 'command',
|
||||
text: 'npm test'
|
||||
}
|
||||
]
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: false,
|
||||
alwaysAllowBrowser: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
{
|
||||
type: 'say',
|
||||
say: 'task',
|
||||
ts: Date.now() - 2000,
|
||||
text: 'Initial task'
|
||||
},
|
||||
{
|
||||
type: 'ask',
|
||||
ask: testCase.ask,
|
||||
ts: Date.now(),
|
||||
text: testCase.text,
|
||||
partial: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Verify no auto-approval message was sent
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: 'askResponse',
|
||||
askResponse: 'yesButtonClicked'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-approves browser actions when alwaysAllowBrowser is enabled', async () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
@@ -153,6 +241,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowBrowser: true,
|
||||
clineMessages: [
|
||||
{
|
||||
@@ -166,6 +255,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// Then send the browser action ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowBrowser: true,
|
||||
clineMessages: [
|
||||
{
|
||||
@@ -207,6 +297,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
clineMessages: [
|
||||
{
|
||||
@@ -220,6 +311,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// Then send the read-only tool ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowReadOnly: true,
|
||||
clineMessages: [
|
||||
{
|
||||
@@ -262,6 +354,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowWrite: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
@@ -276,6 +369,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// Then send the write tool ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowWrite: true,
|
||||
writeDelayMs: 0,
|
||||
clineMessages: [
|
||||
@@ -318,6 +412,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowWrite: true,
|
||||
clineMessages: [
|
||||
{
|
||||
@@ -331,6 +426,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// Then send a non-tool write operation message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowWrite: true,
|
||||
clineMessages: [
|
||||
{
|
||||
@@ -371,6 +467,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
@@ -385,6 +482,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// Then send the command ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
@@ -427,6 +525,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
@@ -441,6 +540,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// Then send the disallowed command ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test'],
|
||||
clineMessages: [
|
||||
@@ -498,6 +598,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// First hydrate state with initial task
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test', 'npm run build', 'echo', 'Select-String'],
|
||||
clineMessages: [
|
||||
@@ -512,6 +613,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// Then send the chained command ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test', 'npm run build', 'echo', 'Select-String'],
|
||||
clineMessages: [
|
||||
@@ -585,6 +687,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
|
||||
// Then send the chained command ask message
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test', 'Select-String'],
|
||||
clineMessages: [
|
||||
@@ -643,6 +746,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test', 'Select-String'],
|
||||
clineMessages: [
|
||||
@@ -656,6 +760,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
})
|
||||
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test', 'Select-String'],
|
||||
clineMessages: [
|
||||
@@ -688,6 +793,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test', 'Select-String'],
|
||||
clineMessages: [
|
||||
@@ -701,6 +807,7 @@ describe('ChatView - Auto Approval Tests', () => {
|
||||
})
|
||||
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowExecute: true,
|
||||
allowedCommands: ['npm test', 'Select-String'],
|
||||
clineMessages: [
|
||||
@@ -748,6 +855,7 @@ describe('ChatView - Sound Playing Tests', () => {
|
||||
|
||||
// First hydrate state with initial task and streaming
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowBrowser: true,
|
||||
clineMessages: [
|
||||
{
|
||||
@@ -768,6 +876,7 @@ describe('ChatView - Sound Playing Tests', () => {
|
||||
|
||||
// Then send the browser action ask message (streaming finished)
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowBrowser: true,
|
||||
clineMessages: [
|
||||
{
|
||||
@@ -807,6 +916,7 @@ describe('ChatView - Sound Playing Tests', () => {
|
||||
|
||||
// First hydrate state with initial task and streaming
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowBrowser: false,
|
||||
clineMessages: [
|
||||
{
|
||||
@@ -827,6 +937,7 @@ describe('ChatView - Sound Playing Tests', () => {
|
||||
|
||||
// Then send the browser action ask message (streaming finished)
|
||||
mockPostMessage({
|
||||
autoApprovalEnabled: true,
|
||||
alwaysAllowBrowser: false,
|
||||
clineMessages: [
|
||||
{
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Checkbox, Dropdown } from "vscrui"
|
||||
import type { DropdownOption } from "vscrui"
|
||||
import {
|
||||
VSCodeCheckbox,
|
||||
VSCodeDropdown,
|
||||
VSCodeLink,
|
||||
VSCodeOption,
|
||||
VSCodeRadio,
|
||||
VSCodeRadioGroup,
|
||||
VSCodeTextField,
|
||||
VSCodeTextField
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useEvent, useInterval } from "react-use"
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import * as vscodemodels from "vscode"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import OpenRouterModelPicker, {
|
||||
ModelDescriptionMarkdown,
|
||||
@@ -51,6 +51,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
const { apiConfiguration, setApiConfiguration, uriScheme, onUpdateApiConfig } = useExtensionState()
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
|
||||
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
|
||||
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
@@ -71,54 +72,48 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
|
||||
} else if (selectedProvider === "lmstudio") {
|
||||
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
|
||||
} else if (selectedProvider === "vscode-lm") {
|
||||
vscode.postMessage({ type: "requestVsCodeLmModels" })
|
||||
}
|
||||
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
|
||||
useEffect(() => {
|
||||
if (selectedProvider === "ollama" || selectedProvider === "lmstudio") {
|
||||
if (selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm") {
|
||||
requestLocalModels()
|
||||
}
|
||||
}, [selectedProvider, requestLocalModels])
|
||||
useInterval(requestLocalModels, selectedProvider === "ollama" || selectedProvider === "lmstudio" ? 2000 : null)
|
||||
|
||||
useInterval(requestLocalModels, selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm" ? 2000 : null)
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
if (message.type === "ollamaModels" && message.ollamaModels) {
|
||||
setOllamaModels(message.ollamaModels)
|
||||
} else if (message.type === "lmStudioModels" && message.lmStudioModels) {
|
||||
setLmStudioModels(message.lmStudioModels)
|
||||
} else if (message.type === "vsCodeLmModels" && message.vsCodeLmModels) {
|
||||
setVsCodeLmModels(message.vsCodeLmModels)
|
||||
}
|
||||
}, [])
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
/*
|
||||
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
|
||||
|
||||
In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
|
||||
|
||||
As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
|
||||
*/
|
||||
const createDropdown = (models: Record<string, ModelInfo>) => {
|
||||
const options: DropdownOption[] = [
|
||||
{ value: "", label: "Select a model..." },
|
||||
...Object.keys(models).map((modelId) => ({
|
||||
value: modelId,
|
||||
label: modelId,
|
||||
}))
|
||||
]
|
||||
return (
|
||||
<VSCodeDropdown
|
||||
<Dropdown
|
||||
id="model-id"
|
||||
value={selectedModelId}
|
||||
onChange={handleInputChange("apiModelId")}
|
||||
style={{ width: "100%" }}>
|
||||
<VSCodeOption value="">Select a model...</VSCodeOption>
|
||||
{Object.keys(models).map((modelId) => (
|
||||
<VSCodeOption
|
||||
key={modelId}
|
||||
value={modelId}
|
||||
style={{
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
maxWidth: "100%",
|
||||
}}>
|
||||
{modelId}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
onChange={(value: unknown) => {handleInputChange("apiModelId")({
|
||||
target: {
|
||||
value: (value as DropdownOption).value
|
||||
}
|
||||
})}}
|
||||
style={{ width: "100%" }}
|
||||
options={options}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -128,23 +123,32 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
<label htmlFor="api-provider">
|
||||
<span style={{ fontWeight: 500 }}>API Provider</span>
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
<Dropdown
|
||||
id="api-provider"
|
||||
value={selectedProvider}
|
||||
onChange={handleInputChange("apiProvider")}
|
||||
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}>
|
||||
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
|
||||
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
|
||||
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
|
||||
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
|
||||
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
|
||||
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
|
||||
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
|
||||
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
|
||||
<VSCodeOption value="glama">Glama</VSCodeOption>
|
||||
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
|
||||
<VSCodeOption value="ollama">Ollama</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
onChange={(value: unknown) => {
|
||||
handleInputChange("apiProvider")({
|
||||
target: {
|
||||
value: (value as DropdownOption).value
|
||||
}
|
||||
})
|
||||
}}
|
||||
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}
|
||||
options={[
|
||||
{ value: "openrouter", label: "OpenRouter" },
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
{ value: "gemini", label: "Google Gemini" },
|
||||
{ value: "deepseek", label: "DeepSeek" },
|
||||
{ value: "openai-native", label: "OpenAI" },
|
||||
{ value: "openai", label: "OpenAI Compatible" },
|
||||
{ value: "vertex", label: "GCP Vertex AI" },
|
||||
{ value: "bedrock", label: "AWS Bedrock" },
|
||||
{ value: "glama", label: "Glama" },
|
||||
{ value: "vscode-lm", label: "VS Code LM API" },
|
||||
{ value: "lmstudio", label: "LM Studio" },
|
||||
{ value: "ollama", label: "Ollama" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedProvider === "anthropic" && (
|
||||
@@ -158,17 +162,16 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
<span style={{ fontWeight: 500 }}>Anthropic API Key</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<VSCodeCheckbox
|
||||
<Checkbox
|
||||
checked={anthropicBaseUrlSelected}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
setAnthropicBaseUrlSelected(isChecked)
|
||||
if (!isChecked) {
|
||||
onChange={(checked: boolean) => {
|
||||
setAnthropicBaseUrlSelected(checked)
|
||||
if (!checked) {
|
||||
setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" })
|
||||
}
|
||||
}}>
|
||||
Use custom base URL
|
||||
</VSCodeCheckbox>
|
||||
</Checkbox>
|
||||
|
||||
{anthropicBaseUrlSelected && (
|
||||
<VSCodeTextField
|
||||
@@ -209,11 +212,12 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
<span style={{ fontWeight: 500 }}>Glama API Key</span>
|
||||
</VSCodeTextField>
|
||||
{!apiConfiguration?.glamaApiKey && (
|
||||
<VSCodeLink
|
||||
href="https://glama.ai/settings/api-keys"
|
||||
style={{ display: "inline", fontSize: "inherit" }}>
|
||||
You can get an Glama API key by signing up here.
|
||||
</VSCodeLink>
|
||||
<VSCodeButtonLink
|
||||
href={getGlamaAuthUrl(uriScheme)}
|
||||
style={{ margin: "5px 0 0 0" }}
|
||||
appearance="secondary">
|
||||
Get Glama API Key
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
@@ -286,15 +290,16 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
</span>
|
||||
)} */}
|
||||
</p>
|
||||
<VSCodeCheckbox
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openRouterUseMiddleOutTransform || false}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
setApiConfiguration({ ...apiConfiguration, openRouterUseMiddleOutTransform: isChecked })
|
||||
onChange={(checked: boolean) => {
|
||||
handleInputChange("openRouterUseMiddleOutTransform")({
|
||||
target: { value: checked },
|
||||
})
|
||||
}}>
|
||||
Compress prompts and message chains to the context size (<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>)
|
||||
</VSCodeCheckbox>
|
||||
<br/>
|
||||
</Checkbox>
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -328,45 +333,44 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
<label htmlFor="aws-region-dropdown">
|
||||
<span style={{ fontWeight: 500 }}>AWS Region</span>
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
<Dropdown
|
||||
id="aws-region-dropdown"
|
||||
value={apiConfiguration?.awsRegion || ""}
|
||||
style={{ width: "100%" }}
|
||||
onChange={handleInputChange("awsRegion")}>
|
||||
<VSCodeOption value="">Select a region...</VSCodeOption>
|
||||
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
|
||||
<VSCodeOption value="us-east-1">us-east-1</VSCodeOption>
|
||||
<VSCodeOption value="us-east-2">us-east-2</VSCodeOption>
|
||||
{/* <VSCodeOption value="us-west-1">us-west-1</VSCodeOption> */}
|
||||
<VSCodeOption value="us-west-2">us-west-2</VSCodeOption>
|
||||
{/* <VSCodeOption value="af-south-1">af-south-1</VSCodeOption> */}
|
||||
{/* <VSCodeOption value="ap-east-1">ap-east-1</VSCodeOption> */}
|
||||
<VSCodeOption value="ap-south-1">ap-south-1</VSCodeOption>
|
||||
<VSCodeOption value="ap-northeast-1">ap-northeast-1</VSCodeOption>
|
||||
<VSCodeOption value="ap-northeast-2">ap-northeast-2</VSCodeOption>
|
||||
{/* <VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption> */}
|
||||
<VSCodeOption value="ap-southeast-1">ap-southeast-1</VSCodeOption>
|
||||
<VSCodeOption value="ap-southeast-2">ap-southeast-2</VSCodeOption>
|
||||
<VSCodeOption value="ca-central-1">ca-central-1</VSCodeOption>
|
||||
<VSCodeOption value="eu-central-1">eu-central-1</VSCodeOption>
|
||||
<VSCodeOption value="eu-west-1">eu-west-1</VSCodeOption>
|
||||
<VSCodeOption value="eu-west-2">eu-west-2</VSCodeOption>
|
||||
<VSCodeOption value="eu-west-3">eu-west-3</VSCodeOption>
|
||||
{/* <VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption> */}
|
||||
{/* <VSCodeOption value="me-south-1">me-south-1</VSCodeOption> */}
|
||||
<VSCodeOption value="sa-east-1">sa-east-1</VSCodeOption>
|
||||
<VSCodeOption value="us-gov-west-1">us-gov-west-1</VSCodeOption>
|
||||
{/* <VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption> */}
|
||||
</VSCodeDropdown>
|
||||
onChange={(value: unknown) => {handleInputChange("awsRegion")({
|
||||
target: {
|
||||
value: (value as DropdownOption).value
|
||||
}
|
||||
})}}
|
||||
options={[
|
||||
{ value: "", label: "Select a region..." },
|
||||
{ value: "us-east-1", label: "us-east-1" },
|
||||
{ value: "us-east-2", label: "us-east-2" },
|
||||
{ value: "us-west-2", label: "us-west-2" },
|
||||
{ value: "ap-south-1", label: "ap-south-1" },
|
||||
{ value: "ap-northeast-1", label: "ap-northeast-1" },
|
||||
{ value: "ap-northeast-2", label: "ap-northeast-2" },
|
||||
{ value: "ap-southeast-1", label: "ap-southeast-1" },
|
||||
{ value: "ap-southeast-2", label: "ap-southeast-2" },
|
||||
{ value: "ca-central-1", label: "ca-central-1" },
|
||||
{ value: "eu-central-1", label: "eu-central-1" },
|
||||
{ value: "eu-west-1", label: "eu-west-1" },
|
||||
{ value: "eu-west-2", label: "eu-west-2" },
|
||||
{ value: "eu-west-3", label: "eu-west-3" },
|
||||
{ value: "sa-east-1", label: "sa-east-1" },
|
||||
{ value: "us-gov-west-1", label: "us-gov-west-1" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<VSCodeCheckbox
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.awsUseCrossRegionInference || false}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
setApiConfiguration({ ...apiConfiguration, awsUseCrossRegionInference: isChecked })
|
||||
onChange={(checked: boolean) => {
|
||||
handleInputChange("awsUseCrossRegionInference")({
|
||||
target: { value: checked },
|
||||
})
|
||||
}}>
|
||||
Use cross-region inference
|
||||
</VSCodeCheckbox>
|
||||
</Checkbox>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
@@ -393,18 +397,24 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
<label htmlFor="vertex-region-dropdown">
|
||||
<span style={{ fontWeight: 500 }}>Google Cloud Region</span>
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
<Dropdown
|
||||
id="vertex-region-dropdown"
|
||||
value={apiConfiguration?.vertexRegion || ""}
|
||||
style={{ width: "100%" }}
|
||||
onChange={handleInputChange("vertexRegion")}>
|
||||
<VSCodeOption value="">Select a region...</VSCodeOption>
|
||||
<VSCodeOption value="us-east5">us-east5</VSCodeOption>
|
||||
<VSCodeOption value="us-central1">us-central1</VSCodeOption>
|
||||
<VSCodeOption value="europe-west1">europe-west1</VSCodeOption>
|
||||
<VSCodeOption value="europe-west4">europe-west4</VSCodeOption>
|
||||
<VSCodeOption value="asia-southeast1">asia-southeast1</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
onChange={(value: unknown) => {handleInputChange("vertexRegion")({
|
||||
target: {
|
||||
value: (value as DropdownOption).value
|
||||
}
|
||||
})}}
|
||||
options={[
|
||||
{ value: "", label: "Select a region..." },
|
||||
{ value: "us-east5", label: "us-east5" },
|
||||
{ value: "us-central1", label: "us-central1" },
|
||||
{ value: "europe-west1", label: "europe-west1" },
|
||||
{ value: "europe-west4", label: "europe-west4" },
|
||||
{ value: "asia-southeast1", label: "asia-southeast1" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
@@ -477,29 +487,26 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
</VSCodeTextField>
|
||||
<OpenAiModelPicker />
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<VSCodeCheckbox
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openAiStreamingEnabled ?? true}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked
|
||||
setApiConfiguration({
|
||||
...apiConfiguration,
|
||||
openAiStreamingEnabled: isChecked
|
||||
onChange={(checked: boolean) => {
|
||||
handleInputChange("openAiStreamingEnabled")({
|
||||
target: { value: checked },
|
||||
})
|
||||
}}>
|
||||
Enable streaming
|
||||
</VSCodeCheckbox>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<VSCodeCheckbox
|
||||
<Checkbox
|
||||
checked={azureApiVersionSelected}
|
||||
onChange={(e: any) => {
|
||||
const isChecked = e.target.checked === true
|
||||
setAzureApiVersionSelected(isChecked)
|
||||
if (!isChecked) {
|
||||
onChange={(checked: boolean) => {
|
||||
setAzureApiVersionSelected(checked)
|
||||
if (!checked) {
|
||||
setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" })
|
||||
}
|
||||
}}>
|
||||
Set Azure API version
|
||||
</VSCodeCheckbox>
|
||||
</Checkbox>
|
||||
{azureApiVersionSelected && (
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.azureApiVersion || ""}
|
||||
@@ -619,6 +626,63 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "vscode-lm" && (
|
||||
<div>
|
||||
<div className="dropdown-container">
|
||||
<label htmlFor="vscode-lm-model">
|
||||
<span style={{ fontWeight: 500 }}>Language Model</span>
|
||||
</label>
|
||||
{vsCodeLmModels.length > 0 ? (
|
||||
<Dropdown
|
||||
id="vscode-lm-model"
|
||||
value={apiConfiguration?.vsCodeLmModelSelector ?
|
||||
`${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` :
|
||||
""}
|
||||
onChange={(value: unknown) => {
|
||||
const valueStr = (value as DropdownOption)?.value;
|
||||
if (!valueStr) {
|
||||
return
|
||||
}
|
||||
const [vendor, family] = valueStr.split('/');
|
||||
handleInputChange("vsCodeLmModelSelector")({
|
||||
target: {
|
||||
value: { vendor, family }
|
||||
}
|
||||
})
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
options={[
|
||||
{ value: "", label: "Select a model..." },
|
||||
...vsCodeLmModels.map((model) => ({
|
||||
value: `${model.vendor}/${model.family}`,
|
||||
label: `${model.vendor} - ${model.family}`
|
||||
}))
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<p style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
The VS Code Language Model API allows you to run models provided by other VS Code extensions (including but not limited to GitHub Copilot).
|
||||
The easiest way to get started is to install the Copilot and Copilot Chat extensions from the VS Code Marketplace.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-errorForeground)",
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
Note: This is a very experimental integration and may not work as expected. Please report any issues to the Roo-Cline GitHub repository.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "ollama" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
@@ -739,8 +803,14 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
)
|
||||
}
|
||||
|
||||
export function getGlamaAuthUrl(uriScheme?: string) {
|
||||
const callbackUrl = `${uriScheme || "vscode"}://rooveterinaryinc.roo-cline/glama`
|
||||
|
||||
return `https://glama.ai/oauth/authorize?callback_url=${encodeURIComponent(callbackUrl)}`
|
||||
}
|
||||
|
||||
export function getOpenRouterAuthUrl(uriScheme?: string) {
|
||||
return `https://openrouter.ai/auth?callback_url=${uriScheme || "vscode"}://saoudrizwan.claude-dev/openrouter`
|
||||
return `https://openrouter.ai/auth?callback_url=${uriScheme || "vscode"}://rooveterinaryinc.roo-cline/openrouter`
|
||||
}
|
||||
|
||||
export const formatPrice = (price: number) => {
|
||||
@@ -932,6 +1002,17 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
|
||||
selectedModelId: apiConfiguration?.lmStudioModelId || "",
|
||||
selectedModelInfo: openAiModelInfoSaneDefaults,
|
||||
}
|
||||
case "vscode-lm":
|
||||
return {
|
||||
selectedProvider: provider,
|
||||
selectedModelId: apiConfiguration?.vsCodeLmModelSelector ?
|
||||
`${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}` :
|
||||
"",
|
||||
selectedModelInfo: {
|
||||
...openAiModelInfoSaneDefaults,
|
||||
supportsImages: false, // VSCode LM API currently doesn't support images
|
||||
},
|
||||
}
|
||||
default:
|
||||
return getProviderData(anthropicModels, anthropicDefaultModelId)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import debounce from "debounce"
|
||||
import { Fzf } from "fzf"
|
||||
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
@@ -44,8 +45,24 @@ const GlamaModelPicker: React.FC = () => {
|
||||
}
|
||||
}, [apiConfiguration, searchTerm])
|
||||
|
||||
const debouncedRefreshModels = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
() => {
|
||||
vscode.postMessage({ type: "refreshGlamaModels" })
|
||||
},
|
||||
50
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
useMount(() => {
|
||||
vscode.postMessage({ type: "refreshGlamaModels" })
|
||||
debouncedRefreshModels()
|
||||
|
||||
// Cleanup debounced function
|
||||
return () => {
|
||||
debouncedRefreshModels.clear()
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Fzf } from "fzf"
|
||||
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import debounce from "debounce"
|
||||
import { useRemark } from "react-remark"
|
||||
import styled from "styled-components"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
@@ -34,18 +35,38 @@ const OpenAiModelPicker: React.FC = () => {
|
||||
}
|
||||
}, [apiConfiguration, searchTerm])
|
||||
|
||||
const debouncedRefreshModels = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
(baseUrl: string, apiKey: string) => {
|
||||
vscode.postMessage({
|
||||
type: "refreshOpenAiModels",
|
||||
values: {
|
||||
baseUrl,
|
||||
apiKey
|
||||
}
|
||||
})
|
||||
},
|
||||
50
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
|
||||
return
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "refreshOpenAiModels", values: {
|
||||
baseUrl: apiConfiguration?.openAiBaseUrl,
|
||||
apiKey: apiConfiguration?.openAiApiKey
|
||||
}
|
||||
})
|
||||
}, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey])
|
||||
debouncedRefreshModels(
|
||||
apiConfiguration.openAiBaseUrl,
|
||||
apiConfiguration.openAiApiKey
|
||||
)
|
||||
|
||||
// Cleanup debounced function
|
||||
return () => {
|
||||
debouncedRefreshModels.clear()
|
||||
}
|
||||
}, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, debouncedRefreshModels])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import debounce from "debounce"
|
||||
import { Fzf } from "fzf"
|
||||
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
@@ -43,8 +44,24 @@ const OpenRouterModelPicker: React.FC = () => {
|
||||
}
|
||||
}, [apiConfiguration, searchTerm])
|
||||
|
||||
const debouncedRefreshModels = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
() => {
|
||||
vscode.postMessage({ type: "refreshOpenRouterModels" })
|
||||
},
|
||||
50
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
useMount(() => {
|
||||
vscode.postMessage({ type: "refreshOpenRouterModels" })
|
||||
debouncedRefreshModels()
|
||||
|
||||
// Cleanup debounced function
|
||||
return () => {
|
||||
debouncedRefreshModels.clear()
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -63,8 +63,10 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
setCustomPrompts: (value: CustomPrompts) => void
|
||||
enhancementApiConfigId?: string
|
||||
setEnhancementApiConfigId: (value: string) => void
|
||||
experimentalDiffStrategy: boolean
|
||||
experimentalDiffStrategy: boolean
|
||||
setExperimentalDiffStrategy: (value: boolean) => void
|
||||
autoApprovalEnabled?: boolean
|
||||
setAutoApprovalEnabled: (value: boolean) => void
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
@@ -93,7 +95,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
mode: codeMode,
|
||||
customPrompts: defaultPrompts,
|
||||
enhancementApiConfigId: '',
|
||||
experimentalDiffStrategy: false,
|
||||
experimentalDiffStrategy: false,
|
||||
autoApprovalEnabled: false,
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
@@ -124,11 +127,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "state": {
|
||||
const newState = message.state!
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
...message.state!
|
||||
...newState
|
||||
}))
|
||||
const config = message.state?.apiConfiguration
|
||||
const config = newState.apiConfiguration
|
||||
const hasKey = checkExistKey(config)
|
||||
setShowWelcome(!hasKey)
|
||||
setDidHydrateState(true)
|
||||
@@ -210,6 +214,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||
writeDelayMs: state.writeDelayMs,
|
||||
screenshotQuality: state.screenshotQuality,
|
||||
experimentalDiffStrategy: state.experimentalDiffStrategy ?? false,
|
||||
setApiConfiguration: (value) => setState((prevState) => ({
|
||||
...prevState,
|
||||
apiConfiguration: value
|
||||
@@ -240,8 +245,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
|
||||
setCustomPrompts: (value) => setState((prevState) => ({ ...prevState, customPrompts: value })),
|
||||
setEnhancementApiConfigId: (value) => setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
|
||||
experimentalDiffStrategy: state.experimentalDiffStrategy ?? false,
|
||||
setExperimentalDiffStrategy: (value) => setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value }))
|
||||
setExperimentalDiffStrategy: (value) => setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value })),
|
||||
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Mock window.matchMedia
|
||||
// Mock crypto.getRandomValues
|
||||
Object.defineProperty(window, 'crypto', {
|
||||
value: {
|
||||
getRandomValues: function(buffer: Uint8Array) {
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
buffer[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Mock matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // Deprecated
|
||||
removeListener: jest.fn(), // Deprecated
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
|
||||
@@ -57,6 +57,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
|
||||
return "You must provide a valid model ID."
|
||||
}
|
||||
break
|
||||
case "vscode-lm":
|
||||
if (!apiConfiguration.vsCodeLmModelSelector) {
|
||||
return "You must provide a valid model selector."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
Reference in New Issue
Block a user