Skip to content

Commit 7e0e3d0

Browse files
committed
Add support for a .rooignore file
1 parent fcf0926 commit 7e0e3d0

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"
@@ -107,6 +108,7 @@ export class Cline {
107108

108109
apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
109110
clineMessages: ClineMessage[] = []
111+
rooIgnoreController?: RooIgnoreController
110112
private askResponse?: ClineAskResponse
111113
private askResponseText?: string
112114
private askResponseImages?: string[]
@@ -157,6 +159,11 @@ export class Cline {
157159
throw new Error("Either historyItem or task/images must be provided")
158160
}
159161

162+
this.rooIgnoreController = new RooIgnoreController(cwd)
163+
this.rooIgnoreController.initialize().catch((error) => {
164+
console.error("Failed to initialize RooIgnoreController:", error)
165+
})
166+
160167
this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
161168

162169
this.apiConfiguration = apiConfiguration
@@ -802,6 +809,7 @@ export class Cline {
802809
this.terminalManager.disposeAll()
803810
this.urlContentFetcher.closeBrowser()
804811
this.browserSession.closeBrowser()
812+
this.rooIgnoreController?.dispose()
805813

806814
// If we're not streaming then `abortStream` (which reverts the diff
807815
// view changes) won't be called, so we need to revert the changes here.
@@ -953,6 +961,8 @@ export class Cline {
953961
})
954962
}
955963

964+
const rooIgnoreInstructions = this.rooIgnoreController?.getInstructions()
965+
956966
const {
957967
browserViewportSize,
958968
mode,
@@ -983,6 +993,7 @@ export class Cline {
983993
this.diffEnabled,
984994
experiments,
985995
enableMcpServerCreation,
996+
rooIgnoreInstructions,
986997
)
987998
})()
988999

@@ -1357,6 +1368,15 @@ export class Cline {
13571368
// wait so we can determine if it's a new file or editing an existing file
13581369
break
13591370
}
1371+
1372+
const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
1373+
if (!accessAllowed) {
1374+
await this.say("rooignore_error", relPath)
1375+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
1376+
1377+
break
1378+
}
1379+
13601380
// Check if file exists using cached map or fs.access
13611381
let fileExists: boolean
13621382
if (this.diffViewProvider.editType !== undefined) {
@@ -1566,6 +1586,14 @@ export class Cline {
15661586
break
15671587
}
15681588

1589+
const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
1590+
if (!accessAllowed) {
1591+
await this.say("rooignore_error", relPath)
1592+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
1593+
1594+
break
1595+
}
1596+
15691597
const absolutePath = path.resolve(cwd, relPath)
15701598
const fileExists = await fileExistsAtPath(absolutePath)
15711599

@@ -1999,6 +2027,15 @@ export class Cline {
19992027
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
20002028
break
20012029
}
2030+
2031+
const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
2032+
if (!accessAllowed) {
2033+
await this.say("rooignore_error", relPath)
2034+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
2035+
2036+
break
2037+
}
2038+
20022039
this.consecutiveMistakeCount = 0
20032040
const absolutePath = path.resolve(cwd, relPath)
20042041
const completeMessage = JSON.stringify({
@@ -2044,7 +2081,12 @@ export class Cline {
20442081
this.consecutiveMistakeCount = 0
20452082
const absolutePath = path.resolve(cwd, relDirPath)
20462083
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
2047-
const result = formatResponse.formatFilesList(absolutePath, files, didHitLimit)
2084+
const result = formatResponse.formatFilesList(
2085+
absolutePath,
2086+
files,
2087+
didHitLimit,
2088+
this.rooIgnoreController,
2089+
)
20482090
const completeMessage = JSON.stringify({
20492091
...sharedMessageProps,
20502092
content: result,
@@ -2085,7 +2127,10 @@ export class Cline {
20852127
}
20862128
this.consecutiveMistakeCount = 0
20872129
const absolutePath = path.resolve(cwd, relDirPath)
2088-
const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath)
2130+
const result = await parseSourceCodeForDefinitionsTopLevel(
2131+
absolutePath,
2132+
this.rooIgnoreController,
2133+
)
20892134
const completeMessage = JSON.stringify({
20902135
...sharedMessageProps,
20912136
content: result,
@@ -2133,7 +2178,13 @@ export class Cline {
21332178
}
21342179
this.consecutiveMistakeCount = 0
21352180
const absolutePath = path.resolve(cwd, relDirPath)
2136-
const results = await regexSearchFiles(cwd, absolutePath, regex, filePattern)
2181+
const results = await regexSearchFiles(
2182+
cwd,
2183+
absolutePath,
2184+
regex,
2185+
filePattern,
2186+
this.rooIgnoreController,
2187+
)
21372188
const completeMessage = JSON.stringify({
21382189
...sharedMessageProps,
21392190
content: results,
@@ -2312,6 +2363,19 @@ export class Cline {
23122363
)
23132364
break
23142365
}
2366+
2367+
const ignoredFileAttemptedToAccess = this.rooIgnoreController?.validateCommand(command)
2368+
if (ignoredFileAttemptedToAccess) {
2369+
await this.say("rooignore_error", ignoredFileAttemptedToAccess)
2370+
pushToolResult(
2371+
formatResponse.toolError(
2372+
formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess),
2373+
),
2374+
)
2375+
2376+
break
2377+
}
2378+
23152379
this.consecutiveMistakeCount = 0
23162380

23172381
const didApprove = await askApproval("command", command)
@@ -3172,29 +3236,39 @@ export class Cline {
31723236

31733237
// 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
31743238
details += "\n\n# VSCode Visible Files"
3175-
const visibleFiles = vscode.window.visibleTextEditors
3239+
const visibleFilePaths = vscode.window.visibleTextEditors
31763240
?.map((editor) => editor.document?.uri?.fsPath)
31773241
.filter(Boolean)
3178-
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
3179-
.join("\n")
3180-
if (visibleFiles) {
3181-
details += `\n${visibleFiles}`
3242+
.map((absolutePath) => path.relative(cwd, absolutePath))
3243+
3244+
// Filter paths through rooIgnoreController
3245+
const allowedVisibleFiles = this.rooIgnoreController
3246+
? this.rooIgnoreController.filterPaths(visibleFilePaths)
3247+
: visibleFilePaths.map((p) => p.toPosix()).join("\n")
3248+
3249+
if (allowedVisibleFiles) {
3250+
details += `\n${allowedVisibleFiles}`
31823251
} else {
31833252
details += "\n(No visible files)"
31843253
}
31853254

31863255
details += "\n\n# VSCode Open Tabs"
31873256
const { maxOpenTabsContext } = (await this.providerRef.deref()?.getState()) ?? {}
31883257
const maxTabs = maxOpenTabsContext ?? 20
3189-
const openTabs = vscode.window.tabGroups.all
3258+
const openTabPaths = vscode.window.tabGroups.all
31903259
.flatMap((group) => group.tabs)
31913260
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
31923261
.filter(Boolean)
31933262
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
31943263
.slice(0, maxTabs)
3195-
.join("\n")
3196-
if (openTabs) {
3197-
details += `\n${openTabs}`
3264+
3265+
// Filter paths through rooIgnoreController
3266+
const allowedOpenTabs = this.rooIgnoreController
3267+
? this.rooIgnoreController.filterPaths(openTabPaths)
3268+
: openTabPaths.map((p) => p.toPosix()).join("\n")
3269+
3270+
if (allowedOpenTabs) {
3271+
details += `\n${allowedOpenTabs}`
31983272
} else {
31993273
details += "\n(No open tabs)"
32003274
}
@@ -3353,7 +3427,7 @@ export class Cline {
33533427
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
33543428
} else {
33553429
const [files, didHitLimit] = await listFiles(cwd, true, 200)
3356-
const result = formatResponse.formatFilesList(cwd, files, didHitLimit)
3430+
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.rooIgnoreController)
33573431
details += result
33583432
}
33593433
}

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)