@@ -5,6 +5,8 @@ import fsSync from "fs"
55import ignore , { Ignore } from "ignore"
66import * as vscode from "vscode"
77
8+ import { getWorkspaceRelativePath , getWorkspaceRootForPath } from "../../utils/pathUtils"
9+
810export const LOCK_TEXT_SYMBOL = "\u{1F512}"
911
1012/**
@@ -15,6 +17,8 @@ export const LOCK_TEXT_SYMBOL = "\u{1F512}"
1517export class RooIgnoreController {
1618 private cwd : string
1719 private ignoreInstance : Ignore
20+ private ignoreInstances = new Map < string , Ignore > ( )
21+ private rooIgnoreContents = new Map < string , string | undefined > ( )
1822 private disposables : vscode . Disposable [ ] = [ ]
1923 rooIgnoreContent : string | undefined
2024
@@ -38,19 +42,23 @@ export class RooIgnoreController {
3842 * Set up the file watcher for .rooignore changes
3943 */
4044 private setupFileWatcher ( ) : void {
41- const rooignorePattern = new vscode . RelativePattern ( this . cwd , ".rooignore" )
45+ this . setupFileWatcherForRoot ( this . cwd )
46+ }
47+
48+ private setupFileWatcherForRoot ( rootPath : string ) : void {
49+ const rooignorePattern = new vscode . RelativePattern ( rootPath , ".rooignore" )
4250 const fileWatcher = vscode . workspace . createFileSystemWatcher ( rooignorePattern )
4351
4452 // Watch for changes and updates
4553 this . disposables . push (
4654 fileWatcher . onDidChange ( ( ) => {
47- this . loadRooIgnore ( )
55+ this . loadRooIgnoreForRoot ( rootPath )
4856 } ) ,
4957 fileWatcher . onDidCreate ( ( ) => {
50- this . loadRooIgnore ( )
58+ this . loadRooIgnoreForRoot ( rootPath )
5159 } ) ,
5260 fileWatcher . onDidDelete ( ( ) => {
53- this . loadRooIgnore ( )
61+ this . loadRooIgnoreForRoot ( rootPath )
5462 } ) ,
5563 )
5664
@@ -62,38 +70,99 @@ export class RooIgnoreController {
6270 * Load custom patterns from .rooignore if it exists
6371 */
6472 private async loadRooIgnore ( ) : Promise < void > {
73+ await this . loadRooIgnoreForRoot ( this . cwd )
74+ }
75+
76+ private async loadRooIgnoreForRoot ( rootPath : string ) : Promise < void > {
6577 try {
6678 // Reset ignore instance to prevent duplicate patterns
67- this . ignoreInstance = ignore ( )
68- const ignorePath = path . join ( this . cwd , ".rooignore" )
79+ const ignoreInstance = ignore ( )
80+ const ignorePath = path . join ( rootPath , ".rooignore" )
6981 if ( await fileExistsAtPath ( ignorePath ) ) {
7082 const content = await fs . readFile ( ignorePath , "utf8" )
71- this . rooIgnoreContent = content
72- this . ignoreInstance . add ( content )
73- this . ignoreInstance . add ( ".rooignore" )
83+ ignoreInstance . add ( content )
84+ ignoreInstance . add ( ".rooignore" )
85+ this . ignoreInstances . set ( rootPath , ignoreInstance )
86+ this . rooIgnoreContents . set ( rootPath , content )
7487 } else {
75- this . rooIgnoreContent = undefined
88+ this . ignoreInstances . set ( rootPath , ignoreInstance )
89+ this . rooIgnoreContents . set ( rootPath , undefined )
90+ }
91+
92+ if ( rootPath === this . cwd ) {
93+ this . ignoreInstance = ignoreInstance
94+ this . rooIgnoreContent = this . rooIgnoreContents . get ( rootPath )
7695 }
7796 } catch ( error ) {
7897 // Should never happen: reading file failed even though it exists
7998 console . error ( "Unexpected error loading .rooignore:" , error )
8099 }
81100 }
82101
102+ private getIgnoreStateForRoot ( rootPath : string ) : { ignoreInstance : Ignore ; content : string | undefined } {
103+ const cached = this . ignoreInstances . get ( rootPath )
104+ if ( cached ) {
105+ return { ignoreInstance : cached , content : this . rooIgnoreContents . get ( rootPath ) }
106+ }
107+
108+ const ignoreInstance = ignore ( )
109+ try {
110+ const ignorePath = path . join ( rootPath , ".rooignore" )
111+ if ( fsSync . existsSync ( ignorePath ) ) {
112+ const content = fsSync . readFileSync ( ignorePath , "utf8" )
113+ ignoreInstance . add ( content )
114+ ignoreInstance . add ( ".rooignore" )
115+ this . ignoreInstances . set ( rootPath , ignoreInstance )
116+ this . rooIgnoreContents . set ( rootPath , content )
117+ this . setupFileWatcherForRoot ( rootPath )
118+ return { ignoreInstance, content }
119+ }
120+ } catch ( error ) {
121+ console . error ( "Unexpected error loading .rooignore:" , error )
122+ }
123+
124+ this . ignoreInstances . set ( rootPath , ignoreInstance )
125+ this . rooIgnoreContents . set ( rootPath , undefined )
126+ this . setupFileWatcherForRoot ( rootPath )
127+ return { ignoreInstance, content : undefined }
128+ }
129+
130+ private getKnownWorkspaceRoots ( ) : string [ ] {
131+ const roots = new Set < string > ( [ this . cwd ] )
132+ for ( const folder of vscode . workspace . workspaceFolders ?? [ ] ) {
133+ roots . add ( folder . uri . fsPath )
134+ }
135+ return [ ...roots ]
136+ }
137+
138+ private getAvailableIgnoreContents ( ) : Array < { rootPath : string ; content : string } > {
139+ return this . getKnownWorkspaceRoots ( )
140+ . map ( ( rootPath ) => ( { rootPath, content : this . getIgnoreStateForRoot ( rootPath ) . content } ) )
141+ . filter ( ( entry ) : entry is { rootPath : string ; content : string } => typeof entry . content === "string" )
142+ }
143+
83144 /**
84145 * Check if a file should be accessible to the LLM
85146 * Automatically resolves symlinks
86147 * @param filePath - Path to check (relative to cwd)
87148 * @returns true if file is accessible, false if ignored
88149 */
89150 validateAccess ( filePath : string ) : boolean {
151+ const absolutePath = path . isAbsolute ( filePath ) ? path . resolve ( filePath ) : path . resolve ( this . cwd , filePath )
152+ const rootPath = getWorkspaceRootForPath ( absolutePath , this . cwd )
153+
154+ // Preserve backward compatibility for files outside the task workspace roots.
155+ if ( ! rootPath ) {
156+ return true
157+ }
158+
159+ const { ignoreInstance, content } = this . getIgnoreStateForRoot ( rootPath )
90160 // Always allow access if .rooignore does not exist
91- if ( ! this . rooIgnoreContent ) {
161+ if ( ! content ) {
92162 return true
93163 }
94- try {
95- const absolutePath = path . resolve ( this . cwd , filePath )
96164
165+ try {
97166 // Follow symlinks to get the real path
98167 let realPath : string
99168 try {
@@ -105,10 +174,10 @@ export class RooIgnoreController {
105174 }
106175
107176 // Convert real path to relative for .rooignore checking
108- const relativePath = path . relative ( this . cwd , realPath ) . toPosix ( )
177+ const relativePath = getWorkspaceRelativePath ( rootPath , realPath )
109178
110179 // Check if the real path is ignored
111- return ! this . ignoreInstance . ignores ( relativePath )
180+ return ! ignoreInstance . ignores ( relativePath )
112181 } catch ( error ) {
113182 // Allow access to files outside cwd or on errors (backward compatibility)
114183 return true
@@ -121,11 +190,6 @@ export class RooIgnoreController {
121190 * @returns path of file that is being accessed if it is being accessed, undefined if command is allowed
122191 */
123192 validateCommand ( command : string ) : string | undefined {
124- // Always allow if no .rooignore exists
125- if ( ! this . rooIgnoreContent ) {
126- return undefined
127- }
128-
129193 // Split command into parts and get the base command
130194 const parts = command . trim ( ) . split ( / \s + / )
131195 const baseCommand = parts [ 0 ] . toLowerCase ( )
@@ -153,12 +217,13 @@ export class RooIgnoreController {
153217 // Check each argument that could be a file path
154218 for ( let i = 1 ; i < parts . length ; i ++ ) {
155219 const arg = parts [ i ]
220+ const isWindowsAbsolutePath = path . win32 . isAbsolute ( arg )
156221 // Skip command flags/options (both Unix and PowerShell style)
157- if ( arg . startsWith ( "-" ) || arg . startsWith ( "/" ) ) {
222+ if ( arg . startsWith ( "-" ) || ( arg . startsWith ( "/" ) && ! path . isAbsolute ( arg ) ) ) {
158223 continue
159224 }
160225 // Ignore PowerShell parameter names
161- if ( arg . includes ( ":" ) ) {
226+ if ( arg . includes ( ":" ) && ! isWindowsAbsolutePath ) {
162227 continue
163228 }
164229 // Validate file access
@@ -204,10 +269,21 @@ export class RooIgnoreController {
204269 * @returns Formatted instructions or undefined if .rooignore doesn't exist
205270 */
206271 getInstructions ( ) : string | undefined {
207- if ( ! this . rooIgnoreContent ) {
272+ const ignoreEntries = this . getAvailableIgnoreContents ( )
273+ if ( ignoreEntries . length === 0 ) {
208274 return undefined
209275 }
210276
211- return `# .rooignore\n\n(The following is provided by a root-level .rooignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${ LOCK_TEXT_SYMBOL } next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${ this . rooIgnoreContent } \n.rooignore`
277+ const sections = ignoreEntries
278+ . map ( ( { rootPath, content } ) => {
279+ const workspaceName =
280+ vscode . workspace . workspaceFolders ?. find ( ( folder ) => folder . uri . fsPath === rootPath ) ?. name ??
281+ path . basename ( rootPath ) ??
282+ rootPath
283+ return `## ${ workspaceName } \n\n${ content } \n.rooignore`
284+ } )
285+ . join ( "\n\n" )
286+
287+ return `# .rooignore\n\n(The following is provided by workspace-root .rooignore files where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${ LOCK_TEXT_SYMBOL } next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${ sections } `
212288 }
213289}
0 commit comments