Compare commits

...

7 Commits

Author SHA1 Message Date
Cline Evaluation b3076cc2c8 bringing back mcp settings button 2025-05-21 22:33:30 -07:00
Cline Evaluation fd1fe64bb9 Merge remote-tracking branch 'origin/main' into pashpashpash/settings-tabs 2025-05-21 22:27:09 -07:00
Cline Evaluation 7990ae3eca py 2025-05-21 22:26:27 -07:00
Cline Evaluation d9688d0c93 changeset 2025-05-21 22:23:20 -07:00
Cline Evaluation ec592ac08a tidying up 2025-05-21 22:21:49 -07:00
Cline Evaluation ddddb5d519 making sure things scroll 2025-05-21 22:08:12 -07:00
Cline Evaluation 678aaceb3a new settings view tabs 2025-05-21 21:34:23 -07:00
14 changed files with 668 additions and 220 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
New settings page split into tabs
+51 -62
View File
@@ -11,6 +11,7 @@
"@floating-ui/react": "^0.27.4",
"@heroui/react": "^2.8.0-beta.2",
"@vscode/webview-ui-toolkit": "^1.4.0",
"clsx": "^2.1.1",
"debounce": "^2.1.1",
"dompurify": "^3.2.4",
"fast-deep-equal": "^3.1.3",
@@ -19,6 +20,7 @@
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"katex": "^0.16.22",
"lucide-react": "^0.511.0",
"mermaid": "^11.4.1",
"package.json": "^2.0.1",
"posthog-js": "^1.224.0",
@@ -37,6 +39,7 @@
"remark-math": "^6.0.0",
"remark-stringify": "^11.0.0",
"styled-components": "^6.1.15",
"tailwind-merge": "^3.3.0",
"unified": "^11.0.5",
"uuid": "^9.0.1"
},
@@ -2513,6 +2516,14 @@
"react": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/system-rsc/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"engines": {
"node": ">=6"
}
},
"node_modules/@heroui/table": {
"version": "2.2.17-beta.2",
"resolved": "https://registry.npmjs.org/@heroui/table/-/table-2.2.17-beta.2.tgz",
@@ -2590,6 +2601,23 @@
"tailwindcss": ">=4.0.0"
}
},
"node_modules/@heroui/theme/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"engines": {
"node": ">=6"
}
},
"node_modules/@heroui/theme/node_modules/tailwind-merge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/@heroui/toast": {
"version": "2.0.8-beta.2",
"resolved": "https://registry.npmjs.org/@heroui/toast/-/toast-2.0.8-beta.2.tgz",
@@ -3615,15 +3643,6 @@
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/focus/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-aria/form": {
"version": "3.0.14",
"resolved": "https://registry.npmjs.org/@react-aria/form/-/form-3.0.14.tgz",
@@ -3839,15 +3858,6 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/grid/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-aria/i18n": {
"version": "3.12.7",
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.7.tgz",
@@ -3970,15 +3980,6 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/landmark/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-aria/link": {
"version": "3.7.10",
"resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.7.10.tgz",
@@ -4290,15 +4291,6 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/spinbutton/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
@@ -4535,15 +4527,6 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/toggle/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-aria/toolbar": {
"version": "3.0.0-beta.14",
"resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.14.tgz",
@@ -4597,15 +4580,6 @@
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-aria/visually-hidden": {
"version": "3.8.21",
"resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.21.tgz",
@@ -8278,10 +8252,9 @@
}
},
"node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"license": "MIT",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
@@ -12487,6 +12460,14 @@
"dev": true,
"license": "ISC"
},
"node_modules/lucide-react": {
"version": "0.511.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz",
"integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -15865,10 +15846,9 @@
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
"license": "MIT",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz",
"integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -15890,6 +15870,15 @@
"tailwindcss": "*"
}
},
"node_modules/tailwind-variants/node_modules/tailwind-merge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.5.tgz",
+3
View File
@@ -18,6 +18,7 @@
"@floating-ui/react": "^0.27.4",
"@heroui/react": "^2.8.0-beta.2",
"@vscode/webview-ui-toolkit": "^1.4.0",
"clsx": "^2.1.1",
"debounce": "^2.1.1",
"dompurify": "^3.2.4",
"fast-deep-equal": "^3.1.3",
@@ -26,6 +27,7 @@
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"katex": "^0.16.22",
"lucide-react": "^0.511.0",
"mermaid": "^11.4.1",
"package.json": "^2.0.1",
"posthog-js": "^1.224.0",
@@ -44,6 +46,7 @@
"remark-math": "^6.0.0",
"remark-stringify": "^11.0.0",
"styled-components": "^6.1.15",
"tailwind-merge": "^3.3.0",
"unified": "^11.0.5",
"uuid": "^9.0.1"
},
@@ -71,7 +71,7 @@ export const BrowserSettingsMenu = () => {
// After a short delay, send a message to scroll to browser settings
setTimeout(async () => {
try {
await UiServiceClient.scrollToSettings({ value: "browser-settings-section" })
await UiServiceClient.scrollToSettings({ value: "browser" })
} catch (error) {
console.error("Error scrolling to browser settings:", error)
}
+93
View File
@@ -0,0 +1,93 @@
import React, { HTMLAttributes, useCallback, forwardRef } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { cn } from "@/utils/cn"
type TabProps = HTMLAttributes<HTMLDivElement>
export const Tab = ({ className, children, ...props }: TabProps) => (
<div className={cn("fixed inset-0 flex flex-col", className)} {...props}>
{children}
</div>
)
export const TabHeader = ({ className, children, ...props }: TabProps) => (
<div className={cn("px-5 py-2.5 border-b border-[var(--vscode-panel-border)]", className)} {...props}>
{children}
</div>
)
export const TabContent = ({ className, children, ...props }: TabProps) => {
const onWheel = useCallback((e: React.WheelEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement
// Prevent scrolling if the target is a listbox or option
// (e.g. selects, dropdowns, etc).
if (target.role === "listbox" || target.role === "option") {
return
}
e.currentTarget.scrollTop += e.deltaY
}, [])
return (
<div className={cn("flex-1 overflow-auto", className)} onWheel={onWheel} {...props}>
{children}
</div>
)
}
export const TabList = forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement> & {
value: string
onValueChange: (value: string) => void
}
>(({ children, className, value, onValueChange, ...props }, ref) => {
const handleTabSelect = useCallback(
(tabValue: string) => {
console.log("Tab selected:", tabValue)
onValueChange(tabValue)
},
[onValueChange],
)
return (
<div ref={ref} role="tablist" className={cn("flex", className)} {...props}>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
// Make sure we're passing the correct props to the TabTrigger
return React.cloneElement(child as React.ReactElement<any>, {
isSelected: child.props.value === value,
onSelect: () => handleTabSelect(child.props.value),
})
}
return child
})}
</div>
)
})
export const TabTrigger = forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement> & {
value: string
isSelected?: boolean
onSelect?: () => void
}
>(({ children, className, value, isSelected, onSelect, ...props }, ref) => {
// Ensure we're using the value prop correctly
return (
<button
ref={ref}
role="tab"
aria-selected={isSelected}
tabIndex={isSelected ? 0 : -1}
className={cn("focus:outline-none", className)}
onClick={onSelect}
data-value={value} // Add data-value attribute for debugging
{...props}>
{children}
</button>
)
})
@@ -1,9 +1,12 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { vscode } from "@/utils/vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { UiServiceClient } from "@/services/grpc-client"
import ServersToggleList from "./ServersToggleList"
const InstalledServersView = () => {
const { mcpServers: servers } = useExtensionState()
const { mcpServers: servers, navigateToSettings } = useExtensionState()
return (
<div style={{ padding: "16px 20px" }}>
@@ -42,6 +45,26 @@ const InstalledServersView = () => {
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
Configure MCP Servers
</VSCodeButton>
<div style={{ textAlign: "center" }}>
<VSCodeLink
onClick={() => {
// First open the settings panel using direct navigation
navigateToSettings()
// After a short delay, send a message to scroll to browser settings
setTimeout(async () => {
try {
await UiServiceClient.scrollToSettings({ value: "features" })
} catch (error) {
console.error("Error scrolling to mcp settings:", error)
}
}, 300)
}}
style={{ fontSize: "12px" }}>
Advanced MCP Settings
</VSCodeLink>
</div>
</div>
</div>
)
@@ -366,11 +366,7 @@ export const BrowserSettingsSection: React.FC = () => {
const isSubSettingsOpen = !(browserSettings.disableToolUse || false)
return (
<div
id="browser-settings-section"
style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Browser Settings</h3>
<div id="browser-settings-section" style={{ marginBottom: 20 }}>
{/* Master Toggle */}
<div style={{ marginBottom: isSubSettingsOpen ? 0 : 10 }}>
<VSCodeCheckbox
@@ -14,8 +14,7 @@ const FeatureSettingsSection = () => {
} = useExtensionState()
return (
<div style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Feature Settings</h3>
<div style={{ marginBottom: 20 }}>
<div>
<VSCodeCheckbox
checked={enableCheckpointsSetting}
@@ -9,7 +9,7 @@ interface PreferredLanguageSettingProps {
const PreferredLanguageSetting: React.FC<PreferredLanguageSettingProps> = ({ chatSettings, setChatSettings }) => {
return (
<div style={{ marginTop: 10, marginBottom: 10 }}>
<div style={{}}>
<label htmlFor="preferred-language-dropdown" className="block mb-1 text-sm font-medium">
Preferred Language
</label>
@@ -0,0 +1,10 @@
import { HTMLAttributes } from "react"
import { cn } from "@/utils/cn"
type SectionProps = HTMLAttributes<HTMLDivElement>
export const Section = ({ className, ...props }: SectionProps) => (
<div className={cn("flex flex-col gap-3 p-5 py-2", className)} {...props} />
)
export default Section
@@ -0,0 +1,25 @@
import { HTMLAttributes } from "react"
import { cn } from "@/utils/cn"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
type SectionHeaderProps = HTMLAttributes<HTMLDivElement> & {
children: React.ReactNode
description?: string
}
export const SectionHeader = ({ description, children, className, ...props }: SectionHeaderProps) => {
return (
<div
className={cn(
`sticky top-0 z-${OPENROUTER_MODEL_PICKER_Z_INDEX + 20} text-[var(--vscode-foreground)] bg-[var(--vscode-panel-background)] px-5 py-3`,
className,
)}
{...props}>
<h4 className="m-0">{children}</h4>
{description && <p className="text-[var(--vscode-descriptionForeground)] text-sm mt-2 mb-0">{description}</p>}
</div>
)
}
export default SectionHeader
@@ -6,7 +6,24 @@ import {
VSCodeOption,
VSCodeTextArea,
} from "@vscode/webview-ui-toolkit/react"
import { memo, useCallback, useEffect, useState } from "react"
import { memo, useCallback, useEffect, useState, useRef } from "react"
import {
Settings,
Webhook,
CheckCheck,
SquareMousePointer,
GitBranch,
Bell,
Database,
SquareTerminal,
FlaskConical,
Globe,
Info,
LucideIcon,
} from "lucide-react"
import HeroTooltip from "@/components/common/HeroTooltip"
import SectionHeader from "./SectionHeader"
import Section from "./Section"
import PreferredLanguageSetting from "./PreferredLanguageSetting" // Added import
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
import { useExtensionState } from "@/context/ExtensionStateContext"
@@ -22,13 +39,91 @@ import FeatureSettingsSection from "./FeatureSettingsSection"
import BrowserSettingsSection from "./BrowserSettingsSection"
import TerminalSettingsSection from "./TerminalSettingsSection"
import { FEATURE_FLAGS } from "@shared/services/feature-flags/feature-flags"
import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
import { cn } from "@/utils/cn"
const { IS_DEV } = process.env
// Styles for the tab system
const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
const settingsTabList =
"w-48 data-[compact=true]:w-12 flex-shrink-0 flex flex-col overflow-y-auto overflow-x-hidden border-r border-[var(--vscode-sideBar-background)]"
const settingsTabTrigger =
"whitespace-nowrap overflow-hidden min-w-0 h-12 px-4 py-3 box-border flex items-center border-l-2 border-transparent text-[var(--vscode-foreground)] opacity-70 bg-transparent hover:bg-[var(--vscode-list-hoverBackground)] data-[compact=true]:w-12 data-[compact=true]:p-4 cursor-pointer"
const settingsTabTriggerActive =
"opacity-100 border-l-2 border-l-[var(--vscode-focusBorder)] border-t-0 border-r-0 border-b-0 bg-[var(--vscode-list-activeSelectionBackground)]"
// Tab definitions
interface SettingsTab {
id: string
name: string
tooltipText: string
headerText: string
icon: LucideIcon
}
export const SETTINGS_TABS: SettingsTab[] = [
{
id: "api-config",
name: "API Configuration",
tooltipText: "API Configuration",
headerText: "API Configuration",
icon: Webhook,
},
{
id: "general",
name: "General",
tooltipText: "General Settings",
headerText: "General Settings",
icon: Settings,
},
{
id: "features",
name: "Features",
tooltipText: "Feature Settings",
headerText: "Feature Settings",
icon: CheckCheck,
},
{
id: "browser",
name: "Browser",
tooltipText: "Browser Settings",
headerText: "Browser Settings",
icon: SquareMousePointer,
},
{
id: "terminal",
name: "Terminal",
tooltipText: "Terminal Settings",
headerText: "Terminal Settings",
icon: SquareTerminal,
},
// Only show in dev mode
...(IS_DEV
? [
{
id: "debug",
name: "Debug",
tooltipText: "Debug Tools",
headerText: "Debug",
icon: FlaskConical,
},
]
: []),
{
id: "about",
name: "About",
tooltipText: "About Cline",
headerText: "About",
icon: Info,
},
]
type SettingsViewProps = {
onDone: () => void
targetSection?: string
}
const SettingsView = ({ onDone }: SettingsViewProps) => {
const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
const {
apiConfiguration,
version,
@@ -126,24 +221,35 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setPendingTabChange(null)
}
break
// Handle tab navigation through targetSection prop instead
case "grpc_response":
if (message.grpc_response?.message?.action === "scrollToSettings") {
setTimeout(() => {
const elementId = message.grpc_response?.message?.value
if (elementId) {
const element = document.getElementById(elementId)
if (element) {
element.scrollIntoView({ behavior: "smooth" })
const tabId = message.grpc_response?.message?.value
if (tabId) {
console.log("Opening settings tab from GRPC response:", tabId)
// Check if the value corresponds to a valid tab ID
const isValidTabId = SETTINGS_TABS.some((tab) => tab.id === tabId)
element.style.transition = "background-color 0.5s ease"
element.style.backgroundColor = "var(--vscode-textPreformat-background)"
if (isValidTabId) {
// Set the active tab directly
setActiveTab(tabId)
} else {
// Fall back to the old behavior of scrolling to an element
setTimeout(() => {
const element = document.getElementById(tabId)
if (element) {
element.scrollIntoView({ behavior: "smooth" })
setTimeout(() => {
element.style.backgroundColor = "transparent"
}, 1200)
}
element.style.transition = "background-color 0.5s ease"
element.style.backgroundColor = "var(--vscode-textPreformat-background)"
setTimeout(() => {
element.style.backgroundColor = "transparent"
}, 1200)
}
}, 300)
}
}, 300)
}
}
break
}
@@ -161,7 +267,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
}
}
const handleTabChange = (tab: "plan" | "act") => {
const handlePlanActModeChange = (tab: "plan" | "act") => {
if (tab === chatSettings.mode) {
return
}
@@ -169,136 +275,325 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
handleSubmit(true)
}
// Track active tab
const [activeTab, setActiveTab] = useState<string>(targetSection || SETTINGS_TABS[0].id)
// Update active tab when targetSection changes
useEffect(() => {
if (targetSection) {
setActiveTab(targetSection)
}
}, [targetSection])
// Enhanced tab change handler with debugging
const handleTabChange = useCallback(
(tabId: string) => {
console.log("Tab change requested:", tabId, "Current:", activeTab)
setActiveTab(tabId)
},
[activeTab],
)
// Debug tab changes
useEffect(() => {
console.log("Active tab changed to:", activeTab)
}, [activeTab])
// Track whether we're in compact mode
const [isCompactMode, setIsCompactMode] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
// Setup resize observer to detect when we should switch to compact mode
useEffect(() => {
if (!containerRef.current) return
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
// If container width is less than 500px, switch to compact mode
setIsCompactMode(entry.contentRect.width < 500)
}
})
observer.observe(containerRef.current)
return () => {
observer?.disconnect()
}
}, [])
return (
<div className="fixed top-0 left-0 right-0 bottom-0 pt-[10px] pr-0 pb-0 pl-5 flex flex-col overflow-hidden">
<div className="flex justify-between items-center mb-[13px] pr-[17px]">
<h3 className="text-[var(--vscode-foreground)] m-0">Settings</h3>
<VSCodeButton onClick={() => handleSubmit(false)}>Save</VSCodeButton>
<Tab>
<TabHeader className="flex justify-between items-center gap-2">
<div className="flex items-center gap-1">
<h3 className="text-[var(--vscode-foreground)] m-0">Settings</h3>
</div>
<div className="flex gap-2">
<VSCodeButton onClick={() => handleSubmit(false)}>Save</VSCodeButton>
</div>
</TabHeader>
{/* Vertical tabs layout */}
<div ref={containerRef} className={cn(settingsTabsContainer, isCompactMode && "narrow")}>
{/* Tab sidebar */}
<TabList
value={activeTab}
onValueChange={handleTabChange}
className={cn(settingsTabList)}
data-compact={isCompactMode}>
{SETTINGS_TABS.map((tab) =>
isCompactMode ? (
<HeroTooltip key={tab.id} content={tab.tooltipText} placement="right">
<div
className={cn(
activeTab === tab.id
? `${settingsTabTrigger} ${settingsTabTriggerActive}`
: settingsTabTrigger,
"focus:ring-0",
)}
data-compact={isCompactMode}
data-testid={`tab-${tab.id}`}
data-value={tab.id}
onClick={() => {
console.log("Compact tab clicked:", tab.id)
handleTabChange(tab.id)
}}>
<div className={cn("flex items-center gap-2", isCompactMode && "justify-center")}>
<tab.icon className="w-4 h-4" />
<span className="tab-label">{tab.name}</span>
</div>
</div>
</HeroTooltip>
) : (
<TabTrigger
key={tab.id}
value={tab.id}
className={cn(
activeTab === tab.id
? `${settingsTabTrigger} ${settingsTabTriggerActive}`
: settingsTabTrigger,
"focus:ring-0",
)}
data-compact={isCompactMode}
data-testid={`tab-${tab.id}`}>
<div className={cn("flex items-center gap-2", isCompactMode && "justify-center")}>
<tab.icon className="w-4 h-4" />
<span className="tab-label">{tab.name}</span>
</div>
</TabTrigger>
),
)}
</TabList>
{/* Helper function to render section header */}
{(() => {
const renderSectionHeader = (tabId: string) => {
const tab = SETTINGS_TABS.find((t) => t.id === tabId)
if (!tab) return null
return (
<SectionHeader>
<div className="flex items-center gap-2">
{(() => {
const Icon = tab.icon
return <Icon className="w-4" />
})()}
<div>{tab.headerText}</div>
</div>
</SectionHeader>
)
}
return (
<TabContent className="flex-1 overflow-auto">
{/* API Configuration Tab */}
{activeTab === "api-config" && (
<div>
{renderSectionHeader("api-config")}
<Section>
{/* Tabs container */}
{planActSeparateModelsSetting ? (
<div className="rounded-md mb-5 bg-[var(--vscode-panel-background)]">
<div className="flex gap-[1px] mb-[10px] -mt-2 border-0 border-b border-solid border-[var(--vscode-panel-border)]">
<TabButton
isActive={chatSettings.mode === "plan"}
onClick={() => handlePlanActModeChange("plan")}>
Plan Mode
</TabButton>
<TabButton
isActive={chatSettings.mode === "act"}
onClick={() => handlePlanActModeChange("act")}>
Act Mode
</TabButton>
</div>
{/* Content container */}
<div className="-mb-3">
<ApiOptions
key={chatSettings.mode}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
</div>
</div>
) : (
<ApiOptions
key={"single"}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
)}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={planActSeparateModelsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setPlanActSeparateModelsSetting(checked)
}}>
Use different models for Plan and Act modes
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Switching between Plan and Act mode will persist the API and model used in the
previous mode. This may be helpful e.g. when using a strong reasoning model to
architect a plan for a cheaper coding model to act on.
</p>
</div>
<div className="mb-[5px]">
<VSCodeTextArea
value={customInstructions ?? ""}
className="w-full"
resize="vertical"
rows={4}
placeholder={
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
}
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
<span className="font-medium">Custom Instructions</span>
</VSCodeTextArea>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
These instructions are added to the end of the system prompt sent with every
request.
</p>
</div>
</Section>
</div>
)}
{/* General Settings Tab */}
{activeTab === "general" && (
<div>
{renderSectionHeader("general")}
<Section>
{chatSettings && (
<PreferredLanguageSetting
chatSettings={chatSettings}
setChatSettings={setChatSettings}
/>
)}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={telemetrySetting === "enabled"}
onChange={(e: any) => {
const checked = e.target.checked === true
setTelemetrySetting(checked ? "enabled" : "disabled")
}}>
Allow anonymous error and usage reporting
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Help improve Cline by sending anonymous usage data and error reports. No code,
prompts, or personal information are ever sent. See our{" "}
<VSCodeLink
href="https://docs.cline.bot/more-info/telemetry"
className="text-inherit">
telemetry overview
</VSCodeLink>{" "}
and{" "}
<VSCodeLink href="https://cline.bot/privacy" className="text-inherit">
privacy policy
</VSCodeLink>{" "}
for more details.
</p>
</div>
</Section>
</div>
)}
{/* Feature Settings Tab */}
{activeTab === "features" && (
<div>
{renderSectionHeader("features")}
<Section>
<FeatureSettingsSection />
</Section>
</div>
)}
{/* Browser Settings Tab */}
{activeTab === "browser" && (
<div>
{renderSectionHeader("browser")}
<Section>
<BrowserSettingsSection />
</Section>
</div>
)}
{/* Terminal Settings Tab */}
{activeTab === "terminal" && (
<div>
{renderSectionHeader("terminal")}
<Section>
<TerminalSettingsSection />
</Section>
</div>
)}
{/* Debug Tab (only in dev mode) */}
{IS_DEV && activeTab === "debug" && (
<div>
{renderSectionHeader("debug")}
<Section>
<VSCodeButton
onClick={handleResetState}
className="mt-[5px] w-auto"
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
Reset State
</VSCodeButton>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
This will reset all global state and secret storage in the extension.
</p>
</Section>
</div>
)}
{/* About Tab */}
{activeTab === "about" && (
<div>
{renderSectionHeader("about")}
<Section>
<div className="text-center text-[var(--vscode-descriptionForeground)] text-xs leading-[1.2] px-0 py-0 pr-2 pb-[15px] mt-auto">
<p className="break-words m-0 p-0">
If you have any questions or feedback, feel free to open an issue at{" "}
<VSCodeLink href="https://github.com/cline/cline" className="inline">
https://github.com/cline/cline
</VSCodeLink>
</p>
<p className="italic mt-[10px] mb-0 p-0">v{version}</p>
</div>
</Section>
</div>
)}
</TabContent>
)
})()}
</div>
<div className="grow overflow-y-scroll pr-2 flex flex-col">
{/* Tabs container */}
{planActSeparateModelsSetting ? (
<div className="border border-solid border-[var(--vscode-panel-border)] rounded-md p-[10px] mb-5 bg-[var(--vscode-panel-background)]">
<div className="flex gap-[1px] mb-[10px] -mt-2 border-0 border-b border-solid border-[var(--vscode-panel-border)]">
<TabButton isActive={chatSettings.mode === "plan"} onClick={() => handleTabChange("plan")}>
Plan Mode
</TabButton>
<TabButton isActive={chatSettings.mode === "act"} onClick={() => handleTabChange("act")}>
Act Mode
</TabButton>
</div>
{/* Content container */}
<div className="-mb-3">
<ApiOptions
key={chatSettings.mode}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
</div>
</div>
) : (
<ApiOptions
key={"single"}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
)}
<div className="mb-[5px]">
<VSCodeTextArea
value={customInstructions ?? ""}
className="w-full"
resize="vertical"
rows={4}
placeholder={'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'}
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
<span className="font-medium">Custom Instructions</span>
</VSCodeTextArea>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
These instructions are added to the end of the system prompt sent with every request.
</p>
</div>
{chatSettings && <PreferredLanguageSetting chatSettings={chatSettings} setChatSettings={setChatSettings} />}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={planActSeparateModelsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setPlanActSeparateModelsSetting(checked)
}}>
Use different models for Plan and Act modes
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Switching between Plan and Act mode will persist the API and model used in the previous mode. This may be
helpful e.g. when using a strong reasoning model to architect a plan for a cheaper coding model to act on.
</p>
</div>
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={telemetrySetting === "enabled"}
onChange={(e: any) => {
const checked = e.target.checked === true
setTelemetrySetting(checked ? "enabled" : "disabled")
}}>
Allow anonymous error and usage reporting
</VSCodeCheckbox>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
Help improve Cline by sending anonymous usage data and error reports. No code, prompts, or personal
information are ever sent. See our{" "}
<VSCodeLink href="https://docs.cline.bot/more-info/telemetry" className="text-inherit">
telemetry overview
</VSCodeLink>{" "}
and{" "}
<VSCodeLink href="https://cline.bot/privacy" className="text-inherit">
privacy policy
</VSCodeLink>{" "}
for more details.
</p>
</div>
{/* Feature Settings Section */}
<FeatureSettingsSection />
{/* Browser Settings Section */}
<BrowserSettingsSection />
{/* Terminal Settings Section */}
<TerminalSettingsSection />
{IS_DEV && (
<>
<div className="mt-[10px] mb-1">Debug</div>
<VSCodeButton
onClick={handleResetState}
className="mt-[5px] w-auto"
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
Reset State
</VSCodeButton>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
This will reset all global state and secret storage in the extension.
</p>
</>
)}
<div className="text-center text-[var(--vscode-descriptionForeground)] text-xs leading-[1.2] px-0 py-0 pr-2 pb-[15px] mt-auto">
<p className="break-words m-0 p-0">
If you have any questions or feedback, feel free to open an issue at{" "}
<VSCodeLink href="https://github.com/cline/cline" className="inline">
https://github.com/cline/cline
</VSCodeLink>
</p>
<p className="italic mt-[10px] mb-0 p-0">v{version}</p>
</div>
</div>
</div>
</Tab>
)
}
@@ -49,10 +49,7 @@ export const TerminalSettingsSection: React.FC = () => {
}
return (
<div
id="terminal-settings-section"
style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Terminal Settings</h3>
<div id="terminal-settings-section" style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
+13
View File
@@ -0,0 +1,13 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
/**
* A utility function that combines clsx and tailwind-merge to handle class name merging
* with proper Tailwind CSS conflict resolution.
*
* @param inputs - Class values to be merged
* @returns A string of merged class names
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}