mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge pull request #1379 from RooVetGit/cte/checkpoints-settings
This commit is contained in:
commit
2e41376f0c
33 changed files with 1467 additions and 1164 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export interface WebviewMessage {
|
|||
| "soundVolume"
|
||||
| "diffEnabled"
|
||||
| "enableCheckpoints"
|
||||
| "checkpointStorage"
|
||||
| "browserViewportSize"
|
||||
| "screenshotQuality"
|
||||
| "openMcpSettings"
|
||||
|
|
|
|||
5
src/shared/checkpoints.ts
Normal file
5
src/shared/checkpoints.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export type CheckpointStorage = "task" | "workspace"
|
||||
|
||||
export const isCheckpointStorage = (value: string): value is CheckpointStorage => {
|
||||
return value === "task" || value === "workspace"
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@ export type GlobalStateKey =
|
|||
| "soundVolume"
|
||||
| "diffEnabled"
|
||||
| "enableCheckpoints"
|
||||
| "checkpointStorage"
|
||||
| "browserViewportSize"
|
||||
| "screenshotQuality"
|
||||
| "fuzzyMatchThreshold"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import React, { memo, useMemo, useState, useEffect } from "react"
|
||||
import React, { memo, useState } from "react"
|
||||
import { DeleteTaskDialog } from "./DeleteTaskDialog"
|
||||
import { Fzf } from "fzf"
|
||||
import prettyBytes from "pretty-bytes"
|
||||
import { Virtuoso } from "react-virtuoso"
|
||||
import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { formatLargeNumber, formatDate } from "@/utils/format"
|
||||
import { highlightFzfMatch } from "@/utils/highlight"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui"
|
||||
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { useTaskSearch } from "./useTaskSearch"
|
||||
import { ExportButton } from "./ExportButton"
|
||||
import { CopyButton } from "./CopyButton"
|
||||
|
||||
|
|
@ -21,95 +20,18 @@ type HistoryViewProps = {
|
|||
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
|
||||
|
||||
const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
const { taskHistory } = useExtensionState()
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [sortOption, setSortOption] = useState<SortOption>("newest")
|
||||
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
|
||||
setLastNonRelevantSort(sortOption)
|
||||
setSortOption("mostRelevant")
|
||||
} else if (!searchQuery && sortOption === "mostRelevant" && lastNonRelevantSort) {
|
||||
setSortOption(lastNonRelevantSort)
|
||||
setLastNonRelevantSort(null)
|
||||
}
|
||||
}, [searchQuery, sortOption, lastNonRelevantSort])
|
||||
|
||||
const handleHistorySelect = (id: string) => {
|
||||
vscode.postMessage({ type: "showTaskWithId", text: id })
|
||||
}
|
||||
const { tasks, searchQuery, setSearchQuery, sortOption, setSortOption, setLastNonRelevantSort } = useTaskSearch()
|
||||
|
||||
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
|
||||
|
||||
const presentableTasks = useMemo(() => {
|
||||
return taskHistory.filter((item) => item.ts && item.task)
|
||||
}, [taskHistory])
|
||||
|
||||
const fzf = useMemo(() => {
|
||||
return new Fzf(presentableTasks, {
|
||||
selector: (item) => item.task,
|
||||
})
|
||||
}, [presentableTasks])
|
||||
|
||||
const taskHistorySearchResults = useMemo(() => {
|
||||
let results = presentableTasks
|
||||
if (searchQuery) {
|
||||
const searchResults = fzf.find(searchQuery)
|
||||
results = searchResults.map((result) => ({
|
||||
...result.item,
|
||||
task: highlightFzfMatch(result.item.task, Array.from(result.positions)),
|
||||
}))
|
||||
}
|
||||
|
||||
// First apply search if needed
|
||||
const searchResults = searchQuery ? results : presentableTasks
|
||||
|
||||
// Then sort the results
|
||||
return [...searchResults].sort((a, b) => {
|
||||
switch (sortOption) {
|
||||
case "oldest":
|
||||
return (a.ts || 0) - (b.ts || 0)
|
||||
case "mostExpensive":
|
||||
return (b.totalCost || 0) - (a.totalCost || 0)
|
||||
case "mostTokens":
|
||||
const aTokens = (a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0)
|
||||
const bTokens = (b.tokensIn || 0) + (b.tokensOut || 0) + (b.cacheWrites || 0) + (b.cacheReads || 0)
|
||||
return bTokens - aTokens
|
||||
case "mostRelevant":
|
||||
// Keep fuse order if searching, otherwise sort by newest
|
||||
return searchQuery ? 0 : (b.ts || 0) - (a.ts || 0)
|
||||
case "newest":
|
||||
default:
|
||||
return (b.ts || 0) - (a.ts || 0)
|
||||
}
|
||||
})
|
||||
}, [presentableTasks, searchQuery, fzf, sortOption])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "10px 17px 10px 20px",
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>History</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
<div style={{ padding: "5px 17px 6px 17px" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||
<div className="fixed inset-0 flex flex-col">
|
||||
<div className="flex flex-col gap-2 px-5 py-2.5 border-b border-vscode-panel-border">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-vscode-foreground m-0">History</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<VSCodeTextField
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Fuzzy search history..."
|
||||
|
|
@ -166,7 +88,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
flexGrow: 1,
|
||||
overflowY: "scroll",
|
||||
}}
|
||||
data={taskHistorySearchResults}
|
||||
data={tasks}
|
||||
data-testid="virtuoso-container"
|
||||
components={{
|
||||
List: React.forwardRef((props, ref) => (
|
||||
|
|
@ -175,15 +97,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
}}
|
||||
itemContent={(index, item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
data-testid={`task-item-${item.id}`}
|
||||
className="history-item"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
borderBottom:
|
||||
index < taskHistory.length - 1 ? "1px solid var(--vscode-panel-border)" : "none",
|
||||
}}
|
||||
onClick={() => handleHistorySelect(item.id)}>
|
||||
key={item.id}
|
||||
className={cn("cursor-pointer", {
|
||||
"border-b border-vscode-panel-border": index < tasks.length - 1,
|
||||
})}
|
||||
onClick={() => vscode.postMessage({ type: "showTaskWithId", text: item.id })}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
|
|
|||
78
webview-ui/src/components/history/useTaskSearch.ts
Normal file
78
webview-ui/src/components/history/useTaskSearch.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -407,12 +407,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col">
|
||||
<div className="flex justify-between items-center px-5 py-2.5">
|
||||
<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
|
||||
<h3 className="text-vscode-foreground m-0">Prompts</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto px-5">
|
||||
<div className="flex-1 overflow-auto p-5">
|
||||
<div className="pb-5 border-b border-vscode-input-border">
|
||||
<div className="mb-5">
|
||||
<div className="font-bold mb-1">Preferred Language</div>
|
||||
|
|
@ -1174,7 +1173,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCreateModeDialogOpen && (
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
176
webview-ui/src/components/settings/AdvancedSettings.tsx
Normal file
176
webview-ui/src/components/settings/AdvancedSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
|||
import { vscode } from "../../utils/vscode"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import { ModelInfoView } from "./ModelInfoView"
|
||||
import { DROPDOWN_Z_INDEX } from "./styles"
|
||||
import { ModelPicker } from "./ModelPicker"
|
||||
import { TemperatureControl } from "./TemperatureControl"
|
||||
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
|
||||
|
|
@ -242,7 +241,6 @@ const ApiOptions = ({
|
|||
id="api-provider"
|
||||
value={selectedProvider}
|
||||
onChange={handleInputChange("apiProvider", dropdownEventTransform)}
|
||||
style={{ minWidth: 130, position: "relative", zIndex: DROPDOWN_Z_INDEX + 1 }}
|
||||
options={[
|
||||
{ value: "openrouter", label: "OpenRouter" },
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
|
|
|
|||
252
webview-ui/src/components/settings/AutoApproveSettings.tsx
Normal file
252
webview-ui/src/components/settings/AutoApproveSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
105
webview-ui/src/components/settings/BrowserSettings.tsx
Normal file
105
webview-ui/src/components/settings/BrowserSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
82
webview-ui/src/components/settings/CheckpointSettings.tsx
Normal file
82
webview-ui/src/components/settings/CheckpointSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -7,25 +7,14 @@ interface ExperimentalFeatureProps {
|
|||
onChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
|
||||
<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>{name}</span>
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginBottom: 15,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
{description}
|
||||
</p>
|
||||
export const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-vscode-errorForeground">⚠️</span>
|
||||
<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
|
||||
<span className="font-medium">{name}</span>
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExperimentalFeature
|
||||
<p className="text-vscode-descriptionForeground text-sm mt-0">{description}</p>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
53
webview-ui/src/components/settings/ExperimentalSettings.tsx
Normal file
53
webview-ui/src/components/settings/ExperimentalSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
69
webview-ui/src/components/settings/NotificationSettings.tsx
Normal file
69
webview-ui/src/components/settings/NotificationSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
9
webview-ui/src/components/settings/Section.tsx
Normal file
9
webview-ui/src/components/settings/Section.tsx
Normal 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} />
|
||||
)
|
||||
15
webview-ui/src/components/settings/SectionHeader.tsx
Normal file
15
webview-ui/src/components/settings/SectionHeader.tsx
Normal 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>
|
||||
)
|
||||
36
webview-ui/src/components/settings/SettingsFooter.tsx
Normal file
36
webview-ui/src/components/settings/SettingsFooter.tsx
Normal 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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import styled from "styled-components"
|
||||
|
||||
export const DROPDOWN_Z_INDEX = 1_000
|
||||
|
||||
export const DropdownWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
@ -78,3 +76,10 @@ export const StyledMarkdown = styled.div`
|
|||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const sliderLabelStyle = {
|
||||
minWidth: "45px",
|
||||
textAlign: "right" as const,
|
||||
lineHeight: "20px",
|
||||
paddingBottom: "2px",
|
||||
}
|
||||
|
|
|
|||
10
webview-ui/src/components/settings/types.ts
Normal file
10
webview-ui/src/components/settings/types.ts
Normal 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ describe("mergeExtensionState", () => {
|
|||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
enableCheckpoints: true,
|
||||
checkpointStorage: "task",
|
||||
preferredLanguage: "English",
|
||||
writeDelayMs: 1000,
|
||||
requestDelaySeconds: 5,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue