diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 74061ef54..49e20dc78 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -1161,7 +1161,6 @@ export class BackgroundManager { properties: props as Record, findBySession: (id) => this.findBySession(id), idleDeferralTimers: this.idleDeferralTimers, - recentlyCompactedSessions: this.recentlyCompactedSessions, validateSessionHasOutput: (id) => this.validateSessionHasOutput(id), checkSessionTodos: (id) => this.checkSessionTodos(id), nudgeCouncilMemberIfNeeded: (task, sid) => this.nudgeCouncilMemberIfNeeded(task, sid), diff --git a/src/tools/glob/extract-directory-prefix.test.ts b/src/tools/glob/extract-directory-prefix.test.ts new file mode 100644 index 000000000..124e9b689 --- /dev/null +++ b/src/tools/glob/extract-directory-prefix.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, test } from "bun:test" +import { extractDirectoryPrefix } from "./extract-directory-prefix" + +describe("extractDirectoryPrefix", () => { + describe("#given a pattern with directory prefix before glob metacharacters", () => { + test("#then extracts the prefix and returns the remaining glob", () => { + expect(extractDirectoryPrefix("apps/backend/**/*.ts")).toEqual({ + prefix: "apps/backend", + glob: "**/*.ts", + }) + }) + + test("#then handles brace expansion after prefix", () => { + expect(extractDirectoryPrefix("apps/backend/**/*.{ts,js,json}")).toEqual({ + prefix: "apps/backend", + glob: "**/*.{ts,js,json}", + }) + }) + + test("#then handles single directory prefix", () => { + expect(extractDirectoryPrefix("src/**/*.tsx")).toEqual({ + prefix: "src", + glob: "**/*.tsx", + }) + }) + + test("#then handles deep prefix", () => { + expect(extractDirectoryPrefix("packages/core/src/**/*.ts")).toEqual({ + prefix: "packages/core/src", + glob: "**/*.ts", + }) + }) + + test("#then handles brace expansion in the first glob segment", () => { + expect(extractDirectoryPrefix("src/{hooks,components}/**/*.tsx")).toEqual({ + prefix: "src", + glob: "{hooks,components}/**/*.tsx", + }) + }) + + test("#then handles question mark metacharacter", () => { + expect(extractDirectoryPrefix("src/component?/**/*.ts")).toEqual({ + prefix: "src", + glob: "component?/**/*.ts", + }) + }) + + test("#then handles bracket metacharacter", () => { + expect(extractDirectoryPrefix("src/[a-z]omponents/**/*.ts")).toEqual({ + prefix: "src", + glob: "[a-z]omponents/**/*.ts", + }) + }) + }) + + describe("#given a pattern without directory prefix", () => { + test("#then returns empty prefix for **/ patterns", () => { + expect(extractDirectoryPrefix("**/*.ts")).toEqual({ + prefix: "", + glob: "**/*.ts", + }) + }) + + test("#then returns empty prefix for simple wildcard", () => { + expect(extractDirectoryPrefix("*.ts")).toEqual({ + prefix: "", + glob: "*.ts", + }) + }) + + test("#then returns empty prefix for brace-only pattern", () => { + expect(extractDirectoryPrefix("*.{ts,js}")).toEqual({ + prefix: "", + glob: "*.{ts,js}", + }) + }) + }) + + describe("#given a fully literal pattern (no metacharacters)", () => { + test("#then returns the pattern as-is with empty prefix", () => { + expect(extractDirectoryPrefix("src/components/Button.tsx")).toEqual({ + prefix: "", + glob: "src/components/Button.tsx", + }) + }) + + test("#then handles single filename", () => { + expect(extractDirectoryPrefix("package.json")).toEqual({ + prefix: "", + glob: "package.json", + }) + }) + }) +}) diff --git a/src/tools/glob/extract-directory-prefix.ts b/src/tools/glob/extract-directory-prefix.ts new file mode 100644 index 000000000..af9f6158e --- /dev/null +++ b/src/tools/glob/extract-directory-prefix.ts @@ -0,0 +1,40 @@ +const GLOB_META = /[*?[\]{]/ + +/** + * Splits a glob pattern into a static directory prefix and a glob-only portion. + * + * Ripgrep's `--glob` flag doesn't match directory prefixes in patterns. + * For example, `rg --files --glob='apps/backend/**\/*.ts' /project` returns nothing + * even though files exist under `/project/apps/backend/`. + * + * This function extracts the leading literal path segments so the caller can + * append them to the search path and pass only the glob portion to ripgrep. + * + * @example + * extractDirectoryPrefix("apps/backend/**\/*.ts") + * // { prefix: "apps/backend", glob: "**\/*.ts" } + * + * extractDirectoryPrefix("**\/*.ts") + * // { prefix: "", glob: "**\/*.ts" } + * + * extractDirectoryPrefix("src/{hooks,components}/**\/*.tsx") + * // { prefix: "src", glob: "{hooks,components}/**\/*.tsx" } + */ +export function extractDirectoryPrefix(pattern: string): { prefix: string; glob: string } { + const segments = pattern.split("/") + + let splitIndex = 0 + for (const segment of segments) { + if (GLOB_META.test(segment)) break + splitIndex++ + } + + if (splitIndex === 0 || splitIndex === segments.length) { + return { prefix: "", glob: pattern } + } + + return { + prefix: segments.slice(0, splitIndex).join("/"), + glob: segments.slice(splitIndex).join("/"), + } +} diff --git a/src/tools/glob/tools.ts b/src/tools/glob/tools.ts index d808377b2..3c6b7e159 100644 --- a/src/tools/glob/tools.ts +++ b/src/tools/glob/tools.ts @@ -4,6 +4,7 @@ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" import { runRgFiles } from "./cli" import { resolveGrepCliWithAutoInstall } from "./constants" import { formatGlobResult } from "./result-formatter" +import { extractDirectoryPrefix } from "./extract-directory-prefix" export function createGlobTools(ctx: PluginInput): Record { const glob: ToolDefinition = tool({ @@ -29,11 +30,13 @@ export function createGlobTools(ctx: PluginInput): Record const dir = typeof runtimeCtx.directory === "string" ? runtimeCtx.directory : ctx.directory const searchPath = args.path ? resolve(dir, args.path) : dir - const paths = [searchPath] + const { prefix, glob: effectivePattern } = extractDirectoryPrefix(args.pattern) + const effectivePath = prefix ? resolve(searchPath, prefix) : searchPath + const paths = [effectivePath] const result = await runRgFiles( { - pattern: args.pattern, + pattern: effectivePattern, paths, }, cli