Compare commits

...

5 Commits

Author SHA1 Message Date
Jose R. Perez 7a981babae feat: settings ui changes 2026-01-22 13:40:53 -05:00
Jose R. Perez 6e3bc1d788 Merge branch conflict resolution: resolve package-lock.json 2026-01-20 13:08:40 -05:00
Jose R. Perez 8347054d52 feat: feature grouping 2026-01-20 13:07:36 -05:00
Jose R. Perez 8e3a50a002 feat: open settings in new window 2026-01-20 09:42:14 -05:00
Jose R. Perez 558a629bd4 feat: open settings in new window 2025-12-29 10:44:36 -05:00
22 changed files with 6728 additions and 4408 deletions
+5334 -3637
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
}
+15
View File
@@ -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) {
+27
View File
@@ -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>
+2 -1
View File
@@ -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 && (
+27
View File
@@ -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)