diff --git a/package.json b/package.json index 68767cbed5..136c91cfa4 100644 --- a/package.json +++ b/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", diff --git a/src/activate/index.ts b/src/activate/index.ts index 658bf467f7..76eebd185e 100644 --- a/src/activate/index.ts +++ b/src/activate/index.ts @@ -1,4 +1,3 @@ export { handleUri } from "./handleUri" export { registerCommands } from "./registerCommands" export { registerCodeActions } from "./registerCodeActions" -export { registerTerminalActions } from "./registerTerminalActions" diff --git a/src/activate/registerTerminalActions.ts b/src/activate/registerTerminalActions.ts deleted file mode 100644 index fbf2a0510c..0000000000 --- a/src/activate/registerTerminalActions.ts +++ /dev/null @@ -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 = { - 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) -} diff --git a/src/core/Cline.ts b/src/core/Cline.ts index bccac0575f..773303c246 100644 --- a/src/core/Cline.ts +++ b/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 diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 4c3068eac6..a09f18d278 100644 --- a/src/core/webview/ClineProvider.ts +++ b/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, diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 9463be25b7..68739e8f06 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/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, diff --git a/src/extension.ts b/src/extension.ts index a05afa4651..3a91e990a3 100644 --- a/src/extension.ts +++ b/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 diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 858b20ca7f..ff9e2a24df 100644 --- a/src/shared/ExtensionMessage.ts +++ b/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 diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 141915fdb8..19e3d3bac3 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -53,6 +53,7 @@ export interface WebviewMessage { | "soundVolume" | "diffEnabled" | "enableCheckpoints" + | "checkpointStorage" | "browserViewportSize" | "screenshotQuality" | "openMcpSettings" diff --git a/src/shared/checkpoints.ts b/src/shared/checkpoints.ts new file mode 100644 index 0000000000..7cd1818c12 --- /dev/null +++ b/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" +} diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index 1f36732466..bdc263735a 100644 --- a/src/shared/globalState.ts +++ b/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", ] diff --git a/webview-ui/src/components/common/MermaidBlock.tsx b/webview-ui/src/components/common/MermaidBlock.tsx index 619188179e..6153570cf2 100644 --- a/webview-ui/src/components/common/MermaidBlock.tsx +++ b/webview-ui/src/components/common/MermaidBlock.tsx @@ -150,7 +150,6 @@ export default function MermaidBlock({ code }: MermaidBlockProps) { } async function svgToPng(svgEl: SVGElement): Promise { - console.log("svgToPng function called") // Clone the SVG to avoid modifying the original const svgClone = svgEl.cloneNode(true) as SVGElement diff --git a/webview-ui/src/components/history/HistoryPreview.tsx b/webview-ui/src/components/history/HistoryPreview.tsx index bf53845da7..f81d8ddacf 100644 --- a/webview-ui/src/components/history/HistoryPreview.tsx +++ b/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 ( -
- -
- - Recent Tasks -
-
- {taskHistory - .filter((item) => item.ts && item.task) - .slice(0, 3) - .map((item) => ( -
handleHistorySelect(item.id)}> -
-
- - {formatDate(item.ts)} - - -
-
- {item.task} -
-
- - Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓ - {formatLargeNumber(item.tokensOut || 0)} - - {!!item.cacheWrites && ( - <> - {" • "} - - Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "} - {formatLargeNumber(item.cacheReads || 0)} - - - )} - {!!item.totalCost && ( - <> - {" • "} - API Cost: ${item.totalCost?.toFixed(4)} - - )} -
-
-
- ))} -
- +
+
+
+ + Recent Tasks
+
+ {taskHistory.slice(0, 3).map((item) => ( +
vscode.postMessage({ type: "showTaskWithId", text: item.id })}> +
+
+ + {formatDate(item.ts)} + + +
+
+ {item.task} +
+
+ + Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓ + {formatLargeNumber(item.tokensOut || 0)} + + {!!item.cacheWrites && ( + <> + {" • "} + + Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "} + {formatLargeNumber(item.cacheReads || 0)} + + + )} + {!!item.totalCost && ( + <> + {" • "} + API Cost: ${item.totalCost?.toFixed(4)} + + )} +
+
+
+ ))}
) } diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index c621da5a03..e65a11a3ec 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/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("newest") - const [lastNonRelevantSort, setLastNonRelevantSort] = useState("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(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 ( -
-
-

History

- Done -
-
-
+
+
+
+

History

+ Done +
+
{ 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) => (
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 })}>
{ + const { taskHistory } = useExtensionState() + const [searchQuery, setSearchQuery] = useState("") + const [sortOption, setSortOption] = useState("newest") + const [lastNonRelevantSort, setLastNonRelevantSort] = useState("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, + } +} diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index adb6b47343..7a24922d88 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -29,28 +29,12 @@ const McpView = ({ onDone }: McpViewProps) => { } = useExtensionState() return ( -
-
-

MCP Servers

+
+
+

MCP Servers

Done
- -
+
{
)} - - {/* Bottom padding */} -
) diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 2bfafeff5c..ccf1e6d700 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -407,12 +407,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { return (
-
+

Prompts

Done
- -
+
Preferred Language
@@ -1174,7 +1173,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
- {isCreateModeDialogOpen && (
& { + rateLimitSeconds: number + terminalOutputLineLimit?: number + maxOpenTabsContext: number + diffEnabled?: boolean + fuzzyMatchThreshold?: number + setCachedStateField: SetCachedStateField< + "rateLimitSeconds" | "terminalOutputLineLimit" | "maxOpenTabsContext" | "diffEnabled" | "fuzzyMatchThreshold" + > + experiments: Record + setExperimentEnabled: SetExperimentEnabled +} + +export const AdvancedSettings = ({ + rateLimitSeconds, + terminalOutputLineLimit, + maxOpenTabsContext, + diffEnabled, + fuzzyMatchThreshold, + setCachedStateField, + experiments, + setExperimentEnabled, + className, + ...props +}: AdvancedSettingsProps) => { + return ( +
+ +
+ +
Advanced
+
+
+ +
+
+
+ Rate limit +
+ setCachedStateField("rateLimitSeconds", parseInt(e.target.value))} + className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" + /> + {rateLimitSeconds}s +
+
+

Minimum time between API requests.

+
+ +
+
+ Terminal output limit +
+ + setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value)) + } + className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" + /> + {terminalOutputLineLimit ?? 500} +
+
+

+ Maximum number of lines to include in terminal output when executing commands. When exceeded + lines will be removed from the middle, saving tokens. +

+
+ +
+
+ Open tabs context limit +
+ setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))} + className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" + /> + {maxOpenTabsContext ?? 20} +
+
+

