Skip to content

fix: resolve symlinks in Files panel and Auto Run folder tree#831

Merged
pedramamini merged 5 commits intomainfrom
fix/829-symlink-support
Apr 17, 2026
Merged

fix: resolve symlinks in Files panel and Auto Run folder tree#831
pedramamini merged 5 commits intomainfrom
fix/829-symlink-support

Conversation

@pedramamini
Copy link
Copy Markdown
Collaborator

@pedramamini pedramamini commented Apr 13, 2026

Summary

  • Symlinked subdirectories and files are now correctly resolved in the Files panel (Right Bar) and Auto Run document tree
  • Dirent.isDirectory()/isFile() return false for symlinks — we now check isSymbolicLink() and follow with fs.stat() to determine the actual target type
  • Remote (SSH) path: augmented the ls -1AF command in readDirRemote to also identify symlinks whose targets are directories, so they can be recursed into
  • Removed overly-strict !entry.isSymlink guards in Auto Run that excluded symlinked .md files and prevented recursion into symlinked directories
  • Fixed fs:countItems to also follow symlinks when counting items recursively

Closes #829

Test plan

  • Create a symlinked subdirectory inside a project (ln -s /some/path ./project/linked-dir)
  • Open Maestro → Right Bar → Files tab: verify linked-dir appears and is expandable
  • Place .md files inside the symlinked directory
  • Set the project as an Auto Run folder: verify documents inside the symlinked directory are discovered
  • Verify broken symlinks don't crash the file listing (should appear as files or be skipped)
  • Verify existing tests pass (22,451 tests passing)

Summary by CodeRabbit

  • Bug Fixes

    • Improved symbolic-link handling across directory scans, listings, image discovery, counting, and backup removal — symlinked files and folders are now resolved, recursed into when appropriate, and broken symlinks are gracefully skipped; markdown and image detection now include valid symlinked files. Added recursion depth protection to avoid infinite loops from directory cycles.
  • Tests

    • Added and updated tests for symlink resolution, broken-symlink behavior, recursion, and cycle protection.

Dirent.isDirectory()/isFile() return false for symlinks since they
inspect the link itself, not the target. This caused symlinked
subdirectories to be invisible in the Files panel and Auto Run
document tree.

Local: after readdir, check isSymbolicLink() and follow with
fs.stat() to determine the actual target type.

Remote: augment the ls -1AF command with a follow-up that
identifies symlinks whose targets are directories, so
readDirRemote now sets isDirectory=true for symlinked dirs.

Also remove overly-strict !entry.isSymlink guards in autorun.ts
that excluded symlinked .md files and prevented recursion into
symlinked directories for backup cleanup.

Closes #829
Update mock Dirent objects in filesystem and autorun tests to include
isSymbolicLink() method, matching the new symlink resolution logic.

Also fix countRecursive in fs:countItems to resolve symlinks via
fs.stat(), consistent with the fs:readDir fix.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

Resolve and follow symlinks when classifying directory entries for local and remote reads; recurse into symlinked directories, treat broken symlinks as files, and add utilities/tests to resolve Dirent target types.

Changes

Cohort / File(s) Summary
Dirent utilities
src/main/utils/dirent-utils.ts, src/__tests__/main/utils/dirent-utils.test.ts
Add resolveDirentType and readDirWithResolvedTypes to classify entries (isDirectory/isFile/isBrokenSymlink) by calling fs.stat for symlinks; tests cover resolution and broken links.
Local filesystem handlers
src/main/ipc/handlers/filesystem.ts, src/main/ipc/handlers/autorun.ts
Use resolved dirent types for fs:readDir, fs:countItems, autorun scanning and markdown detection; recurse into symlinked directories and treat dangling symlinks as files.
Remote filesystem handling (SSH)
src/main/utils/remote-fs.ts, src/main/ipc/handlers/autorun.ts, src/main/ipc/handlers/filesystem.ts
Update readDirRemote to detect symlinked directories and preserve isSymlink while marking directory targets; remove exclusions that previously skipped symlinks so remote handlers use resolved target type.
Tests & mocks
src/__tests__/main/ipc/handlers/autorun.test.ts, src/__tests__/main/ipc/handlers/filesystem.test.ts, src/__tests__/renderer/components/DocumentGraph/graphDataBuilder.test.ts
Extend Dirent-like mocks with isSymbolicLink() and add test cases for symlink behavior (including recursion into symlinked dirs, symlinked files, broken symlinks, and directory-cycle depth cap).

Sequence Diagram

sequenceDiagram
    participant Handler as IPC Handler / Renderer Caller
    participant Readdir as readdir()/readDirRemote
    participant Stat as fs.stat()/remote ls -ld
    participant Resolver as resolveDirentType / utils

    Handler->>Readdir: request directory entries
    Readdir-->>Handler: return Dirent-like entries (may flag symlink)
    alt entry.isSymbolicLink
        Handler->>Stat: stat(fullPath) to follow symlink
        Stat-->>Resolver: provide target info or error
        Resolver-->>Handler: resolved {isDirectory,isFile,isBrokenSymlink}
    else not symlink
        Readdir-->>Handler: use entry.isDirectory / entry.isFile directly
    end
    Handler->>Handler: include files / recurse into directories based on resolved flags
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • #542 — Modifies the same fs:readDir IPC handler and adjacent read-dir behavior; potential overlap with NFC normalization and merging logic.

Suggested labels

ready to merge

Poem

🐰 I hop through links both true and sly,
I stat each tail, I peek and pry.
Broken paths now yield a face,
Symlinked folders find their place.
Hooray — the tree is whole, oh my! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: resolve symlinks in Files panel and Auto Run folder tree' directly aligns with the main objective of the PR, which is to correctly detect and follow symbolic links in the Files panel and Auto Run folder scanner.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #829: symlink detection and fs.stat resolution for local files [#829], remote symlink detection via enhanced ls parsing [#829], Auto Run recursal into symlinked directories [#829], fs:countItems symlink support [#829], and depth capping to prevent infinite recursion cycles [#829].
Out of Scope Changes check ✅ Passed All changes directly address symlink resolution in filesystem operations and Auto Run scanning. Test additions validate symlink behavior. No unrelated modifications or refactoring detected outside the stated scope.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/829-symlink-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 13, 2026

Greptile Summary

This PR fixes symlink handling in the Files panel and Auto Run folder tree by introducing a resolveDirentType utility that calls fs.stat() to determine whether a symlink target is a directory or file, replacing the previous behaviour where Dirent.isDirectory()/isFile() silently returned false for all symlinks. The remote SSH path is also updated to detect directory-symlinks via a secondary shell command and mark them as isDirectory=true before consumers recurse into them.

Confidence Score: 5/5

Safe to merge — the only remaining finding is a P2 style concern about circular-symlink cycle detection.

All changes are correct and well-tested. The circular-symlink concern is bounded by the OS ELOOP limit (~40 levels) and is a known limitation of any recursive symlink-following traversal, so it does not block merge.

src/main/utils/dirent-utils.ts — consider adding cycle detection via a visited-realpath Set if circular symlinks in user projects are a concern.

Important Files Changed

Filename Overview
src/main/utils/dirent-utils.ts New utility correctly follows symlinks via fs.stat(); no cycle detection means circular symlinks cause deep (but ELOOP-bounded) recursion.
src/main/ipc/handlers/autorun.ts scanDirectory and checkForMarkdownFiles correctly resolve symlinks and skip broken ones; same circular-symlink concern applies through recursive calls.
src/main/ipc/handlers/filesystem.ts fs:readDir and fs:countItems now resolve symlinks correctly; isFile: !entry.isDirectory for remote entries is semantically correct after readDirRemote sets isDirectory=true for dir-symlinks.
src/main/utils/remote-fs.ts Augmented ls command with symlink-dir scan using SYMDIR marker; correctly identifies remote dir-symlinks so consumers can recurse into them.
src/tests/main/utils/dirent-utils.test.ts New test file with good coverage of dir-symlinks, file-symlinks, broken symlinks, and the readDirWithResolvedTypes wrapper.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[fs.readdir withFileTypes] --> B{entry.isSymbolicLink?}
    B -- No --> C[Use isDirectory / isFile directly]
    B -- Yes --> D[fs.stat fullPath]
    D -- success --> E[Use stat.isDirectory / stat.isFile]
    D -- ENOENT / ELOOP / error --> F[isBrokenSymlink = true]
    C --> G[Caller decides: recurse dir / include file / skip]
    E --> G
    F --> H{Context}
    H -- scanDirectory --> I[Filter out null — skip broken link]
    H -- checkForMarkdownFiles --> J[isDirectory=false, isFile=false — skip silently]
    H -- fs:readDir --> K[isFile = true — show in file browser]
    H -- countRecursive --> L[fileCount++ — count as file]
Loading

Reviews (2): Last reviewed commit: "test: patch fs/promises mocks exposed by..." | Re-trigger Greptile

Comment thread src/main/utils/remote-fs.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/__tests__/main/ipc/handlers/autorun.test.ts (1)

234-245: Consider adding tests for actual symlink entries.

The mocks consistently return isSymbolicLink: () => false, which tests the non-symlink path. Consider adding test cases where isSymbolicLink: () => true to verify:

  1. Symlinked directories are recursed into after fs.stat() resolves the target
  2. Symlinked .md files are included in listings
  3. Broken symlinks (where fs.stat() throws) are handled gracefully
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/ipc/handlers/autorun.test.ts` around lines 234 - 245, Add
tests in src/__tests__/main/ipc/handlers/autorun.test.ts that include mocked
Dirent-like entries with isSymbolicLink: () => true and exercise the code paths
that call fs.stat; specifically add: (1) a symlinked directory case where
fs.stat resolves to a directory and verify recursion into it, (2) a symlinked
.md file case where fs.stat resolves to a regular file and verify it appears in
listings, and (3) a broken symlink case where fs.stat rejects and verify the
handler swallows/logs the error and continues. Use the same mock setup as the
existing entries but set isSymbolicLink to true and stub fs.stat (or
fs.promises.stat) to resolve or reject accordingly so you validate the symlink
handling logic invoked by the autorun/handler under test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/__tests__/main/ipc/handlers/autorun.test.ts`:
- Around line 234-245: Add tests in
src/__tests__/main/ipc/handlers/autorun.test.ts that include mocked Dirent-like
entries with isSymbolicLink: () => true and exercise the code paths that call
fs.stat; specifically add: (1) a symlinked directory case where fs.stat resolves
to a directory and verify recursion into it, (2) a symlinked .md file case where
fs.stat resolves to a regular file and verify it appears in listings, and (3) a
broken symlink case where fs.stat rejects and verify the handler swallows/logs
the error and continues. Use the same mock setup as the existing entries but set
isSymbolicLink to true and stub fs.stat (or fs.promises.stat) to resolve or
reject accordingly so you validate the symlink handling logic invoked by the
autorun/handler under test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0f846ff5-3cf2-4904-b30f-df200eca25f3

📥 Commits

Reviewing files that changed from the base of the PR and between 94ccdf8 and f88d039.

📒 Files selected for processing (5)
  • src/__tests__/main/ipc/handlers/autorun.test.ts
  • src/__tests__/main/ipc/handlers/filesystem.test.ts
  • src/main/ipc/handlers/autorun.ts
  • src/main/ipc/handlers/filesystem.ts
  • src/main/utils/remote-fs.ts

