#18291 · @t7r0n · opened Mar 19, 2026 at 9:42 PM UTC · last updated Mar 21, 2026 at 6:18 PM UTC

fix(opencode): include linked directories in file search index

appfix
72
+6942 files

Score breakdown

Impact

8.0

Clarity

9.0

Urgency

7.0

Ease Of Review

9.0

Guidelines

9.0

Readiness

9.0

Size

6.0

Trust

5.0

Traction

5.0

Summary

This PR fixes a bug where file search, used for autocomplete, failed to index files located in linked directories on Windows. It updates the file scanner to properly follow symlinks. A regression test is included to verify the fix.

Open in GitHub

Description

Issue for this PR

Closes #18182

Type of change

  • [x] Bug fix
  • [ ] New feature
  • [ ] Refactor / code improvement
  • [ ] Documentation

What does this PR do?

@ file autocomplete and find.files rely on File.search(), which uses a cache built in File.scan(). That scan used Ripgrep.files({ cwd }) without following links, so files under junction/symlink directories were not indexed.

This PR updates the scan to use follow: true and adds a regression test that creates a linked docs directory and verifies docs/linked/entry.ts is returned by File.search().

How did you verify your code works?

  • bunx prettier --check src/file/index.ts test/file/index.test.ts
  • bun test --timeout 30000 test/file/index.test.ts -t "File.search()"
  • bun test --timeout 30000 test/file/ripgrep.test.ts

Screenshots / recordings

N/A (non-UI change)

Checklist

  • [x] I have tested my changes locally
  • [x] I have not included unrelated changes in this PR

Linked Issues

#18182 File reference (@) fails to index files/folders created via Junction or SymbolicLink on Windows

View issue

Comments

PR comments

t7r0n

Added a follow-up fix for the Windows unit timeout seen on this PR.

What changed:

  • Replaced unconditional follow: true in File.scan() with a safer approach:
    • keep the normal non-follow Ripgrep.files({ cwd }) scan for the workspace
    • separately index root linked directories (symlink/junction) by scanning each link path directly
  • Added dedupe guards for files/dirs and linked targets to avoid repeated traversal/cycle amplification.

Why:

  • CI showed unit (windows) hanging in Run unit tests until job timeout.
  • Global recursive follow over links can explode traversal on Windows junction-heavy trees.

Local validation:

  • bun test --timeout 30000 test/file/index.test.ts -t "File.search()" (from packages/opencode)
  • bun test --timeout 30000 test/file/ripgrep.test.ts (from packages/opencode)
  • bun turbo test (repo root)
  • pre-push hook bun turbo typecheck

t7r0n

Rebased this branch on latest and resolved conflicts caused by the file module refactor.\n\nWhat I changed:\n- ported the symlink/junction indexing fix from to (current implementation location)\n- kept the regression test for linked directories in \n- preserved case-insensitive dedupe on Windows and guarded linked-target traversal to avoid recursive loops\n\nThis should clear the merge conflict state and keep behavior aligned with current architecture.

t7r0n

Rebased this branch onto the latest dev and resolved merge conflicts. The PR is now mergeable again and CI has been retriggered on the updated head.

Changed Files

packages/opencode/src/file/index.ts

+444
@@ -385,20 +385,60 @@ export namespace File {
next.dirs = Array.from(dirs).toSorted()
} else {
const seen = new Set<string>()
for await (const file of Ripgrep.files({ cwd: Instance.directory })) {
const seenDirs = new Set<string>()
const seenFiles = new Set<string>()
const seenLinkedTargets = new Set<string>()
const key = (value: string) => {
const normalized = value.replaceAll("\\", "/")
return process.platform === "win32" ? normalized.toLowerCase() : normalized
}
const addFile = (file: string) => {
const fileKey = key(file)
if (seenFiles.has(fileKey)) return
seenFiles.add(fileKey)
next.files.push(file)
let current = file
while (true) {
const dir = path.dirname(current)
if (dir === ".") break
if (dir === current) break
current = dir
if (seen.has(dir)) continue
seen.add(dir)
const dirKey = key(dir)
if

packages/opencode/test/file/index.test.ts

+250
@@ -818,6 +818,31 @@ describe("file/index Filesystem patterns", () => {
},
})
})
test(
"indexes files inside linked directories",
async () => {
await using tmp = await setupSearchableRepo()
await using external = await tmpdir()
await fs.mkdir(path.join(external.path, "linked"), { recursive: true })
await fs.writeFile(path.join(external.path, "linked", "entry.ts"), "export const linked = true\n", "utf-8")
await fs.symlink(external.path, path.join(tmp.path, "docs"), process.platform === "win32" ? "junction" : "dir")
await Instance.provide({
directory: tmp.path,
fn: async () => {
await File.init()
const result = await File.search({ query: "entry.ts", type: "file" })
const normalized = result.map((item) => item.replaceAll("\\", "/"))
expect(normalized).toContain("docs/linked/entry.ts")
},
})
},
{ timeout: 30000 },
)
})
describe("File.read() - diff/patch", () => {