+ Maximum number of VSCode open tabs to include in context. Higher values provide more context but + increase token usage. +

+
+ +
+ { + setCachedStateField("diffEnabled", e.target.checked) + if (!e.target.checked) { + // Reset experimental strategy when diffs are disabled. + setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false) + } + }}> + Enable editing through diffs + +

+ 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. +

+ {diffEnabled && ( +
+ Match precision +
+ { + setCachedStateField("fuzzyMatchThreshold", parseFloat(e.target.value)) + }} + className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" + /> + + {Math.round((fuzzyMatchThreshold || 1) * 100)}% + +
+

+ 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. +

+ setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled)} + /> +
+ )} +
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index 7e2c63c530..b548785dc5 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -142,199 +142,188 @@ const ApiConfigManager = ({ const isOnlyProfile = listApiConfigMeta?.length === 1 return ( -
-
- +
+ - {isRenaming ? ( -
-
- { - 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() - } - }} - /> - - - - - - -
- {error && ( -

- {error} -

- )} -
- ) : ( - <> -
- { - onSelectConfig((value as DropdownOption).value) - }} - style={{ - minWidth: 130, - zIndex: 1002, - }} - role="combobox" - options={listApiConfigMeta.map((config) => ({ - value: config.name, - label: config.name, - }))} - /> - - - - {currentApiConfigName && ( - <> - - - - - - - - )} -
-

- Save different API configurations to quickly switch between providers and settings -

- - )} - - { - if (open) { - setIsCreating(true) - setNewProfileName("") - setError(null) - } else { - resetCreateState() - } - }} - aria-labelledby="new-profile-title"> - - New Configuration Profile - +
+ { 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 && ( -

- {error} -

+ + + + + + +
+ {error && ( +

+ {error} +

+ )} +
+ ) : ( + <> +
+ { + onSelectConfig((value as DropdownOption).value) + }} + role="combobox" + options={listApiConfigMeta.map((config) => ({ + value: config.name, + label: config.name, + }))} + /> + + + + {currentApiConfigName && ( + <> + + + + + + + )} -
- - -
- - -
+
+

+ Save different API configurations to quickly switch between providers and settings. +

+ + )} + + { + if (open) { + setIsCreating(true) + setNewProfileName("") + setError(null) + } else { + resetCreateState() + } + }} + aria-labelledby="new-profile-title"> + + New Configuration Profile + { + 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 && ( +

+ {error} +

+ )} +
+ + +
+
+
) } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index b9050a999f..4466d12aaa 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/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" }, diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx new file mode 100644 index 0000000000..b2da2cab75 --- /dev/null +++ b/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 & { + alwaysAllowReadOnly?: boolean + alwaysAllowWrite?: boolean + writeDelayMs: number + alwaysAllowBrowser?: boolean + alwaysApproveResubmit?: boolean + requestDelaySeconds: number + alwaysAllowMcp?: boolean + alwaysAllowModeSwitch?: boolean + alwaysAllowExecute?: boolean + allowedCommands?: string[] + setCachedStateField: SetCachedStateField +} + +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 ( +
+ +
+ +
Auto-Approve
+
+
+ +
+
+ setCachedStateField("alwaysAllowReadOnly", e.target.checked)}> + Always approve read-only operations + +

+ When enabled, Roo will automatically view directory contents and read files without requiring + you to click the Approve button. +

+
+ +
+ setCachedStateField("alwaysAllowWrite", e.target.checked)}> + Always approve write operations + +

+ Automatically create and edit files without requiring approval +

+ {alwaysAllowWrite && ( +
+
+ setCachedStateField("writeDelayMs", parseInt(e.target.value))} + className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" + /> + {writeDelayMs}ms +
+

+ Delay after writes to allow diagnostics to detect potential problems +

+
+ )} +
+ +
+ setCachedStateField("alwaysAllowBrowser", e.target.checked)}> + Always approve browser actions + +

+ Automatically perform browser actions without requiring approval +
+ Note: Only applies when the model supports computer use +

+
+ +
+ setCachedStateField("alwaysApproveResubmit", e.target.checked)}> + Always retry failed API requests + +

+ Automatically retry failed API requests when server returns an error response +

+ {alwaysApproveResubmit && ( +
+
+ + setCachedStateField("requestDelaySeconds", parseInt(e.target.value)) + } + className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" + /> + {requestDelaySeconds}s +
+

+ Delay before retrying the request +

+
+ )} +
+ +
+ setCachedStateField("alwaysAllowMcp", e.target.checked)}> + Always approve MCP tools + +

+ Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this setting + and the tool's individual "Always allow" checkbox) +

+
+ +
+ setCachedStateField("alwaysAllowModeSwitch", e.target.checked)}> + Always approve mode switching & task creation + +

+ Automatically switch between different AI modes and create new tasks without requiring approval +

+
+ +
+ setCachedStateField("alwaysAllowExecute", e.target.checked)}> + Always approve allowed execute operations + +

+ Automatically execute allowed terminal commands without requiring approval +

+ {alwaysAllowExecute && ( +
+ Allowed Auto-Execute Commands +

+ Command prefixes that can be auto-executed when "Always approve execute operations" is + enabled. Add * to allow all commands (use with caution). +

+
+ setCommandInput(e.target.value)} + onKeyDown={(e: any) => { + if (e.key === "Enter") { + e.preventDefault() + handleAddCommand() + } + }} + placeholder="Enter command prefix (e.g., 'git ')" + style={{ flexGrow: 1 }} + /> + Add +
+
+ {(allowedCommands ?? []).map((cmd, index) => ( +
+ {cmd} + { + const newCommands = (allowedCommands ?? []).filter( + (_, i) => i !== index, + ) + setCachedStateField("allowedCommands", newCommands) + vscode.postMessage({ type: "allowedCommands", commands: newCommands }) + }}> + + +
+ ))} +
+
+ )} +
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/BrowserSettings.tsx b/webview-ui/src/components/settings/BrowserSettings.tsx new file mode 100644 index 0000000000..1ff0a36b9c --- /dev/null +++ b/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 & { + browserToolEnabled?: boolean + browserViewportSize?: string + screenshotQuality?: number + setCachedStateField: SetCachedStateField<"browserToolEnabled" | "browserViewportSize" | "screenshotQuality"> +} + +export const BrowserSettings = ({ + browserToolEnabled, + browserViewportSize, + screenshotQuality, + setCachedStateField, + ...props +}: BrowserSettingsProps) => { + return ( +
+ +
+ +
Browser / Computer Use
+
+
+ +
+
+ setCachedStateField("browserToolEnabled", e.target.checked)}> + Enable browser tool + +

+ When enabled, Roo can use a browser to interact with websites when using models that support + computer use. +

+ {browserToolEnabled && ( +
+
+ +
+ { + 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)" }, + ]} + /> +
+

+ Select the viewport size for browser interactions. This affects how websites are + displayed and interacted with. +

+
+
+
+ Screenshot quality +
+ + setCachedStateField("screenshotQuality", parseInt(e.target.value)) + } + /> + {screenshotQuality ?? 75}% +
+
+

+ Adjust the WebP quality of browser screenshots. Higher values provide clearer + screenshots but increase token usage. +

+
+
+ )} +
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/CheckpointSettings.tsx b/webview-ui/src/components/settings/CheckpointSettings.tsx new file mode 100644 index 0000000000..56d5377b18 --- /dev/null +++ b/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 & { + enableCheckpoints?: boolean + checkpointStorage?: CheckpointStorage + setCachedStateField: SetCachedStateField<"enableCheckpoints" | "checkpointStorage"> +} + +export const CheckpointSettings = ({ + enableCheckpoints, + checkpointStorage = "task", + setCachedStateField, + ...props +}: CheckpointSettingsProps) => { + return ( +
+ +
+ +
Checkpoints
+
+
+ +
+
+ { + setCachedStateField("enableCheckpoints", e.target.checked) + }}> + Enable automatic checkpoints + +

+ When enabled, Roo will automatically create checkpoints during task execution, making it easy to + review changes or revert to earlier states. +

+ {enableCheckpoints && ( +
+
Storage
+ { + if ("target" in e) { + const { value } = e.target as HTMLInputElement + + if (isCheckpointStorage(value)) { + setCachedStateField("checkpointStorage", value) + } + } + }}> + Task + Workspace + + {checkpointStorage === "task" && ( +

+ 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. +

+ )} + {checkpointStorage === "workspace" && ( +

+ 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. +

+ )} +
+ )} +
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/ExperimentalFeature.tsx b/webview-ui/src/components/settings/ExperimentalFeature.tsx index ee813dd17b..e06bfc513a 100644 --- a/webview-ui/src/components/settings/ExperimentalFeature.tsx +++ b/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 ( -
-
- ⚠️ - onChange(e.target.checked)}> - {name} - -
-

- {description} -

+export const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => ( +
+
+ ⚠️ + onChange(e.target.checked)}> + {name} +
- ) -} - -export default ExperimentalFeature +

{description}

