Browse Source

Add a new setting to control the number of workspace files included in the system prompt

Matt Rubens 11 months ago
parent
commit
9bbbaf2288

+ 4 - 2
src/core/Cline.ts

@@ -3501,7 +3501,7 @@ export class Cline extends EventEmitter<ClineEvents> {
 	async getEnvironmentDetails(includeFileDetails: boolean = false) {
 		let details = ""
 
-		const { terminalOutputLineLimit } = (await this.providerRef.deref()?.getState()) ?? {}
+		const { terminalOutputLineLimit, maxWorkspaceFiles } = (await this.providerRef.deref()?.getState()) ?? {}
 
 		// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
 		details += "\n\n# VSCode Visible Files"
@@ -3509,6 +3509,7 @@ export class Cline extends EventEmitter<ClineEvents> {
 			?.map((editor) => editor.document?.uri?.fsPath)
 			.filter(Boolean)
 			.map((absolutePath) => path.relative(cwd, absolutePath))
+			.slice(0, maxWorkspaceFiles ?? 200)
 
 		// Filter paths through rooIgnoreController
 		const allowedVisibleFiles = this.rooIgnoreController
@@ -3715,7 +3716,8 @@ export class Cline extends EventEmitter<ClineEvents> {
 				// don't want to immediately access desktop since it would show permission popup
 				details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
 			} else {
-				const [files, didHitLimit] = await listFiles(cwd, true, 200)
+				const maxFiles = maxWorkspaceFiles ?? 200
+				const [files, didHitLimit] = await listFiles(cwd, true, maxFiles)
 				const { showRooIgnoredFiles } = (await this.providerRef.deref()?.getState()) ?? {}
 				const result = formatResponse.formatFilesList(
 					cwd,

+ 8 - 0
src/core/webview/ClineProvider.ts

@@ -1518,6 +1518,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("maxOpenTabsContext", tabCount)
 						await this.postStateToWebview()
 						break
+					case "maxWorkspaceFiles":
+						const fileCount = Math.min(Math.max(0, message.value ?? 200), 500)
+						await this.updateGlobalState("maxWorkspaceFiles", fileCount)
+						await this.postStateToWebview()
+						break
 					case "browserToolEnabled":
 						await this.updateGlobalState("browserToolEnabled", message.bool ?? true)
 						await this.postStateToWebview()
@@ -2297,6 +2302,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			autoApprovalEnabled,
 			experiments,
 			maxOpenTabsContext,
+			maxWorkspaceFiles,
 			browserToolEnabled,
 			telemetrySetting,
 			showRooIgnoredFiles,
@@ -2359,6 +2365,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			experiments: experiments ?? experimentDefault,
 			mcpServers: this.mcpHub?.getAllServers() ?? [],
 			maxOpenTabsContext: maxOpenTabsContext ?? 20,
+			maxWorkspaceFiles: maxWorkspaceFiles ?? 200,
 			cwd,
 			browserToolEnabled: browserToolEnabled ?? true,
 			telemetrySetting,
@@ -2516,6 +2523,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
 			customModes,
 			maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
+			maxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,
 			openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,
 			browserToolEnabled: stateValues.browserToolEnabled ?? true,
 			telemetrySetting: stateValues.telemetrySetting || "unset",

+ 12 - 0
src/core/webview/__tests__/ClineProvider.test.ts

@@ -448,6 +448,7 @@ describe("ClineProvider", () => {
 			customModes: [],
 			experiments: experimentDefault,
 			maxOpenTabsContext: 20,
+			maxWorkspaceFiles: 200,
 			browserToolEnabled: true,
 			telemetrySetting: "unset",
 			showRooIgnoredFiles: true,
@@ -794,6 +795,17 @@ describe("ClineProvider", () => {
 		expect(state.customModePrompts).toEqual({})
 	})
 
+	test("handles maxWorkspaceFiles message", async () => {
+		await provider.resolveWebviewView(mockWebviewView)
+		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
+
+		await messageHandler({ type: "maxWorkspaceFiles", value: 300 })
+
+		expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
+		expect(mockContext.globalState.update).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
+		expect(mockPostMessage).toHaveBeenCalled()
+	})
+
 	test.only("uses mode-specific custom instructions in Cline initialization", async () => {
 		// Setup mock state
 		const modeCustomInstructions = "Code mode instructions"

+ 1 - 0
src/exports/roo-code.d.ts

@@ -211,6 +211,7 @@ export type GlobalStateKey =
 	| "modelMaxTokens"
 	| "mistralCodestralUrl"
 	| "maxOpenTabsContext"
+	| "maxWorkspaceFiles"
 	| "browserToolEnabled"
 	| "lmStudioSpeculativeDecodingEnabled"
 	| "lmStudioDraftModelId"

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -147,6 +147,7 @@ export interface ExtensionState {
 	customModes: ModeConfig[]
 	toolRequirements?: Record<string, boolean> // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled)
 	maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)
+	maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500)
 	cwd?: string // Current working directory
 	telemetrySetting: TelemetrySetting
 	telemetryKey?: string

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -97,6 +97,7 @@ export interface WebviewMessage {
 		| "checkpointRestore"
 		| "deleteMcpServer"
 		| "maxOpenTabsContext"
+		| "maxWorkspaceFiles"
 		| "humanRelayResponse"
 		| "humanRelayCancel"
 		| "browserToolEnabled"

+ 1 - 0
src/shared/globalState.ts

@@ -116,6 +116,7 @@ export const GLOBAL_STATE_KEYS = [
 	"telemetrySetting",
 	"showRooIgnoredFiles",
 	"remoteBrowserEnabled",
+	"maxWorkspaceFiles",
 ] as const
 
 type CheckGlobalStateKeysExhaustiveness =

+ 1 - 0
webview-ui/src/__mocks__/lucide-react.ts

@@ -4,3 +4,4 @@ export const Check = () => React.createElement("div")
 export const ChevronsUpDown = () => React.createElement("div")
 export const Loader = () => React.createElement("div")
 export const X = () => React.createElement("div")
+export const Database = (props: any) => React.createElement("span", { "data-testid": "database-icon", ...props })

+ 33 - 3
webview-ui/src/components/settings/ContextManagementSettings.tsx

@@ -12,13 +12,17 @@ import { Section } from "./Section"
 type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
 	terminalOutputLineLimit?: number
 	maxOpenTabsContext: number
+	maxWorkspaceFiles: number
 	showRooIgnoredFiles?: boolean
-	setCachedStateField: SetCachedStateField<"terminalOutputLineLimit" | "maxOpenTabsContext" | "showRooIgnoredFiles">
+	setCachedStateField: SetCachedStateField<
+		"terminalOutputLineLimit" | "maxOpenTabsContext" | "maxWorkspaceFiles" | "showRooIgnoredFiles"
+	>
 }
 
 export const ContextManagementSettings = ({
 	terminalOutputLineLimit,
 	maxOpenTabsContext,
+	maxWorkspaceFiles,
 	showRooIgnoredFiles,
 	setCachedStateField,
 	className,
@@ -26,7 +30,7 @@ export const ContextManagementSettings = ({
 }: ContextManagementSettingsProps) => {
 	return (
 		<div className={cn("flex flex-col gap-2", className)} {...props}>
-			<SectionHeader>
+			<SectionHeader description="Control what information is included in the AI's context window, affecting token usage and response quality">
 				<div className="flex items-center gap-2">
 					<Database className="w-4" />
 					<div>Context Management</div>
@@ -48,6 +52,7 @@ export const ContextManagementSettings = ({
 									setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value))
 								}
 								className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+								data-testid="terminal-output-limit-slider"
 							/>
 							<span style={{ ...sliderLabelStyle }}>{terminalOutputLineLimit ?? 500}</span>
 						</div>
@@ -70,6 +75,7 @@ export const ContextManagementSettings = ({
 								value={maxOpenTabsContext ?? 20}
 								onChange={(e) => setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))}
 								className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+								data-testid="open-tabs-limit-slider"
 							/>
 							<span style={{ ...sliderLabelStyle }}>{maxOpenTabsContext ?? 20}</span>
 						</div>
@@ -80,12 +86,36 @@ export const ContextManagementSettings = ({
 					</p>
 				</div>
 
+				<div>
+					<div className="flex flex-col gap-2">
+						<span className="font-medium">Workspace files context limit</span>
+						<div className="flex items-center gap-2">
+							<input
+								type="range"
+								min="0"
+								max="500"
+								step="1"
+								value={maxWorkspaceFiles ?? 200}
+								onChange={(e) => setCachedStateField("maxWorkspaceFiles", parseInt(e.target.value))}
+								className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+								data-testid="workspace-files-limit-slider"
+							/>
+							<span style={{ ...sliderLabelStyle }}>{maxWorkspaceFiles ?? 200}</span>
+						</div>
+					</div>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Maximum number of files to include in current working directory details. Higher values provide
+						more context but increase token usage.
+					</p>
+				</div>
+
 				<div>
 					<VSCodeCheckbox
 						checked={showRooIgnoredFiles}
 						onChange={(e: any) => {
 							setCachedStateField("showRooIgnoredFiles", e.target.checked)
-						}}>
+						}}
+						data-testid="show-rooignored-files-checkbox">
 						<span className="font-medium">Show .rooignore'd files in lists and searches</span>
 					</VSCodeCheckbox>
 					<p className="text-vscode-descriptionForeground text-sm mt-0">

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

@@ -85,6 +85,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		experiments,
 		fuzzyMatchThreshold,
 		maxOpenTabsContext,
+		maxWorkspaceFiles,
 		mcpEnabled,
 		rateLimitSeconds,
 		requestDelaySeconds,
@@ -196,6 +197,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: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 })
 			vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles })
 			vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
 			vscode.postMessage({ type: "updateExperimental", values: experiments })
@@ -410,6 +412,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 					<ContextManagementSettings
 						terminalOutputLineLimit={terminalOutputLineLimit}
 						maxOpenTabsContext={maxOpenTabsContext}
+						maxWorkspaceFiles={maxWorkspaceFiles ?? 200}
 						showRooIgnoredFiles={showRooIgnoredFiles}
 						setCachedStateField={setCachedStateField}
 					/>

+ 20 - 6
webview-ui/src/components/settings/__tests__/ContextManagementSettings.test.tsx

@@ -5,6 +5,7 @@ describe("ContextManagementSettings", () => {
 	const defaultProps = {
 		terminalOutputLineLimit: 500,
 		maxOpenTabsContext: 20,
+		maxWorkspaceFiles: 200,
 		showRooIgnoredFiles: false,
 		setCachedStateField: jest.fn(),
 	}
@@ -18,21 +19,25 @@ describe("ContextManagementSettings", () => {
 
 		// Terminal output limit
 		expect(screen.getByText("Terminal output limit")).toBeInTheDocument()
-		expect(screen.getByRole("slider", { name: /Terminal output limit/i })).toHaveValue("500")
+		expect(screen.getByTestId("terminal-output-limit-slider")).toHaveValue("500")
 
 		// Open tabs context limit
 		expect(screen.getByText("Open tabs context limit")).toBeInTheDocument()
-		expect(screen.getByRole("slider", { name: /Open tabs context limit/i })).toHaveValue("20")
+		expect(screen.getByTestId("open-tabs-limit-slider")).toHaveValue("20")
+
+		// Workspace files limit
+		expect(screen.getByText("Workspace files context limit")).toBeInTheDocument()
+		expect(screen.getByTestId("workspace-files-limit-slider")).toHaveValue("200")
 
 		// Show .rooignore'd files
 		expect(screen.getByText("Show .rooignore'd files in lists and searches")).toBeInTheDocument()
-		expect(screen.getByRole("checkbox")).not.toBeChecked()
+		expect(screen.getByTestId("show-rooignored-files-checkbox")).not.toBeChecked()
 	})
 
 	it("updates terminal output limit", () => {
 		render(<ContextManagementSettings {...defaultProps} />)
 
-		const slider = screen.getByRole("slider", { name: /Terminal output limit/i })
+		const slider = screen.getByTestId("terminal-output-limit-slider")
 		fireEvent.change(slider, { target: { value: "1000" } })
 
 		expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("terminalOutputLineLimit", 1000)
@@ -41,16 +46,25 @@ describe("ContextManagementSettings", () => {
 	it("updates open tabs context limit", () => {
 		render(<ContextManagementSettings {...defaultProps} />)
 
-		const slider = screen.getByRole("slider", { name: /Open tabs context limit/i })
+		const slider = screen.getByTestId("open-tabs-limit-slider")
 		fireEvent.change(slider, { target: { value: "50" } })
 
 		expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("maxOpenTabsContext", 50)
 	})
 
+	it("updates workspace files contextlimit", () => {
+		render(<ContextManagementSettings {...defaultProps} />)
+
+		const slider = screen.getByTestId("workspace-files-limit-slider")
+		fireEvent.change(slider, { target: { value: "50" } })
+
+		expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("maxWorkspaceFiles", 50)
+	})
+
 	it("updates show rooignored files setting", () => {
 		render(<ContextManagementSettings {...defaultProps} />)
 
-		const checkbox = screen.getByRole("checkbox")
+		const checkbox = screen.getByTestId("show-rooignored-files-checkbox")
 		fireEvent.click(checkbox)
 
 		expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("showRooIgnoredFiles", true)

+ 4 - 0
webview-ui/src/context/ExtensionStateContext.tsx

@@ -69,6 +69,8 @@ export interface ExtensionStateContextType extends ExtensionState {
 	customModes: ModeConfig[]
 	setCustomModes: (value: ModeConfig[]) => void
 	setMaxOpenTabsContext: (value: number) => void
+	maxWorkspaceFiles: number
+	setMaxWorkspaceFiles: (value: number) => void
 	setTelemetrySetting: (value: TelemetrySetting) => void
 	remoteBrowserEnabled?: boolean
 	setRemoteBrowserEnabled: (value: boolean) => void
@@ -137,6 +139,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		autoApprovalEnabled: false,
 		customModes: [],
 		maxOpenTabsContext: 20,
+		maxWorkspaceFiles: 200,
 		cwd: "",
 		browserToolEnabled: true,
 		telemetrySetting: "unset",
@@ -280,6 +283,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
 		setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
 		setMaxOpenTabsContext: (value) => setState((prevState) => ({ ...prevState, maxOpenTabsContext: value })),
+		setMaxWorkspaceFiles: (value) => setState((prevState) => ({ ...prevState, maxWorkspaceFiles: value })),
 		setBrowserToolEnabled: (value) => setState((prevState) => ({ ...prevState, browserToolEnabled: value })),
 		setTelemetrySetting: (value) => setState((prevState) => ({ ...prevState, telemetrySetting: value })),
 		setShowRooIgnoredFiles: (value) => setState((prevState) => ({ ...prevState, showRooIgnoredFiles: value })),

+ 1 - 0
webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx

@@ -116,6 +116,7 @@ describe("mergeExtensionState", () => {
 			experiments: {} as Record<ExperimentId, boolean>,
 			customModes: [],
 			maxOpenTabsContext: 20,
+			maxWorkspaceFiles: 100,
 			apiConfiguration: { providerId: "openrouter" } as ApiConfiguration,
 			telemetrySetting: "unset",
 			showRooIgnoredFiles: true,