Skip to content

feat(projects): show a symlink indicator in Add Project file browsers - #8998

Open
choihjin wants to merge 2 commits into
stablyai:mainfrom
choihjin:feat/file-browser-symlink-indicator
Open

feat(projects): show a symlink indicator in Add Project file browsers#8998
choihjin wants to merge 2 commits into
stablyai:mainfrom
choihjin:feat/file-browser-symlink-indicator

Conversation

@choihjin

@choihjin choihjin commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #8989. Stacked on #8987 — that fix is the first commit here; until it merges, this PR's diff shows both commits. Once #8987 lands, this reduces to the single indicator commit.

After #8987, symlinked entries behave as directories in the Add Project / Add Remote Project browsers but are visually indistinguishable from real folders. This adds a muted link indicator next to symlinked entries, matching the in-app file explorer's existing pattern (FileExplorerRow).

To support this on remote hosts, the ssh:browseDir listing protocol now carries symlink state:

  • POSIX listing emits <l|-><d|->/name per entry — a symlink flag from [ -h ] and a directory flag from [ -d ] (which follows the link, so a symlinked directory still classifies as a directory). Names can't contain /, so the first / reliably ends the two-flag prefix; any other stdout noise (motd, blank lines) is skipped by the parser.
  • Windows PowerShell fallback flags links via $_.LinkType (populated only for symlinks/junctions), matching Node's isSymbolicLink() on the local/server path. It deliberately does not use the generic ReparsePoint attribute, which is also set on OneDrive/cloud placeholders and would mislabel ordinary files as links.
  • RemoteDirEntry / DirEntry gain isSymlink; the local/server browseServerDir path already returned it.

Screenshots

Remote file browser on a real Linux SSH host running this branch. linked-dirreal-dir and linked-filereal file.txt are symlinks and now show the link indicator; the real directory and file do not:

Remote file browser showing link indicators only on symlinked entries

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test (30,680 passed; note: run with the shell's GIT_CONFIG_* vars unset — an unrelated relay test asserts an exact git-config count and picks up Orca's own terminal env otherwise)
  • pnpm build
  • Added tests that fail without the change

Tests:

  • ssh-browse.test.ts: locks the new prefix-format command, asserts symlink flags parse correctly for symlinked dirs/files and broken links, and pins the PowerShell LinkType detection (with an explicit not.toContain('ReparsePoint')).
  • RemoteFileBrowser.symlink-indicator.test.tsx (new): renders the browser and asserts the link indicator appears only on symlinked rows.
  • Existing DirEntry fixtures updated for the new field.

Manual verification: drove the built dev app end-to-end against a real Linux SSH host — the indicator appears on exactly the symlinked entries and not on real files/dirs (screenshot above). The new POSIX listing snippet was also exercised directly in sh against fixtures with spaces in names, symlinked dirs/files, and broken links. The Windows PowerShell branch is covered by unit tests (no Windows SSH host available to drive live).

AI Review Report

Reviewed by Claude (Fable 5, author) and independently by Codex (gpt-5.6-sol, reasoning effort high) via codex review.

  • Codex finding (applied): the initial Windows implementation used [IO.FileAttributes]::ReparsePoint, which also matches OneDrive Files On-Demand placeholders and other filter-driver files — it would have shown false-positive link indicators on non-link entries, diverging from the POSIX and local paths. Switched to $_.LinkType, which is populated only for actual symlinks/junctions, and added a regression assertion. No other issues flagged.
  • Cross-platform (macOS/Linux/Windows): POSIX branch uses only portable sh ([ -h ], [ -d ], printf, command ls -1A) — no bashisms. Windows branch keeps the forward-slash resolvedPath and adds link detection. Icon rendering is platform-neutral.
  • SSH/remote/local: still a single exec round-trip per directory; exit-code error semantics for permission-denied/unreadable dirs are preserved (the ls output is captured before the classify loop). The isSymlink field flows through preload → renderer for both the SSH and runtime-server paths.
  • Shell injection: the only user-controlled input (the directory path) keeps the existing shellEscape; the added flag/prefix logic is static text operating on ls output within the escaped directory. The PowerShell literal keeps its single-quote doubling (asserted in tests).
  • i18n: the "Symbolic link" label is added to all five locale catalogs with real translations; catalog parity and coverage gates pass.

Security Audit

No new IPC endpoints, auth, secrets, or dependencies. No new injection surface (see above). The change only adds classification metadata to entries of a directory the user explicitly browses.

Notes

ELI5

In the Add Project folder browser, symlinked folders now show a small link icon. You can tell them apart from normal directories the same way as in the main file explorer.

choihjin added 2 commits July 16, 2026 17:31
…ect browsers

The Add Project host folder browser classified a symlink-to-directory as
a file: browseServerDir used Dirent.isDirectory(), which does not follow
symlinks, and the SSH browse command relied on `ls -p`, which does not
dereference links. Classify symlink entries by their target type (stat
for the local/server listing, `[ -d ]` for the remote POSIX listing) so
linked project folders can be descended into and selected.