+
+) diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx new file mode 100644 index 0000000000..bbf473dbd5 --- /dev/null +++ b/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 & { + setCachedStateField: SetCachedStateField< + "rateLimitSeconds" | "terminalOutputLineLimit" | "maxOpenTabsContext" | "diffEnabled" | "fuzzyMatchThreshold" + > + experiments: Record + setExperimentEnabled: SetExperimentEnabled +} + +export const ExperimentalSettings = ({ + setCachedStateField, + experiments, + setExperimentEnabled, + className, + ...props +}: ExperimentalSettingsProps) => { + return ( +
+ +
+ +
Experimental Features
+
+
+ +
+ {Object.entries(experimentConfigsMap) + .filter((config) => config[0] !== "DIFF_STRATEGY") + .map((config) => ( + + setExperimentEnabled(EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], enabled) + } + /> + ))} +
+
+ ) +} diff --git a/webview-ui/src/components/settings/NotificationSettings.tsx b/webview-ui/src/components/settings/NotificationSettings.tsx new file mode 100644 index 0000000000..1fba9dd412 --- /dev/null +++ b/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 & { + soundEnabled?: boolean + soundVolume?: number + setCachedStateField: SetCachedStateField<"soundEnabled" | "soundVolume"> +} + +export const NotificationSettings = ({ + soundEnabled, + soundVolume, + setCachedStateField, + ...props +}: NotificationSettingsProps) => { + return ( +
+ +
+ +
Notifications
+
+
+ +
+
+ setCachedStateField("soundEnabled", e.target.checked)}> + Enable sound effects + +

+ When enabled, Roo will play sound effects for notifications and events. +

+ {soundEnabled && ( +
+
+ setCachedStateField("soundVolume", parseFloat(e.target.value))} + className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" + aria-label="Volume" + /> + + {((soundVolume ?? 0.5) * 100).toFixed(0)}% + +
+

Volume

+
+ )} +
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/Section.tsx b/webview-ui/src/components/settings/Section.tsx new file mode 100644 index 0000000000..9c05daf298 --- /dev/null +++ b/webview-ui/src/components/settings/Section.tsx @@ -0,0 +1,9 @@ +import { HTMLAttributes } from "react" + +import { cn } from "@/lib/utils" + +type SectionProps = HTMLAttributes + +export const Section = ({ className, ...props }: SectionProps) => ( +
+) diff --git a/webview-ui/src/components/settings/SectionHeader.tsx b/webview-ui/src/components/settings/SectionHeader.tsx new file mode 100644 index 0000000000..1c7bc8553f --- /dev/null +++ b/webview-ui/src/components/settings/SectionHeader.tsx @@ -0,0 +1,15 @@ +import { HTMLAttributes } from "react" + +import { cn } from "@/lib/utils" + +type SectionHeaderProps = HTMLAttributes & { + children: React.ReactNode + description?: string +} + +export const SectionHeader = ({ description, children, className, ...props }: SectionHeaderProps) => ( +
+

{children}

+ {description &&

{description}

} +
+) diff --git a/webview-ui/src/components/settings/SettingsFooter.tsx b/webview-ui/src/components/settings/SettingsFooter.tsx new file mode 100644 index 0000000000..f7419c9973 --- /dev/null +++ b/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 & { + version: string +} + +export const SettingsFooter = ({ version, className, ...props }: SettingsFooterProps) => ( +
+

+ If you have any questions or feedback, feel free to open an issue at{" "} + + github.com/RooVetGit/Roo-Code + {" "} + or join{" "} + + reddit.com/r/RooCode + +

+

Roo Code v{version}

+
+

Reset all global state and secret storage in the extension.

+ vscode.postMessage({ type: "resetState" })} + appearance="secondary" + className="shrink-0"> + + Reset + +
+
+) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 612be3aab3..f03464f0fc 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/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(({ 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(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(({ onDone }, browserToolEnabled, browserViewportSize, enableCheckpoints, + checkpointStorage, diffEnabled, experiments, fuzzyMatchThreshold, @@ -68,7 +80,7 @@ const SettingsView = forwardRef(({ 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(({ onDone }, setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) prevApiConfigName.current = currentApiConfigName - // console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false") setChangeDetected(false) }, [currentApiConfigName, extensionState, isChangeDetected]) - const setCachedStateField = useCallback( - (field: K, value: ExtensionStateContextType[K]) => { - setCachedState((prevState) => { - if (prevState[field] === value) { - return prevState - } + const setCachedStateField: SetCachedStateField = 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( (field: K, value: ApiConfiguration[K]) => { @@ -106,7 +113,6 @@ const SettingsView = forwardRef(({ 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(({ 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(({ 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(({ 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,40 +190,216 @@ const SettingsView = forwardRef(({ onDone }, } }, []) - const handleResetState = () => { - vscode.postMessage({ type: "resetState" }) - } + const providersRef = useRef(null) + const autoApproveRef = useRef(null) + const browserRef = useRef(null) + const checkpointRef = useRef(null) + const notificationsRef = useRef(null) + const advancedRef = useRef(null) + const experimentalRef = useRef(null) - 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 [activeSection, setActiveSection] = useState("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 handleScroll = useCallback((e: React.UIEvent) => { + 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) => ref.current?.scrollIntoView() return ( -
+
+
+
+
+
+

Settings

+
+ {sections.map(({ id, icon: Icon, ref }) => ( + + ))} +
+
+
+ + Save + + checkUnsaveChanges(onDone)}> + Done + +
+
+
+
+ +
+
+ +
+ +
Providers
+
+
+ +
+ + 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", + values: { oldName, newName }, + apiConfiguration, + }) + prevApiConfigName.current = newName + }} + onUpsertConfig={(configName: string) => + vscode.postMessage({ + type: "upsertApiConfiguration", + text: configName, + apiConfiguration, + }) + } + /> + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ @@ -234,656 +415,6 @@ const SettingsView = forwardRef(({ onDone }, -
-

Settings

-
- - checkUnsaveChanges(onDone)}> - Done - -
-
-
-
-

Provider Settings

-
- { - 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", - values: { oldName, newName }, - apiConfiguration, - }) - prevApiConfigName.current = newName - }} - onUpsertConfig={(configName: string) => { - vscode.postMessage({ - type: "upsertApiConfiguration", - text: configName, - apiConfiguration, - }) - }} - /> - -
-
- -
-

Auto-Approve Settings

-

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

- -
- setCachedStateField("alwaysAllowReadOnly", e.target.checked)}> - Always approve read-only operations - -

- When enabled, Roo will automatically view directory contents and read files without - requiring you to click the Approve button. -

-
- -
- setCachedStateField("alwaysAllowWrite", e.target.checked)}> - Always approve write operations - -

- Automatically create and edit files without requiring approval -

- {alwaysAllowWrite && ( -
-
- setCachedStateField("writeDelayMs", parseInt(e.target.value))} - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - /> - {writeDelayMs}ms -
-

- Delay after writes to allow diagnostics to detect potential problems -

-
- )} -
- -
- setCachedStateField("alwaysAllowBrowser", e.target.checked)}> - Always approve browser actions - -

- Automatically perform browser actions without requiring approval -
- Note: Only applies when the model supports computer use -

-
- -
- setCachedStateField("alwaysApproveResubmit", e.target.checked)}> - Always retry failed API requests - -

- Automatically retry failed API requests when server returns an error response -

- {alwaysApproveResubmit && ( -
-
- - setCachedStateField("requestDelaySeconds", parseInt(e.target.value)) - } - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - /> - {requestDelaySeconds}s -
-

- Delay before retrying the request -

-
- )} -
- -
- setCachedStateField("alwaysAllowMcp", e.target.checked)}> - Always approve MCP tools - -

- Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this - setting and the tool's individual "Always allow" checkbox) -

-
- -
- setCachedStateField("alwaysAllowModeSwitch", e.target.checked)}> - Always approve mode switching & task creation - -

- Automatically switch between different AI modes and create new tasks without requiring - approval -

-
- -
- setCachedStateField("alwaysAllowExecute", e.target.checked)}> - Always approve allowed execute operations - -

- Automatically execute allowed terminal commands without requiring approval -

- - {alwaysAllowExecute && ( -
- Allowed Auto-Execute Commands -

- Command prefixes that can be auto-executed when "Always approve execute operations" - is enabled. Add * to allow all commands (use with caution). -

- -
- setCommandInput(e.target.value)} - onKeyDown={(e: any) => { - if (e.key === "Enter") { - e.preventDefault() - handleAddCommand() - } - }} - placeholder="Enter command prefix (e.g., 'git ')" - style={{ flexGrow: 1 }} - /> - Add -
- -
- {(allowedCommands ?? []).map((cmd, index) => ( -
- {cmd} - { - const newCommands = (allowedCommands ?? []).filter( - (_, i) => i !== index, - ) - setCachedStateField("allowedCommands", newCommands) - vscode.postMessage({ - type: "allowedCommands", - commands: newCommands, - }) - }}> - - -
- ))} -
-
- )} -
-
- -
-

Browser Settings

-
- setCachedStateField("browserToolEnabled", e.target.checked)}> - Enable browser tool - -

- When enabled, Roo can use a browser to interact with websites when using models that support - computer use. -

-
- {browserToolEnabled && ( -
-
- -
- { - 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)" }, - ]} - /> -
-

- Select the viewport size for browser interactions. This affects how websites are - displayed and interacted with. -

-
- -
-
- Screenshot quality -
- - setCachedStateField("screenshotQuality", parseInt(e.target.value)) - } - /> - {screenshotQuality ?? 75}% -
-
-

- Adjust the WebP quality of browser screenshots. Higher values provide clearer - screenshots but increase token usage. -

-
-
- )} -
- -
-

