mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: recursively load .roo/rules and AGENTS.md from subdirectories (#10446)
Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
parent
586cf47fb2
commit
08c3431fc5
36 changed files with 736 additions and 128 deletions
|
|
@ -143,6 +143,7 @@ export const globalSettingsSchema = z.object({
|
|||
maxOpenTabsContext: z.number().optional(),
|
||||
maxWorkspaceFiles: z.number().optional(),
|
||||
showRooIgnoredFiles: z.boolean().optional(),
|
||||
enableSubfolderRules: z.boolean().optional(),
|
||||
maxReadFileLine: z.number().optional(),
|
||||
maxImageFileSize: z.number().optional(),
|
||||
maxTotalImageSize: z.number().optional(),
|
||||
|
|
|
|||
|
|
@ -1,15 +1,27 @@
|
|||
import * as path from "path"
|
||||
|
||||
// Use vi.hoisted to ensure mocks are available during hoisting
|
||||
const { mockHomedir, mockStat, mockReadFile, mockReaddir, mockGetRooDirectoriesForCwd, mockGetGlobalRooDirectory } =
|
||||
vi.hoisted(() => ({
|
||||
mockHomedir: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockReaddir: vi.fn(),
|
||||
mockGetRooDirectoriesForCwd: vi.fn(),
|
||||
mockGetGlobalRooDirectory: vi.fn(),
|
||||
}))
|
||||
const {
|
||||
mockHomedir,
|
||||
mockStat,
|
||||
mockReadFile,
|
||||
mockReaddir,
|
||||
mockLstat,
|
||||
mockGetRooDirectoriesForCwd,
|
||||
mockGetAllRooDirectoriesForCwd,
|
||||
mockGetAgentsDirectoriesForCwd,
|
||||
mockGetGlobalRooDirectory,
|
||||
} = vi.hoisted(() => ({
|
||||
mockHomedir: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockReaddir: vi.fn(),
|
||||
mockLstat: vi.fn(),
|
||||
mockGetRooDirectoriesForCwd: vi.fn(),
|
||||
mockGetAllRooDirectoriesForCwd: vi.fn(),
|
||||
mockGetAgentsDirectoriesForCwd: vi.fn(),
|
||||
mockGetGlobalRooDirectory: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock os module
|
||||
vi.mock("os", () => ({
|
||||
|
|
@ -25,12 +37,15 @@ vi.mock("fs/promises", () => ({
|
|||
stat: mockStat,
|
||||
readFile: mockReadFile,
|
||||
readdir: mockReaddir,
|
||||
lstat: mockLstat,
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the roo-config service
|
||||
vi.mock("../../../../services/roo-config", () => ({
|
||||
getRooDirectoriesForCwd: mockGetRooDirectoriesForCwd,
|
||||
getAllRooDirectoriesForCwd: mockGetAllRooDirectoriesForCwd,
|
||||
getAgentsDirectoriesForCwd: mockGetAgentsDirectoriesForCwd,
|
||||
getGlobalRooDirectory: mockGetGlobalRooDirectory,
|
||||
}))
|
||||
|
||||
|
|
@ -46,7 +61,13 @@ describe("custom-instructions global .roo support", () => {
|
|||
vi.clearAllMocks()
|
||||
mockHomedir.mockReturnValue(mockHomeDir)
|
||||
mockGetRooDirectoriesForCwd.mockReturnValue([globalRooDir, projectRooDir])
|
||||
// getAllRooDirectoriesForCwd is now async and returns the same directories by default
|
||||
mockGetAllRooDirectoriesForCwd.mockResolvedValue([globalRooDir, projectRooDir])
|
||||
// getAgentsDirectoriesForCwd returns parent directories (without .roo)
|
||||
mockGetAgentsDirectoriesForCwd.mockResolvedValue([mockCwd])
|
||||
mockGetGlobalRooDirectory.mockReturnValue(globalRooDir)
|
||||
// Default lstat to reject (file not found)
|
||||
mockLstat.mockRejectedValue(new Error("ENOENT"))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -65,7 +86,11 @@ describe("custom-instructions global .roo support", () => {
|
|||
|
||||
// Mock directory reading for global rules
|
||||
mockReaddir.mockResolvedValueOnce([
|
||||
{ name: "rules.md", isFile: () => true, isSymbolicLink: () => false } as any,
|
||||
{
|
||||
name: "rules.md",
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
} as any,
|
||||
])
|
||||
|
||||
// Mock file reading for the rules.md file
|
||||
|
|
@ -87,7 +112,11 @@ describe("custom-instructions global .roo support", () => {
|
|||
|
||||
// Mock directory reading for project rules
|
||||
mockReaddir.mockResolvedValueOnce([
|
||||
{ name: "rules.md", isFile: () => true, isSymbolicLink: () => false } as any,
|
||||
{
|
||||
name: "rules.md",
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
} as any,
|
||||
])
|
||||
|
||||
// Mock file reading
|
||||
|
|
@ -112,8 +141,20 @@ describe("custom-instructions global .roo support", () => {
|
|||
|
||||
// Mock directory reading
|
||||
mockReaddir
|
||||
.mockResolvedValueOnce([{ name: "global.md", isFile: () => true, isSymbolicLink: () => false } as any])
|
||||
.mockResolvedValueOnce([{ name: "project.md", isFile: () => true, isSymbolicLink: () => false } as any])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
name: "global.md",
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
} as any,
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
name: "project.md",
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
} as any,
|
||||
])
|
||||
|
||||
// Mock file reading
|
||||
mockReadFile.mockResolvedValueOnce("global rule content").mockResolvedValueOnce("project rule content")
|
||||
|
|
@ -182,10 +223,18 @@ describe("custom-instructions global .roo support", () => {
|
|||
// Mock directory reading for mode-specific rules
|
||||
mockReaddir
|
||||
.mockResolvedValueOnce([
|
||||
{ name: "global-mode.md", isFile: () => true, isSymbolicLink: () => false } as any,
|
||||
{
|
||||
name: "global-mode.md",
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
} as any,
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
{ name: "project-mode.md", isFile: () => true, isSymbolicLink: () => false } as any,
|
||||
{
|
||||
name: "project-mode.md",
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
} as any,
|
||||
])
|
||||
|
||||
// Mock file reading for mode-specific rules
|
||||
|
|
|
|||
|
|
@ -36,7 +36,16 @@ vi.mock("path", async () => ({
|
|||
.map((arg) => arg.toString().replace(/[/\\]+/g, separator))
|
||||
return cleanArgs.join(separator)
|
||||
}),
|
||||
relative: vi.fn().mockImplementation((from, to) => to),
|
||||
relative: vi.fn().mockImplementation((from, to) => {
|
||||
// Simple relative path computation for test scenarios
|
||||
const separator = process.platform === "win32" ? "\\" : "/"
|
||||
const normalizedFrom = from.replace(/[/\\]+$/, "") // Remove trailing slashes
|
||||
const normalizedTo = to.replace(/[/\\]+/g, separator)
|
||||
if (normalizedTo.startsWith(normalizedFrom + separator)) {
|
||||
return normalizedTo.slice(normalizedFrom.length + 1)
|
||||
}
|
||||
return to
|
||||
}),
|
||||
dirname: vi.fn().mockImplementation((path) => {
|
||||
const separator = process.platform === "win32" ? "\\" : "/"
|
||||
const parts = path.split(/[/\\]/)
|
||||
|
|
@ -200,16 +209,16 @@ describe("loadRuleFiles", () => {
|
|||
})
|
||||
|
||||
const result = await loadRuleFiles("/fake/path")
|
||||
const expectedPath1 =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\file1.txt" : "/fake/path/.roo/rules/file1.txt"
|
||||
const expectedPath2 =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\file2.txt" : "/fake/path/.roo/rules/file2.txt"
|
||||
expect(result).toContain(`# Rules from ${expectedPath1}:`)
|
||||
// Paths in output should be relative to cwd
|
||||
const expectedRelativePath1 = process.platform === "win32" ? ".roo\\rules\\file1.txt" : ".roo/rules/file1.txt"
|
||||
const expectedRelativePath2 = process.platform === "win32" ? ".roo\\rules\\file2.txt" : ".roo/rules/file2.txt"
|
||||
expect(result).toContain(`# Rules from ${expectedRelativePath1}:`)
|
||||
expect(result).toContain("content of file1")
|
||||
expect(result).toContain(`# Rules from ${expectedPath2}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativePath2}:`)
|
||||
expect(result).toContain("content of file2")
|
||||
|
||||
// We expect both checks because our new implementation checks the files again for validation
|
||||
// These are the absolute paths used internally
|
||||
const expectedRulesDir = process.platform === "win32" ? "\\fake\\path\\.roo\\rules" : "/fake/path/.roo/rules"
|
||||
const expectedFile1Path =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\file1.txt" : "/fake/path/.roo/rules/file1.txt"
|
||||
|
|
@ -436,28 +445,25 @@ describe("loadRuleFiles", () => {
|
|||
|
||||
const result = await loadRuleFiles("/fake/path")
|
||||
|
||||
// Check root file content
|
||||
const expectedRootPath =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\root.txt" : "/fake/path/.roo/rules/root.txt"
|
||||
const expectedNested1Path =
|
||||
// Check root file content - paths in output should be relative
|
||||
const expectedRelativeRootPath = process.platform === "win32" ? ".roo\\rules\\root.txt" : ".roo/rules/root.txt"
|
||||
const expectedRelativeNested1Path =
|
||||
process.platform === "win32" ? ".roo\\rules\\subdir\\nested1.txt" : ".roo/rules/subdir/nested1.txt"
|
||||
const expectedRelativeNested2Path =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\rules\\subdir\\nested1.txt"
|
||||
: "/fake/path/.roo/rules/subdir/nested1.txt"
|
||||
const expectedNested2Path =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\rules\\subdir\\subdir2\\nested2.txt"
|
||||
: "/fake/path/.roo/rules/subdir/subdir2/nested2.txt"
|
||||
? ".roo\\rules\\subdir\\subdir2\\nested2.txt"
|
||||
: ".roo/rules/subdir/subdir2/nested2.txt"
|
||||
|
||||
expect(result).toContain(`# Rules from ${expectedRootPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeRootPath}:`)
|
||||
expect(result).toContain("root file content")
|
||||
|
||||
// Check nested files content
|
||||
expect(result).toContain(`# Rules from ${expectedNested1Path}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeNested1Path}:`)
|
||||
expect(result).toContain("nested file 1 content")
|
||||
expect(result).toContain(`# Rules from ${expectedNested2Path}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeNested2Path}:`)
|
||||
expect(result).toContain("nested file 2 content")
|
||||
|
||||
// Verify correct paths were checked
|
||||
// Verify correct absolute paths were checked internally
|
||||
const expectedRootPath2 =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\root.txt" : "/fake/path/.roo/rules/root.txt"
|
||||
const expectedNested1Path2 =
|
||||
|
|
@ -1053,39 +1059,34 @@ describe("addCustomInstructions", () => {
|
|||
{ language: "es" },
|
||||
)
|
||||
|
||||
const expectedTestModeDir =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules-test-mode" : "/fake/path/.roo/rules-test-mode"
|
||||
const expectedRule1Path =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\rules-test-mode\\rule1.txt"
|
||||
: "/fake/path/.roo/rules-test-mode/rule1.txt"
|
||||
const expectedRule2Path =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\rules-test-mode\\rule2.txt"
|
||||
: "/fake/path/.roo/rules-test-mode/rule2.txt"
|
||||
// Paths in output should be relative
|
||||
const expectedRelativeRule1Path =
|
||||
process.platform === "win32" ? ".roo\\rules-test-mode\\rule1.txt" : ".roo/rules-test-mode/rule1.txt"
|
||||
const expectedRelativeRule2Path =
|
||||
process.platform === "win32" ? ".roo\\rules-test-mode\\rule2.txt" : ".roo/rules-test-mode/rule2.txt"
|
||||
|
||||
expect(result).toContain(`# Rules from ${expectedTestModeDir}`)
|
||||
expect(result).toContain(`# Rules from ${expectedRule1Path}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeRule1Path}:`)
|
||||
expect(result).toContain("mode specific rule 1")
|
||||
expect(result).toContain(`# Rules from ${expectedRule2Path}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeRule2Path}:`)
|
||||
expect(result).toContain("mode specific rule 2")
|
||||
|
||||
const expectedTestModeDir2 =
|
||||
// Verify absolute paths were used internally
|
||||
const expectedAbsTestModeDir =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules-test-mode" : "/fake/path/.roo/rules-test-mode"
|
||||
const expectedRule1Path2 =
|
||||
const expectedAbsRule1Path =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\rules-test-mode\\rule1.txt"
|
||||
: "/fake/path/.roo/rules-test-mode/rule1.txt"
|
||||
const expectedRule2Path2 =
|
||||
const expectedAbsRule2Path =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\rules-test-mode\\rule2.txt"
|
||||
: "/fake/path/.roo/rules-test-mode/rule2.txt"
|
||||
|
||||
expect(statMock).toHaveBeenCalledWith(expectedTestModeDir2)
|
||||
expect(statMock).toHaveBeenCalledWith(expectedRule1Path2)
|
||||
expect(statMock).toHaveBeenCalledWith(expectedRule2Path2)
|
||||
expect(readFileMock).toHaveBeenCalledWith(expectedRule1Path2, "utf-8")
|
||||
expect(readFileMock).toHaveBeenCalledWith(expectedRule2Path2, "utf-8")
|
||||
expect(statMock).toHaveBeenCalledWith(expectedAbsTestModeDir)
|
||||
expect(statMock).toHaveBeenCalledWith(expectedAbsRule1Path)
|
||||
expect(statMock).toHaveBeenCalledWith(expectedAbsRule2Path)
|
||||
expect(readFileMock).toHaveBeenCalledWith(expectedAbsRule1Path, "utf-8")
|
||||
expect(readFileMock).toHaveBeenCalledWith(expectedAbsRule2Path, "utf-8")
|
||||
})
|
||||
|
||||
it("should fall back to .roorules-test-mode when .roo/rules-test-mode/ does not exist", async () => {
|
||||
|
|
@ -1188,15 +1189,11 @@ describe("addCustomInstructions", () => {
|
|||
"test-mode",
|
||||
)
|
||||
|
||||
const expectedTestModeDir =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules-test-mode" : "/fake/path/.roo/rules-test-mode"
|
||||
const expectedRule1Path =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\rules-test-mode\\rule1.txt"
|
||||
: "/fake/path/.roo/rules-test-mode/rule1.txt"
|
||||
// Paths in output should be relative
|
||||
const expectedRelativeRule1Path =
|
||||
process.platform === "win32" ? ".roo\\rules-test-mode\\rule1.txt" : ".roo/rules-test-mode/rule1.txt"
|
||||
|
||||
expect(result).toContain(`# Rules from ${expectedTestModeDir}`)
|
||||
expect(result).toContain(`# Rules from ${expectedRule1Path}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeRule1Path}:`)
|
||||
expect(result).toContain("mode specific rule content")
|
||||
|
||||
expect(statCallCount).toBeGreaterThan(0)
|
||||
|
|
@ -1338,31 +1335,25 @@ describe("Rules directory reading", () => {
|
|||
|
||||
const result = await loadRuleFiles("/fake/path")
|
||||
|
||||
// Verify both regular file and symlink target content are included
|
||||
const expectedRegularPath =
|
||||
// Verify both regular file and symlink target content are included (paths should be relative)
|
||||
const expectedRelativeRegularPath =
|
||||
process.platform === "win32" ? ".roo\\rules\\regular.txt" : ".roo/rules/regular.txt"
|
||||
const expectedRelativeSymlinkPath =
|
||||
process.platform === "win32" ? ".roo\\symlink-target.txt" : ".roo/symlink-target.txt"
|
||||
const expectedRelativeSubdirPath =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\rules\\regular.txt"
|
||||
: "/fake/path/.roo/rules/regular.txt"
|
||||
const expectedSymlinkPath =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\symlink-target.txt"
|
||||
: "/fake/path/.roo/symlink-target.txt"
|
||||
const expectedSubdirPath =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\rules\\symlink-target-dir\\subdir_link.txt"
|
||||
: "/fake/path/.roo/rules/symlink-target-dir/subdir_link.txt"
|
||||
const expectedNestedPath =
|
||||
process.platform === "win32"
|
||||
? "\\fake\\path\\.roo\\nested-symlink-target.txt"
|
||||
: "/fake/path/.roo/nested-symlink-target.txt"
|
||||
? ".roo\\rules\\symlink-target-dir\\subdir_link.txt"
|
||||
: ".roo/rules/symlink-target-dir/subdir_link.txt"
|
||||
const expectedRelativeNestedPath =
|
||||
process.platform === "win32" ? ".roo\\nested-symlink-target.txt" : ".roo/nested-symlink-target.txt"
|
||||
|
||||
expect(result).toContain(`# Rules from ${expectedRegularPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeRegularPath}:`)
|
||||
expect(result).toContain("regular file content")
|
||||
expect(result).toContain(`# Rules from ${expectedSymlinkPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeSymlinkPath}:`)
|
||||
expect(result).toContain("symlink target content")
|
||||
expect(result).toContain(`# Rules from ${expectedSubdirPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeSubdirPath}:`)
|
||||
expect(result).toContain("regular file content under symlink target dir")
|
||||
expect(result).toContain(`# Rules from ${expectedNestedPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeNestedPath}:`)
|
||||
expect(result).toContain("nested symlink target content")
|
||||
|
||||
// Verify readlink was called with the symlink path
|
||||
|
|
@ -1424,18 +1415,19 @@ describe("Rules directory reading", () => {
|
|||
|
||||
const result = await loadRuleFiles("/fake/path")
|
||||
|
||||
const expectedFile1Path =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\file1.txt" : "/fake/path/.roo/rules/file1.txt"
|
||||
const expectedFile2Path =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\file2.txt" : "/fake/path/.roo/rules/file2.txt"
|
||||
const expectedFile3Path =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\file3.txt" : "/fake/path/.roo/rules/file3.txt"
|
||||
// Paths in output should be relative
|
||||
const expectedRelativeFile1Path =
|
||||
process.platform === "win32" ? ".roo\\rules\\file1.txt" : ".roo/rules/file1.txt"
|
||||
const expectedRelativeFile2Path =
|
||||
process.platform === "win32" ? ".roo\\rules\\file2.txt" : ".roo/rules/file2.txt"
|
||||
const expectedRelativeFile3Path =
|
||||
process.platform === "win32" ? ".roo\\rules\\file3.txt" : ".roo/rules/file3.txt"
|
||||
|
||||
expect(result).toContain(`# Rules from ${expectedFile1Path}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeFile1Path}:`)
|
||||
expect(result).toContain("content of file1")
|
||||
expect(result).toContain(`# Rules from ${expectedFile2Path}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeFile2Path}:`)
|
||||
expect(result).toContain("content of file2")
|
||||
expect(result).toContain(`# Rules from ${expectedFile3Path}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeFile3Path}:`)
|
||||
expect(result).toContain("content of file3")
|
||||
})
|
||||
|
||||
|
|
@ -1483,17 +1475,16 @@ describe("Rules directory reading", () => {
|
|||
expect(alphaIndex).toBeLessThan(betaIndex)
|
||||
expect(betaIndex).toBeLessThan(zebraIndex)
|
||||
|
||||
// Verify the expected file paths are in the result
|
||||
const expectedAlphaPath =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\alpha.txt" : "/fake/path/.roo/rules/alpha.txt"
|
||||
const expectedBetaPath =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\Beta.txt" : "/fake/path/.roo/rules/Beta.txt"
|
||||
const expectedZebraPath =
|
||||
process.platform === "win32" ? "\\fake\\path\\.roo\\rules\\zebra.txt" : "/fake/path/.roo/rules/zebra.txt"
|
||||
// Verify the expected file paths are in the result (should be relative)
|
||||
const expectedRelativeAlphaPath =
|
||||
process.platform === "win32" ? ".roo\\rules\\alpha.txt" : ".roo/rules/alpha.txt"
|
||||
const expectedRelativeBetaPath = process.platform === "win32" ? ".roo\\rules\\Beta.txt" : ".roo/rules/Beta.txt"
|
||||
const expectedRelativeZebraPath =
|
||||
process.platform === "win32" ? ".roo\\rules\\zebra.txt" : ".roo/rules/zebra.txt"
|
||||
|
||||
expect(result).toContain(`# Rules from ${expectedAlphaPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedBetaPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedZebraPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeAlphaPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeBetaPath}:`)
|
||||
expect(result).toContain(`# Rules from ${expectedRelativeZebraPath}:`)
|
||||
})
|
||||
|
||||
it("should sort symlinks by their symlink names, not target names", async () => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ import type { SystemPromptSettings } from "../types"
|
|||
import { getEffectiveProtocol, isNativeProtocol } from "@roo-code/types"
|
||||
|
||||
import { LANGUAGES } from "../../../shared/language"
|
||||
import { getRooDirectoriesForCwd, getGlobalRooDirectory } from "../../../services/roo-config"
|
||||
import {
|
||||
getRooDirectoriesForCwd,
|
||||
getAllRooDirectoriesForCwd,
|
||||
getAgentsDirectoriesForCwd,
|
||||
getGlobalRooDirectory,
|
||||
} from "../../../services/roo-config"
|
||||
|
||||
/**
|
||||
* Safely read a file and return its trimmed content
|
||||
|
|
@ -87,9 +92,15 @@ async function resolveSymLink(
|
|||
const stats = await fs.stat(resolvedTarget)
|
||||
if (stats.isFile()) {
|
||||
// For symlinks to files, store the symlink path as original and target as resolved
|
||||
fileInfo.push({ originalPath: symlinkPath, resolvedPath: resolvedTarget })
|
||||
fileInfo.push({
|
||||
originalPath: symlinkPath,
|
||||
resolvedPath: resolvedTarget,
|
||||
})
|
||||
} else if (stats.isDirectory()) {
|
||||
const anotherEntries = await fs.readdir(resolvedTarget, { withFileTypes: true, recursive: true })
|
||||
const anotherEntries = await fs.readdir(resolvedTarget, {
|
||||
withFileTypes: true,
|
||||
recursive: true,
|
||||
})
|
||||
// Collect promises for recursive calls within the directory
|
||||
const directoryPromises: Promise<void>[] = []
|
||||
for (const anotherEntry of anotherEntries) {
|
||||
|
|
@ -111,7 +122,10 @@ async function resolveSymLink(
|
|||
*/
|
||||
async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ filename: string; content: string }>> {
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true, recursive: true })
|
||||
const entries = await fs.readdir(dirPath, {
|
||||
withFileTypes: true,
|
||||
recursive: true,
|
||||
})
|
||||
|
||||
// Process all entries - regular files and symlinks that might point to files
|
||||
// Store both original path (for sorting) and resolved path (for reading)
|
||||
|
|
@ -168,32 +182,40 @@ async function readTextFilesFromDirectory(dirPath: string): Promise<Array<{ file
|
|||
|
||||
/**
|
||||
* Format content from multiple files with filenames as headers
|
||||
* @param files - Array of files with filename (absolute path) and content
|
||||
* @param cwd - Current working directory for computing relative paths
|
||||
*/
|
||||
function formatDirectoryContent(dirPath: string, files: Array<{ filename: string; content: string }>): string {
|
||||
function formatDirectoryContent(files: Array<{ filename: string; content: string }>, cwd: string): string {
|
||||
if (files.length === 0) return ""
|
||||
|
||||
return files
|
||||
.map((file) => {
|
||||
return `# Rules from ${file.filename}:\n${file.content}`
|
||||
// Compute relative path for display
|
||||
const displayPath = path.relative(cwd, file.filename)
|
||||
return `# Rules from ${displayPath}:\n${file.content}`
|
||||
})
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Load rule files from global and project-local directories
|
||||
* Global rules are loaded first, then project-local rules which can override global ones
|
||||
* Load rule files from global, project-local, and optionally subfolder directories
|
||||
* Rules are loaded in order: global first, then project-local, then subfolders (alphabetically)
|
||||
*
|
||||
* @param cwd - Current working directory (project root)
|
||||
* @param enableSubfolderRules - Whether to include rules from subdirectories (default: false)
|
||||
*/
|
||||
export async function loadRuleFiles(cwd: string): Promise<string> {
|
||||
export async function loadRuleFiles(cwd: string, enableSubfolderRules: boolean = false): Promise<string> {
|
||||
const rules: string[] = []
|
||||
const rooDirectories = getRooDirectoriesForCwd(cwd)
|
||||
// Use recursive discovery only if enableSubfolderRules is true
|
||||
const rooDirectories = enableSubfolderRules ? await getAllRooDirectoriesForCwd(cwd) : getRooDirectoriesForCwd(cwd)
|
||||
|
||||
// Check for .roo/rules/ directories in order (global first, then project-local)
|
||||
// Check for .roo/rules/ directories in order (global, project-local, and optionally subfolders)
|
||||
for (const rooDir of rooDirectories) {
|
||||
const rulesDir = path.join(rooDir, "rules")
|
||||
if (await directoryExists(rulesDir)) {
|
||||
const files = await readTextFilesFromDirectory(rulesDir)
|
||||
if (files.length > 0) {
|
||||
const content = formatDirectoryContent(rulesDir, files)
|
||||
const content = formatDirectoryContent(files, cwd)
|
||||
rules.push(content)
|
||||
}
|
||||
}
|
||||
|
|
@ -201,7 +223,7 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
|
|||
|
||||
// If we found rules in .roo/rules/ directories, return them
|
||||
if (rules.length > 0) {
|
||||
return "\n" + rules.join("\n\n")
|
||||
return "\n# Rules from .roo directories:\n\n" + rules.join("\n\n")
|
||||
}
|
||||
|
||||
// Fall back to existing behavior for legacy .roorules/.clinerules files
|
||||
|
|
@ -218,16 +240,24 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Load AGENTS.md or AGENT.md file from the project root if it exists
|
||||
* Load AGENTS.md or AGENT.md file from a specific directory
|
||||
* Checks for both AGENTS.md (standard) and AGENT.md (alternative) for compatibility
|
||||
*
|
||||
* @param directory - Directory to check for AGENTS.md
|
||||
* @param showPath - Whether to include the directory path in the header
|
||||
* @param cwd - Current working directory for computing relative paths (optional)
|
||||
*/
|
||||
async function loadAgentRulesFile(cwd: string): Promise<string> {
|
||||
async function loadAgentRulesFileFromDirectory(
|
||||
directory: string,
|
||||
showPath: boolean = false,
|
||||
cwd?: string,
|
||||
): Promise<string> {
|
||||
// Try both filenames - AGENTS.md (standard) first, then AGENT.md (alternative)
|
||||
const filenames = ["AGENTS.md", "AGENT.md"]
|
||||
|
||||
for (const filename of filenames) {
|
||||
try {
|
||||
const agentPath = path.join(cwd, filename)
|
||||
const agentPath = path.join(directory, filename)
|
||||
let resolvedPath = agentPath
|
||||
|
||||
// Check if file exists and handle symlinks
|
||||
|
|
@ -235,7 +265,10 @@ async function loadAgentRulesFile(cwd: string): Promise<string> {
|
|||
const stats = await fs.lstat(agentPath)
|
||||
if (stats.isSymbolicLink()) {
|
||||
// Create a temporary fileInfo array to use with resolveSymLink
|
||||
const fileInfo: Array<{ originalPath: string; resolvedPath: string }> = []
|
||||
const fileInfo: Array<{
|
||||
originalPath: string
|
||||
resolvedPath: string
|
||||
}> = []
|
||||
|
||||
// Use the existing resolveSymLink function to handle symlink resolution
|
||||
await resolveSymLink(agentPath, fileInfo, 0)
|
||||
|
|
@ -253,7 +286,12 @@ async function loadAgentRulesFile(cwd: string): Promise<string> {
|
|||
// Read the content from the resolved path
|
||||
const content = await safeReadFile(resolvedPath)
|
||||
if (content) {
|
||||
return `# Agent Rules Standard (${filename}):\n${content}`
|
||||
// Compute relative path for display if cwd is provided
|
||||
const displayPath = cwd ? path.relative(cwd, directory) : directory
|
||||
const header = showPath
|
||||
? `# Agent Rules Standard (${filename}) from ${displayPath}:`
|
||||
: `# Agent Rules Standard (${filename}):`
|
||||
return `${header}\n${content}`
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently ignore errors - agent rules files are optional
|
||||
|
|
@ -262,6 +300,51 @@ async function loadAgentRulesFile(cwd: string): Promise<string> {
|
|||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Load AGENTS.md or AGENT.md file from the project root if it exists
|
||||
* Checks for both AGENTS.md (standard) and AGENT.md (alternative) for compatibility
|
||||
*
|
||||
* @deprecated Use loadAllAgentRulesFiles for loading from all directories
|
||||
*/
|
||||
async function loadAgentRulesFile(cwd: string): Promise<string> {
|
||||
return loadAgentRulesFileFromDirectory(cwd, false, cwd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all AGENTS.md files from project root and optionally subdirectories with .roo folders
|
||||
* Returns combined content with clear path headers for each file
|
||||
*
|
||||
* @param cwd - Current working directory (project root)
|
||||
* @param enableSubfolderRules - Whether to include AGENTS.md from subdirectories (default: false)
|
||||
* @returns Combined AGENTS.md content from all locations
|
||||
*/
|
||||
async function loadAllAgentRulesFiles(cwd: string, enableSubfolderRules: boolean = false): Promise<string> {
|
||||
const agentRules: string[] = []
|
||||
|
||||
// When subfolder rules are disabled, only load from root
|
||||
if (!enableSubfolderRules) {
|
||||
const content = await loadAgentRulesFileFromDirectory(cwd, false, cwd)
|
||||
if (content && content.trim()) {
|
||||
agentRules.push(content.trim())
|
||||
}
|
||||
return agentRules.join("\n\n")
|
||||
}
|
||||
|
||||
// When enabled, load from root and all subdirectories with .roo folders
|
||||
const directories = await getAgentsDirectoriesForCwd(cwd)
|
||||
|
||||
for (const directory of directories) {
|
||||
// Show path for all directories except the root
|
||||
const showPath = directory !== cwd
|
||||
const content = await loadAgentRulesFileFromDirectory(directory, showPath, cwd)
|
||||
if (content && content.trim()) {
|
||||
agentRules.push(content.trim())
|
||||
}
|
||||
}
|
||||
|
||||
return agentRules.join("\n\n")
|
||||
}
|
||||
|
||||
export async function addCustomInstructions(
|
||||
modeCustomInstructions: string,
|
||||
globalCustomInstructions: string,
|
||||
|
|
@ -275,21 +358,27 @@ export async function addCustomInstructions(
|
|||
): Promise<string> {
|
||||
const sections = []
|
||||
|
||||
// Get the enableSubfolderRules setting (default: false)
|
||||
const enableSubfolderRules = options.settings?.enableSubfolderRules ?? false
|
||||
|
||||
// Load mode-specific rules if mode is provided
|
||||
let modeRuleContent = ""
|
||||
let usedRuleFile = ""
|
||||
|
||||
if (mode) {
|
||||
const modeRules: string[] = []
|
||||
const rooDirectories = getRooDirectoriesForCwd(cwd)
|
||||
// Use recursive discovery only if enableSubfolderRules is true
|
||||
const rooDirectories = enableSubfolderRules
|
||||
? await getAllRooDirectoriesForCwd(cwd)
|
||||
: getRooDirectoriesForCwd(cwd)
|
||||
|
||||
// Check for .roo/rules-${mode}/ directories in order (global first, then project-local)
|
||||
// Check for .roo/rules-${mode}/ directories in order (global, project-local, and optionally subfolders)
|
||||
for (const rooDir of rooDirectories) {
|
||||
const modeRulesDir = path.join(rooDir, `rules-${mode}`)
|
||||
if (await directoryExists(modeRulesDir)) {
|
||||
const files = await readTextFilesFromDirectory(modeRulesDir)
|
||||
if (files.length > 0) {
|
||||
const content = formatDirectoryContent(modeRulesDir, files)
|
||||
const content = formatDirectoryContent(files, cwd)
|
||||
modeRules.push(content)
|
||||
}
|
||||
}
|
||||
|
|
@ -350,15 +439,16 @@ export async function addCustomInstructions(
|
|||
}
|
||||
|
||||
// Add AGENTS.md content if enabled (default: true)
|
||||
// Load from root and optionally subdirectories with .roo folders based on enableSubfolderRules setting
|
||||
if (options.settings?.useAgentRules !== false) {
|
||||
const agentRulesContent = await loadAgentRulesFile(cwd)
|
||||
const agentRulesContent = await loadAllAgentRulesFiles(cwd, enableSubfolderRules)
|
||||
if (agentRulesContent && agentRulesContent.trim()) {
|
||||
rules.push(agentRulesContent.trim())
|
||||
}
|
||||
}
|
||||
|
||||
// Add generic rules
|
||||
const genericRuleContent = await loadRuleFiles(cwd)
|
||||
const genericRuleContent = await loadRuleFiles(cwd, enableSubfolderRules)
|
||||
if (genericRuleContent && genericRuleContent.trim()) {
|
||||
rules.push(genericRuleContent.trim())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export interface SystemPromptSettings {
|
|||
todoListEnabled: boolean
|
||||
browserToolEnabled?: boolean
|
||||
useAgentRules: boolean
|
||||
/** When true, recursively discover and load .roo/rules from subdirectories */
|
||||
enableSubfolderRules?: boolean
|
||||
newTaskRequireTodos: boolean
|
||||
toolProtocol?: ToolProtocol
|
||||
/** When true, model should hide vendor/company identity in responses */
|
||||
|
|
|
|||
|
|
@ -3505,6 +3505,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
maxConcurrentFileReads,
|
||||
maxReadFileLine,
|
||||
apiConfiguration,
|
||||
enableSubfolderRules,
|
||||
} = state ?? {}
|
||||
|
||||
return await (async () => {
|
||||
|
|
@ -3557,6 +3558,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
browserToolEnabled: browserToolEnabled ?? true,
|
||||
useAgentRules:
|
||||
vscode.workspace.getConfiguration(Package.name).get<boolean>("useAgentRules") ?? true,
|
||||
enableSubfolderRules: enableSubfolderRules ?? false,
|
||||
newTaskRequireTodos: vscode.workspace
|
||||
.getConfiguration(Package.name)
|
||||
.get<boolean>("newTaskRequireTodos", false),
|
||||
|
|
|
|||
|
|
@ -1873,6 +1873,7 @@ export class ClineProvider
|
|||
browserToolEnabled,
|
||||
telemetrySetting,
|
||||
showRooIgnoredFiles,
|
||||
enableSubfolderRules,
|
||||
language,
|
||||
maxReadFileLine,
|
||||
maxImageFileSize,
|
||||
|
|
@ -2020,6 +2021,7 @@ export class ClineProvider
|
|||
telemetryKey,
|
||||
machineId,
|
||||
showRooIgnoredFiles: showRooIgnoredFiles ?? false,
|
||||
enableSubfolderRules: enableSubfolderRules ?? false,
|
||||
language: language ?? formatLanguage(vscode.env.language),
|
||||
renderContext: this.renderContext,
|
||||
maxReadFileLine: maxReadFileLine ?? -1,
|
||||
|
|
@ -2270,6 +2272,7 @@ export class ClineProvider
|
|||
browserToolEnabled: stateValues.browserToolEnabled ?? true,
|
||||
telemetrySetting: stateValues.telemetrySetting || "unset",
|
||||
showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? false,
|
||||
enableSubfolderRules: stateValues.enableSubfolderRules ?? false,
|
||||
maxReadFileLine: stateValues.maxReadFileLine ?? -1,
|
||||
maxImageFileSize: stateValues.maxImageFileSize ?? 5,
|
||||
maxTotalImageSize: stateValues.maxTotalImageSize ?? 20,
|
||||
|
|
|
|||
|
|
@ -568,6 +568,7 @@ describe("ClineProvider", () => {
|
|||
browserToolEnabled: true,
|
||||
telemetrySetting: "unset",
|
||||
showRooIgnoredFiles: false,
|
||||
enableSubfolderRules: false,
|
||||
renderContext: "sidebar",
|
||||
maxReadFileLine: 500,
|
||||
maxImageFileSize: 5,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
|
|||
language,
|
||||
maxReadFileLine,
|
||||
maxConcurrentFileReads,
|
||||
enableSubfolderRules,
|
||||
} = await provider.getState()
|
||||
|
||||
// Check experiment to determine which diff strategy to use
|
||||
|
|
@ -93,6 +94,7 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
|
|||
maxConcurrentFileReads: maxConcurrentFileReads ?? 5,
|
||||
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
|
||||
useAgentRules: vscode.workspace.getConfiguration(Package.name).get<boolean>("useAgentRules") ?? true,
|
||||
enableSubfolderRules: enableSubfolderRules ?? false,
|
||||
newTaskRequireTodos: vscode.workspace
|
||||
.getConfiguration(Package.name)
|
||||
.get<boolean>("newTaskRequireTodos", false),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import * as path from "path"
|
||||
|
||||
// Use vi.hoisted to ensure mocks are available during hoisting
|
||||
const { mockStat, mockReadFile, mockHomedir } = vi.hoisted(() => ({
|
||||
const { mockStat, mockReadFile, mockHomedir, mockExecuteRipgrep } = vi.hoisted(() => ({
|
||||
mockStat: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockHomedir: vi.fn(),
|
||||
mockExecuteRipgrep: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock fs/promises module
|
||||
|
|
@ -20,6 +21,11 @@ vi.mock("os", () => ({
|
|||
homedir: mockHomedir,
|
||||
}))
|
||||
|
||||
// Mock executeRipgrep from search service
|
||||
vi.mock("../../search/file-search", () => ({
|
||||
executeRipgrep: mockExecuteRipgrep,
|
||||
}))
|
||||
|
||||
import {
|
||||
getGlobalRooDirectory,
|
||||
getProjectRooDirectoryForCwd,
|
||||
|
|
@ -27,6 +33,9 @@ import {
|
|||
fileExists,
|
||||
readFileIfExists,
|
||||
getRooDirectoriesForCwd,
|
||||
getAllRooDirectoriesForCwd,
|
||||
getAgentsDirectoriesForCwd,
|
||||
discoverSubfolderRooDirectories,
|
||||
loadConfiguration,
|
||||
} from "../index"
|
||||
|
||||
|
|
@ -297,4 +306,188 @@ describe("RooConfigService", () => {
|
|||
expect(mockReadFile).toHaveBeenCalledWith(path.join("/project/path", ".roo", "rules/rules.md"), "utf-8")
|
||||
})
|
||||
})
|
||||
|
||||
describe("discoverSubfolderRooDirectories", () => {
|
||||
it("should return empty array when no subfolder .roo directories found", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValue([])
|
||||
|
||||
const result = await discoverSubfolderRooDirectories("/project/path")
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("should discover .roo directories from subfolders", async () => {
|
||||
// Find any file inside .roo directories
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([
|
||||
{ path: "package-a/.roo/rules/rule.md", type: "file" },
|
||||
{ path: "package-b/.roo/rules-code/rule.md", type: "file" },
|
||||
])
|
||||
|
||||
const result = await discoverSubfolderRooDirectories("/project/path")
|
||||
|
||||
expect(result).toEqual([
|
||||
path.join("/project/path", "package-a", ".roo"),
|
||||
path.join("/project/path", "package-b", ".roo"),
|
||||
])
|
||||
})
|
||||
|
||||
it("should sort discovered directories alphabetically", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([
|
||||
{ path: "zebra/.roo/rules/rule.md", type: "file" },
|
||||
{ path: "apple/.roo/rules/rule.md", type: "file" },
|
||||
{ path: "mango/.roo/rules/rule.md", type: "file" },
|
||||
])
|
||||
|
||||
const result = await discoverSubfolderRooDirectories("/project/path")
|
||||
|
||||
expect(result).toEqual([
|
||||
path.join("/project/path", "apple", ".roo"),
|
||||
path.join("/project/path", "mango", ".roo"),
|
||||
path.join("/project/path", "zebra", ".roo"),
|
||||
])
|
||||
})
|
||||
|
||||
it("should exclude root .roo directory", async () => {
|
||||
// This would match the root .roo, which should be excluded
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([
|
||||
{ path: ".roo/rules/rule.md", type: "file" }, // This is root - should be excluded
|
||||
{ path: "subfolder/.roo/rules/rule.md", type: "file" },
|
||||
])
|
||||
|
||||
const result = await discoverSubfolderRooDirectories("/project/path")
|
||||
|
||||
// Should only include subfolder, not root
|
||||
expect(result).toEqual([path.join("/project/path", "subfolder", ".roo")])
|
||||
})
|
||||
|
||||
it("should handle nested subdirectories", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([
|
||||
{ path: "packages/core/.roo/rules/rule.md", type: "file" },
|
||||
{ path: "packages/utils/.roo/rules-code/rule.md", type: "file" },
|
||||
])
|
||||
|
||||
const result = await discoverSubfolderRooDirectories("/project/path")
|
||||
|
||||
expect(result).toEqual([
|
||||
path.join("/project/path", "packages/core", ".roo"),
|
||||
path.join("/project/path", "packages/utils", ".roo"),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return empty array on ripgrep error", async () => {
|
||||
mockExecuteRipgrep.mockRejectedValue(new Error("ripgrep failed"))
|
||||
|
||||
const result = await discoverSubfolderRooDirectories("/project/path")
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("should deduplicate .roo directories from multiple files", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([
|
||||
{ path: "package-a/.roo/rules/rule1.md", type: "file" },
|
||||
{ path: "package-a/.roo/rules/rule2.md", type: "file" },
|
||||
{ path: "package-a/.roo/rules-code/rule3.md", type: "file" },
|
||||
])
|
||||
|
||||
const result = await discoverSubfolderRooDirectories("/project/path")
|
||||
|
||||
// Should only include package-a/.roo once
|
||||
expect(result).toEqual([path.join("/project/path", "package-a", ".roo")])
|
||||
})
|
||||
|
||||
it("should discover .roo directories with any content", async () => {
|
||||
// Should find .roo directories regardless of what's inside them
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([
|
||||
{ path: "package-a/.roo/rules/rule.md", type: "file" },
|
||||
{ path: "package-b/.roo/rules-code/code-rule.md", type: "file" },
|
||||
{ path: "package-c/.roo/rules-architect/arch-rule.md", type: "file" },
|
||||
{ path: "package-d/.roo/config/settings.json", type: "file" },
|
||||
])
|
||||
|
||||
const result = await discoverSubfolderRooDirectories("/project/path")
|
||||
|
||||
expect(result).toEqual([
|
||||
path.join("/project/path", "package-a", ".roo"),
|
||||
path.join("/project/path", "package-b", ".roo"),
|
||||
path.join("/project/path", "package-c", ".roo"),
|
||||
path.join("/project/path", "package-d", ".roo"),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAllRooDirectoriesForCwd", () => {
|
||||
it("should return global, project, and subfolder directories", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([{ path: "subfolder/.roo/rules/rule.md", type: "file" }])
|
||||
|
||||
const result = await getAllRooDirectoriesForCwd("/project/path")
|
||||
|
||||
expect(result).toEqual([
|
||||
path.join("/mock/home", ".roo"), // global
|
||||
path.join("/project/path", ".roo"), // project
|
||||
path.join("/project/path", "subfolder", ".roo"), // subfolder
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only global and project when no subfolders", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValue([])
|
||||
|
||||
const result = await getAllRooDirectoriesForCwd("/project/path")
|
||||
|
||||
expect(result).toEqual([path.join("/mock/home", ".roo"), path.join("/project/path", ".roo")])
|
||||
})
|
||||
|
||||
it("should maintain order: global, project, subfolders (alphabetically)", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([
|
||||
{ path: "zebra/.roo/rules/rule.md", type: "file" },
|
||||
{ path: "apple/.roo/rules/rule.md", type: "file" },
|
||||
])
|
||||
|
||||
const result = await getAllRooDirectoriesForCwd("/project/path")
|
||||
|
||||
expect(result).toEqual([
|
||||
path.join("/mock/home", ".roo"), // global first
|
||||
path.join("/project/path", ".roo"), // project second
|
||||
path.join("/project/path", "apple", ".roo"), // subfolders alphabetically
|
||||
path.join("/project/path", "zebra", ".roo"),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAgentsDirectoriesForCwd", () => {
|
||||
it("should return root directory and parent directories of subfolder .roo dirs", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([{ path: "package-a/.roo/rules/rule.md", type: "file" }])
|
||||
|
||||
const result = await getAgentsDirectoriesForCwd("/project/path")
|
||||
|
||||
expect(result).toEqual([
|
||||
"/project/path", // root
|
||||
path.join("/project/path", "package-a"), // parent of .roo
|
||||
])
|
||||
})
|
||||
|
||||
it("should always include root even when no subfolders", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValue([])
|
||||
|
||||
const result = await getAgentsDirectoriesForCwd("/project/path")
|
||||
|
||||
expect(result).toEqual(["/project/path"])
|
||||
})
|
||||
|
||||
it("should include multiple subfolder parent directories", async () => {
|
||||
mockExecuteRipgrep.mockResolvedValueOnce([
|
||||
{ path: "package-a/.roo/rules/rule.md", type: "file" },
|
||||
{ path: "package-b/.roo/rules-code/rule.md", type: "file" },
|
||||
{ path: "packages/core/.roo/rules/rule.md", type: "file" },
|
||||
])
|
||||
|
||||
const result = await getAgentsDirectoriesForCwd("/project/path")
|
||||
|
||||
expect(result).toEqual([
|
||||
"/project/path",
|
||||
path.join("/project/path", "package-a"),
|
||||
path.join("/project/path", "package-b"),
|
||||
path.join("/project/path", "packages/core"),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -111,6 +111,89 @@ export async function readFileIfExists(filePath: string): Promise<string | null>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers all .roo directories in subdirectories of the workspace
|
||||
*
|
||||
* @param cwd - Current working directory (workspace root)
|
||||
* @returns Array of absolute paths to .roo directories found in subdirectories,
|
||||
* sorted alphabetically. Does not include the root .roo directory.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const subfolderRoos = await discoverSubfolderRooDirectories('/Users/john/monorepo')
|
||||
* // Returns:
|
||||
* // [
|
||||
* // '/Users/john/monorepo/package-a/.roo',
|
||||
* // '/Users/john/monorepo/package-b/.roo',
|
||||
* // '/Users/john/monorepo/packages/shared/.roo'
|
||||
* // ]
|
||||
* ```
|
||||
*
|
||||
* @example Directory structure:
|
||||
* ```
|
||||
* /Users/john/monorepo/
|
||||
* ├── .roo/ # Root .roo (NOT included - use getProjectRooDirectoryForCwd)
|
||||
* ├── package-a/
|
||||
* │ └── .roo/ # Included
|
||||
* │ └── rules/
|
||||
* ├── package-b/
|
||||
* │ └── .roo/ # Included
|
||||
* │ └── rules-code/
|
||||
* └── packages/
|
||||
* └── shared/
|
||||
* └── .roo/ # Included (nested)
|
||||
* └── rules/
|
||||
* ```
|
||||
*/
|
||||
export async function discoverSubfolderRooDirectories(cwd: string): Promise<string[]> {
|
||||
try {
|
||||
// Dynamic import to avoid vscode dependency at module load time
|
||||
// This is necessary because file-search.ts imports vscode, which is not
|
||||
// available in the webview context
|
||||
const { executeRipgrep } = await import("../search/file-search")
|
||||
|
||||
// Use ripgrep to find any file inside any .roo directory
|
||||
// This efficiently discovers all .roo folders regardless of their content
|
||||
const args = [
|
||||
"--files",
|
||||
"--hidden",
|
||||
"--follow",
|
||||
"-g",
|
||||
"**/.roo/**",
|
||||
"-g",
|
||||
"!node_modules/**",
|
||||
"-g",
|
||||
"!.git/**",
|
||||
cwd,
|
||||
]
|
||||
|
||||
const results = await executeRipgrep({ args, workspacePath: cwd })
|
||||
|
||||
// Extract unique .roo directory paths
|
||||
const rooDirs = new Set<string>()
|
||||
const rootRooDir = path.join(cwd, ".roo")
|
||||
|
||||
for (const result of results) {
|
||||
// Match paths like "subfolder/.roo/anything" or "subfolder/nested/.roo/anything"
|
||||
// Handle both forward slashes (Unix) and backslashes (Windows)
|
||||
const match = result.path.match(/^(.+?)[/\\]\.roo[/\\]/)
|
||||
if (match) {
|
||||
const rooDir = path.join(cwd, match[1], ".roo")
|
||||
// Exclude the root .roo directory (already handled by getProjectRooDirectoryForCwd)
|
||||
if (rooDir !== rootRooDir) {
|
||||
rooDirs.add(rooDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return sorted alphabetically
|
||||
return Array.from(rooDirs).sort()
|
||||
} catch (error) {
|
||||
// If discovery fails (e.g., ripgrep not available), return empty array
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ordered list of .roo directories to check (global first, then project-local)
|
||||
*
|
||||
|
|
@ -156,6 +239,71 @@ export function getRooDirectoriesForCwd(cwd: string): string[] {
|
|||
return directories
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ordered list of all .roo directories including subdirectories
|
||||
*
|
||||
* @param cwd - Current working directory (project path)
|
||||
* @returns Array of directory paths in order: [global, project-local, ...subfolders (alphabetically)]
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // For a monorepo at /Users/john/monorepo with .roo in subfolders
|
||||
* const directories = await getAllRooDirectoriesForCwd('/Users/john/monorepo')
|
||||
* // Returns:
|
||||
* // [
|
||||
* // '/Users/john/.roo', // Global directory
|
||||
* // '/Users/john/monorepo/.roo', // Project-local directory
|
||||
* // '/Users/john/monorepo/package-a/.roo', // Subfolder (alphabetical)
|
||||
* // '/Users/john/monorepo/package-b/.roo' // Subfolder (alphabetical)
|
||||
* // ]
|
||||
* ```
|
||||
*/
|
||||
export async function getAllRooDirectoriesForCwd(cwd: string): Promise<string[]> {
|
||||
const directories: string[] = []
|
||||
|
||||
// Add global directory first
|
||||
directories.push(getGlobalRooDirectory())
|
||||
|
||||
// Add project-local directory second
|
||||
directories.push(getProjectRooDirectoryForCwd(cwd))
|
||||
|
||||
// Discover and add subfolder .roo directories
|
||||
const subfolderDirs = await discoverSubfolderRooDirectories(cwd)
|
||||
directories.push(...subfolderDirs)
|
||||
|
||||
return directories
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets parent directories containing .roo folders, in order from root to subfolders
|
||||
*
|
||||
* @param cwd - Current working directory (project path)
|
||||
* @returns Array of parent directory paths (not .roo paths) containing AGENTS.md or .roo
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const dirs = await getAgentsDirectoriesForCwd('/Users/john/monorepo')
|
||||
* // Returns: ['/Users/john/monorepo', '/Users/john/monorepo/package-a', ...]
|
||||
* ```
|
||||
*/
|
||||
export async function getAgentsDirectoriesForCwd(cwd: string): Promise<string[]> {
|
||||
const directories: string[] = []
|
||||
|
||||
// Always include the root directory
|
||||
directories.push(cwd)
|
||||
|
||||
// Get all subfolder .roo directories
|
||||
const subfolderRooDirs = await discoverSubfolderRooDirectories(cwd)
|
||||
|
||||
// Extract parent directories (remove .roo from path)
|
||||
for (const rooDir of subfolderRooDirs) {
|
||||
const parentDir = path.dirname(rooDir)
|
||||
directories.push(parentDir)
|
||||
}
|
||||
|
||||
return directories
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads configuration from multiple .roo directories with project overriding global
|
||||
*
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ export type ExtensionState = Pick<
|
|||
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)
|
||||
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
|
||||
enableSubfolderRules: boolean // Whether to load rules from subdirectories
|
||||
maxReadFileLine: number // Maximum number of lines to read from a file before truncating
|
||||
maxImageFileSize: number // Maximum size of image files to process in MB
|
||||
maxTotalImageSize: number // Maximum total size for all images in a single read operation in MB
|
||||
|
|
|
|||
25
src/shared/string-extensions.d.ts
vendored
Normal file
25
src/shared/string-extensions.d.ts
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Global string extensions declaration.
|
||||
* This file provides type declarations for String.prototype extensions
|
||||
* that are used across the codebase.
|
||||
*
|
||||
* The actual implementation is in src/utils/path.ts.
|
||||
*
|
||||
* This separate declaration file is necessary because the webview-ui package
|
||||
* includes ../src/shared in its tsconfig.json but not ../src/utils/path.ts.
|
||||
* Without this file, the webview-ui compilation would fail when processing
|
||||
* files that use the toPosix() method.
|
||||
*/
|
||||
declare global {
|
||||
interface String {
|
||||
/**
|
||||
* Convert a path string to POSIX format (forward slashes).
|
||||
* Extended-Length Paths in Windows (\\?\) are preserved.
|
||||
* @returns The path with backslashes converted to forward slashes
|
||||
*/
|
||||
toPosix(): string
|
||||
}
|
||||
}
|
||||
|
||||
// This export is needed to make this file a module
|
||||
export {}
|
||||
|
|
@ -19,6 +19,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
maxOpenTabsContext: number
|
||||
maxWorkspaceFiles: number
|
||||
showRooIgnoredFiles?: boolean
|
||||
enableSubfolderRules?: boolean
|
||||
maxReadFileLine?: number
|
||||
maxImageFileSize?: number
|
||||
maxTotalImageSize?: number
|
||||
|
|
@ -36,6 +37,7 @@ type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
| "maxOpenTabsContext"
|
||||
| "maxWorkspaceFiles"
|
||||
| "showRooIgnoredFiles"
|
||||
| "enableSubfolderRules"
|
||||
| "maxReadFileLine"
|
||||
| "maxImageFileSize"
|
||||
| "maxTotalImageSize"
|
||||
|
|
@ -57,6 +59,7 @@ export const ContextManagementSettings = ({
|
|||
maxOpenTabsContext,
|
||||
maxWorkspaceFiles,
|
||||
showRooIgnoredFiles,
|
||||
enableSubfolderRules,
|
||||
setCachedStateField,
|
||||
maxReadFileLine,
|
||||
maxImageFileSize,
|
||||
|
|
@ -203,6 +206,20 @@ export const ContextManagementSettings = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VSCodeCheckbox
|
||||
checked={enableSubfolderRules}
|
||||
onChange={(e: any) => setCachedStateField("enableSubfolderRules", e.target.checked)}
|
||||
data-testid="enable-subfolder-rules-checkbox">
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:contextManagement.enableSubfolderRules.label")}
|
||||
</label>
|
||||
</VSCodeCheckbox>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
|
||||
{t("settings:contextManagement.enableSubfolderRules.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-medium">{t("settings:contextManagement.maxReadFile.label")}</span>
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
terminalZdotdir,
|
||||
writeDelayMs,
|
||||
showRooIgnoredFiles,
|
||||
enableSubfolderRules,
|
||||
remoteBrowserEnabled,
|
||||
maxReadFileLine,
|
||||
maxImageFileSize,
|
||||
|
|
@ -395,6 +396,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
maxOpenTabsContext: Math.min(Math.max(0, maxOpenTabsContext ?? 20), 500),
|
||||
maxWorkspaceFiles: Math.min(Math.max(0, maxWorkspaceFiles ?? 200), 500),
|
||||
showRooIgnoredFiles: showRooIgnoredFiles ?? true,
|
||||
enableSubfolderRules: enableSubfolderRules ?? false,
|
||||
maxReadFileLine: maxReadFileLine ?? -1,
|
||||
maxImageFileSize: maxImageFileSize ?? 5,
|
||||
maxTotalImageSize: maxTotalImageSize ?? 20,
|
||||
|
|
@ -771,6 +773,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
maxOpenTabsContext={maxOpenTabsContext}
|
||||
maxWorkspaceFiles={maxWorkspaceFiles ?? 200}
|
||||
showRooIgnoredFiles={showRooIgnoredFiles}
|
||||
enableSubfolderRules={enableSubfolderRules}
|
||||
maxReadFileLine={maxReadFileLine}
|
||||
maxImageFileSize={maxImageFileSize}
|
||||
maxTotalImageSize={maxTotalImageSize}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setAlwaysAllowSubtasks: (value: boolean) => void
|
||||
setBrowserToolEnabled: (value: boolean) => void
|
||||
setShowRooIgnoredFiles: (value: boolean) => void
|
||||
setEnableSubfolderRules: (value: boolean) => void
|
||||
setShowAnnouncement: (value: boolean) => void
|
||||
setAllowedCommands: (value: string[]) => void
|
||||
setDeniedCommands: (value: string[]) => void
|
||||
|
|
@ -234,6 +235,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
browserToolEnabled: true,
|
||||
telemetrySetting: "unset",
|
||||
showRooIgnoredFiles: true, // Default to showing .rooignore'd files with lock symbol (current behavior).
|
||||
enableSubfolderRules: false, // Default to disabled - must be enabled to load rules from subdirectories
|
||||
renderContext: "sidebar",
|
||||
maxReadFileLine: -1, // Default max read file line limit
|
||||
maxImageFileSize: 5, // Default max image file size in MB
|
||||
|
|
@ -538,6 +540,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setBrowserToolEnabled: (value) => setState((prevState) => ({ ...prevState, browserToolEnabled: value })),
|
||||
setTelemetrySetting: (value) => setState((prevState) => ({ ...prevState, telemetrySetting: value })),
|
||||
setShowRooIgnoredFiles: (value) => setState((prevState) => ({ ...prevState, showRooIgnoredFiles: value })),
|
||||
setEnableSubfolderRules: (value) => setState((prevState) => ({ ...prevState, enableSubfolderRules: value })),
|
||||
setRemoteBrowserEnabled: (value) => setState((prevState) => ({ ...prevState, remoteBrowserEnabled: value })),
|
||||
setAwsUsePromptCache: (value) => setState((prevState) => ({ ...prevState, awsUsePromptCache: value })),
|
||||
setMaxReadFileLine: (value) => setState((prevState) => ({ ...prevState, maxReadFileLine: value })),
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ describe("mergeExtensionState", () => {
|
|||
apiConfiguration: { providerId: "openrouter" } as ProviderSettings,
|
||||
telemetrySetting: "unset",
|
||||
showRooIgnoredFiles: true,
|
||||
enableSubfolderRules: false,
|
||||
renderContext: "sidebar",
|
||||
maxReadFileLine: 500,
|
||||
cloudUserInfo: null,
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ca/settings.json
generated
4
webview-ui/src/i18n/locales/ca/settings.json
generated
|
|
@ -661,6 +661,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status màx. fitxers",
|
||||
"description": "Nombre màxim d'entrades de fitxers a incloure en el context d'estat de git. Establiu a 0 per desactivar. La informació de la branca i els commits sempre es mostren quan és > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Activa les regles dels subdirectoris",
|
||||
"description": "Descobreix i carrega recursivament fitxers .roo/rules i AGENTS.md des de subdirectoris. Útil per a monorepos amb regles per paquet."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/de/settings.json
generated
4
webview-ui/src/i18n/locales/de/settings.json
generated
|
|
@ -661,6 +661,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git-Status max. Dateien",
|
||||
"description": "Maximale Anzahl von Dateieinträgen, die in den Git-Status-Kontext aufgenommen werden sollen. Auf 0 setzen, um zu deaktivieren. Branch-Informationen und Commits werden immer angezeigt, wenn > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Unterordner-Regeln aktivieren",
|
||||
"description": "Rekursiv .roo/rules und AGENTS.md-Dateien aus Unterverzeichnissen laden. Nützlich für Monorepos mit paketspezifischen Regeln."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
|
|
@ -670,6 +670,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status max files",
|
||||
"description": "Maximum number of file entries to include in git status context. Set to 0 to disable. Branch info is always shown when > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Enable subfolder rules",
|
||||
"description": "Recursively discover and load .roo/rules and AGENTS.md files from subdirectories. Useful for monorepos with per-package rules."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/es/settings.json
generated
4
webview-ui/src/i18n/locales/es/settings.json
generated
|
|
@ -661,6 +661,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status máx. archivos",
|
||||
"description": "Número máximo de entradas de archivo para incluir en el contexto de estado de git. Establézcalo en 0 para deshabilitar. La información de la rama y los commits siempre se muestran cuando es > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Activar reglas de subcarpetas",
|
||||
"description": "Descubrir y cargar recursivamente archivos .roo/rules y AGENTS.md desde subdirectorios. Útil para monorepos con reglas por paquete."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/fr/settings.json
generated
4
webview-ui/src/i18n/locales/fr/settings.json
generated
|
|
@ -661,6 +661,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status max fichiers",
|
||||
"description": "Nombre maximum de fichiers à inclure dans le contexte de statut git. Mettre à 0 pour désactiver. Les informations de branche et les commits sont toujours affichés si > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Activer les règles des sous-dossiers",
|
||||
"description": "Découvrir et charger récursivement les fichiers .roo/rules et AGENTS.md depuis les sous-répertoires. Utile pour les monorepos avec des règles par package."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/hi/settings.json
generated
4
webview-ui/src/i18n/locales/hi/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "गिट स्थिति अधिकतम फ़ाइलें",
|
||||
"description": "गिट स्थिति संदर्भ में शामिल करने के लिए फ़ाइल प्रविष्टियों की अधिकतम संख्या। अक्षम करने के लिए 0 पर सेट करें। शाखा जानकारी और कमिट हमेशा दिखाए जाते हैं जब > 0 होता है।"
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "सबफ़ोल्डर नियम सक्षम करें",
|
||||
"description": "उपनिर्देशिकाओं से .roo/rules और AGENTS.md फ़ाइलों को पुनरावर्ती रूप से खोजें और लोड करें। प्रति-पैकेज नियमों वाले मोनोरेपो के लिए उपयोगी।"
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/id/settings.json
generated
4
webview-ui/src/i18n/locales/id/settings.json
generated
|
|
@ -666,6 +666,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status maks file",
|
||||
"description": "Jumlah maksimum entri file untuk disertakan dalam konteks status git. Atur ke 0 untuk menonaktifkan. Info cabang dan commit selalu ditampilkan saat > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Aktifkan aturan subfolder",
|
||||
"description": "Temukan dan muat file .roo/rules dan AGENTS.md secara rekursif dari subdirektori. Berguna untuk monorepo dengan aturan per-paket."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/it/settings.json
generated
4
webview-ui/src/i18n/locales/it/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status max file",
|
||||
"description": "Numero massimo di voci di file da includere nel contesto dello stato di git. Imposta a 0 per disabilitare. Le informazioni sul ramo e sui commit vengono sempre mostrate quando > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Abilita regole sottocartelle",
|
||||
"description": "Scopri e carica ricorsivamente i file .roo/rules e AGENTS.md dalle sottodirectory. Utile per i monorepo con regole per pacchetto."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ja/settings.json
generated
4
webview-ui/src/i18n/locales/ja/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Gitステータス最大ファイル数",
|
||||
"description": "gitステータスコンテキストに含めるファイルエントリの最大数。無効にするには0に設定します。ブランチ情報とコミットは、> 0の場合に常に表示されます。"
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "サブフォルダルールを有効化",
|
||||
"description": "サブディレクトリから.roo/rulesとAGENTS.mdファイルを再帰的に検出してロードします。パッケージごとのルールを持つモノレポに便利です。"
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ko/settings.json
generated
4
webview-ui/src/i18n/locales/ko/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git 상태 최대 파일",
|
||||
"description": "git 상태 컨텍스트에 포함할 최대 파일 항목 수입니다. 비활성화하려면 0으로 설정하세요. 분기 정보와 커밋은 > 0일 때 항상 표시됩니다."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "하위 폴더 규칙 활성화",
|
||||
"description": "하위 디렉토리에서 .roo/rules 및 AGENTS.md 파일을 재귀적으로 검색하고 로드합니다. 패키지별 규칙이 있는 모노레포에 유용합니다."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/nl/settings.json
generated
4
webview-ui/src/i18n/locales/nl/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status max bestanden",
|
||||
"description": "Maximum aantal bestandsvermeldingen dat in de git-statuscontext moet worden opgenomen. Stel in op 0 om uit te schakelen. Branch-info en commits worden altijd getoond wanneer > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Submap-regels inschakelen",
|
||||
"description": "Recursief .roo/rules en AGENTS.md-bestanden uit submappen ontdekken en laden. Handig voor monorepo's met regels per pakket."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/pl/settings.json
generated
4
webview-ui/src/i18n/locales/pl/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status maks. plików",
|
||||
"description": "Maksymalna liczba wpisów plików do uwzględnienia w kontekście statusu git. Ustaw na 0, aby wyłączyć. Informacje o gałęzi i zatwierdzenia są zawsze pokazywane, gdy > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Włącz reguły podfolderów",
|
||||
"description": "Rekursywnie odkrywaj i ładuj pliki .roo/rules i AGENTS.md z podkatalogów. Przydatne dla monorepo z regułami dla poszczególnych pakietów."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
4
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status máx. arquivos",
|
||||
"description": "Número máximo de entradas de arquivo a serem incluídas no contexto de status do git. Defina como 0 para desativar. Informações sobre o branch e os commits são sempre exibidos quando > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Ativar regras de subpastas",
|
||||
"description": "Descobrir e carregar recursivamente arquivos .roo/rules e AGENTS.md de subdiretórios. Útil para monorepos com regras por pacote."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ru/settings.json
generated
4
webview-ui/src/i18n/locales/ru/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git статус макс. файлов",
|
||||
"description": "Максимальное количество записей файлов для включения в контекст статуса git. Установите значение 0, чтобы отключить. Информация о ветке и коммитах всегда отображается, если значение > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Включить правила подпапок",
|
||||
"description": "Рекурсивно обнаруживать и загружать файлы .roo/rules и AGENTS.md из подкаталогов. Полезно для монорепозиториев с правилами для каждого пакета."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/tr/settings.json
generated
4
webview-ui/src/i18n/locales/tr/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git durumu maks. dosya",
|
||||
"description": "Git durum bağlamına dahil edilecek maksimum dosya girişi sayısı. Devre dışı bırakmak için 0 olarak ayarlayın. Dal bilgisi ve commit'ler > 0 olduğunda her zaman gösterilir."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Alt klasör kurallarını etkinleştir",
|
||||
"description": "Alt dizinlerden .roo/rules ve AGENTS.md dosyalarını yinelemeli olarak keşfet ve yükle. Paket başına kuralları olan monorepo'lar için kullanışlı."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/vi/settings.json
generated
4
webview-ui/src/i18n/locales/vi/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git status tệp tối đa",
|
||||
"description": "Số lượng mục tệp tối đa để bao gồm trong ngữ cảnh trạng thái git. Đặt thành 0 để tắt. Thông tin nhánh và các commit luôn được hiển thị khi > 0."
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "Bật quy tắc thư mục con",
|
||||
"description": "Khám phá và tải đệ quy các tệp .roo/rules và AGENTS.md từ các thư mục con. Hữu ích cho monorepo có quy tắc theo gói."
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
4
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git 状态最大文件数",
|
||||
"description": "git状态上下文中包含的最大文件条目数。设为0禁用。分支信息和提交在>0时始终显示。"
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "启用子文件夹规则",
|
||||
"description": "递归发现并加载子目录中的 .roo/rules 和 AGENTS.md 文件。适用于具有每包规则的 monorepo。"
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
4
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
|
|
@ -662,6 +662,10 @@
|
|||
"maxGitStatusFiles": {
|
||||
"label": "Git 狀態最大檔案數",
|
||||
"description": "git狀態上下文中包含的最大檔案條目數。設為0禁用。分支資訊和提交在>0時始終顯示。"
|
||||
},
|
||||
"enableSubfolderRules": {
|
||||
"label": "啟用子資料夾規則",
|
||||
"description": "遞迴發現並載入子目錄中的 .roo/rules 和 AGENTS.md 檔案。適用於具有每包規則的 monorepo。"
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,10 @@ export default defineConfig(({ mode }) => {
|
|||
// Use a single combined CSS bundle so both webviews share styles
|
||||
cssCodeSplit: false,
|
||||
rollupOptions: {
|
||||
// Externalize vscode module - it's imported by file-search.ts which is
|
||||
// dynamically imported by roo-config/index.ts, but should never be bundled
|
||||
// in the webview since it's not available in the browser context
|
||||
external: ["vscode"],
|
||||
input: {
|
||||
index: resolve(__dirname, "index.html"),
|
||||
"browser-panel": resolve(__dirname, "browser-panel.html"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue