diff --git a/src/core/Cline.ts b/src/core/Cline.ts index c50d48b6cc1..8b5afe98063 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -3501,7 +3501,7 @@ export class Cline extends EventEmitter { 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 { ?.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 { // 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, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b98934400d2..87f33dc4180 100644 --- a/src/core/webview/ClineProvider.ts +++ b/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", diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index abe7a8475af..7530052b85d 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/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" diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index e5338bd7372..1c45dec84bc 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -211,6 +211,7 @@ export type GlobalStateKey = | "modelMaxTokens" | "mistralCodestralUrl" | "maxOpenTabsContext" + | "maxWorkspaceFiles" | "browserToolEnabled" | "lmStudioSpeculativeDecodingEnabled" | "lmStudioDraftModelId" diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index c0bcb4ed65a..5b018ec22ee 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -147,6 +147,7 @@ export interface ExtensionState { customModes: ModeConfig[] toolRequirements?: Record // 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 diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index e9a64d891b3..bb77ea7203a 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -97,6 +97,7 @@ export interface WebviewMessage { | "checkpointRestore" | "deleteMcpServer" | "maxOpenTabsContext" + | "maxWorkspaceFiles" | "humanRelayResponse" | "humanRelayCancel" | "browserToolEnabled" diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index 9896c376974..cec7bdf9cee 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -116,6 +116,7 @@ export const GLOBAL_STATE_KEYS = [ "telemetrySetting", "showRooIgnoredFiles", "remoteBrowserEnabled", + "maxWorkspaceFiles", ] as const type CheckGlobalStateKeysExhaustiveness = diff --git a/webview-ui/src/__mocks__/lucide-react.ts b/webview-ui/src/__mocks__/lucide-react.ts index d85cd25d6a7..64ab05eb341 100644 --- a/webview-ui/src/__mocks__/lucide-react.ts +++ b/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 }) diff --git a/webview-ui/src/components/settings/AdvancedSettings.tsx b/webview-ui/src/components/settings/AdvancedSettings.tsx index c04f28f88ea..e25366331e8 100644 --- a/webview-ui/src/components/settings/AdvancedSettings.tsx +++ b/webview-ui/src/components/settings/AdvancedSettings.tsx @@ -13,29 +13,16 @@ import { Section } from "./Section" type AdvancedSettingsProps = HTMLAttributes & { rateLimitSeconds: number - terminalOutputLineLimit?: number - maxOpenTabsContext: number diffEnabled?: boolean fuzzyMatchThreshold?: number - showRooIgnoredFiles?: boolean - setCachedStateField: SetCachedStateField< - | "rateLimitSeconds" - | "terminalOutputLineLimit" - | "maxOpenTabsContext" - | "diffEnabled" - | "fuzzyMatchThreshold" - | "showRooIgnoredFiles" - > + setCachedStateField: SetCachedStateField<"rateLimitSeconds" | "diffEnabled" | "fuzzyMatchThreshold"> experiments: Record setExperimentEnabled: SetExperimentEnabled } export const AdvancedSettings = ({ rateLimitSeconds, - terminalOutputLineLimit, - maxOpenTabsContext, diffEnabled, fuzzyMatchThreshold, - showRooIgnoredFiles, setCachedStateField, experiments, setExperimentEnabled, @@ -71,52 +58,6 @@ export const AdvancedSettings = ({

Minimum time between API requests.

-
-
- Terminal output limit -
- - setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value)) - } - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - /> - {terminalOutputLineLimit ?? 500} -
-
-

- Maximum number of lines to include in terminal output when executing commands. When exceeded - lines will be removed from the middle, saving tokens. -

-
- -
-
- Open tabs context limit -
- setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))} - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - /> - {maxOpenTabsContext ?? 20} -
-
-

- Maximum number of VSCode open tabs to include in context. Higher values provide more context but - increase token usage. -

