Skip to content

Fix file explorer using Mac caller cwd as remote root for SSH workspaces - #3654

Closed
kimimgo wants to merge 6 commits into
manaflow-ai:mainfrom
kimimgo:fix/ssh-fileExplorer-rootPath
Closed

kimimgo wants to merge 6 commits into
manaflow-ai:mainfrom
kimimgo:fix/ssh-fileExplorer-rootPath

Conversation

@kimimgo

@kimimgo kimimgo commented May 6, 2026 •

Copy link
Copy Markdown

Summary

  • What changed? When opening a workspace via cmux ssh or workspace.remote.configure, the SwiftUI workspace's currentDirectory is the Mac caller cwd (e.g. /Users/<x>/Downloads), unreachable on the remote. The file explorer fed this straight into SSHFileExplorerProvider as the root path, so ssh <dest> "ls -la /Users/..." always failed silently on Linux. Header label fell through to the literal Mac path because homePath derivation never matched.

  • Why? Two bugs in ContentView.swift:3035-3076 syncFileExplorerDirectory:

    1. homePath was derived from the workspace dir (Mac path) trying to match /home/<user> or /root prefixes. For Mac paths it always returned "" — no ~ abbreviation, no anchor.
    2. The same Mac dir was passed to setRootPath. SSH ls always targeted a non-existent path; 2>/dev/null in runSSHListCommand masked the failure as an empty tree.

    Fix: derive homePath from config.destination (user@host) using the Linux convention (/home/<user>, except root → /root). Compute effectiveDir: when dir is Mac-local (/Users/..., /Volumes/...) and a credible homePath exists, fall back to homePath.

  • The pure helpers live in a top-level SSHFileExplorerPathResolver enum (above ContentView) so they unit-test cleanly without an NSApplication or fake Workspace graph.

Testing

Unit tests (added)

cmuxTests/SSHFileExplorerPathResolverTests.swift — 26 cases:

  • remoteHomePath(from:): typical user@host, port suffix, IPv6 (user@[::1]:22), root, dashed/dotted/cased usernames, double-@, whitespace trimming, nil/empty destination, missing @, empty-user/empty-host.
  • effectiveRootPath(workspaceDirectory:remoteHomePath:): /Users/... and /Volumes/... redirect to home; /home/..., /etc/..., /, /private/... are preserved; empty homePath does not override; empty dir preserved.
  • isMacLocalPath: case-sensitive /Users/, /Volumes/; /home/... not Mac-local; / not Mac-local.
  • 3 end-to-end composition tests (Mac caller cwd → remote home routing; root user → /root; remote-style cwd preserved).
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug \
  -destination 'platform=macOS' \
  -only-testing:cmuxTests/SSHFileExplorerPathResolverTests test
# ** TEST SUCCEEDED ** — 26/26 in ~0.02s total

Manual verification

  • Reproduced on cmux 0.64.3 (build 83, aea6cfcde) with imgyu@<host>: pre-fix, cmux ssh → ⌘⇧E → file tree header /Users/imgyukim/workspace/cmux-setup; tree empty (silent SSH ls fail).
  • Post-fix on side-by-side --tag imgyu Debug build: ⌘⇧E in the same SSH workspace shows ~ header and the actual remote home contents.

Demo Video

To follow as a comment on this PR — short clip of before/after ⌘⇧E in an SSH workspace.

  • Video URL or attachment: pending

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed (no doc changes; behavior fix in existing surface)
  • I requested bot reviews after my latest commit (will copy-paste block above as a comment after open)
  • All code review bot comments are resolved (awaiting first pass)
  • All human review comments are resolved (awaiting first pass)

Summary by cubic

