浏览代码

New settings page, inspired by Yellow Bat and Roo <3 (#3720)

* new settings view tabs

* making sure things scroll

* tidying up

* changeset

* py

* bringing back mcp settings button

---------

Co-authored-by: Cline Evaluation <[email protected]>
pashpashpash 7 月之前
父节点
当前提交
c511b91a09

+ 5 - 0
.changeset/brown-pugs-help.md

@@ -0,0 +1,5 @@
+---
+"claude-dev": patch
+---
+
+New settings page split into tabs

+ 51 - 62
webview-ui/package-lock.json

@@ -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 - 0
webview-ui/package.json

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

+ 1 - 1
webview-ui/src/components/browser/BrowserSettingsMenu.tsx

@@ -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 - 0
webview-ui/src/components/common/Tab.tsx

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

+ 24 - 1
webview-ui/src/components/mcp/configuration/tabs/installed/InstalledServersView.tsx

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

+ 1 - 5
webview-ui/src/components/settings/BrowserSettingsSection.tsx

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

+ 1 - 2
webview-ui/src/components/settings/FeatureSettingsSection.tsx

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

+ 1 - 1
webview-ui/src/components/settings/PreferredLanguageSetting.tsx

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

+ 10 - 0
webview-ui/src/components/settings/Section.tsx

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

+ 25 - 0
webview-ui/src/components/settings/SectionHeader.tsx

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

+ 437 - 142
webview-ui/src/components/settings/SettingsView.tsx

@@ -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" })
-
-									element.style.transition = "background-color 0.5s ease"
-									element.style.backgroundColor = "var(--vscode-textPreformat-background)"
-
-									setTimeout(() => {
-										element.style.backgroundColor = "transparent"
-									}, 1200)
-								}
+						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)
+
+							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" })
+
+										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>
-			</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>
+		<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>
-
-				{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 className="flex gap-2">
+					<VSCodeButton onClick={() => handleSubmit(false)}>Save</VSCodeButton>
 				</div>
+			</TabHeader>
 
-				<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>
+			{/* 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>
 
-				{/* 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>
+				{/* 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>
+		</Tab>
 	)
 }
 

+ 1 - 4
webview-ui/src/components/settings/TerminalSettingsSection.tsx

@@ -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 - 0
webview-ui/src/utils/cn.ts

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