Ver Fonte

Merge remote-tracking branch 'origin/main' into feat/context-proxy

Matt Rubens há 9 meses atrás
pai
commit
96945cd004
37 ficheiros alterados com 1446 adições e 1270 exclusões
  1. 0 47
      package.json
  2. 0 1
      src/activate/index.ts
  3. 0 81
      src/activate/registerTerminalActions.ts
  4. 4 2
      src/core/Cline.ts
  5. 25 19
      src/core/webview/ClineProvider.ts
  6. 3 0
      src/core/webview/__tests__/ClineProvider.test.ts
  7. 2 3
      src/extension.ts
  8. 2 0
      src/shared/ExtensionMessage.ts
  9. 1 0
      src/shared/WebviewMessage.ts
  10. 5 0
      src/shared/checkpoints.ts
  11. 7 1
      src/shared/globalState.ts
  12. 0 1
      webview-ui/src/components/common/MermaidBlock.tsx
  13. 51 88
      webview-ui/src/components/history/HistoryPreview.tsx
  14. 17 98
      webview-ui/src/components/history/HistoryView.tsx
  15. 78 0
      webview-ui/src/components/history/useTaskSearch.ts
  16. 4 23
      webview-ui/src/components/mcp/McpView.tsx
  17. 2 4
      webview-ui/src/components/prompts/PromptsView.tsx
  18. 176 0
      webview-ui/src/components/settings/AdvancedSettings.tsx
  19. 170 181
      webview-ui/src/components/settings/ApiConfigManager.tsx
  20. 0 2
      webview-ui/src/components/settings/ApiOptions.tsx
  21. 252 0
      webview-ui/src/components/settings/AutoApproveSettings.tsx
  22. 105 0
      webview-ui/src/components/settings/BrowserSettings.tsx
  23. 82 0
      webview-ui/src/components/settings/CheckpointSettings.tsx
  24. 10 21
      webview-ui/src/components/settings/ExperimentalFeature.tsx
  25. 53 0
      webview-ui/src/components/settings/ExperimentalSettings.tsx
  26. 69 0
      webview-ui/src/components/settings/NotificationSettings.tsx
  27. 9 0
      webview-ui/src/components/settings/Section.tsx
  28. 15 0
      webview-ui/src/components/settings/SectionHeader.tsx
  29. 36 0
      webview-ui/src/components/settings/SettingsFooter.tsx
  30. 224 693
      webview-ui/src/components/settings/SettingsView.tsx
  31. 3 3
      webview-ui/src/components/settings/TemperatureControl.tsx
  32. 18 0
      webview-ui/src/components/settings/__tests__/SettingsView.test.tsx
  33. 7 2
      webview-ui/src/components/settings/styles.ts
  34. 10 0
      webview-ui/src/components/settings/types.ts
  35. 1 0
      webview-ui/src/context/ExtensionStateContext.tsx
  36. 1 0
      webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx
  37. 4 0
      webview-ui/src/index.css

+ 0 - 47
package.json

@@ -128,31 +128,6 @@
 				"command": "roo-cline.addToContext",
 				"title": "Roo Code: Add To Context",
 				"category": "Roo Code"
-			},
-			{
-				"command": "roo-cline.terminalAddToContext",
-				"title": "Roo Code: Add Terminal Content to Context",
-				"category": "Terminal"
-			},
-			{
-				"command": "roo-cline.terminalFixCommand",
-				"title": "Roo Code: Fix This Command",
-				"category": "Terminal"
-			},
-			{
-				"command": "roo-cline.terminalExplainCommand",
-				"title": "Roo Code: Explain This Command",
-				"category": "Terminal"
-			},
-			{
-				"command": "roo-cline.terminalFixCommandInCurrentTask",
-				"title": "Roo Code: Fix This Command (Current Task)",
-				"category": "Terminal"
-			},
-			{
-				"command": "roo-cline.terminalExplainCommandInCurrentTask",
-				"title": "Roo Code: Explain This Command (Current Task)",
-				"category": "Terminal"
 			}
 		],
 		"menus": {
@@ -178,28 +153,6 @@
 					"group": "Roo Code@4"
 				}
 			],