Fixes SSH file explorer rooting by using the remote user’s home instead of the Mac caller cwd so the tree loads and ~ shows correctly. Extracts the path resolver into CMUXSSHPathResolving and updates the CI file-length budget.

  • Bug Fixes

    • Derive remote home from remoteConfiguration.destination (user@host), including root and IPv6/port formats.
    • When the workspace cwd is Mac-local (/Users/..., /Volumes/...), fall back to the remote home; otherwise keep the cwd.
    • Set SSHFileExplorerProvider root to the effective path and only expose homePath when the daemon is ready to avoid empty trees and wrong headers.
  • Dependencies

    • Added CMUXSSHPathResolving SwiftPM package with public nonisolated helpers (remoteHomePath, effectiveRootPath, isMacLocalPath); integrated into Xcode and ContentView.
    • Moved tests to CMUXSSHPathResolvingTests and expanded to 44 cases, including concurrency, parsing edges, and a /Volumes/ trailing-slash boundary pin symmetric to /Users/.
    • Updated .github/swift-file-length-budget.tsv to reflect ContentView shrinking after extraction.

Written for commit 42bb1f9. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Added a reusable SSH path-resolving component to better derive remote home and effective workspace roots.
  • Bug Fixes
    • Improved SSH file-explorer root handling to avoid unnecessary refreshes and keep workspace sync accurate.
  • Tests
    • Added comprehensive unit tests covering SSH destination parsing, path classification, and effective-root logic.

When opening a workspace via `cmux ssh` or `workspace.remote.configure`,
the SwiftUI workspace's `currentDirectory` is the Mac caller cwd
(e.g. /Users/<x>/Downloads). The file explorer's syncFileExplorerDirectory
fed this Mac path straight into SSHFileExplorerProvider as the root path,
so `ssh <dest> "ls -la /Users/..."` always failed silently on Linux.

Two issues in ContentView.swift:3035-3076:

1. homePath derivation used the workspace `dir` (Mac caller cwd) and
   only matched `/home/<user>` or `/root` prefixes. For Mac paths it
   always returned "" — so the `~` abbreviation never kicked in and the
   provider had no anchor to fall back to.

2. The same Mac `dir` was passed to setRootPath. SSH ls was always
   targeting a non-existent remote path. The error was hidden by the
   `2>/dev/null` in runSSHListCommand, surfacing as an empty tree.

Fix:
- Derive homePath from `config.destination` ("user@host"). Linux
  convention: $HOME is /home/<user>, except root → /root.
- Compute effectiveDir: when `dir` is a Mac-local path
  (`/Users/...`, `/Volumes/...`) and we have a homePath, fall back to
  homePath so the SSH ls targets a reachable remote location.

The pure helpers live in a top-level `SSHFileExplorerPathResolver` enum
so they can be unit-tested without bringing up an `NSApplication` or
building a fake `Workspace` graph. cmuxTests/SSHFileExplorerPathResolverTests
covers 26 cases: nil/empty/whitespace destinations, missing `@`,
IPv6/ports, root user, dashed/dotted/cased usernames, double-`@`,
already-remote workspace paths, /Volumes, /etc, /, /private/etc, and
the end-to-end composition (Mac caller cwd → remote home).

Verified on cmux 0.64.3 with imgyu@<host>: file tree header now shows
`~` and lists the remote home contents instead of staying on the Mac
caller path.
@vercel

vercel Bot commented May 6, 2026

Copy link
Copy Markdown

@kimimgo is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@kimimgo

kimimgo commented May 6, 2026

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@coderabbitai

coderabbitai Bot commented May 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces SSHFileExplorerPathResolver, a new local Swift package with path-parsing utilities for SSH destinations. Integrates resolver into ContentView.syncFileExplorerDirectory to compute effective workspace roots. Adds comprehensive unit tests covering parsing, path resolution, and concurrency. Registers package in Xcode project build configuration.

Changes

SSH Path Resolver + Integration

Layer / File(s) Summary
Package Manifest
Packages/CMUXSSHPathResolving/Package.swift
New macOS 13+ package with library target and test target structure.
Core Resolver Implementation
Packages/CMUXSSHPathResolving/Sources/CMUXSSHPathResolving/SSHFileExplorerPathResolver.swift
Three nonisolated static helpers: remoteHomePath(from:) parses SSH destination strings to remote home paths; effectiveRootPath(workspaceDirectory:remoteHomePath:) selects workspace root by checking for macOS-local paths; isMacLocalPath(_:) detects /Users/ or /Volumes/ prefixes.
Unit Tests
Packages/CMUXSSHPathResolving/Tests/CMUXSSHPathResolvingTests/SSHFileExplorerPathResolverTests.swift
Full XCTest suite covering SSH destination parsing (user@host, ports, IPv6, whitespace, edge cases), effective-root selection logic (macOS path redirection, non-Mac path preservation), mac-local path boundary checks, composition validation, and Task.detached concurrency compatibility.
ContentView Integration
Sources/ContentView.swift
syncFileExplorerDirectory refactored to derive homePath and effectiveDir via resolver when workspace is remote, set explorer rootPath to effectiveDir, update provider availability with resolved home, and trigger hydrateExpandedNodes() only when effective root changes.
Xcode Project Wiring
GhosttyTabs.xcodeproj/project.pbxproj
Register CMUXSSHPathResolving as local Swift package reference, add package product dependency to GhosttyTabs target, and define package configuration entries in project manifest.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#1800: Related work on inferring/using remote home paths and canonicalizing SSH workspace roots.

Poem

🐰 I sniffed the paths by moonlit code,

trimmed whitespace off the roaming node,
turned /Users to /home with gentle hops,
and helped ContentView find its ops—
a tiny rabbit patch that never stops.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Logging ❌ Error NSLog in FeedbackComposerClient.submit() logs server errors without #if DEBUG guard, violating swift-logging.md rules. Logs potentially sensitive errorMessage. SSH changes correctly use #if DEBUG. Remove or guard NSLog with #if DEBUG. Use Logger/cmuxDebugLog instead. Logging errorMessage may expose sensitive server response data.
Docstring Coverage ⚠️ Warning Docstring coverage is 21.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (12 passed)
Check name Status Explanation
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.
Cmux Swift Actor Isolation ✅ Passed Nonisolated enum with static methods for string transformations. Proper isolation annotations, called from MainActor View. No implicit MainActor models or Sendable violations.
Cmux Swift Blocking Runtime ✅ Passed No blocking synchronization introduced. SSHFileExplorerPathResolver uses pure nonisolated static functions only. Pre-existing Task.sleep/NSLock not modified.
Cmux No Hacky Sleeps ✅ Passed Check not applicable: PR contains only Swift code. The check applies to non-Swift production code (TypeScript, JavaScript, shell, build/runtime). Swift is excluded.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. New code has pure nonisolated static functions, no DispatchQueue/Combine/completion-handlers. Task.detached is test-only and properly awaited.
Cmux Swift @Concurrent ✅ Passed No async functions added. Three pure nonisolated static helpers perform only string operations. Compliant with concurrency annotation rules.
Cmux Swift File And Package Boundaries ✅ Passed PR extracts pure SSH path parsing logic into new CMUXSSHPathResolving package (80-line source). Zero app-lifecycle dependencies. ContentView net -4 lines. All package-boundary rules satisfied.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state violations. Extracts pure utility functions with no new @Published/@Observable/@StateObject, no GeometryReader changes, no render-time mutations.
Cmux Architecture Rethink ✅ Passed Pure stateless helper functions extracted to clear module. No forbidden patterns: no timing, locks, observers, or competing state ownership. Documented invariants with comprehensive tests.
Title check ✅ Passed The title clearly and specifically describes the main change: fixing the file explorer bug where macOS caller cwd was incorrectly used as the remote root for SSH workspaces.
Description check ✅ Passed The description comprehensively covers all required sections: summary of what changed and why (detailed root cause analysis), thorough testing section with unit test specifics and manual verification steps, and acknowledgment of pending demo video.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@cubic-dev-ai

cubic-dev-ai Bot commented May 6, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@kimimgo I have started the AI code review. It will take a few minutes to complete.

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@kimimgo Sure, I'll review the changes in this PR right away!

[review]

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@greptile-apps

greptile-apps Bot commented May 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a silent failure in the SSH file explorer where the workspace's Mac-local cwd (e.g. /Users/.../Downloads) was passed directly as the remote root path, causing ssh <dest> ls to always return nothing. The fix derives the remote home from config.destination (user@host) and falls back to it whenever the workspace cwd looks Mac-local.

  • Extracts three pure helpers (remoteHomePath, effectiveRootPath, isMacLocalPath) into a new CMUXSSHPathResolving SwiftPM package; all are nonisolated, properly tested (43 cases including concurrency probes), and hooked into ContentView.syncFileExplorerDirectory.
  • Removes the dead remotePath binding and replaces the broken dir-based homePath derivation with a destination-string parse; both the existing-provider and new-provider branches now use the correct effectiveDir and homePath values.
  • Updates the CI file-length budget to reflect ContentView.swift shrinking after the extraction.

