Add setting for whether to include ignored files in lists

This commit is contained in:
Matt Rubens 2025-03-07 11:23:20 -05:00
parent 71f1ab01aa
commit f9d162e157
12 changed files with 178 additions and 21 deletions

View file

@ -2223,11 +2223,13 @@ export class Cline {
this.consecutiveMistakeCount = 0
const absolutePath = path.resolve(cwd, relDirPath)
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
const { showRooIgnoredFiles } = (await this.providerRef.deref()?.getState()) ?? {}
const result = formatResponse.formatFilesList(
absolutePath,
files,
didHitLimit,
this.rooIgnoreController,
showRooIgnoredFiles ?? true,
)
const completeMessage = JSON.stringify({
...sharedMessageProps,
@ -3626,7 +3628,14 @@ export class Cline {
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
} else {
const [files, didHitLimit] = await listFiles(cwd, true, 200)
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.rooIgnoreController)
const { showRooIgnoredFiles } = (await this.providerRef.deref()?.getState()) ?? {}
const result = formatResponse.formatFilesList(
cwd,
files,
didHitLimit,
this.rooIgnoreController,
showRooIgnoredFiles,
)
details += result
}
}

View file

@ -96,7 +96,7 @@ describe("RooIgnore Response Formatting", () => {
]
// Format with controller
const result = formatResponse.formatFilesList(TEST_CWD, files, false, controller as any)
const result = formatResponse.formatFilesList(TEST_CWD, files, false, controller as any, true)
// Should contain each file
expect(result).toContain("src/app.ts")
@ -112,6 +112,55 @@ describe("RooIgnore Response Formatting", () => {
expect(result).not.toContain(`${LOCK_TEXT_SYMBOL} README.md`)
})
/**
* Tests formatFilesList when showRooIgnoredFiles is set to false
*/
it("should hide ignored files when showRooIgnoredFiles is false", async () => {
// Create controller
const controller = new RooIgnoreController(TEST_CWD)
await controller.initialize()
// Mock validateAccess to control which files are ignored
controller.validateAccess = jest.fn().mockImplementation((filePath: string) => {
// Only allow files not matching these patterns
return (
!filePath.includes("node_modules") && !filePath.includes(".git") && !filePath.includes("secrets/")
)
})
// Files list with mixed allowed/ignored files
const files = [
"src/app.ts", // allowed
"node_modules/package.json", // ignored
"README.md", // allowed
".git/HEAD", // ignored
"secrets/keys.json", // ignored
]
// Format with controller and showRooIgnoredFiles = false
const result = formatResponse.formatFilesList(
TEST_CWD,
files,
false,
controller as any,
false, // showRooIgnoredFiles = false
)
// Should contain allowed files
expect(result).toContain("src/app.ts")
expect(result).toContain("README.md")
// Should NOT contain ignored files (even with lock symbols)
expect(result).not.toContain("node_modules/package.json")
expect(result).not.toContain(".git/HEAD")
expect(result).not.toContain("secrets/keys.json")
// Double-check with regex to ensure no form of these filenames appears
expect(result).not.toMatch(/node_modules\/package\.json/i)
expect(result).not.toMatch(/\.git\/HEAD/i)
expect(result).not.toMatch(/secrets\/keys\.json/i)
})
/**
* Tests formatFilesList handles truncation correctly with RooIgnoreController
*/
@ -126,6 +175,7 @@ describe("RooIgnore Response Formatting", () => {
["file1.txt", "file2.txt"],
true, // didHitLimit = true
controller as any,
true,
)
// Should contain truncation message (case-insensitive check)
@ -142,7 +192,7 @@ describe("RooIgnore Response Formatting", () => {
await controller.initialize()
// Format with empty files array
const result = formatResponse.formatFilesList(TEST_CWD, [], false, controller as any)
const result = formatResponse.formatFilesList(TEST_CWD, [], false, controller as any, true)
// Should show "No files found"
expect(result).toBe("No files found.")

View file

@ -60,7 +60,8 @@ Otherwise, if you have not completed the task and do not need additional informa
absolutePath: string,
files: string[],
didHitLimit: boolean,
rooIgnoreController?: RooIgnoreController,
rooIgnoreController: RooIgnoreController | undefined,
showRooIgnoredFiles: boolean,
): string => {
const sorted = files
.map((file) => {
@ -90,20 +91,29 @@ Otherwise, if you have not completed the task and do not need additional informa
return aParts.length - bParts.length
})
const rooIgnoreParsed = rooIgnoreController
? sorted.map((filePath) => {
// path is relative to absolute path, not cwd
// validateAccess expects either path relative to cwd or absolute path
// otherwise, for validating against ignore patterns like "assets/icons", we would end up with just "icons", which would result in the path not being ignored.
const absoluteFilePath = path.resolve(absolutePath, filePath)
const isIgnored = !rooIgnoreController.validateAccess(absoluteFilePath)
if (isIgnored) {
return LOCK_TEXT_SYMBOL + " " + filePath
}
let rooIgnoreParsed: string[] = sorted
return filePath
})
: sorted
if (rooIgnoreController) {
rooIgnoreParsed = []
for (const filePath of sorted) {
// path is relative to absolute path, not cwd
// validateAccess expects either path relative to cwd or absolute path
// otherwise, for validating against ignore patterns like "assets/icons", we would end up with just "icons", which would result in the path not being ignored.
const absoluteFilePath = path.resolve(absolutePath, filePath)
const isIgnored = !rooIgnoreController.validateAccess(absoluteFilePath)
if (isIgnored) {
// If file is ignored and we're not showing ignored files, skip it
if (!showRooIgnoredFiles) {
continue
}
// Otherwise, mark it with a lock symbol
rooIgnoreParsed.push(LOCK_TEXT_SYMBOL + " " + filePath)
} else {
rooIgnoreParsed.push(filePath)
}
}
}
if (didHitLimit) {
return `${rooIgnoreParsed.join(
"\n",

View file

@ -1468,6 +1468,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("browserToolEnabled", message.bool ?? true)
await this.postStateToWebview()
break
case "showRooIgnoredFiles":
await this.updateGlobalState("showRooIgnoredFiles", message.bool ?? true)
await this.postStateToWebview()
break
case "enhancementApiConfigId":
await this.updateGlobalState("enhancementApiConfigId", message.text)
await this.postStateToWebview()
@ -2201,6 +2205,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
maxOpenTabsContext,
browserToolEnabled,
telemetrySetting,
showRooIgnoredFiles,
} = await this.getState()
const telemetryKey = process.env.POSTHOG_API_KEY
const machineId = vscode.env.machineId
@ -2262,6 +2267,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
telemetrySetting,
telemetryKey,
machineId,
showRooIgnoredFiles: showRooIgnoredFiles ?? true,
}
}
@ -2441,6 +2447,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,
browserToolEnabled: stateValues.browserToolEnabled ?? true,
telemetrySetting: stateValues.telemetrySetting || "unset",
showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true,
}
}

View file

@ -445,6 +445,7 @@ describe("ClineProvider", () => {
maxOpenTabsContext: 20,
browserToolEnabled: true,
telemetrySetting: "unset",
showRooIgnoredFiles: true,
}
const message: ExtensionMessage = {
@ -703,6 +704,27 @@ describe("ClineProvider", () => {
expect(state.browserToolEnabled).toBe(true) // Default value should be true
})
test("handles showRooIgnoredFiles setting", async () => {
await provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Test showRooIgnoredFiles with true
await messageHandler({ type: "showRooIgnoredFiles", bool: true })
expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", true)
expect(mockPostMessage).toHaveBeenCalled()
// Test showRooIgnoredFiles with false
jest.clearAllMocks() // Clear all mocks including mockContext.globalState.update
await messageHandler({ type: "showRooIgnoredFiles", bool: false })
expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", false)
expect(mockPostMessage).toHaveBeenCalled()
// Verify state includes showRooIgnoredFiles
const state = await provider.getState()
expect(state).toHaveProperty("showRooIgnoredFiles")
expect(state.showRooIgnoredFiles).toBe(true) // Default value should be true
})
test("handles request delay settings messages", async () => {
await provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]

View file

@ -140,6 +140,7 @@ export interface ExtensionState {
telemetrySetting: TelemetrySetting
telemetryKey?: string
machineId?: string
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
}
export interface ClineMessage {

View file

@ -99,6 +99,7 @@ export interface WebviewMessage {
| "humanRelayCancel"
| "browserToolEnabled"
| "telemetrySetting"
| "showRooIgnoredFiles"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse

View file

@ -97,6 +97,7 @@ export const GLOBAL_STATE_KEYS = [
"lmStudioSpeculativeDecodingEnabled",
"lmStudioDraftModelId",
"telemetrySetting",
"showRooIgnoredFiles",
] as const
// Derive the type from the array - creates a union of string literals

View file

@ -18,19 +18,25 @@ type AdvancedSettingsProps = HTMLAttributes<HTMLDivElement> & {
maxOpenTabsContext: number
diffEnabled?: boolean
fuzzyMatchThreshold?: number
showRooIgnoredFiles?: boolean
setCachedStateField: SetCachedStateField<
"rateLimitSeconds" | "terminalOutputLimit" | "maxOpenTabsContext" | "diffEnabled" | "fuzzyMatchThreshold"
| "rateLimitSeconds"
| "terminalOutputLimit"
| "maxOpenTabsContext"
| "diffEnabled"
| "fuzzyMatchThreshold"
| "showRooIgnoredFiles"
>
experiments: Record<ExperimentId, boolean>
setExperimentEnabled: SetExperimentEnabled
}
export const AdvancedSettings = ({
rateLimitSeconds,
terminalOutputLimit = TERMINAL_OUTPUT_LIMIT,
maxOpenTabsContext,
diffEnabled,
fuzzyMatchThreshold,
showRooIgnoredFiles,
setCachedStateField,
experiments,
setExperimentEnabled,
@ -197,6 +203,20 @@ export const AdvancedSettings = ({
</div>
)}
</div>
<div>
<VSCodeCheckbox
checked={showRooIgnoredFiles}
onChange={(e: any) => {
setCachedStateField("showRooIgnoredFiles", e.target.checked)
}}>
<span className="font-medium">Show .rooignore'd files in lists and searches</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
When enabled, files matching patterns in .rooignore will be shown in lists with a lock symbol.
When disabled, these files will be completely hidden from file lists and searches.
</p>
</div>
</Section>
</div>
)

View file

@ -82,6 +82,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
telemetrySetting,
terminalOutputLimit,
writeDelayMs,
showRooIgnoredFiles,
} = cachedState
// Make sure apiConfiguration is initialized and managed by SettingsView.
@ -179,6 +180,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds })
vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext })
vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles })
vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
vscode.postMessage({ type: "updateExperimental", values: experiments })
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
@ -400,6 +402,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
maxOpenTabsContext={maxOpenTabsContext}
diffEnabled={diffEnabled}
fuzzyMatchThreshold={fuzzyMatchThreshold}
showRooIgnoredFiles={showRooIgnoredFiles}
setCachedStateField={setCachedStateField}
setExperimentEnabled={setExperimentEnabled}
experiments={experiments}

View file

@ -32,6 +32,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setAlwaysAllowMcp: (value: boolean) => void
setAlwaysAllowModeSwitch: (value: boolean) => void
setBrowserToolEnabled: (value: boolean) => void
setShowRooIgnoredFiles: (value: boolean) => void
setShowAnnouncement: (value: boolean) => void
setAllowedCommands: (value: string[]) => void
setSoundEnabled: (value: boolean) => void
@ -138,6 +139,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
cwd: "",
browserToolEnabled: true,
telemetrySetting: "unset",
showRooIgnoredFiles: true, // Default to showing .rooignore'd files with lock symbol (current behavior)
})
const [didHydrateState, setDidHydrateState] = useState(false)
@ -276,6 +278,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setMaxOpenTabsContext: (value) => setState((prevState) => ({ ...prevState, maxOpenTabsContext: value })),
setBrowserToolEnabled: (value) => setState((prevState) => ({ ...prevState, browserToolEnabled: value })),
setTelemetrySetting: (value) => setState((prevState) => ({ ...prevState, telemetrySetting: value })),
setShowRooIgnoredFiles: (value) => setState((prevState) => ({ ...prevState, showRooIgnoredFiles: value })),
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

View file

@ -9,14 +9,19 @@ import { ApiConfiguration } from "../../../../src/shared/api"
// Test component that consumes the context
const TestComponent = () => {
const { allowedCommands, setAllowedCommands, soundEnabled } = useExtensionState()
const { allowedCommands, setAllowedCommands, soundEnabled, showRooIgnoredFiles, setShowRooIgnoredFiles } =
useExtensionState()
return (
<div>
<div data-testid="allowed-commands">{JSON.stringify(allowedCommands)}</div>
<div data-testid="sound-enabled">{JSON.stringify(soundEnabled)}</div>
<div data-testid="show-rooignored-files">{JSON.stringify(showRooIgnoredFiles)}</div>
<button data-testid="update-button" onClick={() => setAllowedCommands(["npm install", "git status"])}>
Update Commands
</button>
<button data-testid="toggle-rooignore-button" onClick={() => setShowRooIgnoredFiles(!showRooIgnoredFiles)}>
Update Commands
</button>
</div>
)
}
@ -42,6 +47,30 @@ describe("ExtensionStateContext", () => {
expect(JSON.parse(screen.getByTestId("sound-enabled").textContent!)).toBe(false)
})
it("initializes with showRooIgnoredFiles set to true", () => {
render(
<ExtensionStateContextProvider>
<TestComponent />
</ExtensionStateContextProvider>,
)
expect(JSON.parse(screen.getByTestId("show-rooignored-files").textContent!)).toBe(true)
})
it("updates showRooIgnoredFiles through setShowRooIgnoredFiles", () => {
render(
<ExtensionStateContextProvider>
<TestComponent />
</ExtensionStateContextProvider>,
)
act(() => {
screen.getByTestId("toggle-rooignore-button").click()
})
expect(JSON.parse(screen.getByTestId("show-rooignored-files").textContent!)).toBe(false)
})
it("updates allowedCommands through setAllowedCommands", () => {
render(
<ExtensionStateContextProvider>
@ -89,7 +118,8 @@ describe("mergeExtensionState", () => {
customModes: [],
maxOpenTabsContext: 20,
apiConfiguration: { providerId: "openrouter" } as ApiConfiguration,
telemetrySetting: "unset", // Adding the required telemetrySetting property
telemetrySetting: "unset",
showRooIgnoredFiles: true,
}
const prevState: ExtensionState = {