diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index a11fae1e11..db06435e67 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -142,6 +142,13 @@ export const globalSettingsSchema = z.object({ maxOpenTabsContext: z.number().optional(), maxWorkspaceFiles: z.number().optional(), + /** + * Controls what is shown in the workspace files context. + * - "files": Shows both files and directories (default, current behavior) + * - "folders": Shows only the folder structure + * @default "files" + */ + workspaceFilesMode: z.enum(["files", "folders"]).optional(), showRooIgnoredFiles: z.boolean().optional(), maxReadFileLine: z.number().optional(), maxImageFileSize: z.number().optional(), @@ -200,6 +207,9 @@ export const globalSettingsSchema = z.object({ export type GlobalSettings = z.infer +// Workspace files mode type for controlling what is shown in the workspace files context +export type WorkspaceFilesMode = NonNullable + export const GLOBAL_SETTINGS_KEYS = globalSettingsSchema.keyof().options /** @@ -349,6 +359,7 @@ export const EVALS_SETTINGS: RooCodeSettings = { rateLimitSeconds: 0, maxOpenTabsContext: 20, maxWorkspaceFiles: 200, + workspaceFilesMode: "files", maxGitStatusFiles: 20, showRooIgnoredFiles: true, maxReadFileLine: -1, // -1 to enable full file reading. diff --git a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts index 65b447ff16..9eaaadb7bd 100644 --- a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts +++ b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts @@ -175,7 +175,7 @@ describe("getEnvironmentDetails", () => { expect(result).toContain("# Current Workspace Directory") expect(result).toContain("Files") - expect(listFiles).toHaveBeenCalledWith(mockCwd, true, 50) + expect(listFiles).toHaveBeenCalledWith(mockCwd, true, 50, false) expect(formatResponse.formatFilesList).toHaveBeenCalledWith( mockCwd, @@ -183,6 +183,8 @@ describe("getEnvironmentDetails", () => { false, mockCline.rooIgnoreController, false, + undefined, + false, ) }) diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index 64cd93737f..a0c929249a 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -284,7 +284,9 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo } if (includeFileDetails) { - details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) Files\n` + const workspaceFilesMode = state?.workspaceFilesMode ?? "files" + const headerText = workspaceFilesMode === "folders" ? "Folder Structure" : "Files" + details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) ${headerText}\n` const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop")) if (isDesktop) { @@ -298,7 +300,8 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo if (maxFiles === 0) { details += "(Workspace files context disabled. Use list_files to explore if needed.)" } else { - const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles) + const foldersOnly = workspaceFilesMode === "folders" + const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles, foldersOnly) const { showRooIgnoredFiles = false } = state ?? {} const result = formatResponse.formatFilesList( @@ -307,6 +310,8 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo didHitLimit, cline.rooIgnoreController, showRooIgnoredFiles, + undefined, + foldersOnly, ) details += result diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index ccb09e68e1..e307804163 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -163,6 +163,7 @@ Otherwise, if you have not completed the task and do not need additional informa rooIgnoreController: RooIgnoreController | undefined, showRooIgnoredFiles: boolean, rooProtectedController?: RooProtectedController, + foldersOnly: boolean = false, ): string => { const sorted = files .map((file) => { @@ -222,11 +223,12 @@ Otherwise, if you have not completed the task and do not need additional informa } } if (didHitLimit) { - return `${rooIgnoreParsed.join( - "\n", - )}\n\n(File list truncated. Use list_files on specific subdirectories if you need to explore further.)` + const truncationMessage = foldersOnly + ? "(Folder list truncated. Use list_files on specific subdirectories if you need to explore further.)" + : "(File list truncated. Use list_files on specific subdirectories if you need to explore further.)" + return `${rooIgnoreParsed.join("\n")}\n\n${truncationMessage}` } else if (rooIgnoreParsed.length === 0 || (rooIgnoreParsed.length === 1 && rooIgnoreParsed[0] === "")) { - return "No files found." + return foldersOnly ? "No folders found." : "No files found." } else { return rooIgnoreParsed.join("\n") } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 715a830fb6..03e5f79380 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1859,6 +1859,7 @@ export class ClineProvider experiments, maxOpenTabsContext, maxWorkspaceFiles, + workspaceFilesMode, browserToolEnabled, telemetrySetting, showRooIgnoredFiles, @@ -2003,6 +2004,7 @@ export class ClineProvider mcpServers: this.mcpHub?.getAllServers() ?? [], maxOpenTabsContext: maxOpenTabsContext ?? 20, maxWorkspaceFiles: maxWorkspaceFiles ?? 200, + workspaceFilesMode: workspaceFilesMode ?? "files", cwd, browserToolEnabled: browserToolEnabled ?? true, telemetrySetting, @@ -2237,6 +2239,7 @@ export class ClineProvider customModes, maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20, maxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200, + workspaceFilesMode: stateValues.workspaceFilesMode ?? "files", openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform, browserToolEnabled: stateValues.browserToolEnabled ?? true, telemetrySetting: stateValues.telemetrySetting || "unset", diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 5366bbb84b..d6f076a82d 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -28,9 +28,15 @@ interface ScanContext { * @param dirPath - Directory path to list files from * @param recursive - Whether to recursively list files in subdirectories * @param limit - Maximum number of files to return + * @param foldersOnly - When true, only returns directories (no files) * @returns Tuple of [file paths array, whether the limit was reached] */ -export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> { +export async function listFiles( + dirPath: string, + recursive: boolean, + limit: number, + foldersOnly: boolean = false, +): Promise<[string[], boolean]> { // Early return for limit of 0 - no need to scan anything if (limit === 0) { return [[], false] @@ -43,6 +49,25 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb return specialResult } + // For folders-only mode, skip file scanning entirely + if (foldersOnly) { + const ignoreInstance = await createIgnoreInstance(dirPath) + const directories = await listFilteredDirectories(dirPath, recursive, ignoreInstance, limit) + + // Sort and format directories + const sortedDirs = directories.sort((a, b) => a.localeCompare(b)) + const trimmedDirs = sortedDirs.slice(0, limit) + const limitReached = trimmedDirs.length >= limit + + // If we hit the limit, ensure all first-level directories are included + if (limitReached && recursive) { + const firstLevelDirs = await getFirstLevelDirectories(dirPath, ignoreInstance) + return ensureFirstLevelDirectoriesIncluded(trimmedDirs, firstLevelDirs, limit) + } + + return [trimmedDirs, limitReached] + } + // Get ripgrep path const rgPath = await getRipgrepPath() diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index f5d4078e48..b6b0fdfb42 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -289,6 +289,7 @@ export type ExtensionState = Pick< | "includeCurrentCost" | "maxGitStatusFiles" | "requestDelaySeconds" + | "workspaceFilesMode" > & { version: string clineMessages: ClineMessage[] diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index fb2ca08b1b..92727ebc62 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -18,6 +18,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { listApiConfigMeta: any[] maxOpenTabsContext: number maxWorkspaceFiles: number + workspaceFilesMode?: "files" | "folders" showRooIgnoredFiles?: boolean maxReadFileLine?: number maxImageFileSize?: number @@ -35,6 +36,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "autoCondenseContextPercent" | "maxOpenTabsContext" | "maxWorkspaceFiles" + | "workspaceFilesMode" | "showRooIgnoredFiles" | "maxReadFileLine" | "maxImageFileSize" @@ -56,6 +58,7 @@ export const ContextManagementSettings = ({ listApiConfigMeta, maxOpenTabsContext, maxWorkspaceFiles, + workspaceFilesMode, showRooIgnoredFiles, setCachedStateField, maxReadFileLine, @@ -145,7 +148,19 @@ export const ContextManagementSettings = ({ {maxWorkspaceFiles ?? 200}
- {t("settings:contextManagement.workspaceFiles.description")} + {workspaceFilesMode === "folders" + ? t("settings:contextManagement.workspaceFiles.descriptionFoldersOnly") + : t("settings:contextManagement.workspaceFiles.description")} +
+
+ + setCachedStateField("workspaceFilesMode", e.target.checked ? "files" : "folders") + } + data-testid="workspace-files-include-files-checkbox"> + {t("settings:contextManagement.workspaceFiles.includeFiles")} +
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index f5676ff9a8..56c46df59b 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -211,6 +211,7 @@ const SettingsView = forwardRef(({ onDone, t includeCurrentTime, includeCurrentCost, maxGitStatusFiles, + workspaceFilesMode, } = cachedState const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) @@ -394,6 +395,7 @@ const SettingsView = forwardRef(({ onDone, t mcpEnabled, maxOpenTabsContext: Math.min(Math.max(0, maxOpenTabsContext ?? 20), 500), maxWorkspaceFiles: Math.min(Math.max(0, maxWorkspaceFiles ?? 200), 500), + workspaceFilesMode: workspaceFilesMode ?? "files", showRooIgnoredFiles: showRooIgnoredFiles ?? true, maxReadFileLine: maxReadFileLine ?? -1, maxImageFileSize: maxImageFileSize ?? 5, @@ -770,6 +772,7 @@ const SettingsView = forwardRef(({ onDone, t listApiConfigMeta={listApiConfigMeta ?? []} maxOpenTabsContext={maxOpenTabsContext} maxWorkspaceFiles={maxWorkspaceFiles ?? 200} + workspaceFilesMode={workspaceFilesMode} showRooIgnoredFiles={showRooIgnoredFiles} maxReadFileLine={maxReadFileLine} maxImageFileSize={maxImageFileSize} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 56488602b4..e541b57a31 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -610,7 +610,14 @@ }, "workspaceFiles": { "label": "Workspace files context limit", - "description": "Maximum number of files to include in current working directory details. Higher values provide more context but increase token usage." + "description": "Maximum number of files to include in current working directory details. Higher values provide more context but increase token usage.", + "descriptionFoldersOnly": "Maximum number of folders to include in current working directory details. Folders-only mode provides structural overview without file listings.", + "includeFiles": "Include individual files (uncheck for folders only)", + "mode": { + "placeholder": "Select display mode", + "filesAndFolders": "Files and Folders", + "foldersOnly": "Folders Only" + } }, "rooignore": { "label": "Show .rooignore'd files in lists and searches",