Confidence Score: 5/5

Safe to merge — the change is a targeted, well-tested bug fix with no regressions on the existing local-workspace or already-remote-cwd paths.

The root cause (Mac cwd fed directly into SSH ls) is correctly addressed by deriving the home from the destination string. All three helpers are pure, synchronisation-free, and nonisolated. The dead remotePath binding from the previous review pass has been removed, nonisolated annotations are already present in the package (addressing all previous actor-isolation comments), and both the existing-provider and new-provider branches now use the corrected effectiveDir and homePath values. The 43-case test suite — including concurrency probes from detached Tasks — pins the boundary behaviour explicitly.

No files require special attention.

Important Files Changed

Filename Overview
Packages/CMUXSSHPathResolving/Sources/CMUXSSHPathResolving/SSHFileExplorerPathResolver.swift New SwiftPM package with three pure, nonisolated helpers for SSH path resolution; logic is correct, well-documented, and handles all edge cases (nil destination, empty user/host, IPv6, root user, port suffix).
Sources/ContentView.swift syncFileExplorerDirectory now derives homePath from config.destination and passes effectiveDir to both the existing-provider update and new-provider creation paths; dead remotePath binding removed; no actor isolation regressions.
Packages/CMUXSSHPathResolving/Tests/CMUXSSHPathResolvingTests/SSHFileExplorerPathResolverTests.swift 43 test cases covering all edge cases including concurrency probes from detached Tasks; boundary pin tests ensure deliberate narrowness of isMacLocalPath prefix allowlist is explicit.
Packages/CMUXSSHPathResolving/Package.swift Standard SwiftPM package manifest targeting macOS 13+, no issues.
GhosttyTabs.xcodeproj/project.pbxproj Adds CMUXSSHPathResolving as a local XCLocalSwiftPackageReference and product dependency; standard wiring, no issues.
.github/swift-file-length-budget.tsv ContentView.swift budget reduced from 15956 to 15919 lines to reflect the code extraction; straightforward CI budget update.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[syncFileExplorerDirectory\ndir = workspace cwd] --> B{isRemoteWorkspace?}
    B -- No --> C[setRootPath to dir\nlocal explorer]
    B -- Yes --> D[remoteHomePath from\nconfig.destination]
    D --> E{destination valid?\nhas @ + user + host}
    E -- No --> F[homePath = empty]
    E -- Yes --> G{user == root?}
    G -- Yes --> H[homePath = /root]
    G -- No --> I[homePath = /home/user]
    F & H & I --> J[effectiveRootPath\ndir + homePath]
    J --> K{isMacLocalPath dir?\n/Users/... or /Volumes/...}
    K -- Yes AND homePath non-empty --> L[effectiveDir = homePath]
    K -- No OR homePath empty --> M[effectiveDir = dir]
    L & M --> N{existing provider\nmatches destination?}
    N -- Yes --> O[updateAvailability\nhomePath if ready]
    O --> P{isReady?}
    P -- Yes --> Q[setRootPath effectiveDir\nhydrateExpandedNodes if changed]
    N -- No --> R[new SSHFileExplorerProvider\ndestination + homePath + isReady]
    R --> S[setProvider + setRootPath effectiveDir]
Loading

Reviews (5): Last reviewed commit: "Pin /Volumes/ trailing-slash boundary sy..." | Re-trigger Greptile

kimimgo and others added 2 commits May 7, 2026 15:02
Moves the three pure path-resolution helpers
(remoteHomePath, effectiveRootPath, isMacLocalPath) out of
ContentView.swift and into their own file so they sit at a
package boundary and can be exercised without instantiating an
NSApplication or a Workspace graph.

