mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Merge branch 'RooCodeInc:main' into main
This commit is contained in:
commit
80413c00c5
10 changed files with 110 additions and 131 deletions
|
|
@ -8,7 +8,10 @@ export const CODEBASE_INDEX_DEFAULTS = {
|
|||
MAX_SEARCH_RESULTS: 200,
|
||||
DEFAULT_SEARCH_RESULTS: 50,
|
||||
SEARCH_RESULTS_STEP: 10,
|
||||
MIN_SEARCH_SCORE: 0,
|
||||
MAX_SEARCH_SCORE: 1,
|
||||
DEFAULT_SEARCH_MIN_SCORE: 0.4,
|
||||
SEARCH_SCORE_STEP: 0.05,
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -190,6 +190,19 @@ describe("getEnvironmentDetails", () => {
|
|||
expect(listFiles).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should skip file listing when maxWorkspaceFiles is 0", async () => {
|
||||
mockProvider.getState.mockResolvedValue({
|
||||
...mockState,
|
||||
maxWorkspaceFiles: 0,
|
||||
})
|
||||
|
||||
const result = await getEnvironmentDetails(mockCline as Task, true)
|
||||
|
||||
expect(listFiles).not.toHaveBeenCalled()
|
||||
expect(result).toContain("Workspace files context disabled")
|
||||
expect(formatResponse.formatFilesList).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should include recently modified files if any", async () => {
|
||||
;(mockCline.fileContextTracker!.getAndClearRecentlyModifiedFiles as Mock).mockReturnValue([
|
||||
"modified1.ts",
|
||||
|
|
|
|||
|
|
@ -252,18 +252,24 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
|
|||
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
|
||||
} else {
|
||||
const maxFiles = maxWorkspaceFiles ?? 200
|
||||
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles)
|
||||
const { showRooIgnoredFiles = true } = state ?? {}
|
||||
|
||||
const result = formatResponse.formatFilesList(
|
||||
cline.cwd,
|
||||
files,
|
||||
didHitLimit,
|
||||
cline.rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
)
|
||||
// Early return for limit of 0
|
||||
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 { showRooIgnoredFiles = true } = state ?? {}
|
||||
|
||||
details += result
|
||||
const result = formatResponse.formatFilesList(
|
||||
cline.cwd,
|
||||
files,
|
||||
didHitLimit,
|
||||
cline.rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
)
|
||||
|
||||
details += result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1841,6 +1841,7 @@ export const webviewMessageHandler = async (
|
|||
codebaseIndexOpenAiCompatibleBaseUrl: settings.codebaseIndexOpenAiCompatibleBaseUrl,
|
||||
codebaseIndexOpenAiCompatibleModelDimension: settings.codebaseIndexOpenAiCompatibleModelDimension,
|
||||
codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults,
|
||||
codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
|
||||
}
|
||||
|
||||
// Save global state first
|
||||
|
|
|
|||
|
|
@ -30,7 +30,12 @@ const mockResolve = (dirPath: string): string => {
|
|||
* @param limit - Maximum number of files to return
|
||||
* @returns Promise resolving to [file paths, limit reached flag]
|
||||
*/
|
||||
export const listFiles = vi.fn((dirPath: string, _recursive: boolean, _limit: number) => {
|
||||
export const listFiles = vi.fn((dirPath: string, _recursive: boolean, limit: number) => {
|
||||
// Early return for limit of 0 - matches the actual implementation
|
||||
if (limit === 0) {
|
||||
return Promise.resolve([[], false])
|
||||
}
|
||||
|
||||
// Special case: Root or home directories
|
||||
// Prevents tests from trying to list all files in these directories
|
||||
if (dirPath === "/" || dirPath === "/root" || dirPath === "/home/user") {
|
||||
|
|
|
|||
|
|
@ -1,123 +1,18 @@
|
|||
import { vi, describe, it, expect, beforeEach } from "vitest"
|
||||
import * as path from "path"
|
||||
|
||||
// Mock ripgrep to avoid filesystem dependencies
|
||||
vi.mock("../../ripgrep", () => ({
|
||||
getBinPath: vi.fn().mockResolvedValue("/mock/path/to/rg"),
|
||||
}))
|
||||
|
||||
// Mock vscode
|
||||
vi.mock("vscode", () => ({
|
||||
env: {
|
||||
appRoot: "/mock/app/root",
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock filesystem operations
|
||||
vi.mock("fs", () => ({
|
||||
promises: {
|
||||
access: vi.fn().mockRejectedValue(new Error("Not found")),
|
||||
readFile: vi.fn().mockResolvedValue(""),
|
||||
readdir: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("child_process", () => ({
|
||||
spawn: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("../../path", () => ({
|
||||
arePathsEqual: vi.fn().mockReturnValue(false),
|
||||
}))
|
||||
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import { listFiles } from "../list-files"
|
||||
import * as childProcess from "child_process"
|
||||
|
||||
describe("list-files symlink support", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.mock("../list-files", async () => {
|
||||
const actual = await vi.importActual("../list-files")
|
||||
return {
|
||||
...actual,
|
||||
handleSpecialDirectories: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
it("should include --follow flag in ripgrep arguments", async () => {
|
||||
const mockSpawn = vi.mocked(childProcess.spawn)
|
||||
const mockProcess = {
|
||||
stdout: {
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "data") {
|
||||
// Simulate some output to complete the process
|
||||
setTimeout(() => callback("test-file.txt\n"), 10)
|
||||
}
|
||||
}),
|
||||
},
|
||||
stderr: {
|
||||
on: vi.fn(),
|
||||
},
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "close") {
|
||||
setTimeout(() => callback(0), 20)
|
||||
}
|
||||
if (event === "error") {
|
||||
// No error simulation
|
||||
}
|
||||
}),
|
||||
kill: vi.fn(),
|
||||
}
|
||||
describe("listFiles", () => {
|
||||
it("should return empty array immediately when limit is 0", async () => {
|
||||
const result = await listFiles("/test/path", true, 0)
|
||||
|
||||
mockSpawn.mockReturnValue(mockProcess as any)
|
||||
|
||||
// Call listFiles to trigger ripgrep execution
|
||||
await listFiles("/test/dir", false, 100)
|
||||
|
||||
// Verify that spawn was called with --follow flag (the critical fix)
|
||||
const [rgPath, args] = mockSpawn.mock.calls[0]
|
||||
expect(rgPath).toBe("/mock/path/to/rg")
|
||||
expect(args).toContain("--files")
|
||||
expect(args).toContain("--hidden")
|
||||
expect(args).toContain("--follow") // This is the critical assertion - the fix should add this flag
|
||||
|
||||
// Platform-agnostic path check - verify the last argument is the resolved path
|
||||
const expectedPath = path.resolve("/test/dir")
|
||||
expect(args[args.length - 1]).toBe(expectedPath)
|
||||
})
|
||||
|
||||
it("should include --follow flag for recursive listings too", async () => {
|
||||
const mockSpawn = vi.mocked(childProcess.spawn)
|
||||
const mockProcess = {
|
||||
stdout: {
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "data") {
|
||||
setTimeout(() => callback("test-file.txt\n"), 10)
|
||||
}
|
||||
}),
|
||||
},
|
||||
stderr: {
|
||||
on: vi.fn(),
|
||||
},
|
||||
on: vi.fn((event, callback) => {
|
||||
if (event === "close") {
|
||||
setTimeout(() => callback(0), 20)
|
||||
}
|
||||
if (event === "error") {
|
||||
// No error simulation
|
||||
}
|
||||
}),
|
||||
kill: vi.fn(),
|
||||
}
|
||||
|
||||
mockSpawn.mockReturnValue(mockProcess as any)
|
||||
|
||||
// Call listFiles with recursive=true
|
||||
await listFiles("/test/dir", true, 100)
|
||||
|
||||
// Verify that spawn was called with --follow flag (the critical fix)
|
||||
const [rgPath, args] = mockSpawn.mock.calls[0]
|
||||
expect(rgPath).toBe("/mock/path/to/rg")
|
||||
expect(args).toContain("--files")
|
||||
expect(args).toContain("--hidden")
|
||||
expect(args).toContain("--follow") // This should be present in recursive mode too
|
||||
|
||||
// Platform-agnostic path check - verify the last argument is the resolved path
|
||||
const expectedPath = path.resolve("/test/dir")
|
||||
expect(args[args.length - 1]).toBe(expectedPath)
|
||||
expect(result).toEqual([[], false])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ import { DIRS_TO_IGNORE } from "./constants"
|
|||
* @returns Tuple of [file paths array, whether the limit was reached]
|
||||
*/
|
||||
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
|
||||
// Early return for limit of 0 - no need to scan anything
|
||||
if (limit === 0) {
|
||||
return [[], false]
|
||||
}
|
||||
|
||||
// Handle special directories
|
||||
const specialResult = await handleSpecialDirectories(dirPath)
|
||||
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ export interface WebviewMessage {
|
|||
codebaseIndexOpenAiCompatibleBaseUrl?: string
|
||||
codebaseIndexOpenAiCompatibleModelDimension?: number
|
||||
codebaseIndexSearchMaxResults?: number
|
||||
codebaseIndexSearchMinScore?: number
|
||||
|
||||
// Secret settings
|
||||
codeIndexOpenAiKey?: string
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ interface LocalCodeIndexSettings {
|
|||
codebaseIndexEmbedderBaseUrl?: string
|
||||
codebaseIndexEmbedderModelId: string
|
||||
codebaseIndexSearchMaxResults?: number
|
||||
codebaseIndexSearchMinScore?: number
|
||||
|
||||
// Secret settings (start empty, will be loaded separately)
|
||||
codeIndexOpenAiKey?: string
|
||||
|
|
@ -85,6 +86,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
codebaseIndexEmbedderBaseUrl: "",
|
||||
codebaseIndexEmbedderModelId: "",
|
||||
codebaseIndexSearchMaxResults: CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
|
||||
codebaseIndexSearchMinScore: CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
|
||||
codeIndexOpenAiKey: "",
|
||||
codeIndexQdrantApiKey: "",
|
||||
codebaseIndexOpenAiCompatibleBaseUrl: "",
|
||||
|
|
@ -114,7 +116,9 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
codebaseIndexEmbedderBaseUrl: codebaseIndexConfig.codebaseIndexEmbedderBaseUrl || "",
|
||||
codebaseIndexEmbedderModelId: codebaseIndexConfig.codebaseIndexEmbedderModelId || "",
|
||||
codebaseIndexSearchMaxResults:
|
||||
codebaseIndexConfig.codebaseIndexSearchMaxResults || CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
|
||||
codebaseIndexConfig.codebaseIndexSearchMaxResults ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
|
||||
codebaseIndexSearchMinScore:
|
||||
codebaseIndexConfig.codebaseIndexSearchMinScore ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
|
||||
codeIndexOpenAiKey: "",
|
||||
codeIndexQdrantApiKey: "",
|
||||
codebaseIndexOpenAiCompatibleBaseUrl: "",
|
||||
|
|
@ -596,6 +600,51 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
|
||||
{isAdvancedSettingsOpen && (
|
||||
<div className="mt-4 space-y-4 pl-4">
|
||||
{/* Search Score Threshold Slider */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">
|
||||
{t("settings:codeIndex.searchMinScoreLabel")}
|
||||
</label>
|
||||
<StandardTooltip content={t("settings:codeIndex.searchMinScoreDescription")}>
|
||||
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground cursor-help" />
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Slider
|
||||
min={CODEBASE_INDEX_DEFAULTS.MIN_SEARCH_SCORE}
|
||||
max={CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_SCORE}
|
||||
step={CODEBASE_INDEX_DEFAULTS.SEARCH_SCORE_STEP}
|
||||
value={[
|
||||
currentSettings.codebaseIndexSearchMinScore ??
|
||||
CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
|
||||
]}
|
||||
onValueChange={(values) =>
|
||||
updateSetting("codebaseIndexSearchMinScore", values[0])
|
||||
}
|
||||
className="flex-1"
|
||||
data-testid="search-min-score-slider"
|
||||
/>
|
||||
<span className="w-12 text-center">
|
||||
{(
|
||||
currentSettings.codebaseIndexSearchMinScore ??
|
||||
CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE
|
||||
).toFixed(2)}
|
||||
</span>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
title={t("settings:codeIndex.resetToDefault")}
|
||||
onClick={() =>
|
||||
updateSetting(
|
||||
"codebaseIndexSearchMinScore",
|
||||
CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
|
||||
)
|
||||
}>
|
||||
<span className="codicon codicon-discard" />
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Maximum Search Results Slider */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -612,7 +661,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
max={CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_RESULTS}
|
||||
step={CODEBASE_INDEX_DEFAULTS.SEARCH_RESULTS_STEP}
|
||||
value={[
|
||||
currentSettings.codebaseIndexSearchMaxResults ||
|
||||
currentSettings.codebaseIndexSearchMaxResults ??
|
||||
CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
|
||||
]}
|
||||
onValueChange={(values) =>
|
||||
|
|
@ -622,7 +671,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
data-testid="search-max-results-slider"
|
||||
/>
|
||||
<span className="w-12 text-center">
|
||||
{currentSettings.codebaseIndexSearchMaxResults ||
|
||||
{currentSettings.codebaseIndexSearchMaxResults ??
|
||||
CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS}
|
||||
</span>
|
||||
<VSCodeButton
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
codebaseIndexEmbedderBaseUrl: "",
|
||||
codebaseIndexEmbedderModelId: "",
|
||||
codebaseIndexSearchMaxResults: undefined,
|
||||
codebaseIndexSearchMinScore: undefined,
|
||||
},
|
||||
codebaseIndexModels: { ollama: {}, openai: {} },
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue