Merge pull request #1379 from RooVetGit/cte/checkpoints-settings

This commit is contained in:
Chris Estreich 2025-03-04 18:51:42 -08:00 committed by GitHub
commit 2e41376f0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1467 additions and 1164 deletions

View file

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

View file

@ -9,6 +9,7 @@ import * as vscode from "vscode"
import simpleGit from "simple-git"
import { ApiConfiguration, ApiProvider, ModelInfo } 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"
@ -313,11 +314,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,
@ -331,8 +334,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
provider: this,
apiConfiguration,
customInstructions: effectiveInstructions,
enableDiff: diffEnabled,
enableDiff,
enableCheckpoints,
checkpointStorage,
fuzzyMatchThreshold,
task,
images,
@ -346,8 +350,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const {
apiConfiguration,
customModePrompts,
diffEnabled,
diffEnabled: enableDiff,
enableCheckpoints,
checkpointStorage,
fuzzyMatchThreshold,
mode,
customInstructions: globalInstructions,
@ -357,12 +362,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,
@ -1022,6 +1032,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)
@ -1947,21 +1963,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)) {
@ -2008,6 +2011,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundEnabled,
diffEnabled,
enableCheckpoints,
checkpointStorage,
taskHistory,
soundVolume,
browserViewportSize,
@ -2058,6 +2062,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,
@ -2191,6 +2196,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundEnabled,
diffEnabled,
enableCheckpoints,
checkpointStorage,
soundVolume,
browserViewportSize,
fuzzyMatchThreshold,
@ -2278,6 +2284,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
this.getGlobalState("enableCheckpoints") as Promise<boolean | undefined>,
this.getGlobalState("checkpointStorage") as Promise<CheckpointStorage | undefined>,
this.getGlobalState("soundVolume") as Promise<number | undefined>,
this.getGlobalState("browserViewportSize") as Promise<string | undefined>,
this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
@ -2395,6 +2402,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundEnabled: soundEnabled ?? false,
diffEnabled: diffEnabled ?? true,
enableCheckpoints: enableCheckpoints ?? true,
checkpointStorage: checkpointStorage ?? "task",
soundVolume,
browserViewportSize: browserViewportSize ?? "900x600",
screenshotQuality: screenshotQuality ?? 75,

View file

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

View file

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

View file

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

View file

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

View file

@ -55,6 +55,7 @@ export type GlobalStateKey =
| "soundVolume"
| "diffEnabled"
| "enableCheckpoints"
| "checkpointStorage"
| "browserViewportSize"
| "screenshotQuality"
| "fuzzyMatchThreshold"

View file

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

View file

@ -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>
<div className="px-5">
{taskHistory
.filter((item) => item.ts && item.task)
.slice(0, 3)
.map((item) => (
<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">
<span>
Tokens: {formatLargeNumber(item.tokensIn || 0)}
{formatLargeNumber(item.tokensOut || 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>
</div>
))}
<div className="flex justify-center">
<Button
variant="ghost"
size="sm"
onClick={() => showHistoryView()}
className="font-normal text-vscode-descriptionForeground">
View all history
</Button>
<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>
{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
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>
Cache: +{formatLargeNumber(item.cacheWrites || 0)} {" "}
{formatLargeNumber(item.cacheReads || 0)}
</span>
</>
)}
{!!item.totalCost && (
<>
{" • "}
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
</>
)}
</div>
</div>
</div>
))}
</div>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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