Also marks all three helpers `nonisolated` so a future
module-level default actor isolation setting cannot silently
re-bind them to the main actor, and drops the now-unused
`remotePath` binding in syncFileExplorerDirectory whose
flatMap consumer was replaced by the resolver call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 10 regression tests around SSHFileExplorerPathResolver:

  - Three detached-Task call sites for remoteHomePath /
    effectiveRootPath / isMacLocalPath, documenting that the
    helpers can be invoked off the main actor and producing
    runtime call-shape coverage that has to be revisited if a
    future Swift 6 default-isolation rollout reaches this code.
  - Boundary cases for isMacLocalPath: exact "/Users" / "/Volumes"
    without a trailing slash do not match, while "/Users/" alone
    does — pinning the deliberate prefix shape so a "let's also
    catch the bare directory" tweak has to come with a side-effect
    audit.
  - Additional remoteHomePath parsing edges: internal spaces,
    surrounding tabs, non-ASCII (CJK) usernames, and a colon-on-
    user-side typo. None of these are realistic SSH inputs but
    they pin the verbatim-preservation contract so an over-eager
    sanitization step can't silently mutate user-supplied
    destinations.

Also tightens a few existing test comments to remove a stale
function name (testEffectiveRootPath_emptyDirFallsBackToHome was
verifying the opposite of its name), drop a fix-history aside
that referred to "the upstream bug report", and replace the
isMacLocalPath / privatePath circular justifications with
direct factual descriptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kimimgo

kimimgo commented May 7, 2026

Copy link
Copy Markdown
Author

Address bot review feedback in two follow-up commits:

  • 9556775a extracts SSHFileExplorerPathResolver to its own file (CodeRabbit pre-merge: file/package boundaries), adds nonisolated to all three static helpers (CodeRabbit / Greptile P2: Swift actor isolation), and removes the unused remotePath binding in syncFileExplorerDirectory (Greptile P1: dead binding under -warnings-as-errors).
  • e53c5ab2 adds 10 regression tests covering off-main-actor invocation, isMacLocalPath /Users vs /Users/ boundary, and extra parsing edges (internal whitespace, non-ASCII usernames, colon-on-user-side typo). Also tightens a stale function name and removes some fix-history aside comments per CLAUDE.md guidance.

All 43 tests pass locally via CMUX_SKIP_ZIG_BUILD=1 xcodebuild -scheme cmux-unit -only-testing:cmuxTests/SSHFileExplorerPathResolverTests test.

Out of scope (tracking for follow-up PRs, not folded in here):

  • 2>/dev/null in runSSHListCommand still swallows SSH stderr — error-surfacing redesign.
  • syncFileExplorerDirectory provider-decision branch still has no integration coverage — needs a second extraction.
  • remoteDaemonStatus.remotePath is currently ignored in favor of the user@host heuristic — daemon contract change.

@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented May 7, 2026

Copy link
Copy Markdown

Address bot review feedback in two follow-up commits:

  • 9556775a extracts SSHFileExplorerPathResolver to its own file (CodeRabbit pre-merge: file/package boundaries), adds nonisolated to all three static helpers (CodeRabbit / Greptile P2: Swift actor isolation), and removes the unused remotePath binding in syncFileExplorerDirectory (Greptile P1: dead binding under -warnings-as-errors).
  • e53c5ab2 adds 10 regression tests covering off-main-actor invocation, isMacLocalPath /Users vs /Users/ boundary, and extra parsing edges (internal whitespace, non-ASCII usernames, colon-on-user-side typo). Also tightens a stale function name and removes some fix-history aside comments per CLAUDE.md guidance.

...

@kimimgo I have started the AI code review. It will take a few minutes to complete.

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@kimimgo Thanks for the thorough follow-up — great to see all the previous feedback addressed systematically:

  • ✅ SSHFileExplorerPathResolver extracted to its own file (package/file boundary concern resolved)
  • ✅ nonisolated added to all three static helpers (Swift actor isolation concern resolved)
  • ✅ Unused remotePath binding removed (dead binding / -warnings-as-errors concern resolved)
  • ✅ 10 additional regression tests added (43 total passing), covering off-main-actor invocation, /Users vs /Users/ boundary, and extra parsing edges

