Skip to content

Follow SSH workspaces in the Files sidebar - #3721

Merged
austinywang merged 14 commits into
mainfrom
issue-3719-files-sidebar-ssh-remote
May 10, 2026
Merged

austinywang merged 14 commits into
mainfrom
issue-3719-files-sidebar-ssh-remote

Conversation

@austinywang

@austinywang austinywang commented May 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3719.

Summary

  • Add a regression-test commit proving an SSH workspace root resolves to the remote home instead of keeping a local macOS path.
  • Route file explorer roots through a single workspace-root request so focused local workspaces use their local cwd and focused SSH workspaces resolve the remote $HOME.
  • Show inline SSH status while the remote home is resolving or unavailable, with localized English/Japanese strings and an ssh:// header/tooltip for the active target.

Verification

  • git diff --cached --check
  • jq empty Resources/Localizable.xcstrings

Not run locally

  • XCTest/build/UI tests are deferred to GitHub Actions per workspace policy; no local xcodebuild was run.

Note

Medium Risk
Touches file-explorer root selection and SSH subprocess execution/cancellation; bugs here can cause missing/incorrect file trees or stuck status messages, but changes are scoped and covered by new tests.

Overview
The Files sidebar now routes all root changes through a single applyWorkspaceRoot API so local tabs track their local cwd while SSH workspaces resolve and display the remote $HOME (instead of reusing a stale macOS path).

SSH file browsing is refactored to use an injectable SSHFileExplorerTransport with cancellation-safe process handling, and the UI surfaces inline, localized status while SSH home is resolving or unavailable (including ssh:// header display + tooltips). New unit tests cover remote-home resolution, switching local→remote behavior, and ensuring cancelled loads don’t clear an SSH-unavailable status.

Reviewed by Cursor Bugbot for commit 367b982. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Makes the Files sidebar follow the focused SSH workspace by resolving the remote $HOME and only loading files when connected. Fixes #3719; clears stale local paths, shows clear status while resolving or unavailable, preserves status after cancelled loads, and syncs with recent sidebar UI changes from main.

  • New Features

    • Route root changes through a single applyWorkspaceRoot API; resolve SSH roots via an injectable transport; clear the tree while pending/unavailable; show ssh://<target> (and :/<path> once resolved) in the header and tooltips.
    • Inline SSH status messages (“Resolving remote home…”, “SSH files unavailable”, with detail) localized in English/Japanese.
  • Bug Fixes

    • Gate SSH listing until connected; cancel and terminate in‑flight SSH processes on root switches; make success paths cancellation-aware so stale results can’t clear an “unavailable” status.
    • Deduplicate/stabilize workspace‑root updates, validate provider identity on failures, make SSH provider state thread‑safe, and keep SSH subprocess work non‑blocking with cancellation‑safe termination.
    • Merge latest Files sidebar changes and retain the Find refresh path while preserving SSH status messaging in the empty state.

Written for commit 367b982. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Remote SSH workspace support in the file explorer with live connection availability and resolved remote-home handling.
  • UI
    • File explorer empty state, header, and search layout surface SSH status messages; path tooltip updated during search.
  • Localization
    • Added English and Japanese SSH status strings for resolving, failure, and unavailable states.
  • Tests
    • Added tests covering remote SSH workspace behavior and home resolution.

Add regression coverage for the Files sidebar root-selection contract before changing production code. The tests model a local workspace switching to an SSH workspace and require the store to resolve the remote HOME through a mock SSH transport instead of retaining the local macOS home path.

Constraint: Issue #3719 requires a failing regression commit before the fix

Constraint: Local xcodebuild and local tests are prohibited for this task

Confidence: high

Scope-risk: narrow

Directive: Keep SSH file explorer root selection behind a mockable transport seam; do not derive remote roots from local currentDirectory

Tested: git diff --check -- cmuxTests/FileExplorerStoreTests.swift

Not-tested: XCTest execution intentionally deferred to GitHub Actions
Remote workspaces were passing the terminal cwd through the local file explorer path flow, so a cmux ssh workspace could inherit the macOS home before the remote shell reported any usable directory. The sidebar now accepts a workspace-root request, resolves SSH roots through an injectable transport, and clears stale local trees while remote home resolution or availability status is pending.

Constraint: Issue #3719 requires the Files sidebar to follow the focused workspace automatically instead of requiring a manual remote root

Constraint: Local xcodebuild and local UI/XCUITests are prohibited in this workspace; CI owns build and test execution

Rejected: Branch inside ContentView using the currentDirectory string as a guessed remote home | it preserves the stale local-path failure and duplicates provider knowledge outside the store

Confidence: medium

Scope-risk: moderate

Directive: Keep file explorer root selection funneled through FileExplorerWorkspaceRoot so local and SSH providers do not diverge again

Tested: git diff --cached --check; jq empty Resources/Localizable.xcstrings

Not-tested: XCTest/build deferred to GitHub Actions per workspace policy
@vercel

vercel Bot commented May 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 9, 2026 0:34am
cmux-staging Building Building Preview, Comment May 9, 2026 0:34am

@coderabbitai

coderabbitai Bot commented May 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements SSH workspace root support for the file explorer: adds connection/transport abstractions, refactors provider to use async HOME resolution with cancellation/deduping, centralizes workspace-root application in the store, surfaces root status messages to the UI, updates observer wiring, and adds tests and localization.

Changes

SSH Workspace Root Support

Layer / File(s) Summary
Data Contracts
Sources/FileExplorerStore.swift
Introduces SSHFileExplorerConnection (destination, port, identityFile, sshOptions), FileExplorerWorkspaceRoot (.none, .local, .remoteSSH), SSHFileExplorerTransport protocol, and makes FileExplorerEntry Sendable.
SSH Provider Refactor
Sources/FileExplorerStore.swift
SSHFileExplorerProvider now stores SSHFileExplorerConnection, delegates to SSHFileExplorerTransport, and implements async resolveHomePath() with provider-unavailable gating.
Process SSH Transport
Sources/FileExplorerStore.swift
Adds ProcessSSHFileExplorerTransport with centralized runSSHCommand, sshArguments construction, remote $HOME resolution via printf $HOME, and ls-based directory listing parsing.
Store Root Orchestration
Sources/FileExplorerStore.swift
Adds rootStatusMessage, updates SSH displayRootPath formatting, introduces applyWorkspaceRoot(...), implements async remote HOME resolution with dedupe keys and cancellation, refactors provider setup/reload, and cancels pending resolution in deinit.
Workspace Observer Wiring
Sources/ContentView.swift
SelectedWorkspaceDirectoryObserver adds Snapshot and merges currentDirectory with remote config/state/detail/daemon publishers; ContentView.syncFileExplorerDirectory() routes file-explorer updates through applyWorkspaceRoot(.none/.local/.remoteSSH) and clears session directory for remote workspaces.
UI Status & Visibility
Sources/FileExplorerView.swift
FileExplorerPanelView now forwards statusMessage from store; FileExplorerContainerView.updateVisibility(...) shows rootStatusMessage in empty state and recalculates header/search visibility; header tooltip behavior updated for quick search/display path.
Localization
Resources/Localizable.xcstrings
Adds fileExplorer.status.sshHomeFailed, fileExplorer.status.sshResolvingHome, fileExplorer.status.sshUnavailable, fileExplorer.status.sshUnavailableWithDetail with en/ja entries.
Tests
cmuxTests/FileExplorerStoreTests.swift
Adds MockSSHFileExplorerTransport, switches tests to setProviderForTesting(...), and adds async tests verifying remote HOME resolution, provider switch to SSH provider, and local→remote switching behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • manaflow-ai/cmux#1963: Prior file-explorer SSH work closely related to these SSH/home-resolution changes.
  • manaflow-ai/cmux#2880: Modifies FileExplorerStore tests and test harness; related test-level changes.
  • manaflow-ai/cmux#3028: Changes SelectedWorkspaceDirectoryObserver/ContentView subscription flow; related observer rewiring.

Poem

🐰 I hop through ssh paths with airy cheer,
Async I sniff where remote HOMEs appear,
When workspaces wander from near to far,
The tree repoints under each ssh:// star,
Status signs whisper when the path is hard to hear.


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (3 errors, 1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Cmux Swift File And Package Boundaries ❌ Error FileExplorerStore.swift (1290 lines, new) exceeds 800-line threshold and mixes UI styling, SSH subprocess, git integration, directory watching, and state management responsibilities. Extract UI theme (225 lines), git integration (100+ lines), and SSH transport to separate files. Reduce FileExplorerStore.swift to core store logic.
Cmux Swift Logging ❌ Error ContentView.swift lines 2519-2520 log SSH destination/displayTarget (hostname+username), exposing personal data. Review comment flagged and proposed sanitized alternative. Replace cmuxDebugLog with sanitized flags (state/hasIdentityFile/hasDetail) instead of dest/target per review comment.
Cmux Architecture Rethink ❌ Error Debug logging in syncFileExplorerDirectory() exposes sensitive data (config.destination, config.displayTarget containing usernames/hostnames) flagged in review comment but not fixed. Remove or redact config.destination and config.displayTarget from cmuxDebugLog per review comment; log only non-sensitive metadata like hasIdentityFile and hasDetail flags.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Actor Isolation ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
Description check ❓ Inconclusive The PR description is comprehensive and well-structured, covering summary of changes, verification steps, and risk assessment. However, key template sections are missing. Add 'Testing' section documenting test cases added/run, 'Demo Video' section (if UI changes apply), and complete the 'Checklist' with explicit checkmarks for all items.
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main objective of the PR: making the Files sidebar follow SSH workspaces.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #3719: routes file explorer roots through a single workspace request, resolves SSH workspaces to remote $HOME instead of local paths, shows inline SSH status messages with localizations, and includes regression tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the stated objectives: localization strings, SSH transport abstraction, workspace root routing, status messaging, and test coverage for SSH workspace behavior.
Cmux Swift Blocking Runtime ✅ Passed No blocking synchronization patterns introduced. SSH operations use detached background tasks. asyncAfter debounces are acceptable UI delays, not blocking waits.
Cmux No Hacky Sleeps ✅ Passed No hacky sleeps in production code. Shell script sleeps are build/test infrastructure (outside scope). Swift covered by swift-blocking-runtime.md. TS/JS use proper async/await.
Cmux Swift Concurrency ✅ Passed Uses async/await for SSH transport, Task.detached with cancellation handlers, proper task lifecycle management. @Published property is UI-layer state. No legacy async patterns introduced.
Cmux Swift @Concurrent ✅ Passed Network-heavy SSH async operations properly use Task.detached for actor hopping, as allowed by the rule. No @concurrent annotation violations detected.
Cmux Swiftui State Layout ✅ Passed No violations. FileExplorerStore is existing (modified only). rootStatusMessage adds to legacy state (allowed). AppKit NSViewRepresentable context. State mutation via onChange.
✨ 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 issue-3719-files-sidebar-ssh-remote

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.

The Swift 5 compiler used by CI does not infer an implicit return from a multi-branch computed property body. Make the local-display fallback explicit so the workspace-root abstraction compiles in Debug and unit-test builds.

Constraint: CI logs showed FileExplorerStore.displayRootPath missing a return in the fallback branch

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Not-tested: Local build/tests deferred to CI; xcodebuild is prohibited in this workspace
@greptile-apps

greptile-apps Bot commented May 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Routes all file-explorer root changes through a single applyWorkspaceRoot API, replacing the scattered per-field SSH provider construction in ContentView. SSH workspaces now resolve the remote $HOME asynchronously via an injectable SSHFileExplorerTransport, surface inline status during resolution/unavailability, and cancel in-flight resolution tasks on workspace switches.

  • FileExplorerStore gains applyWorkspaceRoot, resolveRemoteHome (with deduplication key + identity guard on completion), cancelRemoteHomeResolution, and setRootStatusMessage; loadChildren adds try Task.checkCancellation() after listDirectory returns, correctly gating the success path's setRootStatusMessage(nil) on non-cancellation.
  • ContentView now emits a typed Snapshot through SelectedWorkspaceDirectoryObserver, enabling Equatable-based removeDuplicates across all SSH state fields and a single applyWorkspaceRoot call site.
  • FileExplorerView threads rootStatusMessage into updateVisibility, hides the tree when a status is active, and adds path tooltips to the header label.

Confidence Score: 5/5

Safe to merge; cancellation and provider-identity guards are correctly ordered on the main actor and the new async SSH home resolution is well-isolated.

The core async state machine — resolveRemoteHome task lifecycle, the deduplication key, and the provider identity guard — is correctly implemented for cooperative threading on @mainactor. The try Task.checkCancellation() addition after listDirectory ensures stale root-load results cannot clear an SSH-unavailable status. The only structural concern is that setProvider relies on callers having first invoked setRootPath to trigger cancelAllLoads, which is upheld by all current call sites but is an undocumented ordering constraint.

Sources/FileExplorerStore.swift — the implicit cancel-before-set ordering between setRootPath and setProvider deserves attention as the file grows.

Important Files Changed

Filename Overview
Sources/FileExplorerStore.swift Core change: adds FileExplorerWorkspaceRoot enum, SSHFileExplorerConnection, SSHFileExplorerTransport protocol, ProcessSSHFileExplorerTransport with cancellation-safe subprocess management, and applyWorkspaceRoot API with async SSH home resolution, cancellation key deduplication, and proper teardown ordering.
Sources/ContentView.swift Replaces per-field SSH provider construction in ContentView with a single applyWorkspaceRoot call; adds Snapshot struct to deduplicate workspace-state emissions via Equatable removeDuplicates.
Sources/FileExplorerView.swift Threads rootStatusMessage through updateVisibility to show SSH status in the empty state; hides tree when status is active; adds path tooltips to header label in both normal and search modes.
cmuxTests/FileExplorerStoreTests.swift Adds MockSSHFileExplorerTransport and DeferredListFileExplorerProvider; covers SSH home resolution, local to SSH switching, and cancelled-load status preservation. testCancelledRootLoadDoesNotClearRemoteUnavailableStatus uses Task.sleep(50ms) as a timing fence.
Resources/Localizable.xcstrings Adds four SSH status strings in English and Japanese; format specifiers match usage sites.

Sequence Diagram

sequenceDiagram
    participant CV as ContentView
    participant SWDO as SelectedWorkspaceDirectoryObserver
    participant FES as FileExplorerStore
    participant SSH as SSHFileExplorerProvider
    participant T as ProcessSSHFileExplorerTransport

    SWDO->>SWDO: Snapshot(workspaceId, dir, remoteConfig, connectionState, connectionDetail, daemonStatus)
    SWDO->>SWDO: removeDuplicates() on Snapshot.Equatable
    SWDO->>CV: "directoryChangeGeneration += 1"
    CV->>FES: applyWorkspaceRoot(.remoteSSH(...))
    alt same SSH connection
        FES->>SSH: updateAvailability(isAvailable, homePath: nil)
    else new SSH connection
        FES->>FES: cancelRemoteHomeResolution()
        FES->>FES: setRootPath() reload() cancelAllLoads()
        FES->>SSH: SSHFileExplorerProvider(connection, displayTarget)
        FES->>FES: setProvider(sshProvider, reloadIfAvailable: false)
    end
    alt isAvailable false
        FES->>FES: cancelRemoteHomeResolution()
        FES->>FES: setRootStatusMessage(sshUnavailable)
    else homePath already resolved
        FES->>FES: setRootStatusMessage(nil)
        FES->>FES: setRootPath(currentHomePath) reload()
    else homePath empty
        FES->>FES: setRootStatusMessage(sshResolvingHome)
        FES->>SSH: resolveHomePath()
        SSH->>T: resolveHomePath(connection) nonisolated GCD
        T-->>SSH: /home/dev
        SSH-->>FES: /home/dev
        FES->>FES: MainActor.run guard key match and provider identity
        FES->>SSH: updateAvailability(true, homePath: /home/dev)
        FES->>FES: setRootStatusMessage(nil)
        FES->>FES: setRootPath(/home/dev) reload()
    end
    CV->>FES: applyWorkspaceRoot(.local or .none)
    FES->>FES: cancelRemoteHomeResolution()
    FES->>FES: setRootPath() reload() cancelAllLoads()
Loading

Reviews (9): Last reviewed commit: "Refresh the PR after Files sidebar chang..." | Re-trigger Greptile

Review feedback pointed out that the remote root observer could re-emit on repeated SSH state publications and that the new SSH transport used a legacy DispatchQueue continuation wrapper. Keep the store entry point unchanged, but deduplicate the effective workspace-root snapshot and move blocking SSH subprocess work into a detached task with Sendable inputs and outputs.

Constraint: Greptile review flagged the observer noise and transport concurrency shape before CI completed

Rejected: Leave both as follow-up items | the fixes are narrow and reduce future concurrency and invalidation risk

Confidence: medium

Scope-risk: narrow

Tested: git diff --check; jq empty Resources/Localizable.xcstrings

Not-tested: Local XCTest/build deferred to GitHub Actions; xcodebuild is prohibited in this workspace

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 5 files

You’re at about 98% of the monthly review limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Greptile's current-head review kept the protocol isolation contract as a medium hardening item. Mark the transport requirements nonisolated so future conformers cannot inherit caller-actor isolation when resolving SSH file roots.

Constraint: SSH file listing and home resolution may run from UI-driven store updates but must not block the main actor

Confidence: medium

Scope-risk: narrow

Tested: git diff --check

Not-tested: Local build/tests deferred to GitHub Actions; xcodebuild is prohibited in this workspace

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
Sources/FileExplorerStore.swift (1)

394-425: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

SSH processes are not terminated when the resolution Task is cancelled — threads remain blocked.

withCheckedThrowingContinuation does not automatically throw CancellationError when the enclosing Task is cancelled; it only resumes once continuation.resume(…) is called. Because the DispatchQueue.global work item is already in-flight at that point, and Process.waitUntilExit() blocks until the SSH child exits, every call to cancelRemoteHomeResolution() leaves a running ssh process and a blocked background thread. With ConnectTimeout=5 as the only bound, rapid workspace switching (or a slow remote host) can accumulate multiple blocked threads.

Restructure to create the Process outside the continuation so withTaskCancellationHandler can reach it:

🔧 Suggested fix — terminate process on cancellation
-    func resolveHomePath(connection: SSHFileExplorerConnection) async throws -> String {
-        try await runOnBackground {
-            let output = try Self.runSSHCommand(
-                connection: connection,
-                command: #"printf '%s\n' "$HOME""#
-            )
-            return output.trimmingCharacters(in: .whitespacesAndNewlines)
-        }
-    }
+    func resolveHomePath(connection: SSHFileExplorerConnection) async throws -> String {
+        let process = Process()
+        process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
+        process.arguments = Self.sshArguments(connection: connection, command: #"printf '%s\n' "$HOME""#)
+        let outPipe = Pipe()
+        let errPipe = Pipe()
+        process.standardOutput = outPipe
+        process.standardError = errPipe
+        return try await withTaskCancellationHandler {
+            try await withCheckedThrowingContinuation { continuation in
+                DispatchQueue.global(qos: .userInitiated).async {
+                    do {
+                        try process.run()
+                        let data = outPipe.fileHandleForReading.readDataToEndOfFile()
+                        let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
+                        process.waitUntilExit()
+                        guard process.terminationStatus == 0 else {
+                            throw FileExplorerError.sshCommandFailed(
+                                String(data: errData, encoding: .utf8) ?? "")
+                        }
+                        continuation.resume(returning:
+                            (String(data: data, encoding: .utf8) ?? "")
+                                .trimmingCharacters(in: .whitespacesAndNewlines))
+                    } catch {
+                        continuation.resume(throwing: error)
+                    }
+                }
+            }
+        } onCancel: {
+            process.terminate()
+        }
+    }

Apply the same pattern to listDirectory in this transport.

🤖 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 `@Sources/FileExplorerStore.swift` around lines 394 - 425, The background
wrapper runOnBackground causes SSH child processes started by Self.runSSHCommand
and Self.runSSHListCommand (used by resolveHomePath and listDirectory) to be
unreachable when the surrounding Task is cancelled, leaving ssh processes and
threads blocked; fix by creating and configuring the Process (the SSH child)
outside the withCheckedThrowingContinuation so you can wrap the continuation in
withTaskCancellationHandler that calls process.terminate()/process.kill() on
cancellation, then perform Process.run()/waitInside the continuation and
resume/throw accordingly; apply the same pattern to both resolveHomePath (which
calls runSSHCommand) and listDirectory (which calls runSSHListCommand) so
cancellation reliably stops the SSH process and unblocks the background thread.
🤖 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 `@Sources/ContentView.swift`:
- Around line 2474-2476: The remote explorer availability is currently gated
with tab.remoteConnectionState != .error which leaves it enabled during
.connecting/.disconnected; update the predicate to require the connected state
instead. In the call site where isAvailable is set (currently isAvailable:
tab.remoteConnectionState != .error), change it to check for the explicit
connected enum (e.g. isAvailable: tab.remoteConnectionState == .connected) while
leaving unavailableDetail as-is (tab.remoteConnectionDetail ??
tab.remoteDaemonStatus.detail) so the UI only becomes available when the SSH
workspace is actually connected.

In `@Sources/FileExplorerStore.swift`:
- Around line 922-936: Replace the old String(format:) wrapping with the modern
String(localized:defaultValue:) interpolation: when building the SSH-unavailable
message in FileExplorerStore (the branch that checks detail / !detail.isEmpty),
pass the detail via Swift string interpolation in the defaultValue (e.g., "...
\(detail)") instead of using String(format:), and keep the existing
setRootStatusMessage calls and the other localized key unchanged; update the
code paths that construct the message (the conditional using detail and the
setRootStatusMessage invocation) accordingly.
- Line 839: Replace direct assignments to the `@Published` property
rootStatusMessage with calls to the setter method setRootStatusMessage(_:) so
the equality guard there prevents spurious objectWillChange notifications;
locate the two direct assignments (the success path near where resolveRemoteHome
is used and the other assignment at the second occurrence) and change them to
setRootStatusMessage(nil) or setRootStatusMessage(someMessage) as appropriate
rather than using rootStatusMessage = ... .
- Around line 574-582: In displayRootPath collapse the two optional casts by
binding provider once: use a single if-let like "if let sshProvider = provider
as? SSHFileExplorerProvider { … }" then inside check rootPath.isEmpty to return
either "ssh://\(sshProvider.displayTarget)" or
"ssh://\(sshProvider.displayTarget):\(rootPath)"; otherwise fall back to
FileExplorerRootResolver.displayPath(for: rootPath, homePath:
provider?.homePath). This removes the duplicate "as? SSHFileExplorerProvider"
casts while preserving the empty-rootPath branch and the fallback to
FileExplorerRootResolver.
- Around line 685-694: The method setProvider(_:reloadIfAvailable:) in
FileExplorerStore was made internal unintentionally; revert its visibility back
to private so external module code cannot bypass the FileExplorerStore
lifecycle; update the declaration of setProvider to private (leaving its
signature and behavior unchanged) and ensure callers remain applyWorkspaceRoot
and applyRemoteSSHWorkspaceRoot within FileExplorerStore and tests can continue
to call it via test accessors as needed.

---

Outside diff comments:
In `@Sources/FileExplorerStore.swift`:
- Around line 394-425: The background wrapper runOnBackground causes SSH child
processes started by Self.runSSHCommand and Self.runSSHListCommand (used by
resolveHomePath and listDirectory) to be unreachable when the surrounding Task
is cancelled, leaving ssh processes and threads blocked; fix by creating and
configuring the Process (the SSH child) outside the
withCheckedThrowingContinuation so you can wrap the continuation in
withTaskCancellationHandler that calls process.terminate()/process.kill() on
cancellation, then perform Process.run()/waitInside the continuation and
resume/throw accordingly; apply the same pattern to both resolveHomePath (which
calls runSSHCommand) and listDirectory (which calls runSSHListCommand) so
cancellation reliably stops the SSH process and unblocks the background thread.
🪄 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: b5f393ce-b142-4d93-a025-a171e53865da

📥 Commits

Reviewing files that changed from the base of the PR and between 7142e31 and 38cce19.

📒 Files selected for processing (5)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/FileExplorerStore.swift
  • Sources/FileExplorerView.swift
  • cmuxTests/FileExplorerStoreTests.swift

Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/FileExplorerStore.swift
Comment thread Sources/FileExplorerStore.swift Outdated
Comment thread Sources/FileExplorerStore.swift Outdated
Comment thread Sources/FileExplorerStore.swift
Review feedback showed remote file explorer subprocesses could survive root-switch cancellation, and the sidebar was still willing to probe SSH files before the workspace reached the connected state. Keep the provider lifecycle private, terminate reachable SSH children on cancellation, and route test-only provider injection through an explicit debug helper.

Constraint: Do not run local xcodebuild or XCUITests for this workspace

Rejected: Leave SSH probes available while connecting | this still lets the sidebar show transient remote roots before authentication is complete

Confidence: medium

Scope-risk: moderate

Tested: git diff --check; jq empty Resources/Localizable.xcstrings

Not-tested: local Xcode build/tests per workspace policy

@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.

Caution

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

⚠️ Outside diff range comments (1)
Sources/FileExplorerStore.swift (1)

425-434: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Drain both SSH pipes concurrently to prevent subprocess deadlock.

At lines 432–433, stdout and stderr are read to EOF sequentially. If the process writes to stderr beyond the pipe buffer capacity before the main thread drains stdout, the process blocks waiting for stderr to be consumed, while the main thread blocks on stdout EOF—creating a deadlock. Read both pipes concurrently to prevent this.

Suggested fix (concurrent draining)
         let outPipe = Pipe()
         let errPipe = Pipe()
         process.standardOutput = outPipe
         process.standardError = errPipe

         try process.run()
-        // Read pipe data before waitUntilExit to avoid deadlock when pipe buffer fills
-        let data = outPipe.fileHandleForReading.readDataToEndOfFile()
-        let stderrData = errPipe.fileHandleForReading.readDataToEndOfFile()
+        // Drain both pipes concurrently to avoid blocking if either pipe fills.
+        var data = Data()
+        var stderrData = Data()
+        let group = DispatchGroup()
+        let readQueue = DispatchQueue(label: "com.cmux.fileExplorer.sshRead", attributes: .concurrent)
+        group.enter()
+        readQueue.async {
+            data = outPipe.fileHandleForReading.readDataToEndOfFile()
+            group.leave()
+        }
+        group.enter()
+        readQueue.async {
+            stderrData = errPipe.fileHandleForReading.readDataToEndOfFile()
+            group.leave()
+        }
+        group.wait()
         process.waitUntilExit()
🤖 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 `@Sources/FileExplorerStore.swift` around lines 425 - 434, The code currently
reads stdout then stderr sequentially on the main thread (using
outPipe.fileHandleForReading.readDataToEndOfFile() and
errPipe.fileHandleForReading.readDataToEndOfFile()), which can deadlock if one
buffer fills; change this to drain both pipes concurrently (e.g. spawn two
background reads or use DispatchGroup/async tasks to read outPipe and errPipe in
parallel), collect both Data results, then call process.waitUntilExit(); update
the logic around process.standardOutput/standardError and the variables
capturing stdout/stderr so the concurrent reads return the same Data values used
downstream.
🤖 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.

Outside diff comments:
In `@Sources/FileExplorerStore.swift`:
- Around line 425-434: The code currently reads stdout then stderr sequentially
on the main thread (using outPipe.fileHandleForReading.readDataToEndOfFile() and
errPipe.fileHandleForReading.readDataToEndOfFile()), which can deadlock if one
buffer fills; change this to drain both pipes concurrently (e.g. spawn two
background reads or use DispatchGroup/async tasks to read outPipe and errPipe in
parallel), collect both Data results, then call process.waitUntilExit(); update
the logic around process.standardOutput/standardError and the variables
capturing stdout/stderr so the concurrent reads return the same Data values used
downstream.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce391f7a-1348-4e7b-b66b-b9fc915a1039

📥 Commits

Reviewing files that changed from the base of the PR and between 38cce19 and e5a055d.

📒 Files selected for processing (2)
  • Sources/ContentView.swift
  • Sources/FileExplorerStore.swift

Comment thread Sources/ContentView.swift

@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 `@Sources/FileExplorerStore.swift`:
- Around line 1029-1040: The catch path can clear state for a replaced provider
because it only checks remoteHomeResolutionKey; update the failure handler in
the async MainActor block to also confirm the provider instance matches the SSH
provider (same check used in the success branch) before clearing
remoteHomeResolutionTask/remoteHomeResolutionKey and calling
setRootPath/setRootStatusMessage; i.e., add the same provider identity check
(self.provider === sshProvider) alongside the existing remoteHomeResolutionKey
comparison so stale tasks cannot overwrite a new provider's state.
🪄 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: 676769b0-f37b-420f-9d51-af048004dd97

📥 Commits

Reviewing files that changed from the base of the PR and between e5a055d and 394fcfe.

📒 Files selected for processing (3)
  • Sources/ContentView.swift
  • Sources/FileExplorerStore.swift
  • cmuxTests/FileExplorerStoreTests.swift

Comment thread Sources/FileExplorerStore.swift Outdated
Review feedback found two stale-state paths in the SSH file explorer follow behavior. The selected-workspace observer now derives snapshots from Combine's emitted values instead of re-reading @published properties during willSet, and the SSH home failure path now validates the provider identity before clearing root state.

Constraint: Local xcodebuild and local XCUITests are prohibited for this task

Rejected: Keep reading workspace properties from the merged signal callback | @published emits before assignment, so isolated property changes can be dropped by removeDuplicates

Confidence: high

Scope-risk: narrow

Tested: git diff --check; jq empty Resources/Localizable.xcstrings

Not-tested: Local build/tests deferred to GitHub Actions per workspace policy
Comment thread Sources/FileExplorerStore.swift
CI showed the Combine map closure needs an explicit return after introducing tuple destructuring for emitted @published values. Add the return without changing the observer semantics.

Constraint: Local xcodebuild and local tests are prohibited for this task

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Not-tested: Local build/tests deferred to GitHub Actions per workspace policy
The SSH provider is shared between main-thread root switching and async SSH resolution/listing tasks, so availability and resolved home state now live behind a short lock-protected snapshot.

Constraint: FileExplorerStore remains main-thread oriented while SSH subprocess work runs asynchronously.

Rejected: MainActor-isolate the provider | would couple remote file listing to UI actor hops instead of protecting the small shared state directly

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Not-tested: Local XCTest/XCUITest/build per project policy and pending CI
Comment thread Sources/FileExplorerStore.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 8, 2026

@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 `@Sources/ContentView.swift`:
- Around line 2492-2495: The current cmuxDebugLog call logs sensitive SSH target
fields (config.destination and config.displayTarget); change the log to omit or
mask those values and only emit non-sensitive metadata such as
tab.remoteConnectionState.rawValue, presence flags, byte counts or a boolean
indicating whether destination/displayTarget are present. Locate the
cmuxDebugLog invocation and replace the string to exclude config.destination and
config.displayTarget (or replace them with masked/boolean indicators like
destinationPresent/displayTargetPresent) while keeping the rest of the debug
context.
🪄 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: a95c6da4-bb17-4215-ac8b-bf164f04d6f4

📥 Commits

Reviewing files that changed from the base of the PR and between 10932eb and 651d426.

📒 Files selected for processing (1)
  • Sources/ContentView.swift

Comment thread Sources/ContentView.swift
Remote file explorer diagnostics must not leak SSH targets, and SSH directory probes must not block Swift's cooperative executor. The transport now keeps cancellation-safe subprocess termination while running blocking I/O on a GCD worker, and the DEBUG sync log emits only presence flags.

Constraint: Do not add dependencies or widen the file explorer provider surface while addressing review feedback.

Rejected: Keep Task.detached for the SSH subprocess wait | blocking Process and pipe reads can occupy cooperative executor threads

Rejected: Log masked SSH host/user strings | presence flags provide enough correlation without retaining target data

Confidence: high

Scope-risk: narrow

Tested: git diff --check; grep confirmed no raw SSH sync target fields and no FileExplorerStore Task.detached usage

Not-tested: Local XCTest/XCUITest/build per project policy and pending CI
Comment thread Sources/FileExplorerStore.swift
A root listing can finish successfully after a workspace switch has already cancelled it and marked SSH files unavailable. The success path now cooperates with cancellation before mutating the tree or clearing status, so stale listing results cannot overwrite the current workspace state.

Constraint: Do not run local Xcode builds or XCUITests for this repo; CI owns test execution.

Rejected: Check only the error path | successful subprocess completion after cancellation was the failing case.

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Not-tested: local unit test execution, by repository instruction

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f4dd0d4. Configure here.

Comment thread Sources/FileExplorerStore.swift Outdated
The process-backed SSH transport already trims command output into a usable path, so the provider only validates the returned home path instead of normalizing it a second time.

Rejected: Leave duplicate trimming in place | it blurred the transport/provider boundary and kept a review thread unresolved.

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Not-tested: local unit test execution, by repository instruction
Merged the latest origin/main into the feature branch so CI, reviewer bots, and mergeability are evaluated against the current base. The review findings I rechecked are already present in the branch code; this commit only brings in upstream main and resolves stale branch state.

Constraint: iterate-pr requires syncing the PR branch with the latest base before CI iteration

Rejected: Leave the PR on its older base | would keep branch protection and review automation tied to stale inputs

Confidence: high

Scope-risk: narrow

Directive: Keep this branch evaluated against current main before interpreting skipped or stale CI results

Tested: ./scripts/reload.sh --tag issue-3719-files-sidebar-ssh-remote before base sync

Not-tested: Tagged rebuild and post-push CI are pending after this amended merge commit
@austinywang
austinywang dismissed coderabbitai[bot]’s stale review May 8, 2026 20:49

Stale CodeRabbit change request after addressed feedback; latest CodeRabbit, Greptile, Cursor, CircleCI, GitHub Actions, Vercel, and Socket checks pass on head 9ee803c.

Merged current origin/main into the SSH Files sidebar branch and resolved the FileExplorerView conflict by preserving both sides: the new right-sidebar Find AppKit refresh path from main and this branch's SSH root status message in the empty-state visibility path.

Constraint: GitHub reported the PR could not merge because Sources/FileExplorerView.swift conflicted with current main

Rejected: Keep origin/main's two-argument updateVisibility call | would drop SSH unavailable/resolving status messages from the Files sidebar

Rejected: Keep only the branch version without current main | would leave the PR conflicted and miss the Find typing-lag UI updates

Confidence: high

Scope-risk: narrow

Directive: FileExplorerView updateVisibility must continue receiving rootStatusMessage so SSH unavailable states remain visible

Tested: ./scripts/reload.sh --tag issue-3719-files-sidebar-ssh-remote

Not-tested: Local XCTest suite per repository policy; CI will run after push

This branch was successfully deployed

1 active deployment
Preview – cmux — 367b9827 Deployed May 9, 2026 by vercel[bot]
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.

Files sidebar shows local macOS path in cmux ssh workspace (v0.64.3)

1 participant