diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 16f1d4e99d2..e9353c0c776 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -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 } } diff --git a/src/core/prompts/__tests__/responses-rooignore.test.ts b/src/core/prompts/__tests__/responses-rooignore.test.ts index 23361f2fa59..37b3050dd05 100644 --- a/src/core/prompts/__tests__/responses-rooignore.test.ts +++ b/src/core/prompts/__tests__/responses-rooignore.test.ts @@ -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.") diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 9525b46eac2..3a1eb92a2ee 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -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 + + 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 } - - return filePath - }) - : sorted + // Otherwise, mark it with a lock symbol + rooIgnoreParsed.push(LOCK_TEXT_SYMBOL + " " + filePath) + } else { + rooIgnoreParsed.push(filePath) + } + } + } if (didHitLimit) { return `${rooIgnoreParsed.join( "\n", diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6f656698bd8..e53b1fbf798 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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, } } diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index c0c7b5ad3e0..f9fc5d3ece5 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -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] diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 074f4d63f8f..98ff9b36e15 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -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 { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 2bf57640883..10af6f7a946 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -99,6 +99,7 @@ export interface WebviewMessage { | "humanRelayCancel" | "browserToolEnabled" | "telemetrySetting" + | "showRooIgnoredFiles" text?: string disabled?: boolean askResponse?: ClineAskResponse diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index 7cb1b594593..bfd24f42984 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -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 diff --git a/webview-ui/src/components/settings/AdvancedSettings.tsx b/webview-ui/src/components/settings/AdvancedSettings.tsx index ec78699c42d..f20e9a7c334 100644 --- a/webview-ui/src/components/settings/AdvancedSettings.tsx +++ b/webview-ui/src/components/settings/AdvancedSettings.tsx @@ -18,19 +18,25 @@ type AdvancedSettingsProps = HTMLAttributes & { maxOpenTabsContext: number diffEnabled?: boolean fuzzyMatchThreshold?: number + showRooIgnoredFiles?: boolean setCachedStateField: SetCachedStateField< - "rateLimitSeconds" | "terminalOutputLimit" | "maxOpenTabsContext" | "diffEnabled" | "fuzzyMatchThreshold" + | "rateLimitSeconds" + | "terminalOutputLimit" + | "maxOpenTabsContext" + | "diffEnabled" + | "fuzzyMatchThreshold" + | "showRooIgnoredFiles" > experiments: Record setExperimentEnabled: SetExperimentEnabled } - export const AdvancedSettings = ({ rateLimitSeconds, terminalOutputLimit = TERMINAL_OUTPUT_LIMIT, maxOpenTabsContext, diffEnabled, fuzzyMatchThreshold, + showRooIgnoredFiles, setCachedStateField, experiments, setExperimentEnabled, @@ -197,6 +203,20 @@ export const AdvancedSettings = ({ )} + +
+ { + setCachedStateField("showRooIgnoredFiles", e.target.checked) + }}> + Show .rooignore'd files in lists and searches + +

+ 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. +

+
) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 2ed5746877a..604ef92df34 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -82,6 +82,7 @@ const SettingsView = forwardRef(({ onDone }, telemetrySetting, terminalOutputLimit, writeDelayMs, + showRooIgnoredFiles, } = cachedState // Make sure apiConfiguration is initialized and managed by SettingsView. @@ -179,6 +180,7 @@ const SettingsView = forwardRef(({ 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(({ onDone }, maxOpenTabsContext={maxOpenTabsContext} diffEnabled={diffEnabled} fuzzyMatchThreshold={fuzzyMatchThreshold} + showRooIgnoredFiles={showRooIgnoredFiles} setCachedStateField={setCachedStateField} setExperimentEnabled={setExperimentEnabled} experiments={experiments} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 6d520f0543a..c4daf426ca1 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -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 {children} diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx index 717242b538b..0f66413b502 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx @@ -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 (
{JSON.stringify(allowedCommands)}
{JSON.stringify(soundEnabled)}
+
{JSON.stringify(showRooIgnoredFiles)}
+
) } @@ -42,6 +47,30 @@ describe("ExtensionStateContext", () => { expect(JSON.parse(screen.getByTestId("sound-enabled").textContent!)).toBe(false) }) + it("initializes with showRooIgnoredFiles set to true", () => { + render( + + + , + ) + + expect(JSON.parse(screen.getByTestId("show-rooignored-files").textContent!)).toBe(true) + }) + + it("updates showRooIgnoredFiles through setShowRooIgnoredFiles", () => { + render( + + + , + ) + + act(() => { + screen.getByTestId("toggle-rooignore-button").click() + }) + + expect(JSON.parse(screen.getByTestId("show-rooignored-files").textContent!)).toBe(false) + }) + it("updates allowedCommands through setAllowedCommands", () => { render( @@ -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 = {