The out-of-scope items (2>/dev/null stderr swallowing, integration coverage for the provider-decision branch, and remoteDaemonStatus.remotePath daemon contract) are well-scoped for follow-up PRs. I'll kick off a fresh review of the current state now.

[review]

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/SSHFileExplorerPathResolverTests.swift`:
- Around line 4-8: SSHFileExplorerPathResolver and its helper functions
(remoteHomePath, effectiveRootPath, isMacLocalPath) are currently compiled into
the app module requiring tests to use `@testable` import; extract these helpers
into a new SwiftPM library package (e.g., Packages/SSHPathResolving) and move
their source files there, update their module name and any access control so
tests and other targets can import the library directly, then update the test
target to import the new package instead of using `@testable` import cmux/_DEV;
ensure any references in the app target are updated to import the new package.
- Line 10: The SwiftLint rule `required_deinit` requires an explicit
deinitializer for the test class; add an empty deinit to the
SSHFileExplorerPathResolverTests class (i.e., implement deinit { } inside final
class SSHFileExplorerPathResolverTests) so the linter warning is silenced while
preserving behavior.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 00a38b0e-999d-4d4f-b443-a188857f6992

📥 Commits

Reviewing files that changed from the base of the PR and between 42f5741 and e53c5ab.

📒 Files selected for processing (4)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • Sources/SSHFileExplorerPathResolver.swift
  • cmuxTests/SSHFileExplorerPathResolverTests.swift

Comment on lines +4 to +8
#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

SSHFileExplorerPathResolver should be extracted to a SwiftPM package, not buried in the app target.

The conditional @testable import cmux_DEV / @testable import cmux reveals that SSHFileExplorerPathResolver still compiles into the app target (Sources/). The three helpers (remoteHomePath, effectiveRootPath, isMacLocalPath) have zero dependency on AppKit, SwiftUI, Ghostty globals, or process-wide singletons — the PR itself states they were moved to make them "unit-testable without an application/Workspace." That intent is only half-realised: they're still in the app module, so the test target needs @testable access and any future consumer (CLI, daemon, preview, or a shared iOS target) would have to import the entire app target.

Extracting these helpers into a dedicated SwiftPM package (e.g. Packages/SSHPathResolving/) would let the tests import it directly without @testable, satisfy the project guideline, and make the logic available to non-app consumers without pulling in the full app module.

As per coding guidelines: "Do not implement feature logic that is independent of cmux app lifecycle directly in the app target/module's root Sources/ path when it can compile/test without AppKit, SwiftUI view state, Ghostty globals, or process-wide singletons — extract it behind a SwiftPM package boundary instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/SSHFileExplorerPathResolverTests.swift` around lines 4 - 8,
SSHFileExplorerPathResolver and its helper functions (remoteHomePath,
effectiveRootPath, isMacLocalPath) are currently compiled into the app module
requiring tests to use `@testable` import; extract these helpers into a new
SwiftPM library package (e.g., Packages/SSHPathResolving) and move their source
files there, update their module name and any access control so tests and other
targets can import the library directly, then update the test target to import
the new package instead of using `@testable` import cmux/_DEV; ensure any
references in the app target are updated to import the new package.

The three pure path-resolution helpers
(remoteHomePath, effectiveRootPath, isMacLocalPath) had zero
dependency on AppKit, SwiftUI, Ghostty globals, or
process-wide singletons but were still compiled into the app
target, forcing the test suite to use `@testable import
cmux_DEV` / `@testable import cmux` and preventing any
non-app consumer (CLI, daemon, preview, future shared target)
from reaching them. That is the package-boundary case
documented in
.github/review-bot-rules/swift-file-package-boundaries.md.

Extracts them into a new local SwiftPM package,
`Packages/CMUXSSHPathResolving/`, modelled on the existing
`CMUXDebugLog` / `CMUXAuthCore` packages: macOS 13 platform,
single library product, dedicated test target. The three
helpers are now `public nonisolated static` and the test
target imports the package directly without `@testable`. The
test class also gains an empty `deinit {}` to satisfy the
`required_deinit` SwiftLint rule.