Notification Settings

-
- setCachedStateField("soundEnabled", e.target.checked)}> - Enable sound effects - -

- When enabled, Roo will play sound effects for notifications and events. -

-
- {soundEnabled && ( -
-
- Volume - setCachedStateField("soundVolume", parseFloat(e.target.value))} - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - aria-label="Volume" - /> - - {((soundVolume ?? 0.5) * 100).toFixed(0)}% - -
-
- )} -
- -
-

Advanced Settings

-
-
- Rate limit -
- setCachedStateField("rateLimitSeconds", parseInt(e.target.value))} - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - /> - {rateLimitSeconds}s -
-
-

- Minimum time between API requests. -

-
-
-
- Terminal output limit -
- - setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value)) - } - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - /> - {terminalOutputLineLimit ?? 500} -
-
-

- Maximum number of lines to include in terminal output when executing commands. When exceeded - lines will be removed from the middle, saving tokens. -

-
- -
-
- Open tabs context limit -
- - setCachedStateField("maxOpenTabsContext", parseInt(e.target.value)) - } - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - /> - {maxOpenTabsContext ?? 20} -
-
-

- Maximum number of VSCode open tabs to include in context. Higher values provide more context - but increase token usage. -

-
- -
- { - setCachedStateField("enableCheckpoints", e.target.checked) - }}> - Enable automatic checkpoints - -

- When enabled, Roo will automatically create checkpoints during task execution, making it - easy to review changes or revert to earlier states. -

-
- -
- { - setCachedStateField("diffEnabled", e.target.checked) - if (!e.target.checked) { - // Reset experimental strategy when diffs are disabled - setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false) - } - }}> - Enable editing through diffs - -

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

- - {diffEnabled && ( -
-
- Match precision -
- { - setCachedStateField("fuzzyMatchThreshold", parseFloat(e.target.value)) - }} - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - /> - - {Math.round((fuzzyMatchThreshold || 1) * 100)}% - -
-

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

- - setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled) - } - /> -
-
- )} - - {Object.entries(experimentConfigsMap) - .filter((config) => config[0] !== "DIFF_STRATEGY") - .map((config) => ( - - setExperimentEnabled( - EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], - enabled, - ) - } - /> - ))} -
-
- -
-

- If you have any questions or feedback, feel free to open an issue at{" "} - - github.com/RooVetGit/Roo-Code - {" "} - or join{" "} - - reddit.com/r/RooCode - -

-

- v{extensionState.version} -

- -

- This will reset all global state and secret storage in the extension. -

- - - Reset State - -
-
) }) diff --git a/webview-ui/src/components/settings/TemperatureControl.tsx b/webview-ui/src/components/settings/TemperatureControl.tsx index cbafcc5520..dd8ccc8e58 100644 --- a/webview-ui/src/components/settings/TemperatureControl.tsx +++ b/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 } }}> - Use custom temperature + Use custom temperature -

+

Controls randomness in the model's responses.

@@ -59,7 +59,7 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur /> {inputValue}
-

+

Higher values make output more random, lower values make it more deterministic.

diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index 99bc0a87c2..5e5defec59 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/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 () =>
{String(prop)}
+ }, + }, + ) +}) + // Mock ApiConfigManager component jest.mock("../ApiConfigManager", () => ({ __esModule: true, diff --git a/webview-ui/src/components/settings/styles.ts b/webview-ui/src/components/settings/styles.ts index 85b50579fb..75885c60f4 100644 --- a/webview-ui/src/components/settings/styles.ts +++ b/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", +} diff --git a/webview-ui/src/components/settings/types.ts b/webview-ui/src/components/settings/types.ts new file mode 100644 index 0000000000..234b382be5 --- /dev/null +++ b/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 = ( + field: K, + value: ExtensionStateContextType[K], +) => void + +export type SetExperimentEnabled = (id: ExperimentId, enabled: boolean) => void diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ba735f27b2..bee73c1a36 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/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, diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx index aca1f26bcf..1e7119130f 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx +++ b/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, diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index fd058872a6..5276384b11 100644 --- a/webview-ui/src/index.css +++ b/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 {