feat(settings): add include-files toggle for workspace context

This commit is contained in:
Hannes Rudolph 2025-12-15 08:27:05 -07:00
parent 1d4fc52485
commit 36d46af3e8
10 changed files with 84 additions and 10 deletions

View file

@ -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<typeof globalSettingsSchema>
// Workspace files mode type for controlling what is shown in the workspace files context
export type WorkspaceFilesMode = NonNullable<GlobalSettings["workspaceFilesMode"]>
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.

View file

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

View file

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

View file

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

View file

@ -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",

View file

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

View file

@ -289,6 +289,7 @@ export type ExtensionState = Pick<
| "includeCurrentCost"
| "maxGitStatusFiles"
| "requestDelaySeconds"
| "workspaceFilesMode"
> & {
version: string
clineMessages: ClineMessage[]

View file

@ -18,6 +18,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
listApiConfigMeta: any[]
maxOpenTabsContext: number
maxWorkspaceFiles: number
workspaceFilesMode?: "files" | "folders"
showRooIgnoredFiles?: boolean
maxReadFileLine?: number
maxImageFileSize?: number
@ -35,6 +36,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "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 = ({
<span className="w-10">{maxWorkspaceFiles ?? 200}</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.workspaceFiles.description")}
{workspaceFilesMode === "folders"
? t("settings:contextManagement.workspaceFiles.descriptionFoldersOnly")
: t("settings:contextManagement.workspaceFiles.description")}
</div>
<div className="mt-2">
<VSCodeCheckbox
checked={workspaceFilesMode !== "folders"}
onChange={(e: any) =>
setCachedStateField("workspaceFilesMode", e.target.checked ? "files" : "folders")
}
data-testid="workspace-files-include-files-checkbox">
{t("settings:contextManagement.workspaceFiles.includeFiles")}
</VSCodeCheckbox>
</div>
</div>

View file

@ -211,6 +211,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
includeCurrentTime,
includeCurrentCost,
maxGitStatusFiles,
workspaceFilesMode,
} = cachedState
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@ -394,6 +395,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ onDone, t
listApiConfigMeta={listApiConfigMeta ?? []}
maxOpenTabsContext={maxOpenTabsContext}
maxWorkspaceFiles={maxWorkspaceFiles ?? 200}
workspaceFilesMode={workspaceFilesMode}
showRooIgnoredFiles={showRooIgnoredFiles}
maxReadFileLine={maxReadFileLine}
maxImageFileSize={maxImageFileSize}

View file

@ -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",