`Sources/ContentView.swift` adds `import CMUXSSHPathResolving`
alphabetised into the existing import block. The pbxproj
gains a single XCLocalSwiftPackageReference, a single
XCSwiftPackageProductDependency, and the corresponding
entries in the project's package references and the
GhosttyTabs target's packageProductDependencies — same shape
the other CMUX packages use. The previous in-target
SSHFileExplorerPathResolver.swift / SSHFileExplorerPathResolverTests.swift
file references and Sources-phase entries are removed.

Verification:
  - `cd Packages/CMUXSSHPathResolving && swift test`
    -> 43 tests, 0 failures, 0.006s
  - `CMUX_SKIP_ZIG_BUILD=1 xcodebuild -scheme cmux-unit \
       -destination 'platform=macOS' build`
    -> BUILD SUCCEEDED

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kimimgo

kimimgo commented May 7, 2026

Copy link
Copy Markdown
Author

Address both CodeRabbit CHANGES_REQUESTED items in 4428f9e0:

  • Fix find focus and align search overlay #1 (Major / Heavy lift): SSHFileExplorerPathResolver is moved out of the app target and into a new local SwiftPM package, Packages/CMUXSSHPathResolving/. The package follows the existing CMUXDebugLog / CMUXAuthCore shape (swift-tools-version: 5.9, macOS(.v13), single library product, dedicated test target). The helpers are now public nonisolated static. The test target imports the package directly — no @testable — so the boundary documented in .github/review-bot-rules/swift-file-package-boundaries.md is now actually realised.
  • Disambiguate Sentry environment between production and dev #2 (Minor / Quick win): final class SSHFileExplorerPathResolverTests: XCTestCase gains an empty deinit {} to satisfy the required_deinit SwiftLint rule.

Verification:

  • cd Packages/CMUXSSHPathResolving && swift test — 43 tests, 0 failures, 0.006s
  • CMUX_SKIP_ZIG_BUILD=1 xcodebuild -scheme cmux-unit -destination 'platform=macOS' build — BUILD SUCCEEDED
  • pbxproj wiring added in 4 slots (project packageReferences, GhosttyTabs target packageProductDependencies, XCLocalSwiftPackageReference, XCSwiftPackageProductDependency) following the CMUXDebugLog precedent. Previous in-target file references and Sources-phase entries are removed.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@kimimgo Thanks for the thorough follow-up! Here's a summary of what's been addressed:

  • ✅ Fix find focus and align search overlay #1 (Package boundary): SSHFileExplorerPathResolver is now a proper local SwiftPM package (Packages/CMUXSSHPathResolving/) following the CMUXDebugLog/CMUXAuthCore shape — swift-tools-version: 5.9, macOS(.v13), single library product, dedicated test target, public nonisolated static helpers, and direct (non-@testable) import in tests. This fully realises the boundary documented in .github/review-bot-rules/swift-file-package-boundaries.md.
  • ✅ Disambiguate Sentry environment between production and dev #2 (SwiftLint required_deinit): Empty deinit {} added to SSHFileExplorerPathResolverTests.

Verification looks solid — 43 tests, 0 failures, and a clean xcodebuild build. Kicking off a fresh review now.

[review]