Fixes stablyai#8983
Symlinked entries were visually identical to real files and directories in
the Add Project / Add Remote Project browsers. Render a muted link icon next
to symlinked entries, matching the existing file-explorer pattern.

The SSH browse protocol now carries symlink state: the POSIX listing emits a
`<l|-><d|->/name` prefix (`[ -h ]` for the link flag, `[ -d ]` for the dir
flag), the Windows PowerShell fallback flags links via $_.LinkType, and
RemoteDirEntry / DirEntry gain isSymlink. The local/server browseServerDir
path already returned isSymlink.

Fixes stablyai#8989
@choihjin
choihjin force-pushed the feat/file-browser-symlink-indicator branch from ec38d36 to c10f15e Compare July 16, 2026 08:31
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Remote directory entries now include isSymlink metadata across SSH, local server, preload, and renderer contracts. POSIX and PowerShell listings emit structured flags that the SSH parser decodes. Local browsing follows symlink targets to classify directory status and handles broken links. The remote file browser displays a localized symbolic-link indicator, with updated fixtures, integration tests, fallback tests, and translations.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding symlink indicators in project file browsers.
Description check ✅ Passed The description follows the template and covers summary, screenshots, testing, AI review, security, and notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 72a0abc4-54ef-4baf-a6b7-bc69a797ec0d

📥 Commits

Reviewing files that changed from the base of the PR and between cf02cc4 and c10f15e.

📒 Files selected for processing (16)
  • src/main/ipc/ssh-browse.test.ts
  • src/main/ipc/ssh-browse.ts
  • src/main/runtime/orca-runtime.test.ts
  • src/main/runtime/orca-runtime.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/sidebar/RemoteFileBrowser.paste.test.tsx
  • src/renderer/src/components/sidebar/RemoteFileBrowser.symlink-indicator.test.tsx
  • src/renderer/src/components/sidebar/RemoteFileBrowser.tsx
  • src/renderer/src/components/sidebar/remote-file-browser-helpers.test.ts
  • src/renderer/src/components/sidebar/remote-file-browser-helpers.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json

Comment on lines +12359 to +12367
const mapped = await Promise.all(
entries
.filter((entry) => entry.name !== '.' && entry.name !== '..')
.map(async (entry) => ({
name: entry.name,
isDirectory: await isServerBrowseEntryDirectory(dirPath, entry),
isSymlink: entry.isSymbolicLink()
}))
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Limit concurrency to prevent EMFILE errors on directories with many symlinks.

Using an unbounded Promise.all to stat all symlinks concurrently can trigger EMFILE (too many open files) errors if the directory contains a large number of symbolic links. Because isServerBrowseEntryDirectory catches all errors and silently returns false, affected symlinked directories will incorrectly appear as non-directories in the UI.

Consider processing the entries sequentially to cap concurrent file descriptors.

💡 Proposed fix using sequential processing
-    const mapped = await Promise.all(
-      entries
-        .filter((entry) => entry.name !== '.' && entry.name !== '..')
-        .map(async (entry) => ({
-          name: entry.name,
-          isDirectory: await isServerBrowseEntryDirectory(dirPath, entry),
-          isSymlink: entry.isSymbolicLink()
-        }))
-    )
+    const mapped: DirEntry[] = []
+    for (const entry of entries) {
+      if (entry.name === '.' || entry.name === '..') {
+        continue
+      }
+      mapped.push({
+        name: entry.name,
+        isDirectory: await isServerBrowseEntryDirectory(dirPath, entry),
+        isSymlink: entry.isSymbolicLink()
+      })
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const mapped = await Promise.all(
entries
.filter((entry) => entry.name !== '.' && entry.name !== '..')
.map(async (entry) => ({
name: entry.name,
isDirectory: await isServerBrowseEntryDirectory(dirPath, entry),
isSymlink: entry.isSymbolicLink()
}))
)
const mapped: DirEntry[] = []
for (const entry of entries) {
if (entry.name === '.' || entry.name === '..') {
continue
}
mapped.push({
name: entry.name,
isDirectory: await isServerBrowseEntryDirectory(dirPath, entry),
isSymlink: entry.isSymbolicLink()
})
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This intentionally mirrors the existing fs.readDir handler in src/relay/fs-handler.ts (lines 141–147), which uses the same unbounded Promise.all over isDirectoryEntry and is test-locked — browseServerDir was written to match that behavior.

The concurrency here is bounded by the number of symlinked entries in a single browsed directory (non-symlinks skip stat entirely), so for a project-picker listing it isn't a realistic EMFILE trigger. Adding a concurrency cap to only this side would diverge from the relay twin it deliberately matches; if the fd concern is worth addressing, it's really a pre-existing, codebase-wide item that should be a separate change applied to both handlers rather than a divergence introduced here.

Leaving as-is for parity with the established handler.

@nwparker nwparker added the enhancement New feature or request label Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Show a symlink indicator in the Add Project file browsers

3 participants