Skip to content

Commit 6a0f221

Browse files
authored
Merge pull request #1367 from RooVetGit/rooignore
Add support for a .rooignore file
2 parents e6f4764 + 7e0e3d0 commit 6a0f221

File tree

16 files changed

+1424
-29
lines changed

16 files changed

+1424
-29
lines changed

src/__mocks__/vscode.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ const vscode = {
8484
this.uri = uri
8585
}
8686
},
87+
RelativePattern: class {
88+
constructor(base, pattern) {
89+
this.base = base
90+
this.pattern = pattern
91+
}
92+
},
8793
}
8894

8995
module.exports = vscode

src/core/Cline.ts

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { calculateApiCost } from "../utils/cost"
5959
import { fileExistsAtPath } from "../utils/fs"
6060
import { arePathsEqual, getReadablePath } from "../utils/path"
6161
import { parseMentions } from "./mentions"
62+
import { RooIgnoreController, LOCK_TEXT_SYMBOL } from "./ignore/RooIgnoreController"
6263
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
6364
import { formatResponse } from "./prompts/responses"
6465
import { SYSTEM_PROMPT } from "./prompts/system"
@@ -118,6 +119,7 @@ export class Cline {
118119

119120
apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
120121
clineMessages: ClineMessage[] = []
122+
rooIgnoreController?: RooIgnoreController
121123
private askResponse?: ClineAskResponse
122124
private askResponseText?: string
123125
private askResponseImages?: string[]
@@ -168,6 +170,11 @@ export class Cline {
168170
throw new Error("Either historyItem or task/images must be provided")
169171
}
170172

173+
this.rooIgnoreController = new RooIgnoreController(cwd)
174+
this.rooIgnoreController.initialize().catch((error) => {
175+
console.error("Failed to initialize RooIgnoreController:", error)
176+
})
177+
171178
this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
172179
this.taskNumber = -1
173180
this.apiConfiguration = apiConfiguration
@@ -893,6 +900,7 @@ export class Cline {
893900
this.terminalManager.disposeAll()
894901
this.urlContentFetcher.closeBrowser()
895902
this.browserSession.closeBrowser()
903+
this.rooIgnoreController?.dispose()
896904

897905
// If we're not streaming then `abortStream` (which reverts the diff
898906
// view changes) won't be called, so we need to revert the changes here.
@@ -1044,6 +1052,8 @@ export class Cline {
10441052
})
10451053
}
10461054

1055+
const rooIgnoreInstructions = this.rooIgnoreController?.getInstructions()
1056+
10471057
const {
10481058
browserViewportSize,
10491059
mode,
@@ -1074,6 +1084,7 @@ export class Cline {
10741084
this.diffEnabled,
10751085
experiments,
10761086
enableMcpServerCreation,
1087+
rooIgnoreInstructions,
10771088
)
10781089
})()
10791090