`Sources/ContentView.swift` shrank from 15956 to 15919 lines
when SSHFileExplorerPathResolver moved into the
`CMUXSSHPathResolving` SwiftPM package. Update the tracked
budget to match, per the comment at the top of the TSV
("Reduce counts as files shrink"). Other shrunk entries that
are unrelated to this PR are intentionally left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/CMUXSSHPathResolving/Tests/CMUXSSHPathResolvingTests/SSHFileExplorerPathResolverTests.swift`:
- Around line 338-340: Add a symmetric test asserting that "/Volumes/" is
treated as a Mac local path: create a new unit test (or extend the existing
suite) that calls SSHFileExplorerPathResolver.isMacLocalPath("/Volumes/") and
XCTAssertTrue the result, mirroring the existing pinned test for "/Users/". This
ensures the trailing-slash boundary for the "/Volumes/" prefix is explicitly
verified (similar to the tests around "/Users/") so future changes to prefix
checks won’t regress this case.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 5491b63a-fb68-4fd9-90ea-2ad002dadc28

📥 Commits

Reviewing files that changed from the base of the PR and between e53c5ab and 4428f9e.

📒 Files selected for processing (5)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Packages/CMUXSSHPathResolving/Package.swift
  • Packages/CMUXSSHPathResolving/Sources/CMUXSSHPathResolving/SSHFileExplorerPathResolver.swift
  • Packages/CMUXSSHPathResolving/Tests/CMUXSSHPathResolvingTests/SSHFileExplorerPathResolverTests.swift
  • Sources/ContentView.swift

@kimimgo

kimimgo commented May 7, 2026

Copy link
Copy Markdown
Author

A small process question for maintainers — three follow-up items we deliberately scoped out of this PR (and listed in the earlier follow-up comment):

  1. runSSHListCommand still passes 2>/dev/null to remote ls, so SSH stderr is swallowed even after the path resolution is correct — error surfacing redesign.
  2. syncFileExplorerDirectory provider-decision branch (existing-vs-new provider, isReady gating, pathChanged guard) has no integration coverage — needs a second extraction step before it is unit-testable.
  3. remoteDaemonStatus.remotePath is currently ignored in favor of the user@host heuristic — daemon contract change, may need a follow-up to prefer daemon-reported home with the heuristic as fallback.

Would you like these tracked as separate GitHub issues now (so they don't get lost after this PR merges), bundled into a single tracking issue, or left in the PR thread for you to triage on your own backlog cadence? Happy to file whichever shape works best for the project.

Mirrors `testIsMacLocalPath_usersWithTrailingSlashOnlyIsMacLocal`
so the same prefix-shape contract is locked in for both
mac-local roots. Without the symmetric pin, a future change
that widens the `/Users/` prefix check could go unnoticed for
`/Volumes/`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kimimgo

kimimgo commented May 14, 2026

Copy link
Copy Markdown
Author

Superseded by #3721 (Follow SSH workspaces in the Files sidebar), which fixes the same bug more robustly: it resolves the actual remote $HOME over SSH instead of inferring it heuristically from user@host, plus adds a single applyWorkspaceRoot(.remoteSSH) entry point, inline status messages ("Resolving remote home..." / "SSH files unavailable"), and cancellation-safe SSH subprocess handling. Closing as obsolete.

@kimimgo kimimgo closed this May 14, 2026
@kimimgo

kimimgo commented May 14, 2026 •

Copy link
Copy Markdown
Author

Postmortem (for future readers)

Closed in favor of #3721. Five takeaways from comparing the two solutions to the same bug:

  1. Query, don't infer. I derived the remote home from user@host (/home/<user>). Follow SSH workspaces in the Files sidebar #3721 asks $HOME over SSH. The query is correct on NixOS, FreeBSD, custom passwd entries, jails, containers — wherever the heuristic silently breaks.

  2. Tell, don't ask. My fix kept the decision logic at the call site (provider equality, path diff, conditional setProvider/setRootPath). Follow SSH workspaces in the Files sidebar #3721 exposes a single applyWorkspaceRoot(.remoteSSH(...)) entry point. Eight call sites in the current tree reuse it; with my approach each would carry its own six-step sequence.

  3. Model states as sum types. FileExplorerWorkspaceRoot.{none, local, remoteSSH} lets the compiler enforce exhaustive handling. Scattered if-else does not.

  4. Test behavior, not the heuristic. My 44 unit tests pinned the mapping function. They would stay green even if the mapping itself were wrong, because they encode the wrong assumption as the expected output. Follow SSH workspaces in the Files sidebar #3721 injects a mock SSHFileExplorerTransport and asserts the user-visible outcome (store.rootPath == "/home/dev", displayRootPath == "ssh://...:/home/dev").

  5. Async needs four guards: cancel handler, in-flight task tracking, dedup key, stale-result check. A heuristic is synchronous and skips all four. Switching to a real probe forces them — skipping the probe doesn't avoid the cost, it ships an inferior fix.

Process note: git log upstream/main -- Sources/FileExplorerStore.swift (or a quick search of open PRs touching the file explorer) before opening this PR would have surfaced #3721 in progress.

With respect and gratitude to everyone contributing to cmux.

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.

1 participant