Просмотр исходного кода

Add setting for whether to include ignored files in lists

Matt Rubens 10 месяцев назад
Родитель
Сommit
f9d162e157

+ 10 - 1
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
 			}
 		}

+ 52 - 2
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.")

+ 24 - 14
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",

+ 7 - 0
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,
 		}
 	}
 

+ 22 - 0
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]

+ 1 - 0
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 {

+ 1 - 0
src/shared/WebviewMessage.ts

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

+ 1 - 0
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

+ 22 - 2
webview-ui/src/components/settings/AdvancedSettings.tsx

@@ -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>
 	)

+ 3 - 0
webview-ui/src/components/settings/SettingsView.tsx

@@ -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}

+ 3 - 0
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 <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

+ 32 - 2
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 (
 		<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 = {