-
-
)}
- -
- { - 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/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx new file mode 100644 index 00000000000..d5f4c33f9a0 --- /dev/null +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -0,0 +1,129 @@ +import { HTMLAttributes } from "react" +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { Database } from "lucide-react" + +import { cn } from "@/lib/utils" + +import { SetCachedStateField } from "./types" +import { sliderLabelStyle } from "./styles" +import { SectionHeader } from "./SectionHeader" +import { Section } from "./Section" + +type ContextManagementSettingsProps = HTMLAttributes & { + terminalOutputLineLimit?: number + maxOpenTabsContext: number + maxWorkspaceFiles: number + showRooIgnoredFiles?: boolean + setCachedStateField: SetCachedStateField< + "terminalOutputLineLimit" | "maxOpenTabsContext" | "maxWorkspaceFiles" | "showRooIgnoredFiles" + > +} + +export const ContextManagementSettings = ({ + terminalOutputLineLimit, + maxOpenTabsContext, + maxWorkspaceFiles, + showRooIgnoredFiles, + setCachedStateField, + className, + ...props +}: ContextManagementSettingsProps) => { + return ( +
+ +
+ +
Context Management
+
+
+ +
+
+
+ Terminal output limit +
+ + 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" + /> + {terminalOutputLineLimit ?? 500} +
+
+

+ Maximum number of lines to include in terminal output when executing commands. When exceeded + lines will be removed from the middle, saving tokens. +

+
+ +
+
+ Open tabs context limit +
+ 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" + /> + {maxOpenTabsContext ?? 20} +
+
+

+ Maximum number of VSCode open tabs to include in context. Higher values provide more context but + increase token usage. +

+
+ +
+
+ Workspace files context limit +
+ 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" + /> + {maxWorkspaceFiles ?? 200} +
+
+

+ Maximum number of files to include in current working directory details. Higher values provide + more context but increase token usage. +

+
+ +
+ { + setCachedStateField("showRooIgnoredFiles", e.target.checked) + }} + data-testid="show-rooignored-files-checkbox"> + 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 d49ed2d03a2..1cdc1d00fa4 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -11,6 +11,7 @@ import { AlertTriangle, } from "lucide-react" +import { Database } from "lucide-react" import { ExperimentId } from "../../../../src/shared/experiments" import { TelemetrySetting } from "../../../../src/shared/TelemetrySetting" import { ApiConfiguration } from "../../../../src/shared/api" @@ -39,6 +40,7 @@ import { AutoApproveSettings } from "./AutoApproveSettings" import { BrowserSettings } from "./BrowserSettings" import { CheckpointSettings } from "./CheckpointSettings" import { NotificationSettings } from "./NotificationSettings" +import { ContextManagementSettings } from "./ContextManagementSettings" import { AdvancedSettings } from "./AdvancedSettings" import { SettingsFooter } from "./SettingsFooter" import { Section } from "./Section" @@ -83,6 +85,7 @@ const SettingsView = forwardRef(({ onDone }, experiments, fuzzyMatchThreshold, maxOpenTabsContext, + maxWorkspaceFiles, mcpEnabled, rateLimitSeconds, requestDelaySeconds, @@ -194,6 +197,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: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 }) vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles }) vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName }) vscode.postMessage({ type: "updateExperimental", values: experiments }) @@ -230,6 +234,7 @@ const SettingsView = forwardRef(({ onDone }, const browserRef = useRef(null) const checkpointRef = useRef(null) const notificationsRef = useRef(null) + const contextRef = useRef(null) const advancedRef = useRef(null) const experimentalRef = useRef(null) @@ -242,6 +247,7 @@ const SettingsView = forwardRef(({ onDone }, { id: "browser", icon: SquareMousePointer, ref: browserRef }, { id: "checkpoint", icon: GitBranch, ref: checkpointRef }, { id: "notifications", icon: Bell, ref: notificationsRef }, + { id: "context", icon: Database, ref: contextRef }, { id: "advanced", icon: Cog, ref: advancedRef }, { id: "experimental", icon: FlaskConical, ref: experimentalRef }, ], @@ -255,6 +261,7 @@ const SettingsView = forwardRef(({ onDone }, { ref: browserRef, id: "browser" }, { ref: checkpointRef, id: "checkpoint" }, { ref: notificationsRef, id: "notifications" }, + { ref: contextRef, id: "context" }, { ref: advancedRef, id: "advanced" }, { ref: experimentalRef, id: "experimental" }, ] @@ -401,14 +408,21 @@ const SettingsView = forwardRef(({ onDone }, /> +
+ +
+
{ + const defaultProps = { + terminalOutputLineLimit: 500, + maxOpenTabsContext: 20, + maxWorkspaceFiles: 200, + showRooIgnoredFiles: false, + setCachedStateField: jest.fn(), + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("renders all controls", () => { + render() + + // Terminal output limit + expect(screen.getByText("Terminal output limit")).toBeInTheDocument() + expect(screen.getByTestId("terminal-output-limit-slider")).toHaveValue("500") + + // Open tabs context limit + expect(screen.getByText("Open tabs context limit")).toBeInTheDocument() + 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.getByTestId("show-rooignored-files-checkbox")).not.toBeChecked() + }) + + it("updates terminal output limit", () => { + render() + + const slider = screen.getByTestId("terminal-output-limit-slider") + fireEvent.change(slider, { target: { value: "1000" } }) + + expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("terminalOutputLineLimit", 1000) + }) + + it("updates open tabs context limit", () => { + render() + + 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() + + 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() + + const checkbox = screen.getByTestId("show-rooignored-files-checkbox") + fireEvent.click(checkbox) + + expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("showRooIgnoredFiles", true) + }) +}) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index d79f263e836..b05a1a9f247 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/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 })), diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx index f8acf4f4d3e..6b7742ba065 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx @@ -116,6 +116,7 @@ describe("mergeExtensionState", () => { experiments: {} as Record, customModes: [], maxOpenTabsContext: 20, + maxWorkspaceFiles: 100, apiConfiguration: { providerId: "openrouter" } as ApiConfiguration, telemetrySetting: "unset", showRooIgnoredFiles: true,