Pulls the duplicated Dirent symlink-resolution logic out of the Files
panel, countItems, and Auto Run scanners into a single helper
(src/main/utils/dirent-utils.ts). Each caller now decides how to
present broken symlinks (shown as files in fs:readDir, counted as
files in countItems, skipped during Auto Run's .md walk) while sharing
one implementation.

Also tightens the remote-fs symlink-dir discovery glob to explicitly
exclude "." and ".." via /.[!.]* and /..?* patterns, and adds test
coverage for symlink-to-dir, symlink-to-file, and broken-symlink cases
in the new helper plus the Files panel, countItems, and Auto Run
handlers.
Two pre-existing mocks surfaced real failures once resolveDirentType
is on the hot path:

- fs:countItems "count items in local directory" was missing isFile()
  on its Dirent mocks. The old countRecursive only looked at
  isDirectory/isSymbolicLink, but the new helper also reads isFile()
  to classify non-symlink entries.
- The new dirent-utils test needs readdir mocked alongside stat for
  readDirWithResolvedTypes; swap the stray vi.spyOn for vi.mocked on
  the module-level mock so the fn is discoverable.
@pedramamini
Copy link
Copy Markdown
Collaborator Author

@greptile re-analyze please

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/ipc/handlers/filesystem.ts (1)

407-422: ⚠️ Potential issue | 🔴 Critical

Symlink cycles will cause unbounded recursion in fs:countItems.

With resolveDirentType now returning isDirectory: true for symlinks that point at directories, countRecursive will happily follow a cycle (e.g. /a/link -> /a) forever — there is no depth cap and no visited-path tracking. Previously entry.isDirectory() was false for symlinks so this was impossible. This can stack-overflow the main process or wedge the IPC call indefinitely on benign user filesystems (node_modules hoisting, system symlinks, etc.).

At minimum, add a depth cap (as fs:directorySize already does). Ideally, track visited fs.realpath targets to detect cycles precisely.

🛡️ Suggested guard
-		// Local: standard fs count
-		let fileCount = 0;
-		let folderCount = 0;
-
-		const countRecursive = async (dir: string) => {
-			const entries = await fs.readdir(dir, { withFileTypes: true });
-			for (const entry of entries) {
-				const fullPath = path.join(dir, entry.name);
-				const resolved = await resolveDirentType(entry, fullPath);
-				if (resolved.isDirectory) {
-					folderCount++;
-					await countRecursive(fullPath);
-				} else {
-					// Files, symlinks-to-files, and broken symlinks all count as files
-					fileCount++;
-				}
-			}
-		};
-
-		await countRecursive(dirPath);
+		// Local: standard fs count
+		let fileCount = 0;
+		let folderCount = 0;
+		const visited = new Set<string>();
+		const MAX_DEPTH = 20;
+
+		const countRecursive = async (dir: string, depth: number) => {
+			if (depth >= MAX_DEPTH) return;
+			let realDir: string;
+			try {
+				realDir = await fs.realpath(dir);
+			} catch {
+				return;
+			}
+			if (visited.has(realDir)) return;
+			visited.add(realDir);
+
+			const entries = await fs.readdir(dir, { withFileTypes: true });
+			for (const entry of entries) {
+				const fullPath = path.join(dir, entry.name);
+				const resolved = await resolveDirentType(entry, fullPath);
+				if (resolved.isDirectory) {
+					folderCount++;
+					await countRecursive(fullPath, depth + 1);
+				} else {
+					fileCount++;
+				}
+			}
+		};
+
+		await countRecursive(dirPath, 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/filesystem.ts` around lines 407 - 422, The recursive
walker countRecursive in fs:countItems can follow symlinked directories
indefinitely now that resolveDirentType treats symlinks-to-dirs as directories;
add a recursion safety mechanism by (1) adding a maxDepth parameter (reusing the
same cap pattern as fs:directorySize) passed into countRecursive and bailing out
once reached, and (2) track visited real targets using fs.realpath (store
resolved paths in a Set) and skip traversing if realpath is already seen; update
the logic that increments folderCount/fileCount in countRecursive to still count
the entry but not recurse into visited or over-depth directories so cycles and
deep trees are guarded against.
src/main/ipc/handlers/autorun.ts (1)

79-130: ⚠️ Potential issue | 🔴 Critical

Symlink cycles will cause infinite recursion in scanDirectory.

Before this PR, entry.isDirectory() returned false for symlinks, so cycles were impossible. Now resolveDirentType classifies symlink → directory as a directory and the recursion at line 109 will follow it, including links that point at an ancestor (/folder/a/link -> /folder). There is no depth cap or visited-path set, so the main process can stack-overflow on perfectly reasonable user folders (e.g. a node_modules symlink farm, a link to the project root, .config pointing at $HOME).

Same issue applies to checkForMarkdownFiles below (line 226) and the SSH counterparts scanDirectoryRemote / deleteBackupsRemoteRecursive, which now also recurse through symlinked dirs per the updated readDirRemote classification.

Recommend a shared guard that (a) caps recursion depth and (b) deduplicates by fs.realpath for the local paths; for remote, rely on a depth cap since resolving real paths over SSH is expensive.

🛡️ Sketch of a guarded scanDirectory
-async function scanDirectory(dirPath: string, relativePath: string = ''): Promise<TreeNode[]> {
-	const entries = await fs.readdir(dirPath, { withFileTypes: true });
+const MAX_SCAN_DEPTH = 20;
+async function scanDirectory(
+	dirPath: string,
+	relativePath: string = '',
+	depth: number = 0,
+	visited: Set<string> = new Set()
+): Promise<TreeNode[]> {
+	if (depth >= MAX_SCAN_DEPTH) return [];
+	let realDir: string;
+	try {
+		realDir = await fs.realpath(dirPath);
+	} catch {
+		return [];
+	}
+	if (visited.has(realDir)) return [];
+	visited.add(realDir);
+	const entries = await fs.readdir(dirPath, { withFileTypes: true });
 	const nodes: TreeNode[] = [];
 	// ...
 	for (const entry of sortedEntries) {
 		// ...
 		if (entry.isDir) {
-			const children = await scanDirectory(path.join(dirPath, entry.name), entryRelativePath);
+			const children = await scanDirectory(
+				path.join(dirPath, entry.name),
+				entryRelativePath,
+				depth + 1,
+				visited
+			);

Analogous change for checkForMarkdownFiles, scanDirectoryRemote, and deleteBackupsRemoteRecursive (the remote ones only need the depth cap).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/autorun.ts` around lines 79 - 130, scanDirectory
currently recurses into symlinked directories (via resolveDirentType) and can
infinitely loop; fix by adding a recursion guard: add an optional maxDepth
parameter (e.g. default 50) and a visited RealPath Set to scanDirectory
signature, call fs.realpath on each directory before recursing and skip
recursion if depth exceeded or realpath already in visited; pass an incremented
depth and a cloned/updated visited set into recursive calls; apply the same
pattern to checkForMarkdownFiles, while for scanDirectoryRemote and
deleteBackupsRemoteRecursive implement the maxDepth guard (remote versions
should not rely on realpath resolution due to SSH cost) and skip recursion when
depth exceeds the cap.
♻️ Duplicate comments (1)
src/main/ipc/handlers/autorun.ts (1)

226-253: ⚠️ Potential issue | 🔴 Critical

Same cycle concern as scanDirectory — infinite recursion on symlink loops.

checkForMarkdownFiles is called by autorun:hasDocuments, which runs any time the wizard / autorun UI checks a folder. A symlink cycle will now wedge this handler; please apply the same depth + visited-realpath guard suggested for scanDirectory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/autorun.ts` around lines 226 - 253,
checkForMarkdownFiles can infinite-recurse on symlink cycles; update it to
accept and propagate a recursion guard (e.g., a maxDepth numeric parameter and a
visited Set of realpaths) similar to scanDirectory: at start resolve the
directory's realpath, if already in visited return false, add it to visited,
bail out if currentDepth exceeds a sensible MAX_DEPTH, and pass incremented
depth and the same visited Set into recursive calls; reference function name
checkForMarkdownFiles and the autorun:hasDocuments caller so you ensure the new
optional parameters are threaded through where it’s invoked.
🧹 Nitpick comments (2)
src/main/ipc/handlers/filesystem.ts (1)

265-287: Drive-by: fs:directorySize still uses entry.isDirectory() and will misclassify symlinked dirs.

Not a regression introduced by this PR, and the depth cap at line 262 prevents runaway recursion, but the result of this handler is now inconsistent with fs:readDir / fs:countItems: a symlinked directory is counted as a file here but as a folder elsewhere, and its contents aren't summed into totalSize. If you're normalizing symlink handling across the file, consider migrating this block to resolveDirentType as well (while keeping the depth limit).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/filesystem.ts` around lines 265 - 287, The
directorySize traversal misclassifies symlinked directories because it uses
entry.isDirectory(); update the loop in calculateSize to call the existing
resolveDirentType(entry, fullPath) (same helper used by fs:readDir /
fs:countItems) and switch on that returned type so symlinked directories are
treated like directories (recursing and adding their contents to totalSize)
while preserving the depth cap and the ignore patterns (node_modules,
__pycache__) and keeping the existing try/catch around fs.stat for files.
src/main/utils/dirent-utils.ts (1)

50-63: Consider distinguishing broken-symlink errors from unexpected ones.

The bare catch {} collapses every possible fs.stat failure — including EACCES, EIO, or programmer errors — into isBrokenSymlink: true with no visibility. Per coding guidelines, unexpected errors should be surfaced for Sentry.

Minimum fix: log unknown error codes so broken-link suppression doesn't mask real failures.

🔎 Proposed refinement
-	try {
-		const targetStat = await fs.stat(fullPath);
-		return {
-			isDirectory: targetStat.isDirectory(),
-			isFile: targetStat.isFile(),
-			isBrokenSymlink: false,
-		};
-	} catch {
-		return {
-			isDirectory: false,
-			isFile: false,
-			isBrokenSymlink: true,
-		};
-	}
+	try {
+		const targetStat = await fs.stat(fullPath);
+		return {
+			isDirectory: targetStat.isDirectory(),
+			isFile: targetStat.isFile(),
+			isBrokenSymlink: false,
+		};
+	} catch (err: unknown) {
+		const code = (err as NodeJS.ErrnoException | undefined)?.code;
+		// ENOENT / ELOOP / EACCES are expected for broken or inaccessible symlinks.
+		// Anything else is unexpected and worth reporting for diagnostics.
+		if (code && !['ENOENT', 'ELOOP', 'EACCES', 'ENOTDIR'].includes(code)) {
+			// e.g. logger.warn / captureException from src/utils/sentry.ts
+		}
+		return { isDirectory: false, isFile: false, isBrokenSymlink: true };
+	}

As per coding guidelines: "Do not silently swallow errors. Let unhandled exceptions bubble up to Sentry for error tracking in production."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/utils/dirent-utils.ts` around lines 50 - 63, The catch block around
fs.stat(fullPath) is treating every error as a broken symlink; change it to
catch (err) and inspect err.code — return isBrokenSymlink: true only for known
"broken link" codes (e.g. 'ENOENT', 'ENOTDIR', 'ELOOP'), and for any
other/unknown error either rethrow the error or log it and rethrow so it
surfaces to Sentry; keep the returned shape (isDirectory/isFile/isBrokenSymlink)
when handling known broken-link codes and use fullPath and fs.stat as the
reference points to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/ipc/handlers/filesystem.ts`:
- Around line 124-136: scanMarkdownFiles' recursive helper scanDir can recurse
indefinitely through symlink cycles; add a numeric depth parameter (e.g., depth
or remainingDepth) to scanDir and enforce a maxDepth (use the same maxDepth=10
convention as fileExplorer.ts) so that when depth <= 0 the function returns
without recursing further. Thread this parameter through all recursive calls
from scanDir (decrementing on each recursion) and ensure the top-level
scanMarkdownFiles invocation starts with the maxDepth value; keep the existing
behavior of listing files otherwise.

---

Outside diff comments:
In `@src/main/ipc/handlers/autorun.ts`:
- Around line 79-130: scanDirectory currently recurses into symlinked
directories (via resolveDirentType) and can infinitely loop; fix by adding a
recursion guard: add an optional maxDepth parameter (e.g. default 50) and a
visited RealPath Set to scanDirectory signature, call fs.realpath on each
directory before recursing and skip recursion if depth exceeded or realpath
already in visited; pass an incremented depth and a cloned/updated visited set
into recursive calls; apply the same pattern to checkForMarkdownFiles, while for
scanDirectoryRemote and deleteBackupsRemoteRecursive implement the maxDepth
guard (remote versions should not rely on realpath resolution due to SSH cost)
and skip recursion when depth exceeds the cap.

In `@src/main/ipc/handlers/filesystem.ts`:
- Around line 407-422: The recursive walker countRecursive in fs:countItems can
follow symlinked directories indefinitely now that resolveDirentType treats
symlinks-to-dirs as directories; add a recursion safety mechanism by (1) adding
a maxDepth parameter (reusing the same cap pattern as fs:directorySize) passed
into countRecursive and bailing out once reached, and (2) track visited real
targets using fs.realpath (store resolved paths in a Set) and skip traversing if
realpath is already seen; update the logic that increments folderCount/fileCount
in countRecursive to still count the entry but not recurse into visited or
over-depth directories so cycles and deep trees are guarded against.

---

Duplicate comments:
In `@src/main/ipc/handlers/autorun.ts`:
- Around line 226-253: checkForMarkdownFiles can infinite-recurse on symlink
cycles; update it to accept and propagate a recursion guard (e.g., a maxDepth
numeric parameter and a visited Set of realpaths) similar to scanDirectory: at
start resolve the directory's realpath, if already in visited return false, add
it to visited, bail out if currentDepth exceeds a sensible MAX_DEPTH, and pass
incremented depth and the same visited Set into recursive calls; reference
function name checkForMarkdownFiles and the autorun:hasDocuments caller so you
ensure the new optional parameters are threaded through where it’s invoked.

---

Nitpick comments:
In `@src/main/ipc/handlers/filesystem.ts`:
- Around line 265-287: The directorySize traversal misclassifies symlinked
directories because it uses entry.isDirectory(); update the loop in
calculateSize to call the existing resolveDirentType(entry, fullPath) (same
helper used by fs:readDir / fs:countItems) and switch on that returned type so
symlinked directories are treated like directories (recursing and adding their
contents to totalSize) while preserving the depth cap and the ignore patterns
(node_modules, __pycache__) and keeping the existing try/catch around fs.stat
for files.

In `@src/main/utils/dirent-utils.ts`:
- Around line 50-63: The catch block around fs.stat(fullPath) is treating every
error as a broken symlink; change it to catch (err) and inspect err.code —
return isBrokenSymlink: true only for known "broken link" codes (e.g. 'ENOENT',
'ENOTDIR', 'ELOOP'), and for any other/unknown error either rethrow the error or
log it and rethrow so it surfaces to Sentry; keep the returned shape
(isDirectory/isFile/isBrokenSymlink) when handling known broken-link codes and
use fullPath and fs.stat as the reference points to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6db624ec-2fff-41a3-ba78-a926ac7d31da

📥 Commits

Reviewing files that changed from the base of the PR and between f88d039 and 1c05cce.

📒 Files selected for processing (7)
  • src/__tests__/main/ipc/handlers/autorun.test.ts
  • src/__tests__/main/ipc/handlers/filesystem.test.ts
  • src/__tests__/main/utils/dirent-utils.test.ts
  • src/main/ipc/handlers/autorun.ts
  • src/main/ipc/handlers/filesystem.ts
  • src/main/utils/dirent-utils.ts
  • src/main/utils/remote-fs.ts

Comment thread src/main/ipc/handlers/filesystem.ts
CodeRabbit flagged that the graph builder's scanMarkdownFiles walks
directories without a depth bound. Now that fs:readDir resolves
symlinked directories to isDirectory=true, a cycle like a/link -> a
would recurse indefinitely. loadFileTree in fileExplorer.ts already
caps at depth 10 — match that convention here and bail out with a
warning when the cap is hit. Adds a regression test that stubs readDir
to return a self-referencing dir and asserts the scan terminates with
a bounded number of calls.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/__tests__/renderer/components/DocumentGraph/graphDataBuilder.test.ts (1)

242-278: Good regression coverage for the depth cap.

Cleanly simulates a self-referencing directory cycle and verifies bounded termination plus a non-empty result. The ≤ 12 slack over the actual cap of 10 is reasonable headroom. Optional: also assert console.warn was emitted so the guard's logging contract is pinned down — otherwise a future refactor could silently drop the cap without failing this test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/components/DocumentGraph/graphDataBuilder.test.ts`
around lines 242 - 278, Add an assertion that the depth-cap warning was emitted:
spy on console.warn (or the logger used) before calling buildGraphData in this
test ("should terminate directory scan when symlink cycle would recurse
forever"), run the scan, and assert console.warn was called (optionally with a
message matching the depth-cap or recursion guard). Keep the existing
cyclicReadDir spy and existing assertions (result.nodes and
cyclicReadDir.mock.calls length) and restore/clear the console.warn spy after
the test.
src/renderer/components/DocumentGraph/graphDataBuilder.ts (2)

320-353: Depth cap looks good; consider sharing the constant with fileExplorer.

The depth guard correctly bounds recursion and matches fileExplorer's cap of 10, which is exactly what prevents the symlink-cycle regression. One small suggestion: since the JSDoc explicitly ties this to loadFileTree's cap, consider hoisting SCAN_MAX_DEPTH into a shared constants module (or re-exporting from fileExplorer) so the two limits can't silently drift apart if one side is tuned later. Non-blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/DocumentGraph/graphDataBuilder.ts` around lines 320 -
353, The SCAN_MAX_DEPTH constant used by scanMarkdownFiles should be shared with
fileExplorer.loadFileTree to prevent drift; move SCAN_MAX_DEPTH into a shared
constants module (e.g., export a MAX_SCAN_DEPTH) or re-export the existing
constant from fileExplorer, then import and use that shared symbol in
scanMarkdownFiles instead of the local SCAN_MAX_DEPTH so both scanMarkdownFiles
and loadFileTree reference the same single source of truth.

347-353: Potentially noisy warning on wide cyclic trees.

In a real symlink cycle this console.warn will fire once per branch that reaches depth 10, which can be many times per scan (and scanMarkdownFiles is also called a second time by startBacklinkScan). Consider logging only once per scanMarkdownFiles invocation (e.g., a local warned flag) to avoid log spam in the renderer console. Optional.

♻️ Suggested refactor
 async function scanMarkdownFiles(
 	rootPath: string,
 	onProgress?: ProgressCallback,
 	sshRemoteId?: string
 ): Promise<string[]> {
 	const markdownFiles: string[] = [];
 	let directoriesScanned = 0;
 	let isRootDirectory = true;
+	let depthWarned = false;

 	async function scanDir(currentPath: string, relativePath: string, depth: number): Promise<void> {
 		const isRoot = isRootDirectory;
 		isRootDirectory = false;

 		if (depth >= SCAN_MAX_DEPTH) {
-			// Bail out rather than risk an infinite loop through a symlink cycle.
-			console.warn(
-				`scanMarkdownFiles: reached max depth ${SCAN_MAX_DEPTH} at ${currentPath}; stopping recursion`
-			);
+			if (!depthWarned) {
+				console.warn(
+					`scanMarkdownFiles: reached max depth ${SCAN_MAX_DEPTH} (first hit at ${currentPath}); stopping further recursion`
+				);
+				depthWarned = true;
+			}
 			return;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/DocumentGraph/graphDataBuilder.ts` around lines 347 -
353, The console.warn inside scanMarkdownFiles fires repeatedly when many
branches hit SCAN_MAX_DEPTH causing log spam; add a per-invocation boolean
(e.g., local warned) in scanMarkdownFiles so the warning is emitted only once
per call (set warned when you log and check it before logging), or alternatively
pass a warnOnce flag from startBacklinkScan into scanMarkdownFiles to ensure the
message about SCAN_MAX_DEPTH is logged a single time per scan invocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/__tests__/renderer/components/DocumentGraph/graphDataBuilder.test.ts`:
- Around line 242-278: Add an assertion that the depth-cap warning was emitted:
spy on console.warn (or the logger used) before calling buildGraphData in this
test ("should terminate directory scan when symlink cycle would recurse
forever"), run the scan, and assert console.warn was called (optionally with a
message matching the depth-cap or recursion guard). Keep the existing
cyclicReadDir spy and existing assertions (result.nodes and
cyclicReadDir.mock.calls length) and restore/clear the console.warn spy after
the test.

In `@src/renderer/components/DocumentGraph/graphDataBuilder.ts`:
- Around line 320-353: The SCAN_MAX_DEPTH constant used by scanMarkdownFiles
should be shared with fileExplorer.loadFileTree to prevent drift; move
SCAN_MAX_DEPTH into a shared constants module (e.g., export a MAX_SCAN_DEPTH) or
re-export the existing constant from fileExplorer, then import and use that
shared symbol in scanMarkdownFiles instead of the local SCAN_MAX_DEPTH so both
scanMarkdownFiles and loadFileTree reference the same single source of truth.
- Around line 347-353: The console.warn inside scanMarkdownFiles fires
repeatedly when many branches hit SCAN_MAX_DEPTH causing log spam; add a
per-invocation boolean (e.g., local warned) in scanMarkdownFiles so the warning
is emitted only once per call (set warned when you log and check it before
logging), or alternatively pass a warnOnce flag from startBacklinkScan into
scanMarkdownFiles to ensure the message about SCAN_MAX_DEPTH is logged a single
time per scan invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fdbb9140-541e-4589-8465-108d08bf68fc

📥 Commits

Reviewing files that changed from the base of the PR and between 1c05cce and 0af773a.

📒 Files selected for processing (2)
  • src/__tests__/renderer/components/DocumentGraph/graphDataBuilder.test.ts
  • src/renderer/components/DocumentGraph/graphDataBuilder.ts

@pedramamini pedramamini merged commit 1c50c9b into main Apr 17, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug/Missing feature: Symlinked subdirectories are invisible in Files panel and Auto Run folder picker

1 participant