@@ -1448,6 +1459,15 @@ export class Cline {
14481459
// wait so we can determine if it's a new file or editing an existing file
14491460
break
14501461
}
1462+
1463+
const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
1464+
if (!accessAllowed) {
1465+
await this.say("rooignore_error", relPath)
1466+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
1467+
1468+
break
1469+
}
1470+
14511471
// Check if file exists using cached map or fs.access
14521472
let fileExists: boolean
14531473
if (this.diffViewProvider.editType !== undefined) {
@@ -1657,6 +1677,14 @@ export class Cline {
16571677
break
16581678
}
16591679

1680+
const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
1681+
if (!accessAllowed) {
1682+
await this.say("rooignore_error", relPath)
1683+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
1684+
1685+
break
1686+
}
1687+
16601688
const absolutePath = path.resolve(cwd, relPath)
16611689
const fileExists = await fileExistsAtPath(absolutePath)
16621690

@@ -2115,6 +2143,15 @@ export class Cline {
21152143
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
21162144
break
21172145
}
2146+
2147+
const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
2148+
if (!accessAllowed) {
2149+
await this.say("rooignore_error", relPath)
2150+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
2151+
2152+
break
2153+
}
2154+
21182155
this.consecutiveMistakeCount = 0
21192156
const absolutePath = path.resolve(cwd, relPath)
21202157
const completeMessage = JSON.stringify({
@@ -2160,7 +2197,12 @@ export class Cline {
21602197
this.consecutiveMistakeCount = 0
21612198
const absolutePath = path.resolve(cwd, relDirPath)
21622199
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
2163-
const result = formatResponse.formatFilesList(absolutePath, files, didHitLimit)
2200+
const result = formatResponse.formatFilesList(
2201+
absolutePath,
2202+
files,
2203+
didHitLimit,
2204+
this.rooIgnoreController,
2205+
)
21642206
const completeMessage = JSON.stringify({
21652207
...sharedMessageProps,
21662208
content: result,
@@ -2201,7 +2243,10 @@ export class Cline {
22012243
}
22022244
this.consecutiveMistakeCount = 0
22032245
const absolutePath = path.resolve(cwd, relDirPath)
2204-
const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath)
2246+
const result = await parseSourceCodeForDefinitionsTopLevel(
2247+
absolutePath,
2248+
this.rooIgnoreController,
2249+
)
22052250
const completeMessage = JSON.stringify({
22062251
...sharedMessageProps,
22072252
content: result,
@@ -2249,7 +2294,13 @@ export class Cline {
22492294
}
22502295
this.consecutiveMistakeCount = 0
22512296
const absolutePath = path.resolve(cwd, relDirPath)
2252-
const results = await regexSearchFiles(cwd, absolutePath, regex, filePattern)
2297+
const results = await regexSearchFiles(
2298+
cwd,
2299+
absolutePath,
2300+
regex,
2301+
filePattern,
2302+
this.rooIgnoreController,
2303+
)
22532304
const completeMessage = JSON.stringify({
22542305
...sharedMessageProps,
22552306
content: results,
@@ -2428,6 +2479,19 @@ export class Cline {
24282479
)
24292480
break
24302481
}
2482+
2483+
const ignoredFileAttemptedToAccess = this.rooIgnoreController?.validateCommand(command)
2484+
if (ignoredFileAttemptedToAccess) {
2485+
await this.say("rooignore_error", ignoredFileAttemptedToAccess)
2486+
pushToolResult(
2487+
formatResponse.toolError(
2488+
formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess),
2489+
),
2490+
)
2491+
2492+
break
2493+
}
2494+
24312495
this.consecutiveMistakeCount = 0
24322496

24332497
const didApprove = await askApproval("command", command)
@@ -3349,29 +3413,39 @@ export class Cline {
33493413

33503414
// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
33513415
details += "\n\n# VSCode Visible Files"
3352-
const visibleFiles = vscode.window.visibleTextEditors
3416+
const visibleFilePaths = vscode.window.visibleTextEditors
33533417
?.map((editor) => editor.document?.uri?.fsPath)
33543418
.filter(Boolean)
3355-
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
3356-
.join("\n")
3357-
if (visibleFiles) {
3358-
details += `\n${visibleFiles}`
3419+
.map((absolutePath) => path.relative(cwd, absolutePath))
3420+
3421+
// Filter paths through rooIgnoreController
3422+
const allowedVisibleFiles = this.rooIgnoreController
3423+
? this.rooIgnoreController.filterPaths(visibleFilePaths)
3424+
: visibleFilePaths.map((p) => p.toPosix()).join("\n")
3425+
3426+
if (allowedVisibleFiles) {
3427+
details += `\n${allowedVisibleFiles}`
33593428
} else {
33603429
details += "\n(No visible files)"
33613430
}
33623431

33633432
details += "\n\n# VSCode Open Tabs"
33643433
const { maxOpenTabsContext } = (await this.providerRef.deref()?.getState()) ?? {}
33653434
const maxTabs = maxOpenTabsContext ?? 20
3366-
const openTabs = vscode.window.tabGroups.all
3435+
const openTabPaths = vscode.window.tabGroups.all
33673436
.flatMap((group) => group.tabs)
33683437
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
33693438
.filter(Boolean)
33703439
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
33713440
.slice(0, maxTabs)
3372-
.join("\n")
3373-
if (openTabs) {
3374-
details += `\n${openTabs}`
3441+
3442+
// Filter paths through rooIgnoreController
3443+
const allowedOpenTabs = this.rooIgnoreController
3444+
? this.rooIgnoreController.filterPaths(openTabPaths)
3445+
: openTabPaths.map((p) => p.toPosix()).join("\n")
3446+
3447+
if (allowedOpenTabs) {
3448+
details += `\n${allowedOpenTabs}`
33753449
} else {
33763450
details += "\n(No open tabs)"
33773451
}
@@ -3530,7 +3604,7 @@ export class Cline {
35303604
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
35313605
} else {
35323606
const [files, didHitLimit] = await listFiles(cwd, true, 200)
3533-
const result = formatResponse.formatFilesList(cwd, files, didHitLimit)
3607+
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.rooIgnoreController)
35343608
details += result
35353609
}
35363610
}

src/core/__tests__/Cline.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import * as vscode from "vscode"
99
import * as os from "os"
1010
import * as path from "path"
1111

12+
// Mock RooIgnoreController
13+
jest.mock("../ignore/RooIgnoreController")
14+
1215
// Mock all MCP-related modules
1316
jest.mock(
1417
"@modelcontextprotocol/sdk/types.js",

0 commit comments

Comments
 (0)