mirror of
https://github.com/cline/cline.git
synced 2026-09-04 11:44:01 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a981babae | |||
| 6e3bc1d788 | |||
| 8347054d52 | |||
| 8e3a50a002 | |||
| 558a629bd4 |
Generated
+5334
-3637
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,22 @@ export abstract class WebviewProvider {
|
||||
*/
|
||||
abstract isVisible(): boolean
|
||||
|
||||
/**
|
||||
* Checks if this platform supports opening settings in a separate window.
|
||||
*
|
||||
* @returns True if the platform can open settings in a separate window, false otherwise
|
||||
*/
|
||||
abstract canOpenSettingsInSeparateWindow(): boolean
|
||||
|
||||
/**
|
||||
* Opens the settings view in a separate window/panel.
|
||||
* If the window is already open, it will be focused.
|
||||
* If the platform doesn't support this feature, it should fall back to in-pane navigation.
|
||||
*
|
||||
* @returns A promise that resolves when the window has been opened or focused
|
||||
*/
|
||||
abstract openSettingsInSeparateWindow(): Promise<void>
|
||||
|
||||
/**
|
||||
* Defines and returns the HTML that should be rendered within the webview panel.
|
||||
*
|
||||
|
||||
+9
-2
@@ -126,8 +126,15 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.SettingsButton, () => {
|
||||
sendSettingsButtonClickedEvent()
|
||||
vscode.commands.registerCommand(commands.SettingsButton, async () => {
|
||||
// Check if the webview provider supports opening settings in a separate window
|
||||
const provider = WebviewProvider.getInstance()
|
||||
if (provider.canOpenSettingsInSeparateWindow()) {
|
||||
await provider.openSettingsInSeparateWindow()
|
||||
} else {
|
||||
// Fall back to in-pane navigation
|
||||
sendSettingsButtonClickedEvent()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
+12
@@ -1,3 +1,4 @@
|
||||
import { sendSettingsButtonClickedEvent } from "@/core/controller/ui/subscribeToSettingsButtonClicked"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
|
||||
export class ExternalWebviewProvider extends WebviewProvider {
|
||||
@@ -15,4 +16,15 @@ export class ExternalWebviewProvider extends WebviewProvider {
|
||||
override isVisible() {
|
||||
return true
|
||||
}
|
||||
|
||||
override canOpenSettingsInSeparateWindow(): boolean {
|
||||
// JetBrains/CLI currently doesn't support separate windows
|
||||
// This could be enhanced in the future via host bridge protocol
|
||||
return false
|
||||
}
|
||||
|
||||
override async openSettingsInSeparateWindow(): Promise<void> {
|
||||
// Fall back to in-pane navigation for platforms that don't support separate windows
|
||||
await sendSettingsButtonClickedEvent()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { handleGrpcRequest, handleGrpcRequestCancel } from "@/core/controller/grpc-handler"
|
||||
import { getNonce } from "@/core/webview/getNonce"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import type { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
|
||||
/**
|
||||
* Manages a standalone settings webview panel that can be opened in a separate VS Code window/tab.
|
||||
* This allows users to view and modify settings while keeping the main Cline sidebar visible.
|
||||
*/
|
||||
export class VscodeSettingsWebviewPanel {
|
||||
private static currentPanel: VscodeSettingsWebviewPanel | undefined
|
||||
private readonly panel: vscode.WebviewPanel
|
||||
private readonly controller: Controller
|
||||
private readonly context: vscode.ExtensionContext
|
||||
private disposables: vscode.Disposable[] = []
|
||||
|
||||
/**
|
||||
* Creates or reveals the settings panel.
|
||||
* If a panel already exists, it will be revealed. Otherwise, a new one is created.
|
||||
*/
|
||||
public static async createOrShow(context: vscode.ExtensionContext, controller: Controller): Promise<void> {
|
||||
// If we already have a panel, show it
|
||||
if (VscodeSettingsWebviewPanel.currentPanel) {
|
||||
VscodeSettingsWebviewPanel.currentPanel.panel.reveal(vscode.ViewColumn.One)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, create a new panel
|
||||
const panel = vscode.window.createWebviewPanel("clineSettings", "Cline Settings", vscode.ViewColumn.One, {
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [vscode.Uri.file(HostProvider.get().extensionFsPath)],
|
||||
})
|
||||
|
||||
const instance = new VscodeSettingsWebviewPanel(panel, context, controller)
|
||||
VscodeSettingsWebviewPanel.currentPanel = instance
|
||||
|
||||
// Initialize the panel after construction
|
||||
await instance.initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the current panel if it exists.
|
||||
*/
|
||||
public static dispose(): void {
|
||||
VscodeSettingsWebviewPanel.currentPanel?.disposePanel()
|
||||
}
|
||||
|
||||
private constructor(panel: vscode.WebviewPanel, context: vscode.ExtensionContext, controller: Controller) {
|
||||
this.panel = panel
|
||||
this.context = context
|
||||
this.controller = controller
|
||||
|
||||
// Set the webview's HTML content
|
||||
this.updateHtmlContent()
|
||||
|
||||
// Set up message listener
|
||||
this.panel.webview.onDidReceiveMessage((message) => this.handleWebviewMessage(message), null, this.disposables)
|
||||
|
||||
// Handle panel disposal
|
||||
this.panel.onDidDispose(() => this.disposePanel(), null, this.disposables)
|
||||
|
||||
// Update settings when panel becomes visible
|
||||
this.panel.onDidChangeViewState(
|
||||
() => {
|
||||
if (this.panel.visible) {
|
||||
// Post current state to the panel
|
||||
this.controller.postStateToWebview()
|
||||
}
|
||||
},
|
||||
null,
|
||||
this.disposables,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the panel by sending state and navigating to settings.
|
||||
*/
|
||||
private async initialize(): Promise<void> {
|
||||
// Wait a moment for the webview HTML to load
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
// Get the current state
|
||||
const state = await this.controller.getStateToPostToWebview()
|
||||
|
||||
// Modify state to show settings view by default for this panel
|
||||
const modifiedState: any = {
|
||||
...state,
|
||||
// Force this webview to show settings on initialization
|
||||
showSettings: true,
|
||||
showWelcome: false,
|
||||
showHistory: false,
|
||||
showMcp: false,
|
||||
showAccount: false,
|
||||
}
|
||||
|
||||
// Send state update via gRPC response format (what the webview expects)
|
||||
await this.panel.webview.postMessage({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
request_id: "initial_state",
|
||||
message: {
|
||||
stateJson: JSON.stringify(modifiedState),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the HTML content of the webview panel.
|
||||
* Uses the same React app as the sidebar, but with a flag to indicate it's in a separate window.
|
||||
*/
|
||||
private async updateHtmlContent(): Promise<void> {
|
||||
const webview = this.panel.webview
|
||||
|
||||
// Check if we're in development mode for HMR support
|
||||
const isDev = this.context.extensionMode === vscode.ExtensionMode.Development
|
||||
|
||||
if (isDev) {
|
||||
this.panel.webview.html = await this.getHMRHtmlContent(webview)
|
||||
} else {
|
||||
this.panel.webview.html = this.getHtmlContent(webview)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML content for the webview (production mode).
|
||||
*/
|
||||
private getHtmlContent(webview: vscode.Webview): string {
|
||||
const scriptUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this.context.extensionUri, "webview-ui", "build", "assets", "index.js"),
|
||||
)
|
||||
const stylesUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this.context.extensionUri, "webview-ui", "build", "assets", "index.css"),
|
||||
)
|
||||
const codiconsUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this.context.extensionUri, "node_modules", "@vscode", "codicons", "dist", "codicon.css"),
|
||||
)
|
||||
|
||||
const nonce = getNonce()
|
||||
|
||||
return /*html*/ `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
||||
<link href="${codiconsUri}" rel="stylesheet" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none';
|
||||
connect-src https://*.posthog.com https://*.cline.bot;
|
||||
font-src ${webview.cspSource} data:;
|
||||
style-src ${webview.cspSource} 'unsafe-inline';
|
||||
img-src ${webview.cspSource} https: data:;
|
||||
script-src 'nonce-${nonce}' 'unsafe-eval';">
|
||||
<title>Cline Settings</title>
|
||||
<script nonce="${nonce}">
|
||||
// Tell the React app this is a settings-only panel
|
||||
window.CLINE_SETTINGS_PANEL = true;
|
||||
// Store the VS Code API instance that will be acquired by the platform config
|
||||
window.CLOSE_CLINE_SETTINGS_PANEL = null;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML content for the webview with HMR support (development mode).
|
||||
*/
|
||||
private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
|
||||
// Try to read the dev server port
|
||||
const DEFAULT_PORT = 25463
|
||||
let localPort = DEFAULT_PORT
|
||||
|
||||
try {
|
||||
const path = require("path")
|
||||
const fs = require("fs/promises")
|
||||
const portFilePath = path.join(this.context.extensionPath, "webview-ui", ".vite-port")
|
||||
const portFile = await fs.readFile(portFilePath, "utf8")
|
||||
localPort = parseInt(portFile.trim()) || DEFAULT_PORT
|
||||
} catch (error) {
|
||||
// Use default port if file doesn't exist
|
||||
}
|
||||
|
||||
const localServerUrl = `localhost:${localPort}`
|
||||
const nonce = getNonce()
|
||||
|
||||
const stylesUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this.context.extensionUri, "webview-ui", "build", "assets", "index.css"),
|
||||
)
|
||||
const codiconsUri = webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this.context.extensionUri, "node_modules", "@vscode", "codicons", "dist", "codicon.css"),
|
||||
)
|
||||
|
||||
const scriptUrl = `http://${localServerUrl}/src/main.tsx`
|
||||
|
||||
const reactRefresh = /*html*/ `
|
||||
<script nonce="${nonce}" type="module">
|
||||
import RefreshRuntime from "http://${localServerUrl}/@react-refresh"
|
||||
RefreshRuntime.injectIntoGlobalHook(window)
|
||||
window.$RefreshReg$ = () => {}
|
||||
window.$RefreshSig$ = () => (type) => type
|
||||
window.__vite_plugin_react_preamble_installed__ = true
|
||||
</script>
|
||||
`
|
||||
|
||||
const csp = [
|
||||
"default-src 'none'",
|
||||
`font-src ${webview.cspSource}`,
|
||||
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
|
||||
`img-src ${webview.cspSource} https: data:`,
|
||||
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
|
||||
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
|
||||
]
|
||||
|
||||
return /*html*/ `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="http://localhost:8097"></script>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
|
||||
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
||||
<link href="${codiconsUri}" rel="stylesheet" />
|
||||
<title>Cline Settings</title>
|
||||
<script nonce="${nonce}">
|
||||
// Tell the React app this is a settings-only panel
|
||||
window.CLINE_SETTINGS_PANEL = true;
|
||||
// Provide a function to close this panel
|
||||
window.CLOSE_CLINE_SETTINGS_PANEL = function() {
|
||||
console.log("Closing settings panel via global function");
|
||||
const vsCodeApi = typeof acquireVsCodeApi === "function" ? acquireVsCodeApi() : null;
|
||||
if (vsCodeApi) {
|
||||
vsCodeApi.postMessage({ type: "dispose_panel" });
|
||||
} else {
|
||||
console.error("VS Code API not available");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
${reactRefresh}
|
||||
<script type="module" src="${scriptUrl}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles messages received from the webview.
|
||||
*/
|
||||
private async handleWebviewMessage(message: any): Promise<void> {
|
||||
const postMessageToWebview = (response: ExtensionMessage) => this.panel.webview.postMessage(response)
|
||||
|
||||
switch (message.type) {
|
||||
case "dispose_panel": {
|
||||
// Dispose this panel (same as clicking the X button)
|
||||
console.log("Disposing settings panel via Done button")
|
||||
this.panel.dispose() // This triggers onDidDispose which calls disposePanel()
|
||||
return
|
||||
}
|
||||
case "grpc_request": {
|
||||
if (message.grpc_request) {
|
||||
await handleGrpcRequest(this.controller, postMessageToWebview, message.grpc_request)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "grpc_request_cancel": {
|
||||
if (message.grpc_request_cancel) {
|
||||
await handleGrpcRequestCancel(postMessageToWebview, message.grpc_request_cancel)
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
console.error("Settings panel received unhandled WebviewMessage type:", JSON.stringify(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the panel and cleans up resources.
|
||||
*/
|
||||
private disposePanel(): void {
|
||||
VscodeSettingsWebviewPanel.currentPanel = undefined
|
||||
|
||||
// Clean up disposables
|
||||
while (this.disposables.length) {
|
||||
const disposable = this.disposables.pop()
|
||||
if (disposable) {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose the panel
|
||||
this.panel.dispose()
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,22 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
|
||||
return this.webview?.webview.postMessage(message)
|
||||
}
|
||||
|
||||
override canOpenSettingsInSeparateWindow(): boolean {
|
||||
// VS Code supports opening settings in a separate panel
|
||||
return true
|
||||
}
|
||||
|
||||
override async openSettingsInSeparateWindow(): Promise<void> {
|
||||
// Import the panel class dynamically to avoid circular dependencies
|
||||
const { VscodeSettingsWebviewPanel } = await import("./VscodeSettingsWebviewPanel")
|
||||
VscodeSettingsWebviewPanel.createOrShow(this.context, this.controller)
|
||||
}
|
||||
|
||||
override async dispose() {
|
||||
// Clean up settings panel if it exists
|
||||
const { VscodeSettingsWebviewPanel } = await import("./VscodeSettingsWebviewPanel")
|
||||
VscodeSettingsWebviewPanel.dispose()
|
||||
|
||||
// WebviewView doesn't have a dispose method, it's managed by VSCode
|
||||
// We just need to clean up our disposables
|
||||
while (this.disposables.length) {
|
||||
|
||||
@@ -40,6 +40,9 @@ const AppContent = () => {
|
||||
|
||||
const { clineUser, organizations, activeOrganization } = useClineAuth()
|
||||
|
||||
// Check if this is a settings-only panel
|
||||
const isSettingsPanel = typeof window !== "undefined" && (window as any).CLINE_SETTINGS_PANEL === true
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShowAnnouncement) {
|
||||
setShowAnnouncement(true)
|
||||
@@ -55,6 +58,30 @@ const AppContent = () => {
|
||||
}
|
||||
}, [shouldShowAnnouncement, setShouldShowAnnouncement, setShowAnnouncement])
|
||||
|
||||
// If this is a settings panel, show settings immediately (even before state hydrates)
|
||||
if (isSettingsPanel) {
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col">
|
||||
<SettingsView
|
||||
onDone={() => {
|
||||
// Use the platform config to send dispose message
|
||||
console.log("Done button clicked - using PLATFORM_CONFIG")
|
||||
import("./config/platform.config")
|
||||
.then(({ PLATFORM_CONFIG }) => {
|
||||
PLATFORM_CONFIG.postMessage({
|
||||
type: "dispose_panel",
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to send dispose message:", error)
|
||||
})
|
||||
}}
|
||||
targetSection={settingsTargetSection}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!didHydrateState) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import React from "react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
interface McpDisplayModeDropdownProps {
|
||||
value: McpDisplayMode
|
||||
@@ -12,17 +12,17 @@ interface McpDisplayModeDropdownProps {
|
||||
}
|
||||
|
||||
const McpDisplayModeDropdown: React.FC<McpDisplayModeDropdownProps> = ({ value, onChange, id, className, style, onClick }) => {
|
||||
const handleChange = (e: any) => {
|
||||
const newMode = e.target.value as McpDisplayMode
|
||||
onChange(newMode)
|
||||
}
|
||||
|
||||
return (
|
||||
<VSCodeDropdown className={className} id={id} onChange={handleChange} onClick={onClick} style={style} value={value}>
|
||||
<VSCodeOption value="plain">Plain Text</VSCodeOption>
|
||||
<VSCodeOption value="rich">Rich Display</VSCodeOption>
|
||||
<VSCodeOption value="markdown">Markdown</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
<Select onValueChange={onChange} value={value}>
|
||||
<SelectTrigger className={className} id={id} onClick={onClick} style={style}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="plain">Plain Text</SelectItem>
|
||||
<SelectItem value="rich">Rich Display</SelectItem>
|
||||
<SelectItem value="markdown">Markdown</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from "react"
|
||||
|
||||
interface FeatureGroupProps {
|
||||
title: string
|
||||
description?: string
|
||||
children: React.ReactNode
|
||||
isGridItem?: boolean
|
||||
}
|
||||
|
||||
export const FeatureGroup: React.FC<FeatureGroupProps> = ({ title, description, children, isGridItem = false }) => {
|
||||
return (
|
||||
<div className={isGridItem ? "" : "mb-6"}>
|
||||
{/* Title outside container */}
|
||||
<div className={isGridItem ? "mb-2" : "mb-3"}>
|
||||
<div
|
||||
className="text-base font-medium mb-1"
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Container with lighter background and no border */}
|
||||
<div
|
||||
className="px-3 py-1 rounded-md"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
||||
border: "none",
|
||||
}}>
|
||||
<div className={isGridItem ? "space-y-0" : "space-y-4"}>
|
||||
{React.Children.toArray(children)
|
||||
.filter((child) => child) // Filter out null/undefined/false children
|
||||
.map((child, index, array) => {
|
||||
const isLast = index === array.length - 1
|
||||
const showDivider = array.length > 1 && !isLast
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="py-2">{child}</div>
|
||||
{showDivider && (
|
||||
<div
|
||||
style={{
|
||||
height: "1px",
|
||||
borderBottom: "1px solid rgba(128, 128, 128, 0.15)",
|
||||
marginTop: "2px",
|
||||
marginBottom: "2px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useState } from "react"
|
||||
import { Toggle } from "@/components/ui/toggle"
|
||||
import { SettingsBadge } from "./SettingsBadge"
|
||||
|
||||
type SettingsBadgeVariant = "experimental" | "new" | "dangerous" | "recommended"
|
||||
|
||||
interface FeatureItemProps {
|
||||
label: string
|
||||
checked: boolean | undefined
|
||||
disabled?: boolean
|
||||
onChange: (checked: boolean) => void
|
||||
description?: string
|
||||
badge?: {
|
||||
text: string
|
||||
variant: SettingsBadgeVariant
|
||||
}
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const FeatureItem: React.FC<FeatureItemProps> = ({
|
||||
label,
|
||||
checked,
|
||||
disabled = false,
|
||||
onChange,
|
||||
description,
|
||||
badge,
|
||||
children,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const isExpandable = !!(description || children)
|
||||
|
||||
const handleRowClick = (e: React.MouseEvent) => {
|
||||
// Don't toggle if clicking on the toggle switch itself
|
||||
if ((e.target as HTMLElement).closest('[role="switch"]')) {
|
||||
return
|
||||
}
|
||||
if (isExpandable) {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center justify-between w-full gap-3 py-2 px-2 -mx-2 relative group ${
|
||||
isExpandable ? "cursor-pointer" : ""
|
||||
}`}
|
||||
onClick={handleRowClick}>
|
||||
{/* Left side: Label, chevron, badge */}
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{isExpandable && (
|
||||
<i
|
||||
className={`codicon codicon-chevron-right text-xs transition-transform ${
|
||||
isExpanded ? "rotate-90" : ""
|
||||
}`}
|
||||
style={{ color: "var(--vscode-descriptionForeground)" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={`text-sm transition-opacity ${isExpandable ? "opacity-60 group-hover:opacity-100" : ""}`}
|
||||
style={{ color: "var(--vscode-foreground)" }}>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
{badge && <SettingsBadge variant={badge.variant}>{badge.text}</SettingsBadge>}
|
||||
</div>
|
||||
|
||||
{/* Right side: Toggle switch */}
|
||||
<Toggle checked={checked ?? false} disabled={disabled} onCheckedChange={onChange} />
|
||||
</div>
|
||||
|
||||
{/* Expanded content */}
|
||||
{isExpandable && isExpanded && (
|
||||
<div className="mt-2 pl-8 pr-2 pb-2">
|
||||
{description && (
|
||||
<p
|
||||
className="text-xs mb-2"
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
lineHeight: "1.5",
|
||||
}}>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Sparkles, X } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
interface NewFeature {
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface NewFeaturesCalloutProps {
|
||||
features: NewFeature[]
|
||||
onDismiss?: () => void
|
||||
}
|
||||
|
||||
export const NewFeaturesCallout = ({ features, onDismiss }: NewFeaturesCalloutProps) => {
|
||||
const [dismissed, setDismissed] = useState(false)
|
||||
|
||||
if (dismissed || features.length === 0) return null
|
||||
|
||||
const handleDismiss = () => {
|
||||
setDismissed(true)
|
||||
onDismiss?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative mb-6 rounded-md p-4"
|
||||
style={{
|
||||
border: "1px solid color-mix(in srgb, var(--vscode-button-background) 30%, transparent)",
|
||||
backgroundColor: "color-mix(in srgb, var(--vscode-button-background) 10%, transparent)",
|
||||
}}>
|
||||
<button
|
||||
aria-label="Dismiss"
|
||||
className="absolute top-2 right-2 p-1 rounded hover:bg-white/10 transition-colors"
|
||||
onClick={handleDismiss}
|
||||
style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<Sparkles className="w-5 h-5" style={{ color: "var(--vscode-button-background)" }} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-semibold mb-2" style={{ color: "var(--vscode-foreground)" }}>
|
||||
New Features Available
|
||||
</h3>
|
||||
|
||||
<ul className="space-y-2">
|
||||
{features.map((feature) => (
|
||||
<li className="text-xs" key={feature.id}>
|
||||
<span className="font-medium" style={{ color: "var(--vscode-foreground)" }}>
|
||||
{feature.label}
|
||||
</span>
|
||||
<span style={{ color: "var(--vscode-descriptionForeground)" }}> — {feature.description}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type { NewFeature }
|
||||
@@ -4,9 +4,28 @@ import { HTMLAttributes } from "react"
|
||||
type SectionHeaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children: React.ReactNode
|
||||
description?: string
|
||||
variant?: "page" | "category"
|
||||
}
|
||||
|
||||
export const SectionHeader = ({ description, children, className, ...props }: SectionHeaderProps) => {
|
||||
export const SectionHeader = ({ description, children, className, variant = "page", ...props }: SectionHeaderProps) => {
|
||||
if (variant === "category") {
|
||||
return (
|
||||
<div className={cn("mb-4 mt-6 first:mt-0", className)} {...props}>
|
||||
<h4
|
||||
className="text-[11px] font-semibold uppercase tracking-wide mb-2"
|
||||
style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
{children}
|
||||
</h4>
|
||||
{description && (
|
||||
<p className="text-xs mt-1" style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Default page header variant
|
||||
return (
|
||||
<div className={cn("text-foreground px-5 py-3", className)} {...props}>
|
||||
<h2 className="m-0 text-base">{children}</h2>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface SettingRowProps {
|
||||
children: React.ReactNode
|
||||
highlighted?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const SettingRow: React.FC<SettingRowProps> = ({ children, highlighted, disabled }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-start justify-between gap-4 py-3 px-2 rounded-md transition-colors",
|
||||
highlighted && "ring-1 bg-opacity-5",
|
||||
disabled && "opacity-50",
|
||||
!disabled && "hover:bg-white/5",
|
||||
)}
|
||||
style={{
|
||||
...(highlighted && {
|
||||
ringColor: "color-mix(in srgb, var(--vscode-button-background) 30%, transparent)",
|
||||
backgroundColor: "color-mix(in srgb, var(--vscode-button-background) 5%, transparent)",
|
||||
}),
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingInfo: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return <div className="flex-1 min-w-0">{children}</div>
|
||||
}
|
||||
|
||||
export const SettingLabel: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<div className="text-sm font-medium mb-1" style={{ color: "var(--vscode-foreground)" }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingDescription: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<div className="text-xs leading-relaxed" style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type SettingsBadgeVariant = "experimental" | "new" | "dangerous" | "recommended"
|
||||
|
||||
interface SettingsBadgeProps {
|
||||
children: React.ReactNode
|
||||
variant?: SettingsBadgeVariant
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const SettingsBadge: React.FC<SettingsBadgeProps> = ({ children, variant = "experimental", className }) => {
|
||||
const getVariantStyles = () => {
|
||||
switch (variant) {
|
||||
case "experimental":
|
||||
return {
|
||||
backgroundColor: "color-mix(in srgb, var(--vscode-inputValidation-warningBackground) 40%, transparent)",
|
||||
color: "var(--vscode-inputValidation-warningBorder)",
|
||||
}
|
||||
case "new":
|
||||
return {
|
||||
backgroundColor: "color-mix(in srgb, var(--vscode-button-background) 40%, transparent)",
|
||||
color: "var(--vscode-button-background)",
|
||||
}
|
||||
case "dangerous":
|
||||
return {
|
||||
backgroundColor: "color-mix(in srgb, var(--vscode-inputValidation-errorBackground) 40%, transparent)",
|
||||
color: "var(--vscode-inputValidation-errorBorder)",
|
||||
}
|
||||
case "recommended":
|
||||
return {
|
||||
backgroundColor: "color-mix(in srgb, var(--vscode-button-background) 20%, transparent)",
|
||||
color: "var(--vscode-button-background)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const variantStyles = getVariantStyles()
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn("px-1.5 py-0.5 text-[8px] uppercase inline-flex items-center justify-center", className)}
|
||||
style={{ ...variantStyles, lineHeight: "1.25", borderRadius: "1px" }}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { getEnvironmentColor } from "@/utils/environmentColors"
|
||||
import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
|
||||
import SectionHeader from "./SectionHeader"
|
||||
import AboutSection from "./sections/AboutSection"
|
||||
@@ -109,8 +108,8 @@ const renderSectionHeader = (tabId: string) => {
|
||||
return (
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<tab.icon className="w-4" />
|
||||
<div>{tab.headerText}</div>
|
||||
{tabId !== "features" && <tab.icon className="w-4" />}
|
||||
<div style={{ fontSize: "18px", fontWeight: "normal" }}>{tab.headerText}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
)
|
||||
@@ -135,6 +134,11 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
|
||||
const [activeTab, setActiveTab] = useState<string>(targetSection || SETTINGS_TABS[0].id)
|
||||
|
||||
// Get visible tabs (filter out hidden ones)
|
||||
const visibleTabs = useMemo(() => {
|
||||
return SETTINGS_TABS.filter((tab) => !tab.hidden)
|
||||
}, [])
|
||||
|
||||
// Optimized message handler with early returns
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
@@ -196,20 +200,24 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
// Memoized tab item renderer
|
||||
const renderTabItem = useCallback(
|
||||
(tab: (typeof SETTINGS_TABS)[0]) => {
|
||||
const isActive = activeTab === tab.id
|
||||
return (
|
||||
<TabTrigger className="flex justify-baseline" data-testid={`tab-${tab.id}`} key={tab.id} value={tab.id}>
|
||||
<TabTrigger className="flex justify-baseline w-full" data-testid={`tab-${tab.id}`} key={tab.id} value={tab.id}>
|
||||
<Tooltip key={tab.id}>
|
||||
<TooltipTrigger>
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap overflow-hidden h-12 sm:py-3 box-border flex items-center border-l-2 border-transparent text-foreground opacity-70 bg-transparent hover:bg-list-hover p-4 cursor-pointer gap-2",
|
||||
{
|
||||
"opacity-100 border-l-2 border-l-foreground border-t-0 border-r-0 border-b-0 bg-selection":
|
||||
activeTab === tab.id,
|
||||
},
|
||||
)}>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
<span className="hidden sm:block">{tab.name}</span>
|
||||
<TooltipTrigger asChild className="w-full block">
|
||||
<div className="px-2 py-1.5 cursor-pointer w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center px-4 py-3 transition-all text-foreground rounded w-full",
|
||||
isActive ? "opacity-100" : "opacity-70 hover:opacity-90",
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: isActive ? "var(--vscode-list-hoverBackground)" : "transparent",
|
||||
borderRadius: "3px",
|
||||
}}>
|
||||
<tab.icon className="w-4 h-4 flex-shrink-0 mr-2" />
|
||||
<span className="hidden sm:block flex-1 text-left">{tab.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{tab.tooltipText}</TooltipContent>
|
||||
@@ -238,13 +246,19 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
return <Component {...props} />
|
||||
}, [activeTab, handleResetState, version])
|
||||
|
||||
const titleColor = getEnvironmentColor(environment)
|
||||
|
||||
return (
|
||||
<Tab>
|
||||
<TabHeader className="flex justify-between items-center gap-2">
|
||||
<TabHeader
|
||||
className="flex justify-between items-center gap-2"
|
||||
style={{ paddingLeft: "20px", paddingTop: "20px", paddingBottom: "20px" }}>
|
||||
<div className="flex items-center gap-1">
|
||||
<h3 className="text-md m-0" style={{ color: titleColor }}>
|
||||
<h3
|
||||
className="m-0"
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "24px",
|
||||
fontWeight: "normal",
|
||||
}}>
|
||||
Settings
|
||||
</h3>
|
||||
</div>
|
||||
@@ -254,12 +268,12 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
</TabHeader>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<TabList
|
||||
className="shrink-0 flex flex-col overflow-y-auto border-r border-sidebar-background"
|
||||
onValueChange={setActiveTab}
|
||||
value={activeTab}>
|
||||
{SETTINGS_TABS.filter((tab) => !tab.hidden).map(renderTabItem)}
|
||||
</TabList>
|
||||
<div className="shrink-0 flex flex-col border-r border-sidebar-background" style={{ paddingLeft: "12px" }}>
|
||||
{/* Tab List */}
|
||||
<TabList className="flex-1 flex flex-col overflow-y-auto pt-2" onValueChange={setActiveTab} value={activeTab}>
|
||||
{visibleTabs.map(renderTabItem)}
|
||||
</TabList>
|
||||
</div>
|
||||
|
||||
<TabContent className="flex-1 overflow-auto">{ActiveContent}</TabContent>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { EmptyRequest, StringRequest } from "@shared/proto/cline/common"
|
||||
import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { BROWSER_VIEWPORT_PRESETS } from "../../../../../src/shared/BrowserSettings"
|
||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
import { BrowserServiceClient } from "../../../services/grpc-client"
|
||||
import CollapsibleContent from "../CollapsibleContent"
|
||||
import { DebouncedTextField } from "../common/DebouncedTextField"
|
||||
import { FeatureGroup } from "../FeatureGroup"
|
||||
import { FeatureItem } from "../FeatureItem"
|
||||
import Section from "../Section"
|
||||
import { updateSetting } from "../utils/settingsHandlers"
|
||||
|
||||
@@ -77,7 +79,7 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Function to check connection once without changing UI state immediately
|
||||
// Function to check connection once
|
||||
const checkConnectionOnce = useCallback(() => {
|
||||
if (browserSettings.remoteBrowserHost) {
|
||||
BrowserServiceClient.testBrowserConnection(StringRequest.create({ value: browserSettings.remoteBrowserHost }))
|
||||
@@ -100,7 +102,7 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
}
|
||||
}, [browserSettings.remoteBrowserHost])
|
||||
|
||||
// Setup continuous polling for connection status when remote browser is enabled
|
||||
// Setup continuous polling for connection status
|
||||
useEffect(() => {
|
||||
if (!browserSettings.remoteBrowserEnabled) {
|
||||
setIsCheckingConnection(false)
|
||||
@@ -115,17 +117,11 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
return () => clearInterval(pollInterval)
|
||||
}, [browserSettings.remoteBrowserEnabled, checkConnectionOnce])
|
||||
|
||||
const handleViewportChange = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const selectedSize = BROWSER_VIEWPORT_PRESETS[target.value as keyof typeof BROWSER_VIEWPORT_PRESETS]
|
||||
if (selectedSize) {
|
||||
updateSetting("browserSettings", {
|
||||
viewport: {
|
||||
width: selectedSize.width,
|
||||
height: selectedSize.height,
|
||||
},
|
||||
})
|
||||
}
|
||||
const getCurrentViewportPreset = () => {
|
||||
return Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
|
||||
const typedSize = size as { width: number; height: number }
|
||||
return typedSize.width === browserSettings.viewport.width && typedSize.height === browserSettings.viewport.height
|
||||
})?.[0]
|
||||
}
|
||||
|
||||
const relaunchChromeDebugMode = () => {
|
||||
@@ -150,219 +146,167 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
// Determine if we should show the relaunch button
|
||||
const isRemoteEnabled = Boolean(browserSettings.remoteBrowserEnabled)
|
||||
const shouldShowRelaunchButton = isRemoteEnabled && connectionStatus === false
|
||||
const isSubSettingsOpen = !(browserSettings.disableToolUse || false)
|
||||
const shouldShowRelaunchButton = browserSettings.remoteBrowserEnabled && connectionStatus === false
|
||||
|
||||
return (
|
||||
<div>
|
||||
{renderSectionHeader("browser")}
|
||||
<Section>
|
||||
<div id="browser-settings-section" style={{ marginBottom: 20 }}>
|
||||
{/* Master Toggle */}
|
||||
<div style={{ marginBottom: isSubSettingsOpen ? 0 : 10 }}>
|
||||
<VSCodeCheckbox
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{/* BROWSER CONFIGURATION */}
|
||||
<FeatureGroup isGridItem={false} title="Browser Configuration">
|
||||
{/* Disable Browser Tool Usage */}
|
||||
<FeatureItem
|
||||
checked={browserSettings.disableToolUse || false}
|
||||
onChange={(e) =>
|
||||
updateSetting("browserSettings", { disableToolUse: (e.target as HTMLInputElement).checked })
|
||||
}>
|
||||
Disable browser tool usage
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "4px 0 0 0px",
|
||||
}}>
|
||||
Prevent Cline from using browser actions (e.g. launch, click, type).
|
||||
</p>
|
||||
</div>
|
||||
description="Prevent Cline from using browser actions (e.g. launch, click, type)."
|
||||
label="Disable browser tool usage"
|
||||
onChange={(checked) => updateSetting("browserSettings", { disableToolUse: checked })}
|
||||
/>
|
||||
|
||||
<CollapsibleContent isOpen={isSubSettingsOpen}>
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
|
||||
<VSCodeDropdown
|
||||
onChange={(event) => handleViewportChange(event as Event)}
|
||||
style={{ width: "100%" }}
|
||||
value={
|
||||
Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
|
||||
const typedSize = size as { width: number; height: number }
|
||||
return (
|
||||
typedSize.width === browserSettings.viewport.width &&
|
||||
typedSize.height === browserSettings.viewport.height
|
||||
)
|
||||
})?.[0]
|
||||
}>
|
||||
{Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => (
|
||||
<VSCodeOption key={name} value={name}>
|
||||
{name}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
{/* Viewport Size */}
|
||||
{!(browserSettings.disableToolUse || false) && (
|
||||
<div className="flex items-center justify-between gap-3 px-2">
|
||||
<label className="text-sm font-medium" style={{ color: "var(--vscode-foreground)" }}>
|
||||
Viewport size
|
||||
</label>
|
||||
<div className="pr-2">
|
||||
<Select
|
||||
onValueChange={(presetName) => {
|
||||
const selectedSize =
|
||||
BROWSER_VIEWPORT_PRESETS[presetName as keyof typeof BROWSER_VIEWPORT_PRESETS]
|
||||
if (selectedSize) {
|
||||
updateSetting("browserSettings", {
|
||||
viewport: {
|
||||
width: selectedSize.width,
|
||||
height: selectedSize.height,
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
value={getCurrentViewportPreset()}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => (
|
||||
<SelectItem key={name} value={name}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: 0,
|
||||
}}>
|
||||
Set the size of the browser viewport for screenshots and interactions.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 0 }}>
|
||||
{" "}
|
||||
{/* This div now contains Remote Connection & Chrome Path */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 4,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
{/* Remote Browser Connection */}
|
||||
{!(browserSettings.disableToolUse || false) && (
|
||||
<FeatureItem
|
||||
checked={browserSettings.remoteBrowserEnabled || false}
|
||||
description={`Enable Cline to use your Chrome${
|
||||
isBundled
|
||||
? " (not detected on your machine)"
|
||||
: detectedChromePath
|
||||
? ` (${detectedChromePath})`
|
||||
: ""
|
||||
}. Using a remote browser connection requires starting Chrome in debug mode${
|
||||
browserSettings.remoteBrowserEnabled
|
||||
? " manually (--remote-debugging-port=9222) or using the button below."
|
||||
: "."
|
||||
}`}
|
||||
label="Use remote browser connection"
|
||||
onChange={(enabled) => {
|
||||
updateSetting("browserSettings", { remoteBrowserEnabled: enabled })
|
||||
if (!enabled) {
|
||||
updateSetting("browserSettings", { remoteBrowserHost: undefined })
|
||||
}
|
||||
}}>
|
||||
<VSCodeCheckbox
|
||||
checked={browserSettings.remoteBrowserEnabled}
|
||||
onChange={(e) => {
|
||||
const enabled = (e.target as HTMLInputElement).checked
|
||||
updateSetting("browserSettings", { remoteBrowserEnabled: enabled })
|
||||
// If disabling, also clear the host
|
||||
if (!enabled) {
|
||||
updateSetting("browserSettings", { remoteBrowserHost: undefined })
|
||||
}
|
||||
}}>
|
||||
Use remote browser connection
|
||||
</VSCodeCheckbox>
|
||||
<ConnectionStatusIndicator
|
||||
isChecking={isCheckingConnection}
|
||||
isConnected={connectionStatus}
|
||||
remoteBrowserEnabled={browserSettings.remoteBrowserEnabled}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "0 0 6px 0px",
|
||||
}}>
|
||||
Enable Cline to use your Chrome
|
||||
{isBundled
|
||||
? "(not detected on your machine)"
|
||||
: detectedChromePath
|
||||
? ` (${detectedChromePath})`
|
||||
: ""}
|
||||
. You can specify a custom path below. Using a remote browser connection requires starting Chrome
|
||||
in debug mode
|
||||
{browserSettings.remoteBrowserEnabled ? (
|
||||
<>
|
||||
{" "}
|
||||
manually (<code>--remote-debugging-port=9222</code>) or using the button below. Enter the
|
||||
host address or leave it blank for automatic discovery.
|
||||
</>
|
||||
) : (
|
||||
"."
|
||||
)}
|
||||
</p>
|
||||
{/* Moved remote-specific settings to appear directly after enabling remote connection */}
|
||||
{browserSettings.remoteBrowserEnabled && (
|
||||
<div style={{ marginLeft: 0, marginTop: 8 }}>
|
||||
<DebouncedTextField
|
||||
initialValue={browserSettings.remoteBrowserHost || ""}
|
||||
onChange={(value) =>
|
||||
updateSetting("browserSettings", { remoteBrowserHost: value || undefined })
|
||||
}
|
||||
placeholder="http://localhost:9222"
|
||||
style={{ width: "100%", marginBottom: 8 }}
|
||||
/>
|
||||
{browserSettings.remoteBrowserEnabled && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<DebouncedTextField
|
||||
initialValue={browserSettings.remoteBrowserHost || ""}
|
||||
onChange={(value) =>
|
||||
updateSetting("browserSettings", { remoteBrowserHost: value || undefined })
|
||||
}
|
||||
placeholder="http://localhost:9222"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<ConnectionStatusIndicator
|
||||
isChecking={isCheckingConnection}
|
||||
isConnected={connectionStatus}
|
||||
remoteBrowserEnabled={browserSettings.remoteBrowserEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{shouldShowRelaunchButton && (
|
||||
<div style={{ display: "flex", gap: "10px", marginBottom: 8, justifyContent: "center" }}>
|
||||
{shouldShowRelaunchButton && (
|
||||
<VSCodeButton
|
||||
disabled={debugMode}
|
||||
onClick={relaunchChromeDebugMode}
|
||||
style={{ flex: 1 }}>
|
||||
style={{ width: "100%" }}>
|
||||
{debugMode ? "Launching Browser..." : "Launch Browser with Debug Mode"}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{relaunchResult && (
|
||||
<div
|
||||
style={{
|
||||
padding: "8px",
|
||||
marginBottom: "8px",
|
||||
backgroundColor: relaunchResult.success
|
||||
? "rgba(0, 128, 0, 0.1)"
|
||||
: "rgba(255, 0, 0, 0.1)",
|
||||
color: relaunchResult.success
|
||||
? "var(--vscode-terminal-ansiGreen)"
|
||||
: "var(--vscode-terminal-ansiRed)",
|
||||
borderRadius: "3px",
|
||||
fontSize: "11px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
{relaunchResult.message}
|
||||
</div>
|
||||
)}
|
||||
{relaunchResult && (
|
||||
<div
|
||||
style={{
|
||||
padding: "8px",
|
||||
backgroundColor: relaunchResult.success
|
||||
? "rgba(0, 128, 0, 0.1)"
|
||||
: "rgba(255, 0, 0, 0.1)",
|
||||
color: relaunchResult.success
|
||||
? "var(--vscode-terminal-ansiGreen)"
|
||||
: "var(--vscode-terminal-ansiRed)",
|
||||
borderRadius: "3px",
|
||||
fontSize: "11px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
{relaunchResult.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FeatureItem>
|
||||
)}
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: 0,
|
||||
}}></p>
|
||||
</div>
|
||||
)}
|
||||
{/* Chrome Executable Path section now follows remote-specific settings */}
|
||||
<div style={{ marginBottom: 8, marginTop: 8 }}>
|
||||
<label
|
||||
htmlFor="chrome-executable-path"
|
||||
style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
|
||||
{/* Chrome Executable Path */}
|
||||
{!(browserSettings.disableToolUse || false) && (
|
||||
<div className="px-2">
|
||||
<label className="text-xs font-medium block mb-2" style={{ color: "var(--vscode-foreground)" }}>
|
||||
Chrome Executable Path (Optional)
|
||||
</label>
|
||||
<DebouncedTextField
|
||||
id="chrome-executable-path"
|
||||
initialValue={browserSettings.chromeExecutablePath || ""}
|
||||
onChange={(value) => updateSetting("browserSettings", { chromeExecutablePath: value })}
|
||||
placeholder="e.g., /usr/bin/google-chrome or C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||
placeholder="e.g., /usr/bin/google-chrome or C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "4px 0 0 0",
|
||||
}}>
|
||||
<p className="text-[10px] mt-1" style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
Leave blank to auto-detect.
|
||||
</p>
|
||||
</div>
|
||||
{/* Custom Browser Arguments section */}
|
||||
<div style={{ marginBottom: 8, marginTop: 8 }}>
|
||||
<label
|
||||
htmlFor="custom-browser-args"
|
||||
style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
|
||||
)}
|
||||
|
||||
{/* Custom Browser Arguments */}
|
||||
{!(browserSettings.disableToolUse || false) && (
|
||||
<div className="px-2">
|
||||
<label className="text-xs font-medium block mb-2" style={{ color: "var(--vscode-foreground)" }}>
|
||||
Custom Browser Arguments (Optional)
|
||||
</label>
|
||||
<DebouncedTextField
|
||||
id="custom-browser-args"
|
||||
initialValue={browserSettings.customArgs || ""}
|
||||
onChange={(value) => updateSetting("browserSettings", { customArgs: value })}
|
||||
placeholder="e.g., --no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage --disable-gpu --no-first-run --no-zygote"
|
||||
placeholder="e.g., --no-sandbox --disable-setuid-sandbox"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "4px 0 0 0",
|
||||
}}>
|
||||
<p className="text-[10px] mt-1" style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
Space-separated arguments to pass to the browser executable.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</FeatureGroup>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { EmptyRequest } from "@shared/proto/index.cline"
|
||||
import { OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useEffect, useState } from "react"
|
||||
import McpDisplayModeDropdown from "@/components/mcp/chat-display/McpDisplayModeDropdown"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { isMacOSOrLinux } from "@/utils/platformUtils"
|
||||
import { FeatureGroup } from "../FeatureGroup"
|
||||
import { FeatureItem } from "../FeatureItem"
|
||||
import { type NewFeature } from "../NewFeaturesCallout"
|
||||
import Section from "../Section"
|
||||
import SubagentOutputLineLimitSlider from "../SubagentOutputLineLimitSlider"
|
||||
import { updateSetting } from "../utils/settingsHandlers"
|
||||
@@ -17,6 +18,25 @@ interface FeatureSettingsSectionProps {
|
||||
renderSectionHeader: (tabId: string) => JSX.Element | null
|
||||
}
|
||||
|
||||
// Define new features to highlight
|
||||
const NEW_FEATURES: NewFeature[] = [
|
||||
{
|
||||
id: "subagents-2025-01",
|
||||
label: "Subagents",
|
||||
description: "Spawn subprocesses to handle focused tasks like exploring large codebases",
|
||||
},
|
||||
{
|
||||
id: "background-edit-2025-01",
|
||||
label: "Background Edit",
|
||||
description: "Edit files without opening the diff view in editor",
|
||||
},
|
||||
{
|
||||
id: "parallel-tools-2025-01",
|
||||
label: "Parallel Tool Calling",
|
||||
description: "Call multiple tools in a single response (auto-enabled for GPT-5)",
|
||||
},
|
||||
]
|
||||
|
||||
const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionProps) => {
|
||||
const {
|
||||
enableCheckpointsSetting,
|
||||
@@ -66,422 +86,310 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Track dismissed new features callout
|
||||
const [dismissedFeatures, setDismissedFeatures] = useState<string[]>(() => {
|
||||
const stored = localStorage.getItem("dismissedNewFeatures")
|
||||
return stored ? JSON.parse(stored) : []
|
||||
})
|
||||
|
||||
const visibleNewFeatures = NEW_FEATURES.filter((f) => !dismissedFeatures.includes(f.id))
|
||||
|
||||
const handleDismissNewFeatures = () => {
|
||||
const allIds = NEW_FEATURES.map((f) => f.id)
|
||||
setDismissedFeatures(allIds)
|
||||
localStorage.setItem("dismissedNewFeatures", JSON.stringify(allIds))
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{renderSectionHeader("features")}
|
||||
<Section>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
{/* Subagents - Only show on macOS and Linux */}
|
||||
{isMacOSOrLinux() && PLATFORM_CONFIG.type === PlatformType.VSCODE && (
|
||||
<div
|
||||
className="relative p-3 mb-3 rounded-md"
|
||||
id="subagents-section"
|
||||
style={{
|
||||
border: "1px solid var(--vscode-widget-border)",
|
||||
backgroundColor: "var(--vscode-list-hoverBackground)",
|
||||
}}>
|
||||
<div
|
||||
className="absolute -top-2 -right-2 px-2 py-0.5 rounded text-xs font-semibold"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-button-secondaryBackground)",
|
||||
color: "var(--vscode-button-secondaryForeground)",
|
||||
}}>
|
||||
NEW
|
||||
{/* NEW FEATURES CALLOUT */}
|
||||
{visibleNewFeatures.length > 0 && (
|
||||
<div
|
||||
className="mb-6 rounded-md p-4"
|
||||
style={{
|
||||
border: "1px solid color-mix(in srgb, var(--vscode-button-background) 30%, transparent)",
|
||||
backgroundColor: "color-mix(in srgb, var(--vscode-button-background) 10%, transparent)",
|
||||
position: "relative",
|
||||
}}>
|
||||
<button
|
||||
aria-label="Dismiss"
|
||||
className="absolute top-2 right-2 p-1 rounded hover:bg-white/10 transition-colors"
|
||||
onClick={handleDismissNewFeatures}
|
||||
style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
<i className="codicon codicon-close" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<i
|
||||
className="codicon codicon-sparkle"
|
||||
style={{ color: "var(--vscode-button-background)", fontSize: "20px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mt-1.5 mb-2 px-2 pt-0.5 pb-1.5 rounded"
|
||||
style={{
|
||||
backgroundColor: "color-mix(in srgb, var(--vscode-sideBar-background) 99%, black)",
|
||||
}}>
|
||||
<p
|
||||
className="text-xs mb-2 flex items-start"
|
||||
style={{ color: "var(--vscode-inputValidation-warningForeground)" }}>
|
||||
<span
|
||||
className="codicon codicon-warning mr-1"
|
||||
style={{ fontSize: "12px", marginTop: "1px", flexShrink: 0 }}></span>
|
||||
<span>
|
||||
Cline for CLI is required for subagents. Install it with:
|
||||
<code
|
||||
className="ml-1 px-1 rounded"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-editor-background)",
|
||||
color: "var(--vscode-foreground)",
|
||||
opacity: 0.9,
|
||||
}}>
|
||||
npm install -g cline
|
||||
</code>
|
||||
, then run
|
||||
<code
|
||||
className="ml-1 px-1 rounded"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-editor-background)",
|
||||
color: "var(--vscode-foreground)",
|
||||
opacity: 0.9,
|
||||
}}>
|
||||
cline auth
|
||||
</code>
|
||||
To authenticate with Cline or configure an API provider.
|
||||
</span>
|
||||
</p>
|
||||
{!isClineCliInstalled && (
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await StateServiceClient.installClineCli(EmptyRequest.create())
|
||||
} catch (error) {
|
||||
console.error("Failed to initiate CLI installation:", error)
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-semibold mb-2" style={{ color: "var(--vscode-foreground)" }}>
|
||||
New Features Available
|
||||
</h3>
|
||||
|
||||
<ul className="space-y-2">
|
||||
{visibleNewFeatures.map((feature) => (
|
||||
<li className="text-xs" key={feature.id}>
|
||||
<span className="font-medium" style={{ color: "var(--vscode-foreground)" }}>
|
||||
{feature.label}
|
||||
</span>
|
||||
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
{" "}
|
||||
— {feature.description}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* GRID CONTAINER FOR FEATURE GROUPS */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* CORE BEHAVIOR & SAFETY */}
|
||||
<FeatureGroup
|
||||
description="Controls guardrails, confirmations, and overall risk tolerance."
|
||||
isGridItem
|
||||
title="Core Behavior & Safety">
|
||||
<FeatureItem
|
||||
badge={{ text: "Dangerous", variant: "dangerous" }}
|
||||
checked={yoloModeToggled}
|
||||
description="DANGEROUS: Disables safety checks and user confirmations. Cline will automatically approve all actions."
|
||||
disabled={remoteConfigSettings?.yoloModeToggled !== undefined}
|
||||
label="Enable YOLO Mode"
|
||||
onChange={(checked) => updateSetting("yoloModeToggled", checked)}
|
||||
/>
|
||||
</FeatureGroup>
|
||||
|
||||
{/* REASONING & DECISION MAKING */}
|
||||
<FeatureGroup
|
||||
description="Defines how much effort the model applies when thinking and planning."
|
||||
isGridItem
|
||||
title="Reasoning & Decision Making">
|
||||
<div className="flex items-center justify-between gap-3 px-2">
|
||||
<label
|
||||
className="text-sm font-medium"
|
||||
htmlFor="openai-reasoning-effort-select"
|
||||
style={{ color: "var(--vscode-foreground)" }}>
|
||||
OpenAI Reasoning Effort
|
||||
</label>
|
||||
<div className="pr-2">
|
||||
<Select onValueChange={handleReasoningEffortChange} value={openaiReasoningEffort || "medium"}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minimal">Minimal</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</FeatureGroup>
|
||||
|
||||
{/* TASK EXECUTION & PROGRESS TRACKING */}
|
||||
<FeatureGroup
|
||||
description="Manages how work is structured, tracked, and resumed."
|
||||
isGridItem
|
||||
title="Task Execution & Progress Tracking">
|
||||
<FeatureItem
|
||||
badge={{ text: "Recommended", variant: "recommended" }}
|
||||
checked={enableCheckpointsSetting}
|
||||
description="Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood."
|
||||
label="Enable Checkpoints"
|
||||
onChange={(checked) => updateSetting("enableCheckpointsSetting", checked)}
|
||||
/>
|
||||
|
||||
<FeatureItem
|
||||
badge={{ text: "Recommended", variant: "recommended" }}
|
||||
checked={focusChainSettings?.enabled || false}
|
||||
description="Enables enhanced task progress tracking and automatic focus chain list management."
|
||||
label="Enable Focus Chain"
|
||||
onChange={(checked) =>
|
||||
updateSetting("focusChainSettings", { ...focusChainSettings, enabled: checked })
|
||||
}>
|
||||
{focusChainSettings?.enabled && (
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs font-medium mb-2"
|
||||
htmlFor="focus-chain-remind-interval"
|
||||
style={{ color: "var(--vscode-foreground)" }}>
|
||||
Reminder Interval
|
||||
</label>
|
||||
<VSCodeTextField
|
||||
className="w-20 text-xs"
|
||||
id="focus-chain-remind-interval"
|
||||
onChange={(e: any) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
if (!Number.isNaN(value) && value >= 1 && value <= 100) {
|
||||
updateSetting("focusChainSettings", {
|
||||
...focusChainSettings,
|
||||
remindClineInterval: value,
|
||||
})
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
transform: "scale(0.85)",
|
||||
transformOrigin: "left center",
|
||||
marginLeft: "-2px",
|
||||
}}>
|
||||
Install Now
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</div>
|
||||
<VSCodeCheckbox
|
||||
checked={subagentsEnabled}
|
||||
disabled={!isClineCliInstalled}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("subagentsEnabled", checked)
|
||||
}}>
|
||||
<span className="font-semibold">
|
||||
{subagentsEnabled ? "Subagents Enabled" : "Enable Subagents"}
|
||||
</span>
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs mt-1 mb-0">
|
||||
<span className="text-[var(--vscode-errorForeground)]">Experimental: </span>{" "}
|
||||
<span className="text-description">
|
||||
Allows Cline to spawn subprocesses to handle focused tasks like exploring large codebases,
|
||||
keeping your main context clean.
|
||||
</span>
|
||||
</p>
|
||||
{subagentsEnabled && (
|
||||
<div className="mt-3">
|
||||
<SubagentOutputLineLimitSlider />
|
||||
value={String(focusChainSettings?.remindClineInterval || 6)}
|
||||
/>
|
||||
<p className="text-[10px] mt-1" style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
Messages between reminders (1-100)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FeatureItem>
|
||||
</FeatureGroup>
|
||||
|
||||
<div>
|
||||
<VSCodeCheckbox
|
||||
checked={enableCheckpointsSetting}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("enableCheckpointsSetting", checked)
|
||||
}}>
|
||||
Enable Checkpoints
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground)">
|
||||
Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which
|
||||
may not work well with large workspaces.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<label
|
||||
className="block text-sm font-medium text-(--vscode-foreground) mb-1"
|
||||
htmlFor="mcp-display-mode-dropdown">
|
||||
MCP Display Mode
|
||||
</label>
|
||||
<McpDisplayModeDropdown
|
||||
className="w-full"
|
||||
id="mcp-display-mode-dropdown"
|
||||
onChange={(newMode: McpDisplayMode) => updateSetting("mcpDisplayMode", newMode)}
|
||||
value={mcpDisplayMode}
|
||||
/>
|
||||
<p className="text-xs mt-[5px] text-(--vscode-descriptionForeground)">
|
||||
Controls how MCP responses are displayed: plain text, rich formatting with links/images, or markdown
|
||||
rendering.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<label
|
||||
className="block text-sm font-medium text-(--vscode-foreground) mb-1"
|
||||
htmlFor="openai-reasoning-effort-dropdown">
|
||||
OpenAI Reasoning Effort
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
className="w-full"
|
||||
currentValue={openaiReasoningEffort || "medium"}
|
||||
id="openai-reasoning-effort-dropdown"
|
||||
onChange={(e: any) => {
|
||||
const newValue = e.target.currentValue as OpenaiReasoningEffort
|
||||
handleReasoningEffortChange(newValue)
|
||||
}}>
|
||||
<VSCodeOption value="minimal">Minimal</VSCodeOption>
|
||||
<VSCodeOption value="low">Low</VSCodeOption>
|
||||
<VSCodeOption value="medium">Medium</VSCodeOption>
|
||||
<VSCodeOption value="high">High</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
<p className="text-xs mt-[5px] text-(--vscode-descriptionForeground)">
|
||||
Reasoning effort for the OpenAI family of models(applies to all OpenAI model providers)
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={strictPlanModeEnabled}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("strictPlanModeEnabled", checked)
|
||||
}}>
|
||||
Enable strict plan mode
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground)">
|
||||
Enforces strict tool use while in plan mode, preventing file edits.
|
||||
</p>
|
||||
</div>
|
||||
{
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={focusChainSettings?.enabled || false}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("focusChainSettings", { ...focusChainSettings, enabled: checked })
|
||||
}}>
|
||||
Enable Focus Chain
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground)">
|
||||
Enables enhanced task progress tracking and automatic focus chain list management throughout
|
||||
tasks.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
{focusChainSettings?.enabled && (
|
||||
<div style={{ marginTop: 10, marginLeft: 20 }}>
|
||||
<label
|
||||
className="block text-sm font-medium text-(--vscode-foreground) mb-1"
|
||||
htmlFor="focus-chain-remind-interval">
|
||||
Focus Chain Reminder Interval
|
||||
</label>
|
||||
<VSCodeTextField
|
||||
className="w-20"
|
||||
id="focus-chain-remind-interval"
|
||||
onChange={(e: any) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
if (!Number.isNaN(value) && value >= 1 && value <= 100) {
|
||||
updateSetting("focusChainSettings", {
|
||||
...focusChainSettings,
|
||||
remindClineInterval: value,
|
||||
})
|
||||
}
|
||||
}}
|
||||
value={String(focusChainSettings?.remindClineInterval || 6)}
|
||||
/>
|
||||
<p className="text-xs mt-[5px] text-(--vscode-descriptionForeground)">
|
||||
Interval (in messages) to remind Cline about its focus chain checklist (1-100). Lower values
|
||||
provide more frequent reminders.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{dictationSettings?.featureEnabled && (
|
||||
<div className="mt-2.5">
|
||||
<VSCodeCheckbox
|
||||
checked={dictationSettings?.dictationEnabled}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
const updatedDictationSettings = {
|
||||
...dictationSettings,
|
||||
dictationEnabled: checked,
|
||||
}
|
||||
updateSetting("dictationSettings", updatedDictationSettings)
|
||||
}}>
|
||||
Enable Dictation
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-description mt-1">
|
||||
Enables speech-to-text transcription using your Cline account. Uses the Aqua Voice's Avalon model,
|
||||
at $0.0065 credits per minute of audio processed. 5 minutes max per message.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
{/* CONTEXT & MEMORY OPTIMIZATION */}
|
||||
<FeatureGroup
|
||||
description="Controls how context is condensed and maintained over time."
|
||||
isGridItem
|
||||
title="Context & Memory Optimization">
|
||||
<FeatureItem
|
||||
checked={useAutoCondense}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("useAutoCondense", checked)
|
||||
}}>
|
||||
Enable Auto Compact
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground)">
|
||||
Enables advanced context management system which uses LLM based condensing for next-gen models.{" "}
|
||||
<a
|
||||
className="text-(--vscode-textLink-foreground) hover:text-(--vscode-textLink-activeForeground)"
|
||||
href="https://docs.cline.bot/features/auto-compact"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{clineWebToolsEnabled?.featureFlag && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={clineWebToolsEnabled?.user}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("clineWebToolsEnabled", checked)
|
||||
}}>
|
||||
Enable Cline Web Tools
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground)">
|
||||
Enables websearch and webfetch tools while using the Cline provider.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{worktreesEnabled?.featureFlag && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={worktreesEnabled?.user}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("worktreesEnabled", checked)
|
||||
}}>
|
||||
Enable Worktrees
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground)">
|
||||
Enables git worktree management for running parallel Cline tasks.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2.5">
|
||||
<VSCodeCheckbox
|
||||
checked={nativeToolCallSetting}
|
||||
onChange={(e) => {
|
||||
const enabled = (e?.target as HTMLInputElement).checked
|
||||
updateSetting("nativeToolCallEnabled", enabled)
|
||||
}}>
|
||||
Enable Native Tool Call
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground)">
|
||||
Uses the model's native tool calling API instead of XML-based tool parsing. This will improve
|
||||
performance for supported models.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2.5">
|
||||
<VSCodeCheckbox
|
||||
checked={enableParallelToolCalling}
|
||||
onChange={(e) => {
|
||||
const enabled = (e?.target as HTMLInputElement).checked
|
||||
updateSetting("enableParallelToolCalling", enabled)
|
||||
}}>
|
||||
Enable Parallel Tool Calling
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs">
|
||||
<span className="text-(--vscode-errorForeground)">Experimental: </span>{" "}
|
||||
<span className="text-description">
|
||||
Allows models to call multiple tools in a single response. Automatically enabled for GPT-5 models.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2.5">
|
||||
<VSCodeCheckbox
|
||||
checked={backgroundEditEnabled}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("backgroundEditEnabled", checked)
|
||||
}}>
|
||||
Enable Background Edit
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs">
|
||||
<span className="text-error">Experimental: </span>
|
||||
<span className="text-description">
|
||||
Allows editing files in background without opening the diff view in editor.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{multiRootSetting.featureFlag && (
|
||||
<div className="mt-2.5">
|
||||
<VSCodeCheckbox
|
||||
checked={multiRootSetting.user}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("multiRootEnabled", checked)
|
||||
}}>
|
||||
Enable Multi-Root Workspace
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs">
|
||||
<span className="text-error">Experimental: </span>{" "}
|
||||
<span className="text-description">Allows cline to work across multiple workspaces.</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2.5">
|
||||
<VSCodeCheckbox
|
||||
checked={hooksEnabled}
|
||||
disabled={!isMacOSOrLinux()}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("hooksEnabled", checked)
|
||||
}}>
|
||||
Enable Hooks
|
||||
</VSCodeCheckbox>
|
||||
{!isMacOSOrLinux() ? (
|
||||
<p className="text-xs mt-1" style={{ color: "var(--vscode-inputValidation-warningForeground)" }}>
|
||||
Hooks are not yet supported on Windows. This feature is currently available on macOS and Linux
|
||||
only.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs">
|
||||
<span className="text-(--vscode-errorForeground)">Experimental: </span>{" "}
|
||||
<span className="text-description">
|
||||
Allows execution of hooks from .clinerules/hooks/ directory.
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2.5">
|
||||
<VSCodeCheckbox
|
||||
checked={skillsEnabled}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("skillsEnabled", checked)
|
||||
}}>
|
||||
Enable Skills
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-xs">
|
||||
<span className="text-(--vscode-errorForeground)">Experimental: </span>{" "}
|
||||
<span className="text-description">
|
||||
Enables Skills for reusable, on-demand agent instructions from .cline/skills/ directories.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeCheckbox
|
||||
checked={yoloModeToggled}
|
||||
disabled={remoteConfigSettings?.yoloModeToggled !== undefined}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("yoloModeToggled", checked)
|
||||
}}>
|
||||
Enable YOLO Mode
|
||||
</VSCodeCheckbox>
|
||||
{remoteConfigSettings?.yoloModeToggled !== undefined && (
|
||||
<i className="codicon codicon-lock text-description text-sm" />
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="max-w-xs"
|
||||
hidden={remoteConfigSettings?.yoloModeToggled === undefined}
|
||||
side="top">
|
||||
This setting is managed by your organization's remote configuration
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
description="Enables advanced context management using LLM-based condensing for next-gen models."
|
||||
label="Enable Auto Compact"
|
||||
onChange={(checked) => updateSetting("useAutoCondense", checked)}
|
||||
/>
|
||||
</FeatureGroup>
|
||||
|
||||
<p className="text-xs text-(--vscode-errorForeground)">
|
||||
EXPERIMENTAL & DANGEROUS: This mode disables safety checks and user confirmations. Cline will
|
||||
automatically approve all actions without asking. Use with extreme caution.
|
||||
</p>
|
||||
</div>
|
||||
{/* TOOLING & AGENT CAPABILITIES */}
|
||||
<FeatureGroup
|
||||
description="Controls what tools the agent can invoke and how it invokes them."
|
||||
isGridItem
|
||||
title="Tooling & Agent Capabilities">
|
||||
<FeatureItem
|
||||
badge={{ text: "Recommended", variant: "recommended" }}
|
||||
checked={nativeToolCallSetting}
|
||||
description="Uses the model's native tool calling API instead of XML-based tool parsing."
|
||||
label="Enable Native Tool Call"
|
||||
onChange={(enabled) => updateSetting("nativeToolCallEnabled", enabled)}
|
||||
/>
|
||||
|
||||
<FeatureItem
|
||||
badge={{ text: "Recommended", variant: "recommended" }}
|
||||
checked={enableParallelToolCalling}
|
||||
description="Allows models to call multiple tools in a single response. Auto-enabled for GPT-5 models."
|
||||
label="Enable Parallel Tool Calling"
|
||||
onChange={(enabled) => updateSetting("enableParallelToolCalling", enabled)}
|
||||
/>
|
||||
|
||||
{clineWebToolsEnabled?.featureFlag && (
|
||||
<FeatureItem
|
||||
badge={{ text: "Recommended", variant: "recommended" }}
|
||||
checked={clineWebToolsEnabled?.user}
|
||||
description="Enables websearch and webfetch tools while using the Cline provider."
|
||||
label="Enable Cline Web Tools"
|
||||
onChange={(checked) => updateSetting("clineWebToolsEnabled", checked)}
|
||||
/>
|
||||
)}
|
||||
</FeatureGroup>
|
||||
|
||||
{/* WORKSPACE & FILE OPERATIONS */}
|
||||
<FeatureGroup
|
||||
description="Defines how Cline interacts with files and project structure."
|
||||
isGridItem
|
||||
title="Workspace & File Operations">
|
||||
<FeatureItem
|
||||
badge={{ text: "Experimental", variant: "experimental" }}
|
||||
checked={backgroundEditEnabled}
|
||||
description="Edit files without opening the diff view in editor."
|
||||
label="Enable Background Edit"
|
||||
onChange={(checked) => updateSetting("backgroundEditEnabled", checked)}
|
||||
/>
|
||||
|
||||
{worktreesEnabled?.featureFlag && (
|
||||
<FeatureItem
|
||||
checked={worktreesEnabled?.user}
|
||||
description="Enables git worktree management for running parallel Cline tasks."
|
||||
label="Enable Worktrees"
|
||||
onChange={(checked) => updateSetting("worktreesEnabled", checked)}
|
||||
/>
|
||||
)}
|
||||
</FeatureGroup>
|
||||
|
||||
{/* EXTENSIBILITY & AUTOMATION */}
|
||||
<FeatureGroup
|
||||
description="Enables advanced customization, reuse, and automation behaviors."
|
||||
isGridItem
|
||||
title="Extensibility & Automation">
|
||||
<FeatureItem
|
||||
badge={{ text: "Recommended", variant: "recommended" }}
|
||||
checked={skillsEnabled}
|
||||
description="Enables reusable, on-demand agent instructions from .cline/skills/ directories."
|
||||
label="Enable Skills"
|
||||
onChange={(checked) => updateSetting("skillsEnabled", checked)}
|
||||
/>
|
||||
|
||||
{/* Subagents - Only show on macOS and Linux */}
|
||||
{isMacOSOrLinux() && PLATFORM_CONFIG.type === PlatformType.VSCODE && (
|
||||
<FeatureItem
|
||||
badge={{ text: "Recommended", variant: "recommended" }}
|
||||
checked={subagentsEnabled}
|
||||
description="Spawn subprocesses to handle focused tasks like exploring large codebases."
|
||||
disabled={!isClineCliInstalled}
|
||||
label="Enable Subagents"
|
||||
onChange={(checked) => updateSetting("subagentsEnabled", checked)}>
|
||||
{!isClineCliInstalled && (
|
||||
<div
|
||||
className="mb-3 p-2 rounded text-[11px]"
|
||||
style={{
|
||||
backgroundColor:
|
||||
"color-mix(in srgb, var(--vscode-inputValidation-warningBackground) 20%, transparent)",
|
||||
border: "1px solid var(--vscode-inputValidation-warningBorder)",
|
||||
color: "var(--vscode-inputValidation-warningForeground)",
|
||||
}}>
|
||||
<p className="mb-1 flex items-start">
|
||||
<span
|
||||
className="codicon codicon-warning mr-1"
|
||||
style={{ fontSize: "10px", marginTop: "1px", flexShrink: 0 }}></span>
|
||||
<span>
|
||||
Cline CLI required. Install with:
|
||||
<code
|
||||
className="mx-0.5 px-0.5"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-editor-background)",
|
||||
fontSize: "10px",
|
||||
}}>
|
||||
npm install -g cline
|
||||
</code>
|
||||
</span>
|
||||
</p>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await StateServiceClient.installClineCli(EmptyRequest.create())
|
||||
} catch (error) {
|
||||
console.error("Failed to initiate CLI installation:", error)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
transform: "scale(0.75)",
|
||||
transformOrigin: "left center",
|
||||
marginTop: "2px",
|
||||
}}>
|
||||
Install Now
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subagentsEnabled && (
|
||||
<div>
|
||||
<SubagentOutputLineLimitSlider />
|
||||
</div>
|
||||
)}
|
||||
</FeatureItem>
|
||||
)}
|
||||
</FeatureGroup>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { FeatureGroup } from "../FeatureGroup"
|
||||
import { FeatureItem } from "../FeatureItem"
|
||||
import PreferredLanguageSetting from "../PreferredLanguageSetting"
|
||||
import Section from "../Section"
|
||||
import { updateSetting } from "../utils/settingsHandlers"
|
||||
@@ -16,49 +18,57 @@ const GeneralSettingsSection = ({ renderSectionHeader }: GeneralSettingsSectionP
|
||||
<div>
|
||||
{renderSectionHeader("general")}
|
||||
<Section>
|
||||
<PreferredLanguageSetting />
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{/* LANGUAGE PREFERENCES */}
|
||||
<FeatureGroup isGridItem={false} title="Language">
|
||||
<PreferredLanguageSetting />
|
||||
</FeatureGroup>
|
||||
|
||||
<div className="mb-[5px]">
|
||||
<Tooltip>
|
||||
<TooltipContent hidden={remoteConfigSettings?.telemetrySetting === undefined}>
|
||||
This setting is managed by your organization's remote configuration
|
||||
</TooltipContent>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2 mb-[5px]">
|
||||
<VSCodeCheckbox
|
||||
checked={telemetrySetting === "enabled"}
|
||||
disabled={remoteConfigSettings?.telemetrySetting === "disabled"}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
updateSetting("telemetrySetting", checked ? "enabled" : "disabled")
|
||||
}}>
|
||||
Allow error and usage reporting
|
||||
</VSCodeCheckbox>
|
||||
{!!remoteConfigSettings?.telemetrySetting && (
|
||||
<i className="codicon codicon-lock text-description text-sm" />
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
{/* TELEMETRY & PRIVACY */}
|
||||
<FeatureGroup isGridItem={false} title="Telemetry & Privacy">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<FeatureItem
|
||||
checked={telemetrySetting === "enabled"}
|
||||
description={`Help improve Cline by sending usage data and error reports. No code, prompts, or personal information are ever sent.`}
|
||||
disabled={remoteConfigSettings?.telemetrySetting === "disabled"}
|
||||
label="Allow error and usage reporting"
|
||||
onChange={(checked) =>
|
||||
updateSetting("telemetrySetting", checked ? "enabled" : "disabled")
|
||||
}
|
||||
/>
|
||||
{!!remoteConfigSettings?.telemetrySetting && (
|
||||
<div
|
||||
className="mt-2 text-xs flex items-center gap-2"
|
||||
style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
<i className="codicon codicon-lock text-sm" />
|
||||
<span>This setting is managed by your organization's remote configuration</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent hidden={remoteConfigSettings?.telemetrySetting === undefined}>
|
||||
This setting is managed by your organization's remote configuration
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<p className="text-sm mt-[5px] text-description">
|
||||
Help improve Cline by sending usage data and error reports. No code, prompts, or personal information are
|
||||
ever sent. See our{" "}
|
||||
<VSCodeLink
|
||||
className="text-inherit"
|
||||
href="https://docs.cline.bot/more-info/telemetry"
|
||||
style={{ fontSize: "inherit", textDecoration: "underline" }}>
|
||||
telemetry overview
|
||||
</VSCodeLink>{" "}
|
||||
and{" "}
|
||||
<VSCodeLink
|
||||
className="text-inherit"
|
||||
href="https://cline.bot/privacy"
|
||||
style={{ fontSize: "inherit", textDecoration: "underline" }}>
|
||||
privacy policy
|
||||
</VSCodeLink>{" "}
|
||||
for more details.
|
||||
</p>
|
||||
<div className="mt-2 text-xs" style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
See our{" "}
|
||||
<VSCodeLink
|
||||
href="https://docs.cline.bot/more-info/telemetry"
|
||||
style={{ fontSize: "inherit", textDecoration: "underline" }}>
|
||||
telemetry overview
|
||||
</VSCodeLink>{" "}
|
||||
and{" "}
|
||||
<VSCodeLink
|
||||
href="https://cline.bot/privacy"
|
||||
style={{ fontSize: "inherit", textDecoration: "underline" }}>
|
||||
privacy policy
|
||||
</VSCodeLink>{" "}
|
||||
for more details.
|
||||
</div>
|
||||
</FeatureGroup>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { UpdateTerminalConnectionTimeoutResponse } from "@shared/proto/index.cline"
|
||||
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { useState } from "react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { PlatformType } from "@/config/platform.config"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { usePlatform } from "@/context/PlatformContext"
|
||||
import { StateServiceClient } from "../../../services/grpc-client"
|
||||
import { FeatureGroup } from "../FeatureGroup"
|
||||
import { FeatureItem } from "../FeatureItem"
|
||||
import Section from "../Section"
|
||||
import TerminalOutputLineLimitSlider from "../TerminalOutputLineLimitSlider"
|
||||
import { updateSetting } from "../utils/settingsHandlers"
|
||||
@@ -45,8 +48,6 @@ export const TerminalSettingsSection: React.FC<TerminalSettingsSectionProps> = (
|
||||
StateServiceClient.updateTerminalConnectionTimeout({ timeoutMs })
|
||||
.then((response: UpdateTerminalConnectionTimeoutResponse) => {
|
||||
const timeoutMs = response.timeoutMs
|
||||
// Backend calls postStateToWebview(), so state will update via subscription
|
||||
// Just sync the input value with the confirmed backend value
|
||||
if (timeoutMs !== undefined) {
|
||||
setInputValue((timeoutMs / 1000).toString())
|
||||
}
|
||||
@@ -63,121 +64,124 @@ export const TerminalSettingsSection: React.FC<TerminalSettingsSectionProps> = (
|
||||
}
|
||||
}
|
||||
|
||||
const handleTerminalReuseChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const checked = target.checked
|
||||
updateSetting("terminalReuseEnabled", checked)
|
||||
}
|
||||
|
||||
const handleExecutionModeChange = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const value = target.value === "backgroundExec" ? "backgroundExec" : "vscodeTerminal"
|
||||
updateSetting("vscodeTerminalExecutionMode", value)
|
||||
}
|
||||
|
||||
// Use any to avoid type conflicts between Event and FormEvent
|
||||
const handleDefaultTerminalProfileChange = (event: any) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
const profileId = target.value
|
||||
|
||||
// Save immediately using the consolidated updateSettings approach
|
||||
updateSetting("defaultTerminalProfile", profileId || "default")
|
||||
}
|
||||
|
||||
const profilesToShow = availableTerminalProfiles
|
||||
|
||||
return (
|
||||
<div>
|
||||
{renderSectionHeader("terminal")}
|
||||
<Section>
|
||||
<div className="mb-5" id="terminal-settings-section">
|
||||
<div className="mb-4">
|
||||
<label className="font-medium block mb-1" htmlFor="default-terminal-profile">
|
||||
Default Terminal Profile
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
className="w-full"
|
||||
id="default-terminal-profile"
|
||||
onChange={handleDefaultTerminalProfileChange}
|
||||
value={defaultTerminalProfile || "default"}>
|
||||
{profilesToShow.map((profile) => (
|
||||
<VSCodeOption key={profile.id} title={profile.description} value={profile.id}>
|
||||
{profile.name}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground) mt-1">
|
||||
Select the default terminal Cline will use. 'Default' uses your VSCode global setting.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{/* TERMINAL CONFIGURATION */}
|
||||
<FeatureGroup isGridItem={false} title="Terminal Configuration">
|
||||
{/* Default Terminal Profile */}
|
||||
<div className="flex items-center justify-between gap-3 px-2">
|
||||
<label
|
||||
className="text-sm font-medium"
|
||||
htmlFor="default-terminal-profile"
|
||||
style={{ color: "var(--vscode-foreground)" }}>
|
||||
Default Terminal Profile
|
||||
</label>
|
||||
<div className="pr-2">
|
||||
<Select
|
||||
onValueChange={(profileId) => updateSetting("defaultTerminalProfile", profileId || "default")}
|
||||
value={defaultTerminalProfile || "default"}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profilesToShow.map((profile) => (
|
||||
<SelectItem key={profile.id} value={profile.id}>
|
||||
{profile.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<label className="font-medium block mb-1">Shell integration timeout (seconds)</label>
|
||||
<div className="flex items-center">
|
||||
{/* Shell Integration Timeout */}
|
||||
<div className="flex items-center justify-between gap-3 px-2">
|
||||
<label className="text-sm font-medium" style={{ color: "var(--vscode-foreground)" }}>
|
||||
Shell integration timeout (seconds)
|
||||
</label>
|
||||
<div className="pr-2">
|
||||
<VSCodeTextField
|
||||
className="w-full"
|
||||
className="w-20 text-xs"
|
||||
onBlur={handleInputBlur}
|
||||
onChange={(event) => handleTimeoutChange(event as Event)}
|
||||
placeholder="Enter timeout in seconds"
|
||||
placeholder="Seconds"
|
||||
value={inputValue}
|
||||
/>
|
||||
</div>
|
||||
{inputError && <div className="text-(--vscode-errorForeground) text-xs mt-1">{inputError}</div>}
|
||||
</div>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground)">
|
||||
Set how long Cline waits for shell integration to activate before executing commands. Increase this
|
||||
value if you experience terminal connection timeouts.
|
||||
</p>
|
||||
</div>
|
||||
{inputError && (
|
||||
<div className="text-xs px-2" style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
{inputError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center mb-2">
|
||||
<VSCodeCheckbox
|
||||
checked={terminalReuseEnabled ?? true}
|
||||
onChange={(event) => handleTerminalReuseChange(event as Event)}>
|
||||
Enable aggressive terminal reuse
|
||||
</VSCodeCheckbox>
|
||||
{/* Enable Aggressive Terminal Reuse */}
|
||||
<FeatureItem
|
||||
checked={terminalReuseEnabled ?? true}
|
||||
description="When enabled, Cline will reuse existing terminal windows that aren't in the current working directory. Disable this if you experience issues with task lockout after a terminal command."
|
||||
label="Enable aggressive terminal reuse"
|
||||
onChange={(checked) => updateSetting("terminalReuseEnabled", checked)}
|
||||
/>
|
||||
|
||||
{/* Terminal Execution Mode */}
|
||||
{isVsCodePlatform && (
|
||||
<div className="flex items-center justify-between gap-3 px-2">
|
||||
<label
|
||||
className="text-sm font-medium"
|
||||
htmlFor="terminal-execution-mode"
|
||||
style={{ color: "var(--vscode-foreground)" }}>
|
||||
Terminal Execution Mode
|
||||
</label>
|
||||
<div className="pr-2">
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
updateSetting(
|
||||
"vscodeTerminalExecutionMode",
|
||||
value as "vscodeTerminal" | "backgroundExec",
|
||||
)
|
||||
}
|
||||
value={vscodeTerminalExecutionMode ?? "vscodeTerminal"}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="vscodeTerminal">VS Code Terminal</SelectItem>
|
||||
<SelectItem value="backgroundExec">Background Exec</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Terminal Output Line Limit */}
|
||||
<div className="px-2">
|
||||
<TerminalOutputLineLimitSlider />
|
||||
</div>
|
||||
<p className="text-xs text-(--vscode-descriptionForeground)">
|
||||
When enabled, Cline will reuse existing terminal windows that aren't in the current working directory.
|
||||
Disable this if you experience issues with task lockout after a terminal command.
|
||||
</p>
|
||||
</div>
|
||||
{isVsCodePlatform && (
|
||||
<div className="mb-4">
|
||||
<label className="font-medium block mb-1" htmlFor="terminal-execution-mode">
|
||||
Terminal Execution Mode
|
||||
</label>
|
||||
<VSCodeDropdown
|
||||
className="w-full"
|
||||
id="terminal-execution-mode"
|
||||
onChange={(event) => handleExecutionModeChange(event as Event)}
|
||||
value={vscodeTerminalExecutionMode ?? "vscodeTerminal"}>
|
||||
<VSCodeOption value="vscodeTerminal">VS Code Terminal</VSCodeOption>
|
||||
<VSCodeOption value="backgroundExec">Background Exec</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
<p className="text-xs text-[var(--vscode-descriptionForeground)] mt-1">
|
||||
Choose whether Cline runs commands in the VS Code terminal or a background process.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<TerminalOutputLineLimitSlider />
|
||||
<div className="mt-5 p-3 bg-(--vscode-textBlockQuote-background) rounded border border-(--vscode-textBlockQuote-border)">
|
||||
<p className="text-[13px] m-0">
|
||||
</FeatureGroup>
|
||||
|
||||
{/* HELP & DOCUMENTATION */}
|
||||
<div className="p-3 rounded-md" style={{ backgroundColor: "rgba(255, 255, 255, 0.03)" }}>
|
||||
<p className="text-xs m-0" style={{ color: "var(--vscode-foreground)" }}>
|
||||
<strong>Having terminal issues?</strong> Check our{" "}
|
||||
<a
|
||||
className="text-(--vscode-textLink-foreground) underline hover:no-underline"
|
||||
className="underline hover:no-underline"
|
||||
href="https://docs.cline.bot/troubleshooting/terminal-quick-fixes"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "var(--vscode-textLink-foreground)" }}
|
||||
target="_blank">
|
||||
Terminal Quick Fixes
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
className="text-(--vscode-textLink-foreground) underline hover:no-underline"
|
||||
className="underline hover:no-underline"
|
||||
href="https://docs.cline.bot/troubleshooting/terminal-integration-guide"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "var(--vscode-textLink-foreground)" }}
|
||||
target="_blank">
|
||||
Complete Troubleshooting Guide
|
||||
</a>
|
||||
|
||||
@@ -31,11 +31,12 @@ function SelectTrigger({
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
className={cn(
|
||||
"border-editor-group-border data-[placeholder]:text-input-placeholder [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-editor-group-border focus-visible:ring-ring/20 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-error flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-2",
|
||||
"border-editor-group-border data-[placeholder]:text-input-placeholder [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-editor-group-border focus-visible:ring-ring/20 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-error flex w-fit items-center justify-between gap-2 rounded border px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-2",
|
||||
className,
|
||||
)}
|
||||
data-size={size}
|
||||
data-slot="select-trigger"
|
||||
style={{ backgroundColor: "rgba(255, 255, 255, 0.02)" }}
|
||||
{...props}>
|
||||
{children}
|
||||
{showIcon && (
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"data-[state=checked]:bg-[var(--vscode-button-background)] data-[state=unchecked]:bg-[var(--vscode-input-border)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform",
|
||||
"data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Toggle.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Toggle }
|
||||
@@ -128,7 +128,9 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
// UI view state
|
||||
const [showMcp, setShowMcp] = useState(false)
|
||||
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
// Check if this is a settings-only panel
|
||||
const isSettingsPanel = typeof window !== "undefined" && (window as any).CLINE_SETTINGS_PANEL === true
|
||||
const [showSettings, setShowSettings] = useState(isSettingsPanel)
|
||||
const [settingsTargetSection, setSettingsTargetSection] = useState<string | undefined>(undefined)
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showAccount, setShowAccount] = useState(false)
|
||||
|
||||
Reference in New Issue
Block a user