-			"terminal/context": [
-				{
-					"command": "roo-cline.terminalAddToContext",
-					"group": "Roo Code@1"
-				},
-				{
-					"command": "roo-cline.terminalFixCommand",
-					"group": "Roo Code@2"
-				},
-				{
-					"command": "roo-cline.terminalExplainCommand",
-					"group": "Roo Code@3"
-				},
-				{
-					"command": "roo-cline.terminalFixCommandInCurrentTask",
-					"group": "Roo Code@5"
-				},
-				{
-					"command": "roo-cline.terminalExplainCommandInCurrentTask",
-					"group": "Roo Code@6"
-				}
-			],
 			"view/title": [
 				{
 					"command": "roo-cline.plusButtonClicked",

+ 0 - 1
src/activate/index.ts

@@ -1,4 +1,3 @@
 export { handleUri } from "./handleUri"
 export { registerCommands } from "./registerCommands"
 export { registerCodeActions } from "./registerCodeActions"
-export { registerTerminalActions } from "./registerTerminalActions"

+ 0 - 81
src/activate/registerTerminalActions.ts

@@ -1,81 +0,0 @@
-import * as vscode from "vscode"
-import { ClineProvider } from "../core/webview/ClineProvider"
-import { TerminalManager } from "../integrations/terminal/TerminalManager"
-
-const TERMINAL_COMMAND_IDS = {
-	ADD_TO_CONTEXT: "roo-cline.terminalAddToContext",
-	FIX: "roo-cline.terminalFixCommand",
-	FIX_IN_CURRENT_TASK: "roo-cline.terminalFixCommandInCurrentTask",
-	EXPLAIN: "roo-cline.terminalExplainCommand",
-	EXPLAIN_IN_CURRENT_TASK: "roo-cline.terminalExplainCommandInCurrentTask",
-} as const
-
-export const registerTerminalActions = (context: vscode.ExtensionContext) => {
-	const terminalManager = new TerminalManager()
-
-	registerTerminalAction(context, terminalManager, TERMINAL_COMMAND_IDS.ADD_TO_CONTEXT, "TERMINAL_ADD_TO_CONTEXT")
-
-	registerTerminalActionPair(
-		context,
-		terminalManager,
-		TERMINAL_COMMAND_IDS.FIX,
-		"TERMINAL_FIX",
-		"What would you like Roo to fix?",
-	)
-
-	registerTerminalActionPair(
-		context,
-		terminalManager,
-		TERMINAL_COMMAND_IDS.EXPLAIN,
-		"TERMINAL_EXPLAIN",
-		"What would you like Roo to explain?",
-	)
-}
-
-const registerTerminalAction = (
-	context: vscode.ExtensionContext,
-	terminalManager: TerminalManager,
-	command: string,
-	promptType: "TERMINAL_ADD_TO_CONTEXT" | "TERMINAL_FIX" | "TERMINAL_EXPLAIN",
-	inputPrompt?: string,
-) => {
-	context.subscriptions.push(
-		vscode.commands.registerCommand(command, async (args: any) => {
-			let content = args.selection
-			if (!content || content === "") {
-				content = await terminalManager.getTerminalContents(promptType === "TERMINAL_ADD_TO_CONTEXT" ? -1 : 1)
-			}
-
-			if (!content) {
-				vscode.window.showWarningMessage("No terminal content selected")
-				return
-			}
-
-			const params: Record<string, any> = {
-				terminalContent: content,
-			}
-
-			if (inputPrompt) {
-				params.userInput =
-					(await vscode.window.showInputBox({
-						prompt: inputPrompt,
-					})) ?? ""
-			}
-
-			await ClineProvider.handleTerminalAction(command, promptType, params)
-		}),
-	)
-}
-
-const registerTerminalActionPair = (
-	context: vscode.ExtensionContext,
-	terminalManager: TerminalManager,
-	baseCommand: string,
-	promptType: "TERMINAL_ADD_TO_CONTEXT" | "TERMINAL_FIX" | "TERMINAL_EXPLAIN",
-	inputPrompt?: string,
-) => {
-	// Register new task version
-	registerTerminalAction(context, terminalManager, baseCommand, promptType, inputPrompt)
-	// Register current task version
-	registerTerminalAction(context, terminalManager, `${baseCommand}InCurrentTask`, promptType, inputPrompt)
-}

+ 4 - 2
src/core/Cline.ts

@@ -10,6 +10,7 @@ import getFolderSize from "get-folder-size"
 import * as path from "path"
 import { serializeError } from "serialize-error"
 import * as vscode from "vscode"
+
 import { ApiHandler, buildApiHandler } from "../api"
 import { ApiStream } from "../api/transform/stream"
 import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
@@ -31,6 +32,7 @@ import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
 import { listFiles } from "../services/glob/list-files"
 import { regexSearchFiles } from "../services/ripgrep"
 import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
+import { CheckpointStorage } from "../shared/checkpoints"
 import { ApiConfiguration } from "../shared/api"
 import { findLastIndex } from "../shared/array"
 import { combineApiRequests } from "../shared/combineApiRequests"
@@ -81,7 +83,7 @@ export type ClineOptions = {
 	customInstructions?: string
 	enableDiff?: boolean
 	enableCheckpoints?: boolean
-	checkpointStorage?: "task" | "workspace"
+	checkpointStorage?: CheckpointStorage
 	fuzzyMatchThreshold?: number
 	task?: string
 	images?: string[]
@@ -121,7 +123,7 @@ export class Cline {
 
 	// checkpoints
 	private enableCheckpoints: boolean
-	private checkpointStorage: "task" | "workspace"
+	private checkpointStorage: CheckpointStorage
 	private checkpointService?: RepoPerTaskCheckpointService | RepoPerWorkspaceCheckpointService
 
 	// streaming

+ 25 - 19
src/core/webview/ClineProvider.ts

@@ -9,6 +9,7 @@ import * as vscode from "vscode"
 import simpleGit from "simple-git"
 
 import { ApiConfiguration, ApiProvider, ModelInfo, API_CONFIG_KEYS } from "../../shared/api"
+import { CheckpointStorage } from "../../shared/checkpoints"
 import { findLast } from "../../shared/array"
 import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
 import { GlobalFileNames } from "../../shared/globalFileNames"
@@ -316,11 +317,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 	public async initClineWithTask(task?: string, images?: string[]) {
 		await this.clearTask()
+
 		const {
 			apiConfiguration,
 			customModePrompts,
-			diffEnabled,
+			diffEnabled: enableDiff,
 			enableCheckpoints,
+			checkpointStorage,
 			fuzzyMatchThreshold,
 			mode,
 			customInstructions: globalInstructions,
@@ -334,8 +337,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			provider: this,
 			apiConfiguration,
 			customInstructions: effectiveInstructions,
-			enableDiff: diffEnabled,
+			enableDiff,
 			enableCheckpoints,
+			checkpointStorage,
 			fuzzyMatchThreshold,
 			task,
 			images,
@@ -349,8 +353,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		const {
 			apiConfiguration,
 			customModePrompts,
-			diffEnabled,
+			diffEnabled: enableDiff,
 			enableCheckpoints,
+			checkpointStorage,
 			fuzzyMatchThreshold,
 			mode,
 			customInstructions: globalInstructions,
@@ -360,12 +365,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		const modePrompt = customModePrompts?.[mode] as PromptComponent
 		const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
 
+		// TODO: The `checkpointStorage` value should be derived from the
+		// task data on disk; the current setting could be different than
+		// the setting at the time the task was created.
+
 		this.cline = new Cline({
 			provider: this,
 			apiConfiguration,
 			customInstructions: effectiveInstructions,
-			enableDiff: diffEnabled,
+			enableDiff,
 			enableCheckpoints,
+			checkpointStorage,
 			fuzzyMatchThreshold,
 			historyItem,
 			experiments,
@@ -1035,6 +1045,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("enableCheckpoints", enableCheckpoints)
 						await this.postStateToWebview()
 						break
+					case "checkpointStorage":
+						console.log(`[ClineProvider] checkpointStorage: ${message.text}`)
+						const checkpointStorage = message.text ?? "task"
+						await this.updateGlobalState("checkpointStorage", checkpointStorage)
+						await this.postStateToWebview()
+						break
 					case "browserViewportSize":
 						const browserViewportSize = message.text ?? "900x600"
 						await this.updateGlobalState("browserViewportSize", browserViewportSize)
@@ -1875,21 +1891,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			await fs.unlink(legacyMessagesFilePath)
 		}
 
-		const { enableCheckpoints } = await this.getState()
-		const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
-
-		// Delete checkpoints branch.
-		if (enableCheckpoints && baseDir) {
-			const branchSummary = await simpleGit(baseDir)
-				.branch(["-D", `roo-code-checkpoints-${id}`])
-				.catch(() => undefined)
-
-			if (branchSummary) {
-				console.log(`[deleteTaskWithId${id}] deleted checkpoints branch`)
-			}
-		}
-
-		// Delete checkpoints directory
+		// Delete checkpoints directory.
+		// TODO: Also delete the workspace branch if it exists.
 		const checkpointsDir = path.join(taskDirPath, "checkpoints")
 
 		if (await fileExistsAtPath(checkpointsDir)) {
@@ -1936,6 +1939,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundEnabled,
 			diffEnabled,
 			enableCheckpoints,
+			checkpointStorage,
 			taskHistory,
 			soundVolume,
 			browserViewportSize,
@@ -1986,6 +1990,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundEnabled: soundEnabled ?? false,
 			diffEnabled: diffEnabled ?? true,
 			enableCheckpoints: enableCheckpoints ?? true,
+			checkpointStorage: checkpointStorage ?? "task",
 			shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
 			allowedCommands,
 			soundVolume: soundVolume ?? 0.5,
@@ -2140,6 +2145,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundEnabled: stateValues.soundEnabled ?? false,
 			diffEnabled: stateValues.diffEnabled ?? true,
 			enableCheckpoints: stateValues.enableCheckpoints ?? false,
+			checkpointStorage: stateValues.checkpointStorage ?? "task",
 			soundVolume: stateValues.soundVolume,
 			browserViewportSize: stateValues.browserViewportSize ?? "900x600",
 			screenshotQuality: stateValues.screenshotQuality ?? 75,

+ 3 - 0
src/core/webview/__tests__/ClineProvider.test.ts

@@ -417,6 +417,7 @@ describe("ClineProvider", () => {
 			soundEnabled: false,
 			diffEnabled: false,
 			enableCheckpoints: false,
+			checkpointStorage: "task",
 			writeDelayMs: 1000,
 			browserViewportSize: "900x600",
 			fuzzyMatchThreshold: 1.0,
@@ -744,6 +745,7 @@ describe("ClineProvider", () => {
 			mode: "code",
 			diffEnabled: true,
 			enableCheckpoints: false,
+			checkpointStorage: "task",
 			fuzzyMatchThreshold: 1.0,
 			experiments: experimentDefault,
 		} as any)
@@ -762,6 +764,7 @@ describe("ClineProvider", () => {
 			customInstructions: modeCustomInstructions,
 			enableDiff: true,
 			enableCheckpoints: false,
+			checkpointStorage: "task",
 			fuzzyMatchThreshold: 1.0,
 			task: "Test task",
 			experiments: experimentDefault,

+ 2 - 3
src/extension.ts

@@ -5,7 +5,7 @@ import { createClineAPI } from "./exports"
 import "./utils/path" // Necessary to have access to String.prototype.toPosix.
 import { CodeActionProvider } from "./core/CodeActionProvider"
 import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
-import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate"
+import { handleUri, registerCommands, registerCodeActions } from "./activate"
 import { McpServerManager } from "./services/mcp/McpServerManager"
 
 /**
@@ -81,12 +81,11 @@ export function activate(context: vscode.ExtensionContext) {
 	)
 
 	registerCodeActions(context)
-	registerTerminalActions(context)
 
 	return createClineAPI(outputChannel, sidebarProvider)
 }
 
-// This method is called when your extension is deactivated
+// This method is called when your extension is deactivated.
 export async function deactivate() {
 	outputChannel.appendLine("Roo-Code extension deactivated")
 	// Clean up MCP server manager

+ 2 - 0
src/shared/ExtensionMessage.ts

@@ -7,6 +7,7 @@ import { GitCommit } from "../utils/git"
 import { Mode, CustomModePrompts, ModeConfig } from "./modes"
 import { CustomSupportPrompts } from "./support-prompt"
 import { ExperimentId } from "./experiments"
+import { CheckpointStorage } from "./checkpoints"
 
 export interface LanguageModelChatSelector {
 	vendor?: string
@@ -114,6 +115,7 @@ export interface ExtensionState {
 	soundVolume?: number
 	diffEnabled?: boolean
 	enableCheckpoints: boolean
+	checkpointStorage: CheckpointStorage
 	browserViewportSize?: string
 	screenshotQuality?: number
 	fuzzyMatchThreshold?: number

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -53,6 +53,7 @@ export interface WebviewMessage {
 		| "soundVolume"
 		| "diffEnabled"
 		| "enableCheckpoints"
+		| "checkpointStorage"
 		| "browserViewportSize"
 		| "screenshotQuality"
 		| "openMcpSettings"

+ 5 - 0
src/shared/checkpoints.ts

@@ -0,0 +1,5 @@
+export type CheckpointStorage = "task" | "workspace"
+
+export const isCheckpointStorage = (value: string): value is CheckpointStorage => {
+	return value === "task" || value === "workspace"
+}

+ 7 - 1
src/shared/globalState.ts

@@ -71,6 +71,7 @@ export type GlobalStateKey =
 	| "soundVolume"
 	| "diffEnabled"
 	| "enableCheckpoints"
+	| "checkpointStorage"
 	| "browserViewportSize"
 	| "screenshotQuality"
 	| "fuzzyMatchThreshold"
@@ -102,7 +103,9 @@ export type GlobalStateKey =
 	| "modelMaxThinkingTokens"
 	| "mistralCodestralUrl"
 	| "maxOpenTabsContext"
-	| "browserToolEnabled" // Setting to enable/disable the browser tool
+	| "browserToolEnabled"
+	| "lmStudioSpeculativeDecodingEnabled"
+	| "lmStudioDraftModelId"
 
 export const GLOBAL_STATE_KEYS: GlobalStateKey[] = [
 	"apiProvider",
@@ -175,4 +178,7 @@ export const GLOBAL_STATE_KEYS: GlobalStateKey[] = [
 	"modelMaxTokens",
 	"mistralCodestralUrl",
 	"maxOpenTabsContext",
+	"browserToolEnabled",
+	"lmStudioSpeculativeDecodingEnabled",
+	"lmStudioDraftModelId",
 ]

+ 0 - 1
webview-ui/src/components/common/MermaidBlock.tsx

@@ -150,7 +150,6 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
 }
 
 async function svgToPng(svgEl: SVGElement): Promise<string> {
-	console.log("svgToPng function called")
 	// Clone the SVG to avoid modifying the original
 	const svgClone = svgEl.cloneNode(true) as SVGElement
 

+ 51 - 88
webview-ui/src/components/history/HistoryPreview.tsx

@@ -14,101 +14,64 @@ type HistoryPreviewProps = {
 const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
 	const { taskHistory } = useExtensionState()
 
-	const handleHistorySelect = (id: string) => {
-		vscode.postMessage({ type: "showTaskWithId", text: id })
-	}
-
 	return (
-		<div style={{ flexShrink: 0 }}>
-			<style>
-				{`
-					.history-preview-item {
-						background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent);
-						border-radius: 4px;
-						position: relative;
-						overflow: hidden;
-						opacity: 0.8;
-						cursor: pointer;
-						margin-bottom: 12px;
-					}
-					.history-preview-item:hover {
-						background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 100%, transparent);
-						opacity: 1;
-						pointer-events: auto;
-					}
-				`}
-			</style>
-			<div
-				style={{
-					color: "var(--vscode-descriptionForeground)",
-					margin: "10px 20px 10px 20px",
-					display: "flex",
-					alignItems: "center",
-				}}>
-				<span className="codicon codicon-comment-discussion scale-90 mr-1" />
-				<span className="font-medium text-xs uppercase">Recent Tasks</span>
+		<div className="flex flex-col gap-3 shrink-0 mx-5">
+			<div className="flex items-center justify-between text-vscode-descriptionForeground">
+				<div className="flex items-center gap-1">
+					<span className="codicon codicon-comment-discussion scale-90 mr-1" />
+					<span className="font-medium text-xs uppercase">Recent Tasks</span>
+				</div>
+				<Button variant="ghost" size="sm" onClick={() => showHistoryView()} className="uppercase">
+					View All
+				</Button>
 			</div>
-			<div className="px-5">
-				{taskHistory
-					.filter((item) => item.ts && item.task)
-					.slice(0, 3)
-					.map((item) => (
+			{taskHistory.slice(0, 3).map((item) => (
+				<div
+					key={item.id}
+					className="bg-vscode-toolbar-hoverBackground/50 hover:bg-vscode-toolbar-hoverBackground/75 rounded-xs relative overflow-hidden opacity-90 hover:opacity-100 cursor-pointer"
+					onClick={() => vscode.postMessage({ type: "showTaskWithId", text: item.id })}>
+					<div className="flex flex-col gap-2 p-3 pt-1">
+						<div className="flex justify-between items-center">
+							<span className="text-xs font-medium text-vscode-descriptionForeground uppercase">
+								{formatDate(item.ts)}
+							</span>
+							<CopyButton itemTask={item.task} />
+						</div>
 						<div
-							key={item.id}
-							className="history-preview-item"
-							onClick={() => handleHistorySelect(item.id)}>
-							<div className="flex flex-col gap-2 p-3 pt-1">
-								<div className="flex justify-between items-center">
-									<span className="text-xs font-medium text-vscode-descriptionForeground uppercase">
-										{formatDate(item.ts)}
-									</span>
-									<CopyButton itemTask={item.task} />
-								</div>
-								<div
-									className="text-vscode-descriptionForeground overflow-hidden whitespace-pre-wrap"
-									style={{
-										display: "-webkit-box",
-										WebkitLineClamp: 3,
-										WebkitBoxOrient: "vertical",
-										wordBreak: "break-word",
-										overflowWrap: "anywhere",
-									}}>
-									{item.task}
-								</div>
-								<div className="text-xs text-vscode-descriptionForeground">
+							className="text-vscode-descriptionForeground overflow-hidden whitespace-pre-wrap"
+							style={{
+								display: "-webkit-box",
+								WebkitLineClamp: 3,
+								WebkitBoxOrient: "vertical",
+								wordBreak: "break-word",
+								overflowWrap: "anywhere",
+							}}>
+							{item.task}
+						</div>
+						<div className="text-xs text-vscode-descriptionForeground">
+							<span>
+								Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓
+								{formatLargeNumber(item.tokensOut || 0)}
+							</span>
+							{!!item.cacheWrites && (
+								<>
+									{" • "}
 									<span>
-										Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓
-										{formatLargeNumber(item.tokensOut || 0)}
+										Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "}
+										{formatLargeNumber(item.cacheReads || 0)}
 									</span>
-									{!!item.cacheWrites && (
-										<>
-											{" • "}
-											<span>
-												Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "}
-												{formatLargeNumber(item.cacheReads || 0)}
-											</span>
-										</>
-									)}
-									{!!item.totalCost && (
-										<>
-											{" • "}
-											<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
-										</>
-									)}
-								</div>
-							</div>
+								</>
+							)}
+							{!!item.totalCost && (
+								<>
+									{" • "}
+									<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
+								</>
+							)}
 						</div>
-					))}
-				<div className="flex justify-center">
-					<Button
-						variant="ghost"
-						size="sm"
-						onClick={() => showHistoryView()}
-						className="font-normal text-vscode-descriptionForeground">
-						View all history
-					</Button>
+					</div>
 				</div>
-			</div>
+			))}
 		</div>
 	)
 }

+ 17 - 98
webview-ui/src/components/history/HistoryView.tsx

@@ -1,16 +1,15 @@
-import React, { memo, useMemo, useState, useEffect } from "react"
+import React, { memo, useState } from "react"
 import { DeleteTaskDialog } from "./DeleteTaskDialog"
-import { Fzf } from "fzf"
 import prettyBytes from "pretty-bytes"
 import { Virtuoso } from "react-virtuoso"
 import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
 
 import { vscode } from "@/utils/vscode"
 import { formatLargeNumber, formatDate } from "@/utils/format"
-import { highlightFzfMatch } from "@/utils/highlight"
+import { cn } from "@/lib/utils"
 import { Button } from "@/components/ui"
 
-import { useExtensionState } from "../../context/ExtensionStateContext"
+import { useTaskSearch } from "./useTaskSearch"
 import { ExportButton } from "./ExportButton"
 import { CopyButton } from "./CopyButton"
 
@@ -21,95 +20,18 @@ type HistoryViewProps = {
 type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
 
 const HistoryView = ({ onDone }: HistoryViewProps) => {
-	const { taskHistory } = useExtensionState()
-	const [searchQuery, setSearchQuery] = useState("")
-	const [sortOption, setSortOption] = useState<SortOption>("newest")
-	const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
-
-	useEffect(() => {
-		if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
-			setLastNonRelevantSort(sortOption)
-			setSortOption("mostRelevant")
-		} else if (!searchQuery && sortOption === "mostRelevant" && lastNonRelevantSort) {
-			setSortOption(lastNonRelevantSort)
-			setLastNonRelevantSort(null)
-		}
-	}, [searchQuery, sortOption, lastNonRelevantSort])
-
-	const handleHistorySelect = (id: string) => {
-		vscode.postMessage({ type: "showTaskWithId", text: id })
-	}
+	const { tasks, searchQuery, setSearchQuery, sortOption, setSortOption, setLastNonRelevantSort } = useTaskSearch()
 
 	const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
 
-	const presentableTasks = useMemo(() => {
-		return taskHistory.filter((item) => item.ts && item.task)
-	}, [taskHistory])
-
-	const fzf = useMemo(() => {
-		return new Fzf(presentableTasks, {
-			selector: (item) => item.task,
-		})
-	}, [presentableTasks])
-
-	const taskHistorySearchResults = useMemo(() => {
-		let results = presentableTasks
-		if (searchQuery) {
-			const searchResults = fzf.find(searchQuery)
-			results = searchResults.map((result) => ({
-				...result.item,
-				task: highlightFzfMatch(result.item.task, Array.from(result.positions)),
-			}))
-		}
-
-		// First apply search if needed
-		const searchResults = searchQuery ? results : presentableTasks
-
-		// Then sort the results
-		return [...searchResults].sort((a, b) => {
-			switch (sortOption) {
-				case "oldest":
-					return (a.ts || 0) - (b.ts || 0)
-				case "mostExpensive":
-					return (b.totalCost || 0) - (a.totalCost || 0)
-				case "mostTokens":
-					const aTokens = (a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0)
-					const bTokens = (b.tokensIn || 0) + (b.tokensOut || 0) + (b.cacheWrites || 0) + (b.cacheReads || 0)
-					return bTokens - aTokens
-				case "mostRelevant":
-					// Keep fuse order if searching, otherwise sort by newest
-					return searchQuery ? 0 : (b.ts || 0) - (a.ts || 0)
-				case "newest":
-				default:
-					return (b.ts || 0) - (a.ts || 0)
-			}
-		})
-	}, [presentableTasks, searchQuery, fzf, sortOption])
-
 	return (
-		<div
-			style={{
-				position: "fixed",
-				top: 0,
-				left: 0,
-				right: 0,
-				bottom: 0,
-				display: "flex",
-				flexDirection: "column",
-				overflow: "hidden",
-			}}>
-			<div
-				style={{
-					display: "flex",
-					justifyContent: "space-between",
-					alignItems: "center",
-					padding: "10px 17px 10px 20px",
-				}}>
-				<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>History</h3>
-				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
-			</div>
-			<div style={{ padding: "5px 17px 6px 17px" }}>
-				<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
+		<div className="fixed inset-0 flex flex-col">
+			<div className="flex flex-col gap-2 px-5 py-2.5 border-b border-vscode-panel-border">
+				<div className="flex justify-between items-center">
+					<h3 className="text-vscode-foreground m-0">History</h3>
+					<VSCodeButton onClick={onDone}>Done</VSCodeButton>
+				</div>
+				<div className="flex flex-col gap-2">
 					<VSCodeTextField
 						style={{ width: "100%" }}
 						placeholder="Fuzzy search history..."
@@ -166,7 +88,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 						flexGrow: 1,
 						overflowY: "scroll",
 					}}
-					data={taskHistorySearchResults}
+					data={tasks}
 					data-testid="virtuoso-container"
 					components={{
 						List: React.forwardRef((props, ref) => (
@@ -175,15 +97,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 					}}
 					itemContent={(index, item) => (
 						<div
-							key={item.id}
 							data-testid={`task-item-${item.id}`}
-							className="history-item"
-							style={{
-								cursor: "pointer",
-								borderBottom:
-									index < taskHistory.length - 1 ? "1px solid var(--vscode-panel-border)" : "none",
-							}}
-							onClick={() => handleHistorySelect(item.id)}>
+							key={item.id}
+							className={cn("cursor-pointer", {
+								"border-b border-vscode-panel-border": index < tasks.length - 1,
+							})}
+							onClick={() => vscode.postMessage({ type: "showTaskWithId", text: item.id })}>
 							<div
 								style={{
 									display: "flex",

+ 78 - 0
webview-ui/src/components/history/useTaskSearch.ts

@@ -0,0 +1,78 @@
+import { useState, useEffect, useMemo } from "react"
+import { Fzf } from "fzf"
+
+import { highlightFzfMatch } from "@/utils/highlight"
+import { useExtensionState } from "@/context/ExtensionStateContext"
+
+type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
+
+export const useTaskSearch = () => {
+	const { taskHistory } = useExtensionState()
+	const [searchQuery, setSearchQuery] = useState("")
+	const [sortOption, setSortOption] = useState<SortOption>("newest")
+	const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
+
+	useEffect(() => {
+		if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
+			setLastNonRelevantSort(sortOption)
+			setSortOption("mostRelevant")
+		} else if (!searchQuery && sortOption === "mostRelevant" && lastNonRelevantSort) {
+			setSortOption(lastNonRelevantSort)
+			setLastNonRelevantSort(null)
+		}
+	}, [searchQuery, sortOption, lastNonRelevantSort])
+
+	const presentableTasks = useMemo(() => {
+		return taskHistory.filter((item) => item.ts && item.task)
+	}, [taskHistory])
+
+	const fzf = useMemo(() => {
+		return new Fzf(presentableTasks, {
+			selector: (item) => item.task,
+		})
+	}, [presentableTasks])
+
+	const tasks = useMemo(() => {
+		let results = presentableTasks
+		if (searchQuery) {
+			const searchResults = fzf.find(searchQuery)
+			results = searchResults.map((result) => ({
+				...result.item,
+				task: highlightFzfMatch(result.item.task, Array.from(result.positions)),
+			}))
+		}
+
+		// First apply search if needed
+		const searchResults = searchQuery ? results : presentableTasks
+
+		// Then sort the results
+		return [...searchResults].sort((a, b) => {
+			switch (sortOption) {
+				case "oldest":
+					return (a.ts || 0) - (b.ts || 0)
+				case "mostExpensive":
+					return (b.totalCost || 0) - (a.totalCost || 0)
+				case "mostTokens":
+					const aTokens = (a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0)
+					const bTokens = (b.tokensIn || 0) + (b.tokensOut || 0) + (b.cacheWrites || 0) + (b.cacheReads || 0)
+					return bTokens - aTokens
+				case "mostRelevant":
+					// Keep fuse order if searching, otherwise sort by newest
+					return searchQuery ? 0 : (b.ts || 0) - (a.ts || 0)
+				case "newest":
+				default:
+					return (b.ts || 0) - (a.ts || 0)
+			}
+		})
+	}, [presentableTasks, searchQuery, fzf, sortOption])
+
+	return {
+		tasks,
+		searchQuery,
+		setSearchQuery,
+		sortOption,
+		setSortOption,
+		lastNonRelevantSort,
+		setLastNonRelevantSort,
+	}
+}

+ 4 - 23
webview-ui/src/components/mcp/McpView.tsx

@@ -29,28 +29,12 @@ const McpView = ({ onDone }: McpViewProps) => {
 	} = useExtensionState()
 
 	return (
-		<div
-			style={{
-				position: "fixed",
-				top: 0,
-				left: 0,
-				right: 0,
-				bottom: 0,
-				display: "flex",
-				flexDirection: "column",
-			}}>
-			<div
-				style={{
-					display: "flex",
-					justifyContent: "space-between",
-					alignItems: "center",
-					padding: "10px 17px 10px 20px",
-				}}>
-				<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
+		<div className="fixed inset-0 flex flex-col">
+			<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
+				<h3 className="text-vscode-foreground m-0">MCP Servers</h3>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
 			</div>
-
-			<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
+			<div className="flex-1 overflow-auto p-5">
 				<div
 					style={{
 						color: "var(--vscode-foreground)",
@@ -119,9 +103,6 @@ const McpView = ({ onDone }: McpViewProps) => {
 						</div>
 					</>
 				)}
-
-				{/* Bottom padding */}
-				<div style={{ height: "20px" }} />
 			</div>
 		</div>
 	)

+ 2 - 4
webview-ui/src/components/prompts/PromptsView.tsx

@@ -407,12 +407,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 
 	return (
 		<div className="fixed inset-0 flex flex-col">
-			<div className="flex justify-between items-center px-5 py-2.5">
+			<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
 				<h3 className="text-vscode-foreground m-0">Prompts</h3>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
 			</div>
-
-			<div className="flex-1 overflow-auto px-5">
+			<div className="flex-1 overflow-auto p-5">
 				<div className="pb-5 border-b border-vscode-input-border">
 					<div className="mb-5">
 						<div className="font-bold mb-1">Preferred Language</div>
@@ -1174,7 +1173,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 					</div>
 				</div>
 			</div>
-
 			{isCreateModeDialogOpen && (
 				<div
 					style={{

+ 176 - 0
webview-ui/src/components/settings/AdvancedSettings.tsx

@@ -0,0 +1,176 @@
+import { HTMLAttributes } from "react"
+import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
+import { Cog } from "lucide-react"
+
+import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
+
+import { cn } from "@/lib/utils"
+
+import { SetCachedStateField, SetExperimentEnabled } from "./types"
+import { sliderLabelStyle } from "./styles"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+import { ExperimentalFeature } from "./ExperimentalFeature"
+
+type AdvancedSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	rateLimitSeconds: number
+	terminalOutputLineLimit?: number
+	maxOpenTabsContext: number
+	diffEnabled?: boolean
+	fuzzyMatchThreshold?: number
+	setCachedStateField: SetCachedStateField<
+		"rateLimitSeconds" | "terminalOutputLineLimit" | "maxOpenTabsContext" | "diffEnabled" | "fuzzyMatchThreshold"
+	>
+	experiments: Record<ExperimentId, boolean>
+	setExperimentEnabled: SetExperimentEnabled
+}
+
+export const AdvancedSettings = ({
+	rateLimitSeconds,
+	terminalOutputLineLimit,
+	maxOpenTabsContext,
+	diffEnabled,
+	fuzzyMatchThreshold,
+	setCachedStateField,
+	experiments,
+	setExperimentEnabled,
+	className,
+	...props
+}: AdvancedSettingsProps) => {
+	return (
+		<div className={cn("flex flex-col gap-2", className)} {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<Cog className="w-4" />
+					<div>Advanced</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<div className="flex flex-col gap-2">
+						<span className="font-medium">Rate limit</span>
+						<div className="flex items-center gap-2">
+							<input
+								type="range"
+								min="0"
+								max="60"
+								step="1"
+								value={rateLimitSeconds}
+								onChange={(e) => setCachedStateField("rateLimitSeconds", parseInt(e.target.value))}
+								className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+							/>
+							<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
+						</div>
+					</div>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">Minimum time between API requests.</p>
+				</div>
+
+				<div>
+					<div className="flex flex-col gap-2">
+						<span className="font-medium">Terminal output limit</span>
+						<div className="flex items-center gap-2">
+							<input
+								type="range"
+								min="100"
+								max="5000"
+								step="100"
+								value={terminalOutputLineLimit ?? 500}
+								onChange={(e) =>
+									setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value))
+								}
+								className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+							/>
+							<span style={{ ...sliderLabelStyle }}>{terminalOutputLineLimit ?? 500}</span>
+						</div>
+					</div>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Maximum number of lines to include in terminal output when executing commands. When exceeded
+						lines will be removed from the middle, saving tokens.
+					</p>
+				</div>
+
+				<div>
+					<div className="flex flex-col gap-2">
+						<span className="font-medium">Open tabs context limit</span>
+						<div className="flex items-center gap-2">
+							<input
+								type="range"
+								min="0"
+								max="500"
+								step="1"
+								value={maxOpenTabsContext ?? 20}
+								onChange={(e) => setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))}
+								className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+							/>
+							<span style={{ ...sliderLabelStyle }}>{maxOpenTabsContext ?? 20}</span>
+						</div>
+					</div>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Maximum number of VSCode open tabs to include in context. Higher values provide more context but
+						increase token usage.
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={diffEnabled}
+						onChange={(e: any) => {
+							setCachedStateField("diffEnabled", e.target.checked)
+							if (!e.target.checked) {
+								// Reset experimental strategy when diffs are disabled.
+								setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
+							}
+						}}>
+						<span className="font-medium">Enable editing through diffs</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo will be able to edit files more quickly and will automatically reject
+						truncated full-file writes. Works best with the latest Claude 3.7 Sonnet model.
+					</p>
+					{diffEnabled && (
+						<div
+							style={{
+								display: "flex",
+								flexDirection: "column",
+								gap: "5px",
+								marginTop: "10px",
+								marginBottom: "10px",
+								paddingLeft: "10px",
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<span className="font-medium">Match precision</span>
+							<div className="flex items-center gap-2">
+								<input
+									type="range"
+									min="0.8"
+									max="1"
+									step="0.005"
+									value={fuzzyMatchThreshold ?? 1.0}
+									onChange={(e) => {
+										setCachedStateField("fuzzyMatchThreshold", parseFloat(e.target.value))
+									}}
+									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+								/>
+								<span style={{ ...sliderLabelStyle }}>
+									{Math.round((fuzzyMatchThreshold || 1) * 100)}%
+								</span>
+							</div>
+							<p className="text-vscode-descriptionForeground text-sm mt-0">
+								This slider controls how precisely code sections must match when applying diffs. Lower
+								values allow more flexible matching but increase the risk of incorrect replacements. Use
+								values below 100% with extreme caution.
+							</p>
+							<ExperimentalFeature
+								key={EXPERIMENT_IDS.DIFF_STRATEGY}
+								{...experimentConfigsMap.DIFF_STRATEGY}
+								enabled={experiments[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false}
+								onChange={(enabled) => setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled)}
+							/>
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

+ 170 - 181
webview-ui/src/components/settings/ApiConfigManager.tsx

@@ -142,199 +142,188 @@ const ApiConfigManager = ({
 	const isOnlyProfile = listApiConfigMeta?.length === 1
 
 	return (
-		<div style={{ marginBottom: 5 }}>
-			<div
-				style={{
-					display: "flex",
-					flexDirection: "column",
-					gap: "2px",
-				}}>
-				<label htmlFor="config-profile">
-					<span style={{ fontWeight: "500" }}>Configuration Profile</span>
-				</label>
+		<div className="flex flex-col gap-1">
+			<label htmlFor="config-profile">
+				<span className="font-medium">Configuration Profile</span>
+			</label>
 
-				{isRenaming ? (
-					<div
-						data-testid="rename-form"
-						style={{ display: "flex", gap: "4px", alignItems: "center", flexDirection: "column" }}>
-						<div style={{ display: "flex", gap: "4px", alignItems: "center", width: "100%" }}>
-							<VSCodeTextField
-								ref={inputRef}
-								value={inputValue}
-								onInput={(e: unknown) => {
-									const target = e as { target: { value: string } }
-									setInputValue(target.target.value)
-									setError(null)
-								}}
-								placeholder="Enter new name"
-								style={{ flexGrow: 1 }}
-								onKeyDown={(e: unknown) => {
-									const event = e as { key: string }
-									if (event.key === "Enter" && inputValue.trim()) {
-										handleSave()
-									} else if (event.key === "Escape") {
-										handleCancel()
-									}
-								}}
-							/>
-							<VSCodeButton
-								appearance="icon"
-								disabled={!inputValue.trim()}
-								onClick={handleSave}
-								title="Save"
-								style={{
-									padding: 0,
-									margin: 0,
-									height: "28px",
-									width: "28px",
-									minWidth: "28px",
-								}}>
-								<span className="codicon codicon-check" />
-							</VSCodeButton>
-							<VSCodeButton
-								appearance="icon"
-								onClick={handleCancel}
-								title="Cancel"
-								style={{
-									padding: 0,
-									margin: 0,
-									height: "28px",
-									width: "28px",
-									minWidth: "28px",
-								}}>
-								<span className="codicon codicon-close" />
-							</VSCodeButton>
-						</div>
-						{error && (
-							<p className="text-red-500 text-sm mt-2" data-testid="error-message">
-								{error}
-							</p>
-						)}
-					</div>
-				) : (
-					<>
-						<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
-							<Dropdown
-								id="config-profile"
-								value={currentApiConfigName}
-								onChange={(value: unknown) => {
-									onSelectConfig((value as DropdownOption).value)
-								}}
-								style={{
-									minWidth: 130,
-									zIndex: 1002,
-								}}
-								role="combobox"
-								options={listApiConfigMeta.map((config) => ({
-									value: config.name,
-									label: config.name,
-								}))}
-							/>
-							<VSCodeButton
-								appearance="icon"
-								onClick={handleAdd}
-								title="Add profile"
-								style={{
-									padding: 0,
-									margin: 0,
-									height: "28px",
-									width: "28px",
-									minWidth: "28px",
-								}}>
-								<span className="codicon codicon-add" />
-							</VSCodeButton>
-							{currentApiConfigName && (
-								<>
-									<VSCodeButton
-										appearance="icon"
-										onClick={handleStartRename}
-										title="Rename profile"
-										style={{
-											padding: 0,
-											margin: 0,
-											height: "28px",
-											width: "28px",
-											minWidth: "28px",
-										}}>
-										<span className="codicon codicon-edit" />
-									</VSCodeButton>
-									<VSCodeButton
-										appearance="icon"
-										onClick={handleDelete}
-										title={isOnlyProfile ? "Cannot delete the only profile" : "Delete profile"}
-										disabled={isOnlyProfile}
-										style={{
-											padding: 0,
-											margin: 0,
-											height: "28px",
-											width: "28px",
-											minWidth: "28px",
-										}}>
-										<span className="codicon codicon-trash" />
-									</VSCodeButton>
-								</>
-							)}
-						</div>
-						<p
-							style={{
-								fontSize: "12px",
-								margin: "5px 0 12px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							Save different API configurations to quickly switch between providers and settings
-						</p>
-					</>
-				)}
-
-				<Dialog
-					open={isCreating}
-					onOpenChange={(open: boolean) => {
-						if (open) {
-							setIsCreating(true)
-							setNewProfileName("")
-							setError(null)
-						} else {
-							resetCreateState()
-						}
-					}}
-					aria-labelledby="new-profile-title">
-					<DialogContent className="p-4 max-w-sm">
-						<DialogTitle>New Configuration Profile</DialogTitle>
-						<Input
-							ref={newProfileInputRef}
-							value={newProfileName}
+			{isRenaming ? (
+				<div
+					data-testid="rename-form"
+					style={{ display: "flex", gap: "4px", alignItems: "center", flexDirection: "column" }}>
+					<div style={{ display: "flex", gap: "4px", alignItems: "center", width: "100%" }}>
+						<VSCodeTextField
+							ref={inputRef}
+							value={inputValue}
 							onInput={(e: unknown) => {
 								const target = e as { target: { value: string } }
-								setNewProfileName(target.target.value)
+								setInputValue(target.target.value)
 								setError(null)
 							}}
-							placeholder="Enter profile name"
-							style={{ width: "100%" }}
+							placeholder="Enter new name"
+							style={{ flexGrow: 1 }}
 							onKeyDown={(e: unknown) => {
 								const event = e as { key: string }
-								if (event.key === "Enter" && newProfileName.trim()) {
-									handleNewProfileSave()
+								if (event.key === "Enter" && inputValue.trim()) {
+									handleSave()
 								} else if (event.key === "Escape") {
-									resetCreateState()
+									handleCancel()
 								}
 							}}
 						/>
-						{error && (
-							<p className="text-red-500 text-sm mt-2" data-testid="error-message">
-								{error}
-							</p>
+						<VSCodeButton
+							appearance="icon"
+							disabled={!inputValue.trim()}
+							onClick={handleSave}
+							title="Save"
+							style={{
+								padding: 0,
+								margin: 0,
+								height: "28px",
+								width: "28px",
+								minWidth: "28px",
+							}}>
+							<span className="codicon codicon-check" />
+						</VSCodeButton>
+						<VSCodeButton
+							appearance="icon"
+							onClick={handleCancel}
+							title="Cancel"
+							style={{
+								padding: 0,
+								margin: 0,
+								height: "28px",
+								width: "28px",
+								minWidth: "28px",
+							}}>
+							<span className="codicon codicon-close" />
+						</VSCodeButton>
+					</div>
+					{error && (
+						<p className="text-red-500 text-sm mt-2" data-testid="error-message">
+							{error}
+						</p>
+					)}
+				</div>
+			) : (
+				<>
+					<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
+						<Dropdown
+							id="config-profile"
+							value={currentApiConfigName}
+							onChange={(value: unknown) => {
+								onSelectConfig((value as DropdownOption).value)
+							}}
+							role="combobox"
+							options={listApiConfigMeta.map((config) => ({
+								value: config.name,
+								label: config.name,
+							}))}
+						/>
+						<VSCodeButton
+							appearance="icon"
+							onClick={handleAdd}
+							title="Add profile"
+							style={{
+								padding: 0,
+								margin: 0,
+								height: "28px",
+								width: "28px",
+								minWidth: "28px",
+							}}>
+							<span className="codicon codicon-add" />
+						</VSCodeButton>
+						{currentApiConfigName && (
+							<>
+								<VSCodeButton
+									appearance="icon"
+									onClick={handleStartRename}
+									title="Rename profile"
+									style={{
+										padding: 0,
+										margin: 0,
+										height: "28px",
+										width: "28px",
+										minWidth: "28px",
+									}}>
+									<span className="codicon codicon-edit" />
+								</VSCodeButton>
+								<VSCodeButton
+									appearance="icon"
+									onClick={handleDelete}
+									title={isOnlyProfile ? "Cannot delete the only profile" : "Delete profile"}
+									disabled={isOnlyProfile}
+									style={{
+										padding: 0,
+										margin: 0,
+										height: "28px",
+										width: "28px",
+										minWidth: "28px",
+									}}>
+									<span className="codicon codicon-trash" />
+								</VSCodeButton>
+							</>
 						)}
-						<div className="flex justify-end gap-2 mt-4">
-							<Button variant="secondary" onClick={resetCreateState}>
-								Cancel
-							</Button>
-							<Button variant="default" disabled={!newProfileName.trim()} onClick={handleNewProfileSave}>
-								Create Profile
-							</Button>
-						</div>
-					</DialogContent>
-				</Dialog>
-			</div>
+					</div>
+					<p
+						style={{
+							fontSize: "12px",
+							margin: "5px 0 12px",
+							color: "var(--vscode-descriptionForeground)",
+						}}>
+						Save different API configurations to quickly switch between providers and settings.
+					</p>
+				</>
+			)}
+
+			<Dialog
+				open={isCreating}
+				onOpenChange={(open: boolean) => {
+					if (open) {
+						setIsCreating(true)
+						setNewProfileName("")
+						setError(null)
+					} else {
+						resetCreateState()
+					}
+				}}
+				aria-labelledby="new-profile-title">
+				<DialogContent className="p-4 max-w-sm">
+					<DialogTitle>New Configuration Profile</DialogTitle>
+					<Input
+						ref={newProfileInputRef}
+						value={newProfileName}
+						onInput={(e: unknown) => {
+							const target = e as { target: { value: string } }
+							setNewProfileName(target.target.value)
+							setError(null)
+						}}
+						placeholder="Enter profile name"
+						style={{ width: "100%" }}
+						onKeyDown={(e: unknown) => {
+							const event = e as { key: string }
+							if (event.key === "Enter" && newProfileName.trim()) {
+								handleNewProfileSave()
+							} else if (event.key === "Escape") {
+								resetCreateState()
+							}
+						}}
+					/>
+					{error && (
+						<p className="text-red-500 text-sm mt-2" data-testid="error-message">
+							{error}
+						</p>
+					)}
+					<div className="flex justify-end gap-2 mt-4">
+						<Button variant="secondary" onClick={resetCreateState}>
+							Cancel
+						</Button>
+						<Button variant="default" disabled={!newProfileName.trim()} onClick={handleNewProfileSave}>
+							Create Profile
+						</Button>
+					</div>
+				</DialogContent>
+			</Dialog>
 		</div>
 	)
 }

+ 0 - 2
webview-ui/src/components/settings/ApiOptions.tsx

@@ -37,7 +37,6 @@ import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
 import { vscode } from "../../utils/vscode"
 import VSCodeButtonLink from "../common/VSCodeButtonLink"
 import { ModelInfoView } from "./ModelInfoView"
-import { DROPDOWN_Z_INDEX } from "./styles"
 import { ModelPicker } from "./ModelPicker"
 import { TemperatureControl } from "./TemperatureControl"
 import { validateApiConfiguration, validateModelId } from "@/utils/validate"
@@ -242,7 +241,6 @@ const ApiOptions = ({
 					id="api-provider"
 					value={selectedProvider}
 					onChange={handleInputChange("apiProvider", dropdownEventTransform)}
-					style={{ minWidth: 130, position: "relative", zIndex: DROPDOWN_Z_INDEX + 1 }}
 					options={[
 						{ value: "openrouter", label: "OpenRouter" },
 						{ value: "anthropic", label: "Anthropic" },

+ 252 - 0
webview-ui/src/components/settings/AutoApproveSettings.tsx

@@ -0,0 +1,252 @@
+import { HTMLAttributes, useState } from "react"
+import { VSCodeButton, VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+import { CheckCheck } from "lucide-react"
+
+import { vscode } from "@/utils/vscode"
+import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
+
+import { SetCachedStateField } from "./types"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+
+type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	alwaysAllowReadOnly?: boolean
+	alwaysAllowWrite?: boolean
+	writeDelayMs: number
+	alwaysAllowBrowser?: boolean
+	alwaysApproveResubmit?: boolean
+	requestDelaySeconds: number
+	alwaysAllowMcp?: boolean
+	alwaysAllowModeSwitch?: boolean
+	alwaysAllowExecute?: boolean
+	allowedCommands?: string[]
+	setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
+}
+
+export const AutoApproveSettings = ({
+	alwaysAllowReadOnly,
+	alwaysAllowWrite,
+	writeDelayMs,
+	alwaysAllowBrowser,
+	alwaysApproveResubmit,
+	requestDelaySeconds,
+	alwaysAllowMcp,
+	alwaysAllowModeSwitch,
+	alwaysAllowExecute,
+	allowedCommands,
+	setCachedStateField,
+	className,
+	...props
+}: AutoApproveSettingsProps) => {
+	const [commandInput, setCommandInput] = useState("")
+
+	const handleAddCommand = () => {
+		const currentCommands = allowedCommands ?? []
+		if (commandInput && !currentCommands.includes(commandInput)) {
+			const newCommands = [...currentCommands, commandInput]
+			setCachedStateField("allowedCommands", newCommands)
+			setCommandInput("")
+			vscode.postMessage({ type: "allowedCommands", commands: newCommands })
+		}
+	}
+
+	return (
+		<div {...props}>
+			<SectionHeader description="Allow Roo to automatically perform operations without requiring approval. Enable these settings only if you fully trust the AI and understand the associated security risks.">
+				<div className="flex items-center gap-2">
+					<CheckCheck className="w-4" />
+					<div>Auto-Approve</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowReadOnly}
+						onChange={(e: any) => setCachedStateField("alwaysAllowReadOnly", e.target.checked)}>
+						<span className="font-medium">Always approve read-only operations</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo will automatically view directory contents and read files without requiring
+						you to click the Approve button.
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowWrite}
+						onChange={(e: any) => setCachedStateField("alwaysAllowWrite", e.target.checked)}>
+						<span className="font-medium">Always approve write operations</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically create and edit files without requiring approval
+					</p>
+					{alwaysAllowWrite && (
+						<div
+							style={{
+								marginTop: 10,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
+								<input
+									type="range"
+									min="0"
+									max="5000"
+									step="100"
+									value={writeDelayMs}
+									onChange={(e) => setCachedStateField("writeDelayMs", parseInt(e.target.value))}
+									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+								/>
+								<span style={{ minWidth: "45px", textAlign: "left" }}>{writeDelayMs}ms</span>
+							</div>
+							<p className="text-vscode-descriptionForeground text-sm mt-1">
+								Delay after writes to allow diagnostics to detect potential problems
+							</p>
+						</div>
+					)}
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowBrowser}
+						onChange={(e: any) => setCachedStateField("alwaysAllowBrowser", e.target.checked)}>
+						<span className="font-medium">Always approve browser actions</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically perform browser actions without requiring approval
+						<br />
+						Note: Only applies when the model supports computer use
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysApproveResubmit}
+						onChange={(e: any) => setCachedStateField("alwaysApproveResubmit", e.target.checked)}>
+						<span className="font-medium">Always retry failed API requests</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically retry failed API requests when server returns an error response
+					</p>
+					{alwaysApproveResubmit && (
+						<div
+							style={{
+								marginTop: 10,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
+								<input
+									type="range"
+									min="5"
+									max="100"
+									step="1"
+									value={requestDelaySeconds}
+									onChange={(e) =>
+										setCachedStateField("requestDelaySeconds", parseInt(e.target.value))
+									}
+									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+								/>
+								<span style={{ minWidth: "45px", textAlign: "left" }}>{requestDelaySeconds}s</span>
+							</div>
+							<p className="text-vscode-descriptionForeground text-sm mt-0">
+								Delay before retrying the request
+							</p>
+						</div>
+					)}
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowMcp}
+						onChange={(e: any) => setCachedStateField("alwaysAllowMcp", e.target.checked)}>
+						<span className="font-medium">Always approve MCP tools</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this setting
+						and the tool's individual "Always allow" checkbox)
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowModeSwitch}
+						onChange={(e: any) => setCachedStateField("alwaysAllowModeSwitch", e.target.checked)}>
+						<span className="font-medium">Always approve mode switching & task creation</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically switch between different AI modes and create new tasks without requiring approval
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowExecute}
+						onChange={(e: any) => setCachedStateField("alwaysAllowExecute", e.target.checked)}>
+						<span className="font-medium">Always approve allowed execute operations</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically execute allowed terminal commands without requiring approval
+					</p>
+					{alwaysAllowExecute && (
+						<div
+							style={{
+								marginTop: 10,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<span className="font-medium">Allowed Auto-Execute Commands</span>
+							<p className="text-vscode-descriptionForeground text-sm mt-0">
+								Command prefixes that can be auto-executed when "Always approve execute operations" is
+								enabled. Add * to allow all commands (use with caution).
+							</p>
+							<div style={{ display: "flex", gap: "5px", marginTop: "10px" }}>
+								<VSCodeTextField
+									value={commandInput}
+									onInput={(e: any) => setCommandInput(e.target.value)}
+									onKeyDown={(e: any) => {
+										if (e.key === "Enter") {
+											e.preventDefault()
+											handleAddCommand()
+										}
+									}}
+									placeholder="Enter command prefix (e.g., 'git ')"
+									style={{ flexGrow: 1 }}
+								/>
+								<VSCodeButton onClick={handleAddCommand}>Add</VSCodeButton>
+							</div>
+							<div
+								style={{
+									marginTop: "10px",
+									display: "flex",
+									flexWrap: "wrap",
+									gap: "5px",
+								}}>
+								{(allowedCommands ?? []).map((cmd, index) => (
+									<div
+										key={index}
+										className="border border-vscode-input-border bg-primary text-primary-foreground flex items-center gap-1 rounded-xs px-1.5 p-0.5">
+										<span>{cmd}</span>
+										<VSCodeButton
+											appearance="icon"
+											className="text-primary-foreground"
+											onClick={() => {
+												const newCommands = (allowedCommands ?? []).filter(
+													(_, i) => i !== index,
+												)
+												setCachedStateField("allowedCommands", newCommands)
+												vscode.postMessage({ type: "allowedCommands", commands: newCommands })
+											}}>
+											<span className="codicon codicon-close" />
+										</VSCodeButton>
+									</div>
+								))}
+							</div>
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

+ 105 - 0
webview-ui/src/components/settings/BrowserSettings.tsx

@@ -0,0 +1,105 @@
+import { HTMLAttributes } from "react"
+import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
+import { Dropdown, type DropdownOption } from "vscrui"
+import { SquareMousePointer } from "lucide-react"
+
+import { SetCachedStateField } from "./types"
+import { sliderLabelStyle } from "./styles"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+
+type BrowserSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	browserToolEnabled?: boolean
+	browserViewportSize?: string
+	screenshotQuality?: number
+	setCachedStateField: SetCachedStateField<"browserToolEnabled" | "browserViewportSize" | "screenshotQuality">
+}
+
+export const BrowserSettings = ({
+	browserToolEnabled,
+	browserViewportSize,
+	screenshotQuality,
+	setCachedStateField,
+	...props
+}: BrowserSettingsProps) => {
+	return (
+		<div {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<SquareMousePointer className="w-4" />
+					<div>Browser / Computer Use</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<VSCodeCheckbox
+						checked={browserToolEnabled}
+						onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
+						<span className="font-medium">Enable browser tool</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo can use a browser to interact with websites when using models that support
+						computer use.
+					</p>
+					{browserToolEnabled && (
+						<div
+							style={{
+								marginLeft: 0,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<div>
+								<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
+									Viewport size
+								</label>
+								<div className="dropdown-container">
+									<Dropdown
+										value={browserViewportSize}
+										onChange={(value: unknown) => {
+											setCachedStateField("browserViewportSize", (value as DropdownOption).value)
+										}}
+										style={{ width: "100%" }}
+										options={[
+											{ value: "1280x800", label: "Large Desktop (1280x800)" },
+											{ value: "900x600", label: "Small Desktop (900x600)" },
+											{ value: "768x1024", label: "Tablet (768x1024)" },
+											{ value: "360x640", label: "Mobile (360x640)" },
+										]}
+									/>
+								</div>
+								<p className="text-vscode-descriptionForeground text-sm mt-0">
+									Select the viewport size for browser interactions. This affects how websites are
+									displayed and interacted with.
+								</p>
+							</div>
+							<div>
+								<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
+									<span className="font-medium">Screenshot quality</span>
+									<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
+										<input
+											type="range"
+											min="1"
+											max="100"
+											step="1"
+											value={screenshotQuality ?? 75}
+											className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+											onChange={(e) =>
+												setCachedStateField("screenshotQuality", parseInt(e.target.value))
+											}
+										/>
+										<span style={{ ...sliderLabelStyle }}>{screenshotQuality ?? 75}%</span>
+									</div>
+								</div>
+								<p className="text-vscode-descriptionForeground text-sm mt-0">
+									Adjust the WebP quality of browser screenshots. Higher values provide clearer
+									screenshots but increase token usage.
+								</p>
+							</div>
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

+ 82 - 0
webview-ui/src/components/settings/CheckpointSettings.tsx

@@ -0,0 +1,82 @@
+import { HTMLAttributes } from "react"
+import { VSCodeCheckbox, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
+import { GitBranch } from "lucide-react"
+
+import { CheckpointStorage, isCheckpointStorage } from "../../../../src/shared/checkpoints"
+
+import { SetCachedStateField } from "./types"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+
+type CheckpointSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	enableCheckpoints?: boolean
+	checkpointStorage?: CheckpointStorage
+	setCachedStateField: SetCachedStateField<"enableCheckpoints" | "checkpointStorage">
+}
+
+export const CheckpointSettings = ({
+	enableCheckpoints,
+	checkpointStorage = "task",
+	setCachedStateField,
+	...props
+}: CheckpointSettingsProps) => {
+	return (
+		<div {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<GitBranch className="w-4" />
+					<div>Checkpoints</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<VSCodeCheckbox
+						checked={enableCheckpoints}
+						onChange={(e: any) => {
+							setCachedStateField("enableCheckpoints", e.target.checked)
+						}}>
+						<span className="font-medium">Enable automatic checkpoints</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo will automatically create checkpoints during task execution, making it easy to
+						review changes or revert to earlier states.
+					</p>
+					{enableCheckpoints && (
+						<div>
+							<div className="font-medium">Storage</div>
+							<VSCodeRadioGroup
+								role="radiogroup"
+								value={checkpointStorage}
+								onChange={(e) => {
+									if ("target" in e) {
+										const { value } = e.target as HTMLInputElement
+
+										if (isCheckpointStorage(value)) {
+											setCachedStateField("checkpointStorage", value)
+										}
+									}
+								}}>
+								<VSCodeRadio value="task">Task</VSCodeRadio>
+								<VSCodeRadio value="workspace">Workspace</VSCodeRadio>
+							</VSCodeRadioGroup>
+							{checkpointStorage === "task" && (
+								<p className="text-vscode-descriptionForeground text-sm mt-0">
+									Each task will have it's own dedicated git repository for storing checkpoints. This
+									provides the best isolation between tasks but uses more disk space.
+								</p>
+							)}
+							{checkpointStorage === "workspace" && (
+								<p className="text-vscode-descriptionForeground text-sm mt-0">
+									Each VSCode workspace will have it's own dedicated git repository for storing
+									checkpoints and tasks within a workspace will share this repository. This option
+									provides better performance and disk space efficiency.
+								</p>
+							)}
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

+ 10 - 21
webview-ui/src/components/settings/ExperimentalFeature.tsx

@@ -7,25 +7,14 @@ interface ExperimentalFeatureProps {
 	onChange: (value: boolean) => void
 }
 
-const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => {
-	return (
-		<div>
-			<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-				<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
-				<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
-					<span style={{ fontWeight: "500" }}>{name}</span>
-				</VSCodeCheckbox>
-			</div>
-			<p
-				style={{
-					fontSize: "12px",
-					marginBottom: 15,
-					color: "var(--vscode-descriptionForeground)",
-				}}>
-				{description}
-			</p>
+export const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => (
+	<div>
+		<div className="flex items-center gap-2">
+			<span className="text-vscode-errorForeground">⚠️</span>
+			<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
+				<span className="font-medium">{name}</span>
+			</VSCodeCheckbox>
 		</div>
-	)
-}
-
-export default ExperimentalFeature
+		<p className="text-vscode-descriptionForeground text-sm mt-0">{description}</p>
+	</div>
+)

+ 53 - 0
webview-ui/src/components/settings/ExperimentalSettings.tsx

@@ -0,0 +1,53 @@
+import { HTMLAttributes } from "react"
+import { FlaskConical } from "lucide-react"
+
+import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
+
+import { cn } from "@/lib/utils"
+
+import { SetCachedStateField, SetExperimentEnabled } from "./types"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+import { ExperimentalFeature } from "./ExperimentalFeature"
+
+type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	setCachedStateField: SetCachedStateField<
+		"rateLimitSeconds" | "terminalOutputLineLimit" | "maxOpenTabsContext" | "diffEnabled" | "fuzzyMatchThreshold"
+	>
+	experiments: Record<ExperimentId, boolean>
+	setExperimentEnabled: SetExperimentEnabled
+}
+
+export const ExperimentalSettings = ({
+	setCachedStateField,
+	experiments,
+	setExperimentEnabled,
+	className,
+	...props
+}: ExperimentalSettingsProps) => {
+	return (
+		<div className={cn("flex flex-col gap-2", className)} {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<FlaskConical className="w-4" />
+					<div>Experimental Features</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				{Object.entries(experimentConfigsMap)
+					.filter((config) => config[0] !== "DIFF_STRATEGY")
+					.map((config) => (
+						<ExperimentalFeature
+							key={config[0]}
+							{...config[1]}
+							enabled={experiments[EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS]] ?? false}
+							onChange={(enabled) =>
+								setExperimentEnabled(EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], enabled)
+							}
+						/>
+					))}
+			</Section>
+		</div>
+	)
+}

+ 69 - 0
webview-ui/src/components/settings/NotificationSettings.tsx

@@ -0,0 +1,69 @@
+import { HTMLAttributes } from "react"
+import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
+import { Bell } from "lucide-react"
+
+import { SetCachedStateField } from "./types"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+
+type NotificationSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	soundEnabled?: boolean
+	soundVolume?: number
+	setCachedStateField: SetCachedStateField<"soundEnabled" | "soundVolume">
+}
+
+export const NotificationSettings = ({
+	soundEnabled,
+	soundVolume,
+	setCachedStateField,
+	...props
+}: NotificationSettingsProps) => {
+	return (
+		<div {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<Bell className="w-4" />
+					<div>Notifications</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<VSCodeCheckbox
+						checked={soundEnabled}
+						onChange={(e: any) => setCachedStateField("soundEnabled", e.target.checked)}>
+						<span className="font-medium">Enable sound effects</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo will play sound effects for notifications and events.
+					</p>
+					{soundEnabled && (
+						<div
+							style={{
+								marginLeft: 0,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
+								<input
+									type="range"
+									min="0"
+									max="1"
+									step="0.01"
+									value={soundVolume ?? 0.5}
+									onChange={(e) => setCachedStateField("soundVolume", parseFloat(e.target.value))}
+									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+									aria-label="Volume"
+								/>
+								<span style={{ minWidth: "35px", textAlign: "left" }}>
+									{((soundVolume ?? 0.5) * 100).toFixed(0)}%
+								</span>
+							</div>
+							<p className="text-vscode-descriptionForeground text-sm mt-1">Volume</p>
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

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

@@ -0,0 +1,9 @@
+import { HTMLAttributes } from "react"
+
+import { cn } from "@/lib/utils"
+
+type SectionProps = HTMLAttributes<HTMLDivElement>
+
+export const Section = ({ className, ...props }: SectionProps) => (
+	<div className={cn("flex flex-col gap-2 p-5", className)} {...props} />
+)

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

@@ -0,0 +1,15 @@
+import { HTMLAttributes } from "react"
+
+import { cn } from "@/lib/utils"
+
+type SectionHeaderProps = HTMLAttributes<HTMLDivElement> & {
+	children: React.ReactNode
+	description?: string
+}
+
+export const SectionHeader = ({ description, children, className, ...props }: SectionHeaderProps) => (
+	<div className={cn("sticky top-0 z-10 bg-vscode-panel-border px-5 py-4", className)} {...props}>
+		<h4 className="m-0">{children}</h4>
+		{description && <p className="text-vscode-descriptionForeground text-sm mt-2 mb-0">{description}</p>}
+	</div>
+)

+ 36 - 0
webview-ui/src/components/settings/SettingsFooter.tsx

@@ -0,0 +1,36 @@
+import { HTMLAttributes } from "react"
+
+import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
+
+import { vscode } from "@/utils/vscode"
+import { cn } from "@/lib/utils"
+
+type SettingsFooterProps = HTMLAttributes<HTMLDivElement> & {
+	version: string
+}
+
+export const SettingsFooter = ({ version, className, ...props }: SettingsFooterProps) => (
+	<div className={cn("text-vscode-descriptionForeground p-5", className)} {...props}>
+		<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
+			If you have any questions or feedback, feel free to open an issue at{" "}
+			<VSCodeLink href="https://github.com/RooVetGit/Roo-Code" style={{ display: "inline" }}>
+				github.com/RooVetGit/Roo-Code
+			</VSCodeLink>{" "}
+			or join{" "}
+			<VSCodeLink href="https://www.reddit.com/r/RooCode/" style={{ display: "inline" }}>
+				reddit.com/r/RooCode
+			</VSCodeLink>
+		</p>
+		<p className="italic">Roo Code v{version}</p>
+		<div className="flex justify-between items-center gap-3">
+			<p>Reset all global state and secret storage in the extension.</p>
+			<VSCodeButton
+				onClick={() => vscode.postMessage({ type: "resetState" })}
+				appearance="secondary"
+				className="shrink-0">
+				<span className="codicon codicon-warning text-vscode-errorForeground mr-1" />
+				Reset
+			</VSCodeButton>
+		</div>
+	</div>
+)

+ 224 - 693
webview-ui/src/components/settings/SettingsView.tsx

@@ -1,7 +1,12 @@
 import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
-import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import { Button, Dropdown, type DropdownOption } from "vscrui"
+import { Button as VSCodeButton } from "vscrui"
+import { CheckCheck, SquareMousePointer, Webhook, GitBranch, Bell, Cog, FlaskConical } from "lucide-react"
 
+import { ExperimentId } from "../../../../src/shared/experiments"
+import { ApiConfiguration } from "../../../../src/shared/api"
+
+import { vscode } from "@/utils/vscode"
+import { ExtensionStateContextType, useExtensionState } from "@/context/ExtensionStateContext"
 import {
 	AlertDialog,
 	AlertDialogContent,
@@ -11,37 +16,43 @@ import {
 	AlertDialogAction,
 	AlertDialogHeader,
 	AlertDialogFooter,
+	Button,
 } from "@/components/ui"
 
-import { vscode } from "../../utils/vscode"
-import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext"
-import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
-import { ApiConfiguration } from "../../../../src/shared/api"
-
-import ExperimentalFeature from "./ExperimentalFeature"
+import { SetCachedStateField, SetExperimentEnabled } from "./types"
+import { SectionHeader } from "./SectionHeader"
 import ApiConfigManager from "./ApiConfigManager"
 import ApiOptions from "./ApiOptions"
-
-type SettingsViewProps = {
-	onDone: () => void
-}
+import { AutoApproveSettings } from "./AutoApproveSettings"
+import { BrowserSettings } from "./BrowserSettings"
+import { CheckpointSettings } from "./CheckpointSettings"
+import { NotificationSettings } from "./NotificationSettings"
+import { AdvancedSettings } from "./AdvancedSettings"
+import { SettingsFooter } from "./SettingsFooter"
+import { Section } from "./Section"
+import { ExperimentalSettings } from "./ExperimentalSettings"
 
 export interface SettingsViewRef {
 	checkUnsaveChanges: (then: () => void) => void
 }
 
+type SettingsViewProps = {
+	onDone: () => void
+}
+
 const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone }, ref) => {
 	const extensionState = useExtensionState()
-	const [commandInput, setCommandInput] = useState("")
+	const { currentApiConfigName, listApiConfigMeta, uriScheme, version } = extensionState
+
 	const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
-	const [cachedState, setCachedState] = useState(extensionState)
 	const [isChangeDetected, setChangeDetected] = useState(false)
-	const prevApiConfigName = useRef(extensionState.currentApiConfigName)
-	const confirmDialogHandler = useRef<() => void>()
 	const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
 
-	// TODO: Reduce WebviewMessage/ExtensionState complexity
-	const { currentApiConfigName } = extensionState
+	const prevApiConfigName = useRef(currentApiConfigName)
+	const confirmDialogHandler = useRef<() => void>()
+
+	const [cachedState, setCachedState] = useState(extensionState)
+
 	const {
 		alwaysAllowReadOnly,
 		allowedCommands,
@@ -54,6 +65,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		browserToolEnabled,
 		browserViewportSize,
 		enableCheckpoints,
+		checkpointStorage,
 		diffEnabled,
 		experiments,
 		fuzzyMatchThreshold,
@@ -68,7 +80,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		writeDelayMs,
 	} = cachedState
 
-	//Make sure apiConfiguration is initialized and managed by SettingsView
+	// Make sure apiConfiguration is initialized and managed by SettingsView.
 	const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
 
 	useEffect(() => {
@@ -80,24 +92,19 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 
 		setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
 		prevApiConfigName.current = currentApiConfigName
-		// console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false")
 		setChangeDetected(false)
 	}, [currentApiConfigName, extensionState, isChangeDetected])
 
-	const setCachedStateField = useCallback(
-		<K extends keyof ExtensionStateContextType>(field: K, value: ExtensionStateContextType[K]) => {
-			setCachedState((prevState) => {
-				if (prevState[field] === value) {
-					return prevState
-				}
+	const setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType> = useCallback((field, value) => {
+		setCachedState((prevState) => {
+			if (prevState[field] === value) {
+				return prevState
+			}
 
-				// console.log(`setCachedStateField(${field} -> ${value}): setChangeDetected -> true`)
-				setChangeDetected(true)
-				return { ...prevState, [field]: value }
-			})
-		},
-		[],
-	)
+			setChangeDetected(true)
+			return { ...prevState, [field]: value }
+		})
+	}, [])
 
 	const setApiConfigurationField = useCallback(
 		<K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => {
@@ -106,7 +113,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 					return prevState
 				}
 
-				// console.log(`setApiConfigurationField(${field} -> ${value}): setChangeDetected -> true`)
 				setChangeDetected(true)
 
 				return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } }
@@ -115,13 +121,12 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		[],
 	)
 
-	const setExperimentEnabled = useCallback((id: ExperimentId, enabled: boolean) => {
+	const setExperimentEnabled: SetExperimentEnabled = useCallback((id: ExperimentId, enabled: boolean) => {
 		setCachedState((prevState) => {
 			if (prevState.experiments?.[id] === enabled) {
 				return prevState
 			}
 
-			// console.log("setExperimentEnabled: setChangeDetected -> true")
 			setChangeDetected(true)
 
 			return {
@@ -146,6 +151,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 			vscode.postMessage({ type: "soundVolume", value: soundVolume })
 			vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
 			vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
+			vscode.postMessage({ type: "checkpointStorage", text: checkpointStorage })
 			vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
 			vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
 			vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
@@ -160,7 +166,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 			vscode.postMessage({ type: "updateExperimental", values: experiments })
 			vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
 			vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
-			// console.log("handleSubmit: setChangeDetected -> false")
 			setChangeDetected(false)
 		}
 	}
@@ -185,108 +190,125 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		}
 	}, [])
 
-	const handleResetState = () => {
-		vscode.postMessage({ type: "resetState" })
-	}
+	const providersRef = useRef<HTMLDivElement>(null)
+	const autoApproveRef = useRef<HTMLDivElement>(null)
+	const browserRef = useRef<HTMLDivElement>(null)
+	const checkpointRef = useRef<HTMLDivElement>(null)
+	const notificationsRef = useRef<HTMLDivElement>(null)
+	const advancedRef = useRef<HTMLDivElement>(null)
+	const experimentalRef = useRef<HTMLDivElement>(null)
+
+	const [activeSection, setActiveSection] = useState<string>("providers")
+
+	const sections = useMemo(
+		() => [
+			{ id: "providers", icon: Webhook, ref: providersRef },
+			{ id: "autoApprove", icon: CheckCheck, ref: autoApproveRef },
+			{ id: "browser", icon: SquareMousePointer, ref: browserRef },
+			{ id: "checkpoint", icon: GitBranch, ref: checkpointRef },
+			{ id: "notifications", icon: Bell, ref: notificationsRef },
+			{ id: "advanced", icon: Cog, ref: advancedRef },
+			{ id: "experimental", icon: FlaskConical, ref: experimentalRef },
+		],
+		[providersRef, autoApproveRef, browserRef, checkpointRef, notificationsRef, advancedRef, experimentalRef],
+	)
 
-	const handleAddCommand = () => {
-		const currentCommands = allowedCommands ?? []
-		if (commandInput && !currentCommands.includes(commandInput)) {
-			const newCommands = [...currentCommands, commandInput]
-			setCachedStateField("allowedCommands", newCommands)
-			setCommandInput("")
-			vscode.postMessage({ type: "allowedCommands", commands: newCommands })
+	const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
+		const sections = [
+			{ ref: providersRef, id: "providers" },
+			{ ref: autoApproveRef, id: "autoApprove" },
+			{ ref: browserRef, id: "browser" },
+			{ ref: checkpointRef, id: "checkpoint" },
+			{ ref: notificationsRef, id: "notifications" },
+			{ ref: advancedRef, id: "advanced" },
+			{ ref: experimentalRef, id: "experimental" },
+		]
+
+		for (const section of sections) {
+			const element = section.ref.current
+
+			if (element) {
+				const { top } = element.getBoundingClientRect()
+
+				if (top >= 0 && top <= 50) {
+					setActiveSection(section.id)
+					break
+				}
+			}
 		}
-	}
+	}, [])
 
-	const sliderLabelStyle = {
-		minWidth: "45px",
-		textAlign: "right" as const,
-		lineHeight: "20px",
-		paddingBottom: "2px",
-	}
+	const scrollToSection = (ref: React.RefObject<HTMLDivElement>) => ref.current?.scrollIntoView()
 
 	return (
-		<div
-			style={{
-				position: "fixed",
-				top: 0,
-				left: 0,
-				right: 0,
-				bottom: 0,
-				padding: "10px 0px 0px 20px",
-				display: "flex",
-				flexDirection: "column",
-				overflow: "hidden",
-			}}>
-			<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
-				<AlertDialogContent>
-					<AlertDialogHeader>
-						<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
-						<AlertDialogDescription>
-							<span className={`codicon codicon-warning align-middle mr-1`} />
-							Do you want to discard changes and continue?
-						</AlertDialogDescription>
-					</AlertDialogHeader>
-					<AlertDialogFooter>
-						<AlertDialogAction onClick={() => onConfirmDialogResult(true)}>Yes</AlertDialogAction>
-						<AlertDialogCancel onClick={() => onConfirmDialogResult(false)}>No</AlertDialogCancel>
-					</AlertDialogFooter>
-				</AlertDialogContent>
-			</AlertDialog>
-			<div
-				style={{
-					display: "flex",
-					justifyContent: "space-between",
-					alignItems: "center",
-					marginBottom: "17px",
-					paddingRight: 17,
-				}}>
-				<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
-				<div
-					style={{
-						display: "flex",
-						justifyContent: "space-between",
-						gap: "6px",
-					}}>
-					<Button
-						appearance={isSettingValid ? "primary" : "secondary"}
-						className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
-						title={!isSettingValid ? errorMessage : isChangeDetected ? "Save changes" : "Nothing changed"}
-						onClick={handleSubmit}
-						disabled={!isChangeDetected || !isSettingValid}>
-						Save
-					</Button>
-					<VSCodeButton
-						appearance="secondary"
-						title="Discard unsaved changes and close settings panel"
-						onClick={() => checkUnsaveChanges(onDone)}>
-						Done
-					</VSCodeButton>
+		<div className="fixed inset-0 flex flex-col overflow-hidden">
+			<div className="px-5 py-2.5 border-b border-vscode-panel-border">
+				<div className="flex flex-col">
+					<div className="flex justify-between items-center">
+						<div className="flex items-center gap-2">
+							<h3 className="text-vscode-foreground m-0">Settings</h3>
+							<div className="hidden [@media(min-width:430px)]:flex items-center">
+								{sections.map(({ id, icon: Icon, ref }) => (
+									<Button
+										key={id}
+										variant="ghost"
+										size="icon"
+										className={activeSection === id ? "opacity-100" : "opacity-40"}
+										onClick={() => scrollToSection(ref)}>
+										<Icon />
+									</Button>
+								))}
+							</div>
+						</div>
+						<div className="flex gap-2">
+							<VSCodeButton
+								appearance={isSettingValid ? "primary" : "secondary"}
+								className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
+								title={
+									!isSettingValid
+										? errorMessage
+										: isChangeDetected
+											? "Save changes"
+											: "Nothing changed"
+								}
+								onClick={handleSubmit}
+								disabled={!isChangeDetected || !isSettingValid}>
+								Save
+							</VSCodeButton>
+							<VSCodeButton
+								appearance="secondary"
+								title="Discard unsaved changes and close settings panel"
+								onClick={() => checkUnsaveChanges(onDone)}>
+								Done
+							</VSCodeButton>
+						</div>
+					</div>
 				</div>
 			</div>
+
 			<div
-				style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
-				<div style={{ marginBottom: 40 }}>
-					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Provider Settings</h3>
-					<div style={{ marginBottom: 15 }}>
+				className="flex flex-col flex-1 overflow-auto divide-y divide-vscode-panel-border"
+				onScroll={handleScroll}>
+				<div ref={providersRef}>
+					<SectionHeader>
+						<div className="flex items-center gap-2">
+							<Webhook className="w-4" />
+							<div>Providers</div>
+						</div>
+					</SectionHeader>
+
+					<Section>
 						<ApiConfigManager
 							currentApiConfigName={currentApiConfigName}
-							listApiConfigMeta={extensionState.listApiConfigMeta}
-							onSelectConfig={(configName: string) => {
-								checkUnsaveChanges(() => {
-									vscode.postMessage({
-										type: "loadApiConfiguration",
-										text: configName,
-									})
-								})
-							}}
-							onDeleteConfig={(configName: string) => {
-								vscode.postMessage({
-									type: "deleteApiConfiguration",
-									text: configName,
-								})
-							}}
+							listApiConfigMeta={listApiConfigMeta}
+							onSelectConfig={(configName: string) =>
+								checkUnsaveChanges(() =>
+									vscode.postMessage({ type: "loadApiConfiguration", text: configName }),
+								)
+							}
+							onDeleteConfig={(configName: string) =>
+								vscode.postMessage({ type: "deleteApiConfiguration", text: configName })
+							}
 							onRenameConfig={(oldName: string, newName: string) => {
 								vscode.postMessage({
 									type: "renameApiConfiguration",
@@ -295,595 +317,104 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 								})
 								prevApiConfigName.current = newName
 							}}
-							onUpsertConfig={(configName: string) => {
+							onUpsertConfig={(configName: string) =>
 								vscode.postMessage({
 									type: "upsertApiConfiguration",
 									text: configName,
 									apiConfiguration,
 								})
-							}}
+							}
 						/>
 						<ApiOptions
-							uriScheme={extensionState.uriScheme}
+							uriScheme={uriScheme}
 							apiConfiguration={apiConfiguration}
 							setApiConfigurationField={setApiConfigurationField}
 							errorMessage={errorMessage}
 							setErrorMessage={setErrorMessage}
 						/>
-					</div>
+					</Section>
 				</div>
 
-				<div style={{ marginBottom: 40 }}>
-					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Auto-Approve Settings</h3>
-					<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
-						The following settings allow Roo to automatically perform operations without requiring approval.
-						Enable these settings only if you fully trust the AI and understand the associated security
-						risks.
-					</p>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowReadOnly}
-							onChange={(e: any) => setCachedStateField("alwaysAllowReadOnly", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve read-only operations</span>
-						</VSCodeCheckbox>
-						<p
-							style={{
-								fontSize: "12px",
-								marginTop: "5px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							When enabled, Roo will automatically view directory contents and read files without
-							requiring you to click the Approve button.
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowWrite}
-							onChange={(e: any) => setCachedStateField("alwaysAllowWrite", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve write operations</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically create and edit files without requiring approval
-						</p>
-						{alwaysAllowWrite && (
-							<div
-								style={{
-									marginTop: 10,
-									paddingLeft: 10,
-									borderLeft: "2px solid var(--vscode-button-background)",
-								}}>
-								<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
-									<input
-										type="range"
-										min="0"
-										max="5000"
-										step="100"
-										value={writeDelayMs}
-										onChange={(e) => setCachedStateField("writeDelayMs", parseInt(e.target.value))}
-										className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-									/>
-									<span style={{ minWidth: "45px", textAlign: "left" }}>{writeDelayMs}ms</span>
-								</div>
-								<p
-									style={{
-										fontSize: "12px",
-										marginTop: "5px",
-										color: "var(--vscode-descriptionForeground)",
-									}}>
-									Delay after writes to allow diagnostics to detect potential problems
-								</p>
-							</div>
-						)}
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowBrowser}
-							onChange={(e: any) => setCachedStateField("alwaysAllowBrowser", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve browser actions</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically perform browser actions without requiring approval
-							<br />
-							Note: Only applies when the model supports computer use
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysApproveResubmit}
-							onChange={(e: any) => setCachedStateField("alwaysApproveResubmit", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always retry failed API requests</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically retry failed API requests when server returns an error response
-						</p>
-						{alwaysApproveResubmit && (
-							<div
-								style={{
-									marginTop: 10,
-									paddingLeft: 10,
-									borderLeft: "2px solid var(--vscode-button-background)",
-								}}>
-								<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
-									<input
-										type="range"
-										min="5"
-										max="100"
-										step="1"
-										value={requestDelaySeconds}
-										onChange={(e) =>
-											setCachedStateField("requestDelaySeconds", parseInt(e.target.value))
-										}
-										className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-									/>
-									<span style={{ minWidth: "45px", textAlign: "left" }}>{requestDelaySeconds}s</span>
-								</div>
-								<p
-									style={{
-										fontSize: "12px",
-										marginTop: "5px",
-										color: "var(--vscode-descriptionForeground)",
-									}}>
-									Delay before retrying the request
-								</p>
-							</div>
-						)}
-					</div>
-
-					<div style={{ marginBottom: 5 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowMcp}
-							onChange={(e: any) => setCachedStateField("alwaysAllowMcp", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve MCP tools</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this
-							setting and the tool's individual "Always allow" checkbox)
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowModeSwitch}
-							onChange={(e: any) => setCachedStateField("alwaysAllowModeSwitch", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve mode switching & task creation</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically switch between different AI modes and create new tasks without requiring
-							approval
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowExecute}
-							onChange={(e: any) => setCachedStateField("alwaysAllowExecute", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve allowed execute operations</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically execute allowed terminal commands without requiring approval
-						</p>
-
-						{alwaysAllowExecute && (
-							<div
-								style={{
-									marginTop: 10,
-									paddingLeft: 10,
-									borderLeft: "2px solid var(--vscode-button-background)",
-								}}>
-								<span style={{ fontWeight: "500" }}>Allowed Auto-Execute Commands</span>
-								<p
-									style={{
-										fontSize: "12px",
-										marginTop: "5px",
-										color: "var(--vscode-descriptionForeground)",
-									}}>
-									Command prefixes that can be auto-executed when "Always approve execute operations"
-									is enabled. Add * to allow all commands (use with caution).
-								</p>
-
-								<div style={{ display: "flex", gap: "5px", marginTop: "10px" }}>
-									<VSCodeTextField
-										value={commandInput}
-										onInput={(e: any) => setCommandInput(e.target.value)}
-										onKeyDown={(e: any) => {
-											if (e.key === "Enter") {
-												e.preventDefault()
-												handleAddCommand()
-											}
-										}}
-										placeholder="Enter command prefix (e.g., 'git ')"
-										style={{ flexGrow: 1 }}
-									/>
-									<VSCodeButton onClick={handleAddCommand}>Add</VSCodeButton>
-								</div>
-
-								<div
-									style={{
-										marginTop: "10px",
-										display: "flex",
-										flexWrap: "wrap",
-										gap: "5px",
-									}}>
-									{(allowedCommands ?? []).map((cmd, index) => (
-										<div
-											key={index}
-											className="border border-vscode-input-border bg-primary text-primary-foreground flex items-center gap-1 rounded-xs px-1.5 p-0.5">
-											<span>{cmd}</span>
-											<VSCodeButton
-												appearance="icon"
-												className="text-primary-foreground"
-												onClick={() => {
-													const newCommands = (allowedCommands ?? []).filter(
-														(_, i) => i !== index,
-													)
-													setCachedStateField("allowedCommands", newCommands)
-													vscode.postMessage({
-														type: "allowedCommands",
-														commands: newCommands,
-													})
-												}}>
-												<span className="codicon codicon-close" />
-											</VSCodeButton>
-										</div>
-									))}
-								</div>
-							</div>
-						)}
-					</div>
+				<div ref={autoApproveRef}>
+					<AutoApproveSettings
+						alwaysAllowReadOnly={alwaysAllowReadOnly}
+						alwaysAllowWrite={alwaysAllowWrite}
+						writeDelayMs={writeDelayMs}
+						alwaysAllowBrowser={alwaysAllowBrowser}
+						alwaysApproveResubmit={alwaysApproveResubmit}
+						requestDelaySeconds={requestDelaySeconds}
+						alwaysAllowMcp={alwaysAllowMcp}
+						alwaysAllowModeSwitch={alwaysAllowModeSwitch}
+						alwaysAllowExecute={alwaysAllowExecute}
+						allowedCommands={allowedCommands}
+						setCachedStateField={setCachedStateField}
+					/>
 				</div>
 
-				<div style={{ marginBottom: 40 }}>
-					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Browser Settings</h3>
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={browserToolEnabled}
-							onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Enable browser tool</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							When enabled, Roo can use a browser to interact with websites when using models that support
-							computer use.
-						</p>
-					</div>
-					{browserToolEnabled && (
-						<div
-							style={{
-								marginLeft: 0,
-								paddingLeft: 10,
-								borderLeft: "2px solid var(--vscode-button-background)",
-							}}>
-							<div style={{ marginBottom: 15 }}>
-								<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
-									Viewport size
-								</label>
-								<div className="dropdown-container">
-									<Dropdown
-										value={browserViewportSize}
-										onChange={(value: unknown) => {
-											setCachedStateField("browserViewportSize", (value as DropdownOption).value)
-										}}
-										style={{ width: "100%" }}
-										options={[
-											{ value: "1280x800", label: "Large Desktop (1280x800)" },
-											{ value: "900x600", label: "Small Desktop (900x600)" },
-											{ value: "768x1024", label: "Tablet (768x1024)" },
-											{ value: "360x640", label: "Mobile (360x640)" },
-										]}
-									/>
-								</div>
-								<p
-									style={{
-										fontSize: "12px",
-										marginTop: "5px",
-										color: "var(--vscode-descriptionForeground)",
-									}}>
-									Select the viewport size for browser interactions. This affects how websites are
-									displayed and interacted with.
-								</p>
-							</div>
-
-							<div style={{ marginBottom: 15 }}>
-								<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
-									<span style={{ fontWeight: "500" }}>Screenshot quality</span>
-									<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-										<input
-											type="range"
-											min="1"
-											max="100"
-											step="1"
-											value={screenshotQuality ?? 75}
-											className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-											onChange={(e) =>
-												setCachedStateField("screenshotQuality", parseInt(e.target.value))
-											}
-										/>
-										<span style={{ ...sliderLabelStyle }}>{screenshotQuality ?? 75}%</span>
-									</div>
-								</div>
-								<p
-									style={{
-										fontSize: "12px",
-										marginTop: "5px",
-										color: "var(--vscode-descriptionForeground)",
-									}}>
-									Adjust the WebP quality of browser screenshots. Higher values provide clearer
-									screenshots but increase token usage.
-								</p>
-							</div>
-						</div>
-					)}
+				<div ref={browserRef}>
+					<BrowserSettings
+						browserToolEnabled={browserToolEnabled}
+						browserViewportSize={browserViewportSize}
+						screenshotQuality={screenshotQuality}
+						setCachedStateField={setCachedStateField}
+					/>
 				</div>
 
-				<div style={{ marginBottom: 40 }}>
-					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Notification Settings</h3>
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={soundEnabled}
-							onChange={(e: any) => setCachedStateField("soundEnabled", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Enable sound effects</span>
-						</VSCodeCheckbox>
-						<p
-							style={{
-								fontSize: "12px",
-								marginTop: "5px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							When enabled, Roo will play sound effects for notifications and events.
-						</p>
-					</div>
-					{soundEnabled && (
-						<div
-							style={{
-								marginLeft: 0,
-								paddingLeft: 10,
-								borderLeft: "2px solid var(--vscode-button-background)",
-							}}>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<span style={{ fontWeight: "500", minWidth: "100px" }}>Volume</span>
-								<input
-									type="range"
-									min="0"
-									max="1"
-									step="0.01"
-									value={soundVolume ?? 0.5}
-									onChange={(e) => setCachedStateField("soundVolume", parseFloat(e.target.value))}
-									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-									aria-label="Volume"
-								/>
-								<span style={{ minWidth: "35px", textAlign: "left" }}>
-									{((soundVolume ?? 0.5) * 100).toFixed(0)}%
-								</span>
-							</div>
-						</div>
-					)}
+				<div ref={checkpointRef}>
+					<CheckpointSettings
+						enableCheckpoints={enableCheckpoints}
+						checkpointStorage={checkpointStorage}
+						setCachedStateField={setCachedStateField}
+					/>
 				</div>
 
-				<div style={{ marginBottom: 40 }}>
-					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Advanced Settings</h3>
-					<div style={{ marginBottom: 15 }}>
-						<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
-							<span style={{ fontWeight: "500" }}>Rate limit</span>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<input
-									type="range"
-									min="0"
-									max="60"
-									step="1"
-									value={rateLimitSeconds}
-									onChange={(e) => setCachedStateField("rateLimitSeconds", parseInt(e.target.value))}
-									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-								/>
-								<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
-							</div>
-						</div>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Minimum time between API requests.
-						</p>
-					</div>
-					<div style={{ marginBottom: 15 }}>
-						<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
-							<span style={{ fontWeight: "500" }}>Terminal output limit</span>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<input
-									type="range"
-									min="100"
-									max="5000"
-									step="100"
-									value={terminalOutputLineLimit ?? 500}
-									onChange={(e) =>
-										setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value))
-									}
-									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-								/>
-								<span style={{ ...sliderLabelStyle }}>{terminalOutputLineLimit ?? 500}</span>
-							</div>
-						</div>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Maximum number of lines to include in terminal output when executing commands. When exceeded
-							lines will be removed from the middle, saving tokens.
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
-							<span style={{ fontWeight: "500" }}>Open tabs context limit</span>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<input
-									type="range"
-									min="0"
-									max="500"
-									step="1"
-									value={maxOpenTabsContext ?? 20}
-									onChange={(e) =>
-										setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))
-									}
-									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-								/>
-								<span style={{ ...sliderLabelStyle }}>{maxOpenTabsContext ?? 20}</span>
-							</div>
-						</div>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Maximum number of VSCode open tabs to include in context. Higher values provide more context
-							but increase token usage.
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={enableCheckpoints}
-							onChange={(e: any) => {
-								setCachedStateField("enableCheckpoints", e.target.checked)
-							}}>
-							<span style={{ fontWeight: "500" }}>Enable automatic checkpoints</span>
-						</VSCodeCheckbox>
-						<p
-							style={{
-								fontSize: "12px",
-								marginTop: "5px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							When enabled, Roo will automatically create checkpoints during task execution, making it
-							easy to review changes or revert to earlier states.
-						</p>
-					</div>
+				<div ref={notificationsRef}>
+					<NotificationSettings
+						soundEnabled={soundEnabled}
+						soundVolume={soundVolume}
+						setCachedStateField={setCachedStateField}
+					/>
+				</div>
 
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={diffEnabled}
-							onChange={(e: any) => {
-								setCachedStateField("diffEnabled", e.target.checked)
-								if (!e.target.checked) {
-									// Reset experimental strategy when diffs are disabled
-									setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
-								}
-							}}>
-							<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
-						</VSCodeCheckbox>
-						<p
-							style={{
-								fontSize: "12px",
-								marginTop: "5px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							When enabled, Roo will be able to edit files more quickly and will automatically reject
-							truncated full-file writes. Works best with the latest Claude 3.7 Sonnet model.
-						</p>
-
-						{diffEnabled && (
-							<div style={{ marginTop: 10 }}>
-								<div
-									style={{
-										display: "flex",
-										flexDirection: "column",
-										gap: "5px",
-										marginTop: "10px",
-										marginBottom: "10px",
-										paddingLeft: "10px",
-										borderLeft: "2px solid var(--vscode-button-background)",
-									}}>
-									<span style={{ fontWeight: "500" }}>Match precision</span>
-									<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-										<input
-											type="range"
-											min="0.8"
-											max="1"
-											step="0.005"
-											value={fuzzyMatchThreshold ?? 1.0}
-											onChange={(e) => {
-												setCachedStateField("fuzzyMatchThreshold", parseFloat(e.target.value))
-											}}
-											className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-										/>
-										<span style={{ ...sliderLabelStyle }}>
-											{Math.round((fuzzyMatchThreshold || 1) * 100)}%
-										</span>
-									</div>
-									<p
-										style={{
-											fontSize: "12px",
-											marginTop: "5px",
-											color: "var(--vscode-descriptionForeground)",
-										}}>
-										This slider controls how precisely code sections must match when applying diffs.
-										Lower values allow more flexible matching but increase the risk of incorrect
-										replacements. Use values below 100% with extreme caution.
-									</p>
-									<ExperimentalFeature
-										key={EXPERIMENT_IDS.DIFF_STRATEGY}
-										{...experimentConfigsMap.DIFF_STRATEGY}
-										enabled={experiments[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false}
-										onChange={(enabled) =>
-											setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled)
-										}
-									/>
-								</div>
-							</div>
-						)}
-
-						{Object.entries(experimentConfigsMap)
-							.filter((config) => config[0] !== "DIFF_STRATEGY")
-							.map((config) => (
-								<ExperimentalFeature
-									key={config[0]}
-									{...config[1]}
-									enabled={
-										experiments[EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS]] ?? false
-									}
-									onChange={(enabled) =>
-										setExperimentEnabled(
-											EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS],
-											enabled,
-										)
-									}
-								/>
-							))}
-					</div>
+				<div ref={advancedRef}>
+					<AdvancedSettings
+						rateLimitSeconds={rateLimitSeconds}
+						terminalOutputLineLimit={terminalOutputLineLimit}
+						maxOpenTabsContext={maxOpenTabsContext}
+						diffEnabled={diffEnabled}
+						fuzzyMatchThreshold={fuzzyMatchThreshold}
+						setCachedStateField={setCachedStateField}
+						setExperimentEnabled={setExperimentEnabled}
+						experiments={experiments}
+					/>
 				</div>
 
-				<div
-					style={{
-						textAlign: "center",
-						color: "var(--vscode-descriptionForeground)",
-						fontSize: "12px",
-						lineHeight: "1.2",
-						marginTop: "auto",
-						padding: "10px 8px 15px 0px",
-					}}>
-					<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
-						If you have any questions or feedback, feel free to open an issue at{" "}
-						<VSCodeLink href="https://github.com/RooVetGit/Roo-Code" style={{ display: "inline" }}>
-							github.com/RooVetGit/Roo-Code
-						</VSCodeLink>{" "}
-						or join{" "}
-						<VSCodeLink href="https://www.reddit.com/r/RooCode/" style={{ display: "inline" }}>
-							reddit.com/r/RooCode
-						</VSCodeLink>
-					</p>
-					<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0, marginBottom: 100 }}>
-						v{extensionState.version}
-					</p>
-
-					<p
-						style={{
-							fontSize: "12px",
-							marginTop: "5px",
-							color: "var(--vscode-descriptionForeground)",
-						}}>
-						This will reset all global state and secret storage in the extension.
-					</p>
-
-					<VSCodeButton
-						onClick={handleResetState}
-						appearance="secondary"
-						style={{ marginTop: "5px", width: "auto" }}>
-						Reset State
-					</VSCodeButton>
+				<div ref={experimentalRef}>
+					<ExperimentalSettings
+						setCachedStateField={setCachedStateField}
+						setExperimentEnabled={setExperimentEnabled}
+						experiments={experiments}
+					/>
 				</div>
+
+				<SettingsFooter version={version} />
 			</div>
+
+			<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
+				<AlertDialogContent>
+					<AlertDialogHeader>
+						<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
+						<AlertDialogDescription>
+							<span className={`codicon codicon-warning align-middle mr-1`} />
+							Do you want to discard changes and continue?
+						</AlertDialogDescription>
+					</AlertDialogHeader>
+					<AlertDialogFooter>
+						<AlertDialogAction onClick={() => onConfirmDialogResult(true)}>Yes</AlertDialogAction>
+						<AlertDialogCancel onClick={() => onConfirmDialogResult(false)}>No</AlertDialogCancel>
+					</AlertDialogFooter>
+				</AlertDialogContent>
+			</AlertDialog>
 		</div>
 	)
 })

+ 3 - 3
webview-ui/src/components/settings/TemperatureControl.tsx

@@ -32,10 +32,10 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
 						setInputValue(value ?? 0) // Use the value from apiConfiguration, if set
 					}
 				}}>
-				<span style={{ fontWeight: "500" }}>Use custom temperature</span>
+				<span className="font-medium">Use custom temperature</span>
 			</VSCodeCheckbox>
 
-			<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
+			<p className="text-vscode-descriptionForeground text-sm mt-0">
 				Controls randomness in the model's responses.
 			</p>
 
@@ -59,7 +59,7 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
 						/>
 						<span>{inputValue}</span>
 					</div>
-					<p style={{ fontSize: "12px", marginTop: "8px", color: "var(--vscode-descriptionForeground)" }}>
+					<p className="text-vscode-descriptionForeground text-sm mt-1">
 						Higher values make output more random, lower values make it more deterministic.
 					</p>
 				</div>

+ 18 - 0
webview-ui/src/components/settings/__tests__/SettingsView.test.tsx

@@ -1,3 +1,5 @@
+// npx jest src/components/settings/__tests__/SettingsView.test.ts
+
 import { render, screen, fireEvent } from "@testing-library/react"
 import SettingsView from "../SettingsView"
 import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
@@ -10,6 +12,22 @@ jest.mock("../../../utils/vscode", () => ({
 	},
 }))
 
+// Mock all lucide-react icons with a proxy to handle any icon requested
+jest.mock("lucide-react", () => {
+	return new Proxy(
+		{},
+		{
+			get: function (obj, prop) {
+				// Return a component factory for any icon that's requested
+				if (prop === "__esModule") {
+					return true
+				}
+				return () => <div data-testid={`${String(prop)}-icon`}>{String(prop)}</div>
+			},
+		},
+	)
+})
+
 // Mock ApiConfigManager component
 jest.mock("../ApiConfigManager", () => ({
 	__esModule: true,

+ 7 - 2
webview-ui/src/components/settings/styles.ts

@@ -1,7 +1,5 @@
 import styled from "styled-components"
 
-export const DROPDOWN_Z_INDEX = 1_000
-
 export const DropdownWrapper = styled.div`
 	position: relative;
 	width: 100%;
@@ -78,3 +76,10 @@ export const StyledMarkdown = styled.div`
 		}
 	}
 `
+
+export const sliderLabelStyle = {
+	minWidth: "45px",
+	textAlign: "right" as const,
+	lineHeight: "20px",
+	paddingBottom: "2px",
+}

+ 10 - 0
webview-ui/src/components/settings/types.ts

@@ -0,0 +1,10 @@
+import { ExperimentId } from "../../../../src/shared/experiments"
+
+import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
+
+export type SetCachedStateField<K extends keyof ExtensionStateContextType> = (
+	field: K,
+	value: ExtensionStateContextType[K],
+) => void
+
+export type SetExperimentEnabled = (id: ExperimentId, enabled: boolean) => void

+ 1 - 0
webview-ui/src/context/ExtensionStateContext.tsx

@@ -107,6 +107,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		soundVolume: 0.5,
 		diffEnabled: false,
 		enableCheckpoints: true,
+		checkpointStorage: "task",
 		fuzzyMatchThreshold: 1.0,
 		preferredLanguage: "English",
 		writeDelayMs: 1000,

+ 1 - 0
webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx

@@ -79,6 +79,7 @@ describe("mergeExtensionState", () => {
 			taskHistory: [],
 			shouldShowAnnouncement: false,
 			enableCheckpoints: true,
+			checkpointStorage: "task",
 			preferredLanguage: "English",
 			writeDelayMs: 1000,
 			requestDelaySeconds: 5,

+ 4 - 0
webview-ui/src/index.css

@@ -96,6 +96,10 @@
 	--color-vscode-list-hoverForeground: var(--vscode-list-hoverForeground);
 	--color-vscode-list-hoverBackground: var(--vscode-list-hoverBackground);
 	--color-vscode-list-focusBackground: var(--vscode-list-focusBackground);
+
+	--color-vscode-toolbar-hoverBackground: var(--vscode-toolbar-hoverBackground);
+
+	--color-vscode-panel-border: var(--vscode-panel-border);
 }
 
 @layer base {