Add logic to prevent auto-approving edits of configuration files (#4667)

* Add logic to prevent auto-approving edits of configuration files

* Fix tests

* Update patterns
This commit is contained in:
Matt Rubens 2025-06-13 19:58:55 -04:00 committed by GitHub
parent 10b2fb32ed
commit 7d0b22f9e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 488 additions and 32 deletions

View file

@ -36,6 +36,7 @@ export const globalSettingsSchema = z.object({
alwaysAllowReadOnlyOutsideWorkspace: z.boolean().optional(),
alwaysAllowWrite: z.boolean().optional(),
alwaysAllowWriteOutsideWorkspace: z.boolean().optional(),
alwaysAllowWriteProtected: z.boolean().optional(),
writeDelayMs: z.number().optional(),
alwaysAllowBrowser: z.boolean().optional(),
alwaysApproveResubmit: z.boolean().optional(),
@ -177,6 +178,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
alwaysAllowReadOnlyOutsideWorkspace: false,
alwaysAllowWrite: true,
alwaysAllowWriteOutsideWorkspace: false,
alwaysAllowWriteProtected: false,
writeDelayMs: 1000,
alwaysAllowBrowser: true,
alwaysApproveResubmit: true,

View file

@ -153,6 +153,7 @@ export const clineMessageSchema = z.object({
checkpoint: z.record(z.string(), z.unknown()).optional(),
progressStatus: toolProgressStatusSchema.optional(),
contextCondense: contextCondenseSchema.optional(),
isProtected: z.boolean().optional(),
})
export type ClineMessage = z.infer<typeof clineMessageSchema>

View file

@ -261,8 +261,15 @@ export async function presentAssistantMessage(cline: Task) {
type: ClineAsk,
partialMessage?: string,
progressStatus?: ToolProgressStatus,
isProtected?: boolean,
) => {
const { response, text, images } = await cline.ask(type, partialMessage, false, progressStatus)
const { response, text, images } = await cline.ask(
type,
partialMessage,
false,
progressStatus,
isProtected || false,
)
if (response !== "yesButtonClicked") {
// Handle both messageResponse and noButtonClicked with text.

View file

@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as path from "path"
import * as diff from "diff"
import { RooIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/RooIgnoreController"
import { RooProtectedController } from "../protect/RooProtectedController"
export const formatResponse = {
toolDenied: () => `The user denied this operation.`,
@ -95,6 +96,7 @@ Otherwise, if you have not completed the task and do not need additional informa
didHitLimit: boolean,
rooIgnoreController: RooIgnoreController | undefined,
showRooIgnoredFiles: boolean,
rooProtectedController?: RooProtectedController,
): string => {
const sorted = files
.map((file) => {
@ -143,7 +145,13 @@ Otherwise, if you have not completed the task and do not need additional informa
// Otherwise, mark it with a lock symbol
rooIgnoreParsed.push(LOCK_TEXT_SYMBOL + " " + filePath)
} else {
rooIgnoreParsed.push(filePath)
// Check if file is write-protected (only for non-ignored files)
const isWriteProtected = rooProtectedController?.isWriteProtected(absoluteFilePath) || false
if (isWriteProtected) {
rooIgnoreParsed.push("🛡️ " + filePath)
} else {
rooIgnoreParsed.push(filePath)
}
}
}
}

View file

@ -0,0 +1,103 @@
import path from "path"
import ignore, { Ignore } from "ignore"
export const SHIELD_SYMBOL = "\u{1F6E1}"
/**
* Controls write access to Roo configuration files by enforcing protection patterns.
* Prevents auto-approved modifications to sensitive Roo configuration files.
*/
export class RooProtectedController {
private cwd: string
private ignoreInstance: Ignore
// Predefined list of protected Roo configuration patterns
private static readonly PROTECTED_PATTERNS = [
".rooignore",
".roomodes",
".roorules*",
".clinerules*",
".roo/**",
".rooprotected", // For future use
]
constructor(cwd: string) {
this.cwd = cwd
// Initialize ignore instance with protected patterns
this.ignoreInstance = ignore()
this.ignoreInstance.add(RooProtectedController.PROTECTED_PATTERNS)
}
/**
* Check if a file is write-protected
* @param filePath - Path to check (relative to cwd)
* @returns true if file is write-protected, false otherwise
*/
isWriteProtected(filePath: string): boolean {
try {
// Normalize path to be relative to cwd and use forward slashes
const absolutePath = path.resolve(this.cwd, filePath)
const relativePath = path.relative(this.cwd, absolutePath).toPosix()
// Use ignore library to check if file matches any protected pattern
return this.ignoreInstance.ignores(relativePath)
} catch (error) {
// If there's an error processing the path, err on the side of caution
// Ignore is designed to work with relative file paths, so will throw error for paths outside cwd
console.error(`Error checking protection for ${filePath}:`, error)
return false
}
}
/**
* Get set of write-protected files from a list
* @param paths - Array of paths to filter (relative to cwd)
* @returns Set of protected file paths
*/
getProtectedFiles(paths: string[]): Set<string> {
const protectedFiles = new Set<string>()
for (const filePath of paths) {
if (this.isWriteProtected(filePath)) {
protectedFiles.add(filePath)
}
}
return protectedFiles
}
/**
* Filter an array of paths, marking which ones are protected
* @param paths - Array of paths to check (relative to cwd)
* @returns Array of objects with path and protection status
*/
annotatePathsWithProtection(paths: string[]): Array<{ path: string; isProtected: boolean }> {
return paths.map((filePath) => ({
path: filePath,
isProtected: this.isWriteProtected(filePath),
}))
}
/**
* Get display message for protected file operations
*/
getProtectionMessage(): string {
return "This is a Roo configuration file and requires approval for modifications"
}
/**
* Get formatted instructions about protected files for the LLM
* @returns Formatted instructions about file protection
*/
getInstructions(): string {
const patterns = RooProtectedController.PROTECTED_PATTERNS.join(", ")
return `# Protected Files\n\n(The following Roo configuration file patterns are write-protected and always require approval for modifications, regardless of autoapproval settings. When using list_files, you'll notice a ${SHIELD_SYMBOL} next to files that are write-protected.)\n\nProtected patterns: ${patterns}`
}
/**
* Get the list of protected patterns (for testing/debugging)
*/
static getProtectedPatterns(): readonly string[] {
return RooProtectedController.PROTECTED_PATTERNS
}
}

View file

@ -0,0 +1,141 @@
import path from "path"
import { RooProtectedController } from "../RooProtectedController"
describe("RooProtectedController", () => {
const TEST_CWD = "/test/workspace"
let controller: RooProtectedController
beforeEach(() => {
controller = new RooProtectedController(TEST_CWD)
})
describe("isWriteProtected", () => {
it("should protect .rooignore file", () => {
expect(controller.isWriteProtected(".rooignore")).toBe(true)
})
it("should protect files in .roo directory", () => {
expect(controller.isWriteProtected(".roo/config.json")).toBe(true)
expect(controller.isWriteProtected(".roo/settings/user.json")).toBe(true)
expect(controller.isWriteProtected(".roo/modes/custom.json")).toBe(true)
})
it("should protect .rooprotected file", () => {
expect(controller.isWriteProtected(".rooprotected")).toBe(true)
})
it("should protect .roomodes files", () => {
expect(controller.isWriteProtected(".roomodes")).toBe(true)
})
it("should protect .roorules* files", () => {
expect(controller.isWriteProtected(".roorules")).toBe(true)
expect(controller.isWriteProtected(".roorules.md")).toBe(true)
})
it("should protect .clinerules* files", () => {
expect(controller.isWriteProtected(".clinerules")).toBe(true)
expect(controller.isWriteProtected(".clinerules.md")).toBe(true)
})
it("should not protect other files starting with .roo", () => {
expect(controller.isWriteProtected(".roosettings")).toBe(false)
expect(controller.isWriteProtected(".rooconfig")).toBe(false)
})
it("should not protect regular files", () => {
expect(controller.isWriteProtected("src/index.ts")).toBe(false)
expect(controller.isWriteProtected("package.json")).toBe(false)
expect(controller.isWriteProtected("README.md")).toBe(false)
})
it("should not protect files that contain 'roo' but don't start with .roo", () => {
expect(controller.isWriteProtected("src/roo-utils.ts")).toBe(false)
expect(controller.isWriteProtected("config/roo.config.js")).toBe(false)
})
it("should handle nested paths correctly", () => {
expect(controller.isWriteProtected(".roo/config.json")).toBe(true) // .roo/** matches at root
expect(controller.isWriteProtected("nested/.rooignore")).toBe(true) // .rooignore matches anywhere by default
expect(controller.isWriteProtected("nested/.roomodes")).toBe(true) // .roomodes matches anywhere by default
expect(controller.isWriteProtected("nested/.roorules.md")).toBe(true) // .roorules* matches anywhere by default
})
it("should handle absolute paths by converting to relative", () => {
const absolutePath = path.join(TEST_CWD, ".rooignore")
expect(controller.isWriteProtected(absolutePath)).toBe(true)
})
it("should handle paths with different separators", () => {
expect(controller.isWriteProtected(".roo\\config.json")).toBe(true)
expect(controller.isWriteProtected(".roo/config.json")).toBe(true)
})
})
describe("getProtectedFiles", () => {
it("should return set of protected files from a list", () => {
const files = ["src/index.ts", ".rooignore", "package.json", ".roo/config.json", "README.md"]
const protectedFiles = controller.getProtectedFiles(files)
expect(protectedFiles).toEqual(new Set([".rooignore", ".roo/config.json"]))
})
it("should return empty set when no files are protected", () => {
const files = ["src/index.ts", "package.json", "README.md"]
const protectedFiles = controller.getProtectedFiles(files)
expect(protectedFiles).toEqual(new Set())
})
})
describe("annotatePathsWithProtection", () => {
it("should annotate paths with protection status", () => {
const files = ["src/index.ts", ".rooignore", ".roo/config.json", "package.json"]
const annotated = controller.annotatePathsWithProtection(files)
expect(annotated).toEqual([
{ path: "src/index.ts", isProtected: false },
{ path: ".rooignore", isProtected: true },
{ path: ".roo/config.json", isProtected: true },
{ path: "package.json", isProtected: false },
])
})
})
describe("getProtectionMessage", () => {
it("should return appropriate protection message", () => {
const message = controller.getProtectionMessage()
expect(message).toBe("This is a Roo configuration file and requires approval for modifications")
})
})
describe("getInstructions", () => {
it("should return formatted instructions about protected files", () => {
const instructions = controller.getInstructions()
expect(instructions).toContain("# Protected Files")
expect(instructions).toContain("write-protected")
expect(instructions).toContain(".rooignore")
expect(instructions).toContain(".roo/**")
expect(instructions).toContain("\u{1F6E1}") // Shield symbol
})
})
describe("getProtectedPatterns", () => {
it("should return the list of protected patterns", () => {
const patterns = RooProtectedController.getProtectedPatterns()
expect(patterns).toEqual([
".rooignore",
".roomodes",
".roorules*",
".clinerules*",
".roo/**",
".rooprotected",
])
})
})
})

View file

@ -65,6 +65,7 @@ import { SYSTEM_PROMPT } from "../prompts/system"
import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
import { RooIgnoreController } from "../ignore/RooIgnoreController"
import { RooProtectedController } from "../protect/RooProtectedController"
import { type AssistantMessageContent, parseAssistantMessage, presentAssistantMessage } from "../assistant-message"
import { truncateConversationIfNeeded } from "../sliding-window"
import { ClineProvider } from "../webview/ClineProvider"
@ -144,6 +145,7 @@ export class Task extends EventEmitter<ClineEvents> {
toolRepetitionDetector: ToolRepetitionDetector
rooIgnoreController?: RooIgnoreController
rooProtectedController?: RooProtectedController
fileContextTracker: FileContextTracker
urlContentFetcher: UrlContentFetcher
terminalProcess?: RooTerminalProcess
@ -223,6 +225,7 @@ export class Task extends EventEmitter<ClineEvents> {
this.taskNumber = -1
this.rooIgnoreController = new RooIgnoreController(this.cwd)
this.rooProtectedController = new RooProtectedController(this.cwd)
this.fileContextTracker = new FileContextTracker(provider, this.taskId)
this.rooIgnoreController.initialize().catch((error) => {
@ -406,6 +409,7 @@ export class Task extends EventEmitter<ClineEvents> {
text?: string,
partial?: boolean,
progressStatus?: ToolProgressStatus,
isProtected?: boolean,
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
// If this Cline instance was aborted by the provider, then the only
// thing keeping us alive is a promise still running in the background,
@ -433,6 +437,7 @@ export class Task extends EventEmitter<ClineEvents> {
lastMessage.text = text
lastMessage.partial = partial
lastMessage.progressStatus = progressStatus
lastMessage.isProtected = isProtected
// TODO: Be more efficient about saving and posting only new
// data or one whole message at a time so ignore partial for
// saves, and only post parts of partial message instead of
@ -444,7 +449,7 @@ export class Task extends EventEmitter<ClineEvents> {
// state.
askTs = Date.now()
this.lastMessageTs = askTs
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial })
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial, isProtected })
throw new Error("Current ask promise was ignored (#2)")
}
} else {
@ -471,6 +476,7 @@ export class Task extends EventEmitter<ClineEvents> {
lastMessage.text = text
lastMessage.partial = false
lastMessage.progressStatus = progressStatus
lastMessage.isProtected = isProtected
await this.saveClineMessages()
this.updateClineMessage(lastMessage)
} else {
@ -480,7 +486,7 @@ export class Task extends EventEmitter<ClineEvents> {
this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected })
}
}
} else {
@ -490,7 +496,7 @@ export class Task extends EventEmitter<ClineEvents> {
this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected })
}
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })

View file

@ -66,6 +66,9 @@ export async function insertContentTool(
return
}
// Check if file is write-protected
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
const absolutePath = path.resolve(cline.cwd, relPath)
const fileExists = await fileExistsAtPath(absolutePath)
@ -124,10 +127,11 @@ export async function insertContentTool(
...sharedMessageProps,
diff,
lineNumber: lineNumber,
isProtected: isWriteProtected,
} satisfies ClineSayTool)
const didApprove = await cline
.ask("tool", completeMessage, false)
.ask("tool", completeMessage, isWriteProtected)
.then((response) => response.response === "yesButtonClicked")
if (!didApprove) {

View file

@ -64,6 +64,7 @@ export async function listFilesTool(
didHitLimit,
cline.rooIgnoreController,
showRooIgnoredFiles,
cline.rooProtectedController,
)
const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result } satisfies ClineSayTool)

View file

@ -161,7 +161,6 @@ Expected structure:
Original error: ${errorMessage}`
throw new Error(detailedError)
}
} else if (legacyPath && typeof legacyDiffContent === "string") {
// Handle legacy parameters (old way)
usingLegacyParams = true
@ -236,6 +235,9 @@ Original error: ${errorMessage}`
continue
}
// Check if file is write-protected
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
// Verify file exists
const absolutePath = path.resolve(cline.cwd, relPath)
const fileExists = await fileExistsAtPath(absolutePath)
@ -258,6 +260,11 @@ Original error: ${errorMessage}`
// Handle batch approval if there are multiple files
if (operationsToApprove.length > 1) {
// Check if any files are write-protected
const hasProtectedFiles = operationsToApprove.some(
(opResult) => cline.rooProtectedController?.isWriteProtected(opResult.path) || false,
)
// Prepare batch diff data
const batchDiffs = operationsToApprove.map((opResult) => {
const readablePath = getReadablePath(cline.cwd, opResult.path)
@ -279,9 +286,10 @@ Original error: ${errorMessage}`
const completeMessage = JSON.stringify({
tool: "appliedDiff",
batchDiffs,
isProtected: hasProtectedFiles,
} satisfies ClineSayTool)
const { response, text, images } = await cline.ask("tool", completeMessage, false)
const { response, text, images } = await cline.ask("tool", completeMessage, hasProtectedFiles)
// Process batch response
if (response === "yesButtonClicked") {
@ -485,9 +493,11 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""}
await cline.diffViewProvider.scrollToFirstDiff()
// For batch operations, we've already gotten approval
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
const sharedMessageProps: ClineSayTool = {
tool: "appliedDiff",
path: getReadablePath(cline.cwd, relPath),
isProtected: isWriteProtected,
}
// If single file, ask for approval
@ -511,7 +521,9 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""}
)
}
didApprove = await askApproval("tool", operationMessage, toolProgressStatus)
// Check if file is write-protected
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
didApprove = await askApproval("tool", operationMessage, toolProgressStatus, isWriteProtected)
}
if (!didApprove) {

View file

@ -123,6 +123,9 @@ export async function searchAndReplaceTool(
return
}
// Check if file is write-protected
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(validRelPath) || false
const absolutePath = path.resolve(cline.cwd, validRelPath)
const fileExists = await fileExistsAtPath(absolutePath)
@ -207,9 +210,13 @@ export async function searchAndReplaceTool(
await cline.diffViewProvider.update(newContent, true)
// Request user approval for changes
const completeMessage = JSON.stringify({ ...sharedMessageProps, diff } satisfies ClineSayTool)
const completeMessage = JSON.stringify({
...sharedMessageProps,
diff,
isProtected: isWriteProtected,
} satisfies ClineSayTool)
const didApprove = await cline
.ask("tool", completeMessage, false)
.ask("tool", completeMessage, isWriteProtected)
.then((response) => response.response === "yesButtonClicked")
if (!didApprove) {

View file

@ -56,6 +56,9 @@ export async function writeToFileTool(
return
}
// Check if file is write-protected
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
// Check if file exists using cached map or fs.access
let fileExists: boolean
@ -90,6 +93,7 @@ export async function writeToFileTool(
path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
content: newContent,
isOutsideWorkspace,
isProtected: isWriteProtected,
}
try {
@ -201,7 +205,7 @@ export async function writeToFileTool(
: undefined,
} satisfies ClineSayTool)
const didApprove = await askApproval("tool", completeMessage)
const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected)
if (!didApprove) {
await cline.diffViewProvider.revertChanges()

View file

@ -1276,6 +1276,7 @@ export class ClineProvider
alwaysAllowReadOnlyOutsideWorkspace,
alwaysAllowWrite,
alwaysAllowWriteOutsideWorkspace,
alwaysAllowWriteProtected,
alwaysAllowExecute,
alwaysAllowBrowser,
alwaysAllowMcp,
@ -1369,6 +1370,7 @@ export class ClineProvider
alwaysAllowReadOnlyOutsideWorkspace: alwaysAllowReadOnlyOutsideWorkspace ?? false,
alwaysAllowWrite: alwaysAllowWrite ?? false,
alwaysAllowWriteOutsideWorkspace: alwaysAllowWriteOutsideWorkspace ?? false,
alwaysAllowWriteProtected: alwaysAllowWriteProtected ?? false,
alwaysAllowExecute: alwaysAllowExecute ?? false,
alwaysAllowBrowser: alwaysAllowBrowser ?? false,
alwaysAllowMcp: alwaysAllowMcp ?? false,
@ -1529,6 +1531,7 @@ export class ClineProvider
alwaysAllowReadOnlyOutsideWorkspace: stateValues.alwaysAllowReadOnlyOutsideWorkspace ?? false,
alwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,
alwaysAllowWriteOutsideWorkspace: stateValues.alwaysAllowWriteOutsideWorkspace ?? false,
alwaysAllowWriteProtected: stateValues.alwaysAllowWriteProtected ?? false,
alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,

View file

@ -155,6 +155,10 @@ export const webviewMessageHandler = async (
await updateGlobalState("alwaysAllowWriteOutsideWorkspace", message.bool ?? undefined)
await provider.postStateToWebview()
break
case "alwaysAllowWriteProtected":
await updateGlobalState("alwaysAllowWriteProtected", message.bool ?? undefined)
await provider.postStateToWebview()
break
case "alwaysAllowExecute":
await updateGlobalState("alwaysAllowExecute", message.bool ?? undefined)
await provider.postStateToWebview()

View file

@ -151,6 +151,7 @@ export type ExtensionState = Pick<
| "alwaysAllowReadOnlyOutsideWorkspace"
| "alwaysAllowWrite"
| "alwaysAllowWriteOutsideWorkspace"
| "alwaysAllowWriteProtected"
// | "writeDelayMs" // Optional in GlobalSettings, required here.
| "alwaysAllowBrowser"
| "alwaysApproveResubmit"
@ -276,6 +277,7 @@ export interface ClineSayTool {
mode?: string
reason?: string
isOutsideWorkspace?: boolean
isProtected?: boolean
additionalFileCount?: number // Number of additional files in the same read_file request
search?: string
replace?: string

View file

@ -29,6 +29,7 @@ export interface WebviewMessage {
| "alwaysAllowReadOnlyOutsideWorkspace"
| "alwaysAllowWrite"
| "alwaysAllowWriteOutsideWorkspace"
| "alwaysAllowWriteProtected"
| "alwaysAllowExecute"
| "webviewDidLaunch"
| "newTask"

View file

@ -8,6 +8,7 @@ export type AskApproval = (
type: ClineAsk,
partialMessage?: string,
progressStatus?: ToolProgressStatus,
forceApproval?: boolean,
) => Promise<boolean>
export type HandleError = (action: string, error: Error) => Promise<void>

View file

@ -313,11 +313,20 @@ export const ChatRowContent = ({
return (
<>
<div style={headerStyle}>
{toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")}
{tool.isProtected ? (
<span
className="codicon codicon-lock"
style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
/>
) : (
toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")
)}
<span style={{ fontWeight: "bold" }}>
{tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: t("chat:fileOperations.wantsToEdit")}
{tool.isProtected
? t("chat:fileOperations.wantsToEditProtected")
: tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: t("chat:fileOperations.wantsToEdit")}
</span>
</div>
<CodeAccordian
@ -335,15 +344,24 @@ export const ChatRowContent = ({
return (
<>
<div style={headerStyle}>
{toolIcon("insert")}
{tool.isProtected ? (
<span
className="codicon codicon-lock"
style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
/>
) : (
toolIcon("insert")
)}
<span style={{ fontWeight: "bold" }}>
{tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: tool.lineNumber === 0
? t("chat:fileOperations.wantsToInsertAtEnd")
: t("chat:fileOperations.wantsToInsertWithLineNumber", {
lineNumber: tool.lineNumber,
})}
{tool.isProtected
? t("chat:fileOperations.wantsToEditProtected")
: tool.isOutsideWorkspace
? t("chat:fileOperations.wantsToEditOutsideWorkspace")
: tool.lineNumber === 0
? t("chat:fileOperations.wantsToInsertAtEnd")
: t("chat:fileOperations.wantsToInsertWithLineNumber", {
lineNumber: tool.lineNumber,
})}
</span>
</div>
<CodeAccordian
@ -361,11 +379,20 @@ export const ChatRowContent = ({
return (
<>
<div style={headerStyle}>
{toolIcon("replace")}
{tool.isProtected ? (
<span
className="codicon codicon-lock"
style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
/>
) : (
toolIcon("replace")
)}
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? t("chat:fileOperations.wantsToSearchReplace")
: t("chat:fileOperations.didSearchReplace")}
{tool.isProtected && message.type === "ask"
? t("chat:fileOperations.wantsToEditProtected")
: message.type === "ask"
? t("chat:fileOperations.wantsToSearchReplace")
: t("chat:fileOperations.didSearchReplace")}
</span>
</div>
<CodeAccordian
@ -405,8 +432,19 @@ export const ChatRowContent = ({
return (
<>
<div style={headerStyle}>
{toolIcon("new-file")}
<span style={{ fontWeight: "bold" }}>{t("chat:fileOperations.wantsToCreate")}</span>
{tool.isProtected ? (
<span
className="codicon codicon-lock"
style={{ color: "var(--vscode-editorWarning-foreground)", marginBottom: "-1.5px" }}
/>
) : (
toolIcon("new-file")
)}
<span style={{ fontWeight: "bold" }}>
{tool.isProtected
? t("chat:fileOperations.wantsToEditProtected")
: t("chat:fileOperations.wantsToCreate")}
</span>
</div>
<CodeAccordian
path={tool.path}

View file

@ -79,6 +79,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
alwaysAllowReadOnlyOutsideWorkspace,
alwaysAllowWrite,
alwaysAllowWriteOutsideWorkspace,
alwaysAllowWriteProtected,
alwaysAllowExecute,
alwaysAllowMcp,
allowedCommands,
@ -914,13 +915,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}
const isOutsideWorkspace = !!tool.isOutsideWorkspace
const isProtected = message.isProtected
if (isReadOnlyToolAction(message)) {
return alwaysAllowReadOnly && (!isOutsideWorkspace || alwaysAllowReadOnlyOutsideWorkspace)
}
if (isWriteToolAction(message)) {
return alwaysAllowWrite && (!isOutsideWorkspace || alwaysAllowWriteOutsideWorkspace)
return (
alwaysAllowWrite &&
(!isOutsideWorkspace || alwaysAllowWriteOutsideWorkspace) &&
(!isProtected || alwaysAllowWriteProtected)
)
}
}
@ -934,6 +940,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
isReadOnlyToolAction,
alwaysAllowWrite,
alwaysAllowWriteOutsideWorkspace,
alwaysAllowWriteProtected,
isWriteToolAction,
alwaysAllowExecute,
isAllowedCommand,

View file

@ -16,6 +16,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
alwaysAllowReadOnlyOutsideWorkspace?: boolean
alwaysAllowWrite?: boolean
alwaysAllowWriteOutsideWorkspace?: boolean
alwaysAllowWriteProtected?: boolean
writeDelayMs: number
alwaysAllowBrowser?: boolean
alwaysApproveResubmit?: boolean
@ -30,6 +31,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "alwaysAllowReadOnlyOutsideWorkspace"
| "alwaysAllowWrite"
| "alwaysAllowWriteOutsideWorkspace"
| "alwaysAllowWriteProtected"
| "writeDelayMs"
| "alwaysAllowBrowser"
| "alwaysApproveResubmit"
@ -47,6 +49,7 @@ export const AutoApproveSettings = ({
alwaysAllowReadOnlyOutsideWorkspace,
alwaysAllowWrite,
alwaysAllowWriteOutsideWorkspace,
alwaysAllowWriteProtected,
writeDelayMs,
alwaysAllowBrowser,
alwaysApproveResubmit,
@ -138,10 +141,23 @@ export const AutoApproveSettings = ({
{t("settings:autoApprove.write.outsideWorkspace.label")}
</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.write.outsideWorkspace.description")}
</div>
</div>
<div>
<VSCodeCheckbox
checked={alwaysAllowWriteProtected}
onChange={(e: any) =>
setCachedStateField("alwaysAllowWriteProtected", e.target.checked)
}
data-testid="always-allow-write-protected-checkbox">
<span className="font-medium">{t("settings:autoApprove.write.protected.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:autoApprove.write.protected.description")}
</div>
</div>
<div>
<div className="flex items-center gap-2">
<Slider

View file

@ -131,6 +131,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
alwaysAllowSubtasks,
alwaysAllowWrite,
alwaysAllowWriteOutsideWorkspace,
alwaysAllowWriteProtected,
alwaysApproveResubmit,
autoCondenseContext,
autoCondenseContextPercent,
@ -253,6 +254,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
})
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
vscode.postMessage({ type: "alwaysAllowWriteOutsideWorkspace", bool: alwaysAllowWriteOutsideWorkspace })
vscode.postMessage({ type: "alwaysAllowWriteProtected", bool: alwaysAllowWriteProtected })
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser })
vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp })
@ -571,6 +573,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
alwaysAllowReadOnlyOutsideWorkspace={alwaysAllowReadOnlyOutsideWorkspace}
alwaysAllowWrite={alwaysAllowWrite}
alwaysAllowWriteOutsideWorkspace={alwaysAllowWriteOutsideWorkspace}
alwaysAllowWriteProtected={alwaysAllowWriteProtected}
writeDelayMs={writeDelayMs}
alwaysAllowBrowser={alwaysAllowBrowser}
alwaysApproveResubmit={alwaysApproveResubmit}

View file

@ -145,6 +145,7 @@
"didRead": "Roo ha llegit aquest fitxer:",
"wantsToEdit": "Roo vol editar aquest fitxer:",
"wantsToEditOutsideWorkspace": "Roo vol editar aquest fitxer fora de l'espai de treball:",
"wantsToEditProtected": "Roo vol editar un fitxer de configuració protegit:",
"wantsToCreate": "Roo vol crear un nou fitxer:",
"wantsToSearchReplace": "Roo vol realitzar cerca i substitució en aquest fitxer:",
"didSearchReplace": "Roo ha realitzat cerca i substitució en aquest fitxer:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Incloure fitxers fora de l'espai de treball",
"description": "Permetre a Roo crear i editar fitxers fora de l'espai de treball actual sense requerir aprovació."
},
"protected": {
"label": "Incloure fitxers protegits",
"description": "Permetre a Roo crear i editar fitxers protegits (com .rooignore i fitxers de configuració .roo/) sense requerir aprovació."
}
},
"browser": {

View file

@ -146,6 +146,7 @@
"didRead": "Roo hat diese Datei gelesen:",
"wantsToEdit": "Roo möchte diese Datei bearbeiten:",
"wantsToEditOutsideWorkspace": "Roo möchte diese Datei außerhalb des Arbeitsbereichs bearbeiten:",
"wantsToEditProtected": "Roo möchte eine geschützte Konfigurationsdatei bearbeiten:",
"wantsToCreate": "Roo möchte eine neue Datei erstellen:",
"wantsToSearchReplace": "Roo möchte in dieser Datei suchen und ersetzen:",
"didSearchReplace": "Roo hat Suchen und Ersetzen in dieser Datei durchgeführt:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Dateien außerhalb des Arbeitsbereichs einbeziehen",
"description": "Roo erlauben, Dateien außerhalb des aktuellen Arbeitsbereichs ohne Genehmigung zu erstellen und zu bearbeiten."
},
"protected": {
"label": "Geschützte Dateien einbeziehen",
"description": "Roo erlauben, geschützte Dateien (wie .rooignore und .roo/ Konfigurationsdateien) ohne Genehmigung zu erstellen und zu bearbeiten."
}
},
"browser": {

View file

@ -156,6 +156,7 @@
"didRead": "Roo read this file:",
"wantsToEdit": "Roo wants to edit this file:",
"wantsToEditOutsideWorkspace": "Roo wants to edit this file outside of the workspace:",
"wantsToEditProtected": "Roo wants to edit a protected configuration file:",
"wantsToApplyBatchChanges": "Roo wants to apply changes to multiple files:",
"wantsToCreate": "Roo wants to create a new file:",
"wantsToSearchReplace": "Roo wants to search and replace in this file:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Include files outside workspace",
"description": "Allow Roo to create and edit files outside the current workspace without requiring approval."
},
"protected": {
"label": "Include protected files",
"description": "Allow Roo to create and edit protected files (like .rooignore and .roo/ configuration files) without requiring approval."
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Roo leyó este archivo:",
"wantsToEdit": "Roo quiere editar este archivo:",
"wantsToEditOutsideWorkspace": "Roo quiere editar este archivo fuera del espacio de trabajo:",
"wantsToEditProtected": "Roo quiere editar un archivo de configuración protegido:",
"wantsToCreate": "Roo quiere crear un nuevo archivo:",
"wantsToSearchReplace": "Roo quiere realizar búsqueda y reemplazo en este archivo:",
"didSearchReplace": "Roo realizó búsqueda y reemplazo en este archivo:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Incluir archivos fuera del espacio de trabajo",
"description": "Permitir a Roo crear y editar archivos fuera del espacio de trabajo actual sin requerir aprobación."
},
"protected": {
"label": "Incluir archivos protegidos",
"description": "Permitir a Roo crear y editar archivos protegidos (como .rooignore y archivos de configuración .roo/) sin requerir aprobación."
}
},
"browser": {

View file

@ -142,6 +142,7 @@
"didRead": "Roo a lu ce fichier :",
"wantsToEdit": "Roo veut éditer ce fichier :",
"wantsToEditOutsideWorkspace": "Roo veut éditer ce fichier en dehors de l'espace de travail :",
"wantsToEditProtected": "Roo veut éditer un fichier de configuration protégé :",
"wantsToCreate": "Roo veut créer un nouveau fichier :",
"wantsToSearchReplace": "Roo veut effectuer une recherche et remplacement sur ce fichier :",
"didSearchReplace": "Roo a effectué une recherche et remplacement sur ce fichier :",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Inclure les fichiers en dehors de l'espace de travail",
"description": "Permettre à Roo de créer et modifier des fichiers en dehors de l'espace de travail actuel sans nécessiter d'approbation."
},
"protected": {
"label": "Inclure les fichiers protégés",
"description": "Permettre à Roo de créer et modifier des fichiers protégés (comme .rooignore et les fichiers de configuration .roo/) sans nécessiter d'approbation."
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Roo ने इस फ़ाइल को पढ़ा:",
"wantsToEdit": "Roo इस फ़ाइल को संपादित करना चाहता है:",
"wantsToEditOutsideWorkspace": "Roo कार्यक्षेत्र के बाहर इस फ़ाइल को संपादित करना चाहता है:",
"wantsToEditProtected": "Roo एक सुरक्षित कॉन्फ़िगरेशन फ़ाइल को संपादित करना चाहता है:",
"wantsToCreate": "Roo एक नई फ़ाइल बनाना चाहता है:",
"wantsToSearchReplace": "Roo इस फ़ाइल में खोज और प्रतिस्थापन करना चाहता है:",
"didSearchReplace": "Roo ने इस फ़ाइल में खोज और प्रतिस्थापन किया:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "वर्कस्पेस के बाहर की फाइलें शामिल करें",
"description": "Roo को अनुमोदन की आवश्यकता के बिना वर्तमान वर्कस्पेस के बाहर फाइलें बनाने और संपादित करने की अनुमति दें।"
},
"protected": {
"label": "संरक्षित फाइलें शामिल करें",
"description": "Roo को अनुमोदन की आवश्यकता के बिना संरक्षित फाइलें (.rooignore और .roo/ कॉन्फ़िगरेशन फाइलें जैसी) बनाने और संपादित करने की अनुमति दें।"
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Roo ha letto questo file:",
"wantsToEdit": "Roo vuole modificare questo file:",
"wantsToEditOutsideWorkspace": "Roo vuole modificare questo file al di fuori dell'area di lavoro:",
"wantsToEditProtected": "Roo vuole modificare un file di configurazione protetto:",
"wantsToCreate": "Roo vuole creare un nuovo file:",
"wantsToSearchReplace": "Roo vuole eseguire ricerca e sostituzione in questo file:",
"didSearchReplace": "Roo ha eseguito ricerca e sostituzione in questo file:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Includi file al di fuori dell'area di lavoro",
"description": "Permetti a Roo di creare e modificare file al di fuori dell'area di lavoro attuale senza richiedere approvazione."
},
"protected": {
"label": "Includi file protetti",
"description": "Permetti a Roo di creare e modificare file protetti (come .rooignore e file di configurazione .roo/) senza richiedere approvazione."
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Rooはこのファイルを読みました:",
"wantsToEdit": "Rooはこのファイルを編集したい:",
"wantsToEditOutsideWorkspace": "Rooはワークスペース外のこのファイルを編集したい:",
"wantsToEditProtected": "Rooは保護された設定ファイルを編集したい:",
"wantsToCreate": "Rooは新しいファイルを作成したい:",
"wantsToSearchReplace": "Rooはこのファイルで検索と置換を行う:",
"didSearchReplace": "Rooはこのファイルで検索と置換を実行しました:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "ワークスペース外のファイルを含める",
"description": "Rooが承認なしで現在のワークスペース外のファイルを作成・編集することを許可します。"
},
"protected": {
"label": "保護されたファイルを含める",
"description": "Rooが保護されたファイル.rooignoreや.roo/設定ファイルなど)を承認なしで作成・編集することを許可します。"
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Roo가 이 파일을 읽었습니다:",
"wantsToEdit": "Roo가 이 파일을 편집하고 싶어합니다:",
"wantsToEditOutsideWorkspace": "Roo가 워크스페이스 외부의 이 파일을 편집하고 싶어합니다:",
"wantsToEditProtected": "Roo가 보호된 설정 파일을 편집하고 싶어합니다:",
"wantsToCreate": "Roo가 새 파일을 만들고 싶어합니다:",
"wantsToSearchReplace": "Roo가 이 파일에서 검색 및 바꾸기를 수행하고 싶어합니다:",
"didSearchReplace": "Roo가 이 파일에서 검색 및 바꾸기를 수행했습니다:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "워크스페이스 외부 파일 포함",
"description": "Roo가 승인 없이 현재 워크스페이스 외부의 파일을 생성하고 편집할 수 있도록 허용합니다."
},
"protected": {
"label": "보호된 파일 포함",
"description": "Roo가 보호된 파일(.rooignore 및 .roo/ 구성 파일 등)을 승인 없이 생성하고 편집할 수 있도록 허용합니다."
}
},
"browser": {

View file

@ -140,6 +140,7 @@
"didRead": "Roo heeft dit bestand gelezen:",
"wantsToEdit": "Roo wil dit bestand bewerken:",
"wantsToEditOutsideWorkspace": "Roo wil dit bestand buiten de werkruimte bewerken:",
"wantsToEditProtected": "Roo wil een beveiligd configuratiebestand bewerken:",
"wantsToCreate": "Roo wil een nieuw bestand aanmaken:",
"wantsToSearchReplace": "Roo wil zoeken en vervangen in dit bestand:",
"didSearchReplace": "Roo heeft zoeken en vervangen uitgevoerd op dit bestand:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Inclusief bestanden buiten werkruimte",
"description": "Sta Roo toe om bestanden buiten de huidige werkruimte aan te maken en te bewerken zonder goedkeuring."
},
"protected": {
"label": "Inclusief beschermde bestanden",
"description": "Sta Roo toe om beschermde bestanden (zoals .rooignore en .roo/ configuratiebestanden) aan te maken en te bewerken zonder goedkeuring."
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Roo przeczytał ten plik:",
"wantsToEdit": "Roo chce edytować ten plik:",
"wantsToEditOutsideWorkspace": "Roo chce edytować ten plik poza obszarem roboczym:",
"wantsToEditProtected": "Roo chce edytować chroniony plik konfiguracyjny:",
"wantsToCreate": "Roo chce utworzyć nowy plik:",
"wantsToSearchReplace": "Roo chce wykonać wyszukiwanie i zamianę w tym pliku:",
"didSearchReplace": "Roo wykonał wyszukiwanie i zamianę w tym pliku:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Uwzględnij pliki poza obszarem roboczym",
"description": "Pozwól Roo na tworzenie i edycję plików poza bieżącym obszarem roboczym bez konieczności zatwierdzania."
},
"protected": {
"label": "Uwzględnij pliki chronione",
"description": "Pozwól Roo na tworzenie i edycję plików chronionych (takich jak .rooignore i pliki konfiguracyjne .roo/) bez konieczności zatwierdzania."
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Roo leu este arquivo:",
"wantsToEdit": "Roo quer editar este arquivo:",
"wantsToEditOutsideWorkspace": "Roo quer editar este arquivo fora do espaço de trabalho:",
"wantsToEditProtected": "Roo quer editar um arquivo de configuração protegido:",
"wantsToCreate": "Roo quer criar um novo arquivo:",
"wantsToSearchReplace": "Roo quer realizar busca e substituição neste arquivo:",
"didSearchReplace": "Roo realizou busca e substituição neste arquivo:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Incluir arquivos fora do espaço de trabalho",
"description": "Permitir que o Roo crie e edite arquivos fora do espaço de trabalho atual sem exigir aprovação."
},
"protected": {
"label": "Incluir arquivos protegidos",
"description": "Permitir que o Roo crie e edite arquivos protegidos (como .rooignore e arquivos de configuração .roo/) sem exigir aprovação."
}
},
"browser": {

View file

@ -140,6 +140,7 @@
"didRead": "Roo прочитал этот файл:",
"wantsToEdit": "Roo хочет отредактировать этот файл:",
"wantsToEditOutsideWorkspace": "Roo хочет отредактировать этот файл вне рабочей области:",
"wantsToEditProtected": "Roo хочет отредактировать защищённый файл конфигурации:",
"wantsToCreate": "Roo хочет создать новый файл:",
"wantsToSearchReplace": "Roo хочет выполнить поиск и замену в этом файле:",
"didSearchReplace": "Roo выполнил поиск и замену в этом файле:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Включая файлы вне рабочей области",
"description": "Разрешить Roo создавать и редактировать файлы вне текущей рабочей области без необходимости одобрения."
},
"protected": {
"label": "Включить защищенные файлы",
"description": "Разрешить Roo создавать и редактировать защищенные файлы (такие как .rooignore и файлы конфигурации .roo/) без необходимости одобрения."
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Roo bu dosyayı okudu:",
"wantsToEdit": "Roo bu dosyayı düzenlemek istiyor:",
"wantsToEditOutsideWorkspace": "Roo çalışma alanı dışındaki bu dosyayı düzenlemek istiyor:",
"wantsToEditProtected": "Roo korumalı bir yapılandırma dosyasını düzenlemek istiyor:",
"wantsToCreate": "Roo yeni bir dosya oluşturmak istiyor:",
"wantsToSearchReplace": "Roo bu dosyada arama ve değiştirme yapmak istiyor:",
"didSearchReplace": "Roo bu dosyada arama ve değiştirme yaptı:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Çalışma alanı dışındaki dosyaları dahil et",
"description": "Roo'nun onay gerektirmeden mevcut çalışma alanı dışında dosya oluşturmasına ve düzenlemesine izin ver."
},
"protected": {
"label": "Korumalı dosyaları dahil et",
"description": "Roo'nun korumalı dosyaları (.rooignore ve .roo/ yapılandırma dosyaları gibi) onay gerektirmeden oluşturmasına ve düzenlemesine izin ver."
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Roo đã đọc tệp này:",
"wantsToEdit": "Roo muốn chỉnh sửa tệp này:",
"wantsToEditOutsideWorkspace": "Roo muốn chỉnh sửa tệp này bên ngoài không gian làm việc:",
"wantsToEditProtected": "Roo muốn chỉnh sửa tệp cấu hình được bảo vệ:",
"wantsToCreate": "Roo muốn tạo một tệp mới:",
"wantsToSearchReplace": "Roo muốn thực hiện tìm kiếm và thay thế trong tệp này:",
"didSearchReplace": "Roo đã thực hiện tìm kiếm và thay thế trong tệp này:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "Bao gồm các tệp ngoài không gian làm việc",
"description": "Cho phép Roo tạo và chỉnh sửa các tệp bên ngoài không gian làm việc hiện tại mà không yêu cầu phê duyệt."
},
"protected": {
"label": "Bao gồm các tệp được bảo vệ",
"description": "Cho phép Roo tạo và chỉnh sửa các tệp được bảo vệ (như .rooignore và các tệp cấu hình .roo/) mà không yêu cầu phê duyệt."
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "已读取文件:",
"wantsToEdit": "需要编辑文件:",
"wantsToEditOutsideWorkspace": "需要编辑外部文件:",
"wantsToEditProtected": "需要编辑受保护的配置文件:",
"wantsToCreate": "需要新建文件:",
"wantsToSearchReplace": "需要在此文件中搜索和替换:",
"didSearchReplace": "已完成搜索和替换:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "包含工作区外的文件",
"description": "允许 Roo 创建和编辑当前工作区外的文件,无需批准。"
},
"protected": {
"label": "包含受保护的文件",
"description": "允许 Roo 创建和编辑受保护的文件(如 .rooignore 和 .roo/ 配置文件),无需批准。"
}
},
"browser": {

View file

@ -145,6 +145,7 @@
"didRead": "Roo 已讀取此檔案:",
"wantsToEdit": "Roo 想要編輯此檔案:",
"wantsToEditOutsideWorkspace": "Roo 想要編輯此工作區外的檔案:",
"wantsToEditProtected": "Roo 想要編輯受保護的設定檔案:",
"wantsToCreate": "Roo 想要建立新檔案:",
"wantsToSearchReplace": "Roo 想要在此檔案中搜尋和取代:",
"didSearchReplace": "Roo 已在此檔案執行搜尋和取代:",

View file

@ -83,6 +83,10 @@
"outsideWorkspace": {
"label": "包含工作區外的檔案",
"description": "允許 Roo 在目前工作區外建立和編輯檔案,無需核准。"
},
"protected": {
"label": "包含受保護的檔案",
"description": "允許 Roo 建立和編輯受保護的檔案(如 .rooignore 和 .roo/ 設定檔),無需核准。"
}
},
"browser": {