Skip to content

Fix SSH Files explorer using local $HOME and follow remote cd (#5360)#6037

Open
FunJim wants to merge 10 commits into
manaflow-ai:mainfrom
FunJim:fix-5360-ssh-files-explorer-root
Open

Fix SSH Files explorer using local $HOME and follow remote cd (#5360)#6037
FunJim wants to merge 10 commits into
manaflow-ai:mainfrom
FunJim:fix-5360-ssh-files-explorer-root

Conversation

@FunJim

@FunJim FunJim commented Jun 13, 2026

Copy link
Copy Markdown

Closes #5360.

Summary

Two layered root causes; both fixed:

  • A. Workspace.currentDirectory was seeded with the local macOS $HOME at workspace init, and the SSH file-explorer call sites unconditionally forwarded that value as the remote rootPath — so a fresh SSH workspace tried to ls /Users/<mac-user> on the Linux remote and failed with a bare "SSH command failed". Track the origin of currentDirectory (localSeed / localKnown / remoteReport) and only forward .remoteReport-confirmed paths. When the helper returns nil, FileExplorerStore.applyRemoteSSHWorkspaceRoot's existing resolveRemoteHome fallback correctly roots at the remote $HOME.
  • B. Even with (A) fixed, cd on the remote shell never updated the Files panel. The cmux shell integration's report_pwd hook delivers via _cmux_send_bg (Unix-socket only). For SSH workspaces CMUX_SOCKET_PATH=127.0.0.1:<relayPort> (TCP), so every cwd change was dropped on the floor. Add a JSON-RPC counterpart surface.report_pwd and call it from the relay-only branch of _cmux_prompt_command / _cmux_precmd.

Also surfaces the SSH stderr in FileExplorerError.sshCommandFailed(detail)'s localized message instead of dropping it, so the panel shows e.g. SSH command failed: ls: cannot access '/foo' rather than the bare string.

Implementation notes

  • currentDirectoryOrigin is promoted to .remoteReport only when source == .liveReport; the restore path that replays persisted panel directories (source: .restoredSnapshotMetadata) cannot re-promote a stale .localSeed snapshot path.
  • surface.report_pwd mirrors surface.report_tty — same param parsing in Packages/CmuxControlSocket/.../ControlCommandCoordinator+Surface{,3}.swift, same surfaceReportSurfaceFields reuse, same shape of resolution enum (minus .pending since the cmuxd-attached shell already has a known surface id), implementation in Sources/TerminalController+ControlSurfaceContext4.swift.
  • Bash + zsh integrations get _cmux_report_pwd_via_relay; the cwd-report fires inside the relay-only early-return branch of _cmux_prompt_command (the downstream cwd block is gated on a Unix socket and was unreachable for SSH workspaces).
  • Local OSC 7 PWD events from the parent macOS shell that's running ssh user@host are now ignored on remote workspaces (the parent shell's pwd is always a local path; only the relay-delivered report is authoritative for SSH workspaces).
  • SessionWorkspaceSnapshot.currentDirectoryOrigin is Optional with default nil so old session-com.cmuxterm.app.json files decode unchanged; restore treats missing as .localSeed.
  • Two-commit regression policy: 85c0ea7cc adds cmuxTests/FileExplorerSSHRootSeedingTests (six cases), 140085f1e adds the helpers; CI on the first commit fails to compile on the unresolved helper symbols, on the green tip all six tests pass.
  • Localization audit: new fileExplorer.error.sshFailedWithDetail xcstrings entry in en/ja/ko, matching the locales of the sibling fileExplorer.error.sshFailed.

Test plan

  • xcodebuild test -scheme cmux-unit -only-testing:cmuxTests/FileExplorerSSHRootSeedingTests — all 6 cases pass on HEAD; the same target on the regression-test commit alone fails to compile with exactly the predicted unresolved symbols (currentDirectoryOrigin, fileExplorerRemoteRootPath, debugSetFocusedPanelIdForTests, CurrentDirectoryOrigin.localSeed/.localKnown/.remoteReport, SessionWorkspaceSnapshot.currentDirectoryOrigin).
  • ./scripts/lint-pbxproj-test-wiring.sh — ok (191 test files), the new test file has all four pbxproj entries.
  • Manual: fresh cmux ssh devcloud from macOS to a Linux remote where remote $HOME = /root ≠ local /Users/funjim. Files panel mounted at /root (the actual remote home), not at the macOS home.
  • Manual: cd /tmp && cd /data/workspace in the remote shell — Files panel re-rooted on each cd.
  • Manual: Local (non-SSH) workspace still tracks the local cwd via OSC 7 — no regression.

Known minor follow-ups (not blocking)

  • Workspace.applyTabSelectionNow (Sources/Workspace.swift:17317) and a sibling site (~18893) write currentDirectory = panelDirectories[panelId] directly without going through updatePanelDirectory, bypassing origin tracking. Not on the SSH workspace Files explorer uses local $HOME as remote root → "SSH command failed" #5360 hot path; the abstraction is leaky there but the practical state is correct because for remote workspaces every panelDirectories[panelId] was last written via .liveReport already. Could be tightened in a follow-up by routing those two writes through updatePanelDirectory.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Fixes the SSH Files explorer mounting at the local $HOME and not following cd. It now roots at the remote $HOME and updates when the remote cwd changes (fixes #5360).

  • Bug Fixes
    • Track currentDirectoryOrigin and only forward fileExplorerRemoteRootPath when it’s .remoteReport; otherwise fall back to the remote $HOME.
    • On restore, coerce persisted .remoteReport to .localSeed until a live report arrives, preventing stale paths from seeding the SSH root.
    • Add JSON-RPC surface.report_pwd and send from bash/zsh via the relay; fire from the relay-only branch and cache the last PWD only after a successful send.
    • Ignore local OSC 7 PWD events for SSH workspaces to stop local paths from overriding remote state.
    • Show only the first non-empty line of SSH stderr in error messages (e.g., “SSH command failed: ls: …”) for clearer feedback.

Written for commit 3db84fa. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added remote working-directory reporting (surface.report_pwd) with relay/RPC support, and exposed the reported remote root in the UI/file explorer.
  • Bug Fixes
    • Prevented remote/SSH prompt updates from overwriting the local Ghostty working directory.
    • Improved remote SSH file explorer root selection to follow confirmed remote cwd only.
  • Changes
    • SSH file-explorer errors now include stderr details when available.
  • Tests
    • Added coverage for working-directory origin behavior, snapshot backward compatibility, remote root seeding, and SSH error formatting.

FunJim added 9 commits June 13, 2026 13:16
…m local cwd (manaflow-ai#5360)

Adds FileExplorerSSHRootSeedingTests covering:
- Workspace.init seeds currentDirectory with .localSeed origin so the SSH
  file explorer falls back to remote $HOME instead of /Users/<user>.
- updatePanelDirectory on a remote workspace flips the origin to
  .remoteReport, exposing the directory via fileExplorerRemoteRootPath.
- Legacy SessionWorkspaceSnapshot JSON without currentDirectoryOrigin
  decodes back-compat as nil (the restorer treats it as .localSeed).
- FileExplorerError.sshCommandFailed embeds the underlying SSH stderr
  in its errorDescription rather than dropping it on the floor.

Test fails to build until the helper API lands in the next commit.
…anaflow-ai#5360)

restoreSessionSnapshot calls updatePanelDirectory(source:.restoredSnapshotMetadata)
per panel, which would have re-promoted a stale .localSeed snapshot value to
.remoteReport on app launch — defeating the fix for SSH workspaces whose
persisted currentDirectory is the legacy local /Users/<user> path. Gate the
origin write on .liveReport so only freshly-observed paths flip the origin.
…5360)

The SSH file-explorer mounts in ContentView.syncFileExplorerDirectory and
RightSidebarToolPanel.syncFileExplorerRoot were forwarding
tab.currentDirectory as the remote rootPath. Workspace.init seeds
currentDirectory with the local macOS $HOME, so on a fresh SSH workspace
that path was sent to the remote and listing /Users/<user> failed.

Use the new Workspace.fileExplorerRemoteRootPath helper, which returns
nil unless currentDirectoryOrigin is .remoteReport. When nil,
FileExplorerStore.applyRemoteSSHWorkspaceRoot's existing
resolveRemoteHome fallback fires and roots at the actual remote $HOME.
…anaflow-ai#5360)

Plan-suggested JSON used hand-rolled placeholder field names ('kind',
'pane.id', 'pane.tabIds') that did not match the actual
SessionWorkspaceLayoutSnapshot/SessionPaneLayoutSnapshot Codable shape
('type', 'pane.panelIds'). Decoder failed with 'Key type not found'
on a real run. Use the actual field names so the back-compat test
exercises real Codable behavior.
…s panel (manaflow-ai#5360)

Background: Workspace.currentDirectory and the file explorer that follows it
are wired to the cmux shell-integration's report_pwd hook. Locally the hook
delivers via _cmux_send_bg over the Unix socket. For SSH workspaces the
shell on the remote host has CMUX_SOCKET_PATH set to a TCP relay
(127.0.0.1:port), and _cmux_send_bg's "nc -U" Unix-socket call silently
fails — every cd from the remote shell was getting dropped on the floor,
so the Files panel stayed pinned at the resolved $HOME forever.

Fix:
- Add a JSON-RPC counterpart to the v1 sidebar report_pwd:
  surface.report_pwd(workspace_id, surface_id, path) on the relay.
  Mirrors surface.report_tty: ControlCommandCoordinator+Surface dispatches,
  ControlCommandCoordinator+Surface3 parses params, the new
  ControlSurfaceReportPWDResolution carries the outcome, and
  ControlSurfaceContext exposes the seam.
  TerminalController+ControlSurfaceContext4 implements it the same way
  controlSidebarScheduleScopedDirectoryUpdate does — resolve the surface
  id, then tabManager.updateSurfaceDirectory(...) (which routes through
  Workspace.updatePanelDirectory(.liveReport), already flips
  currentDirectoryOrigin to .remoteReport for SSH workspaces).
- bash + zsh integrations gain _cmux_report_pwd_via_relay and use it from
  _cmux_prompt_command when _cmux_socket_uses_remote_relay is true.
  Local Unix-socket path is unchanged.
…file panel (manaflow-ai#5360)

The previous commit added a relay-side surface.report_pwd so the remote
shell can update the file panel via cd. But the local Ghostty PWD action
(GHOSTTY_ACTION_PWD) was still firing for the parent local shell that
ran `ssh user@host`, carrying the LOCAL pwd (e.g. /Users/<mac-user>).
With currentDirectoryOrigin promotion gated on `isRemoteWorkspace` and
.liveReport source, that local report was incorrectly flipping origin
to .remoteReport and pinning the file panel at the local pwd.

For SSH workspaces, treat OSC 7 from the local Ghostty surface as
unreliable — its parent shell sees only local cwds. The relay-delivered
surface.report_pwd is the only authoritative pwd source. Skip the
GHOSTTY_ACTION_PWD dispatch entirely when isRemoteWorkspace is true.
Local workspaces are unchanged.
…rompt_command (manaflow-ai#5360)

_cmux_prompt_command guards every block on cmux_has_unix_socket and
returns early when the socket is a TCP relay (lines 1473–1478 bash,
1665–1670 zsh). The downstream CWD-report block was only ever reached
on Unix-socket sessions, so SSH-workspace shells never sent
report_pwd at all. The previous commit added a relay-only RPC helper
but it was unreachable from the prompt hook.

Move the CWD report into the relay-only branch (right next to the
ports-kick refresh) and revert the now-redundant relay branch in the
downstream block. Local Unix-socket workspaces still use the legacy
text-protocol report; SSH workspaces now send
surface.report_pwd over the relay every time PWD changes.
@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds remote shell PWD reporting via a new surface.report_pwd JSON-RPC method, shell relay helpers for bash/zsh, server-side recording and workspace-origin tracking, UI wiring to expose remote roots to the File Explorer, error-localization, and unit tests.

Changes

Remote PWD Reporting Integration

Layer / File(s) Summary
Control Socket surface.report_pwd contract
Packages/CmuxControlSocket/Sources/.../ControlCommandCoordinator+Surface.swift, ...+Surface3.swift, ControlSurfaceContext.swift, ControlSurfaceReportPWDResolution.swift
New surface.report_pwd JSON-RPC routing and handler, ControlSurfaceContext protocol method, and ControlSurfaceReportPWDResolution enum for success/error outcomes.
Server-side handler
Sources/TerminalController+ControlSurfaceContext4.swift
controlSurfaceReportPWD resolves workspace and surface, updates surface directory via TabManager, and returns recorded or workspace-not-found resolutions.
Workspace directory-origin state machine
Sources/Workspace.swift, Sources/SessionPersistence.swift
Adds CurrentDirectoryOrigin enum, persists and restores with back-compat handling, transitions origin on live-report updates, and exposes fileExplorerRemoteRootPath only when origin is .remoteReport.
UI integration of remote-root state
Sources/ContentView.swift, Sources/RightSidebarToolPanel.swift, Sources/GhosttyTerminalView.swift
Updates File Explorer root paths to use fileExplorerRemoteRootPath and guards GhosttyTerminalView PWD updates to skip remote workspaces.
Shell relay integration for PWD reporting
Resources/shell-integration/cmux-bash-integration.bash, Resources/shell-integration/cmux-zsh-integration.zsh
Adds _cmux_report_pwd_via_relay for bash/zsh and integrates PWD-change detection into prompt hooks; dispatches surface.report_pwd via relay and advances cached last-PWD only on success.
Error reporting, localization, project wiring, and tests
Sources/FileExplorerStore.swift, Resources/Localizable.xcstrings, cmux.xcodeproj/project.pbxproj, cmuxTests/FileExplorerSSHRootSeedingTests.swift
FileExplorerError.sshCommandFailed(String) carries detail; localized template fileExplorer.error.sshFailedWithDetail added; new tests validate origin seeding, remote report transitions, legacy snapshot decoding, and SSH error formatting.

Sequence Diagram(s)

sequenceDiagram
  participant Shell as Remote Shell
  participant Integration as bash/zsh Integration
  participant Relay as Relay Transport
  participant Coordinator as ControlCommandCoordinator
  participant Handler as TerminalController
  participant Workspace as Workspace Model
  participant UI as File Explorer UI
  Shell->>Integration: cwd changes
  Integration->>Relay: surface.report_pwd (workspace_id, surface_id?, path)
  Relay->>Coordinator: forward JSON-RPC request
  Coordinator->>Handler: surfaceReportPWD(params)
  Handler->>Workspace: updateSurfaceDirectory(path)
  Handler->>Workspace: set currentDirectoryOrigin = .remoteReport
  Workspace->>UI: fileExplorerRemoteRootPath (trimmed path)
  UI->>UI: render remote file explorer at reported path
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#5241: Related remote SSH file-explorer rootPath changes and overlapping file edits (ContentView.swift, RightSidebarToolPanel.swift, FileExplorerStore.swift).
  • manaflow-ai/cmux#2398: Related shell-integration prompt/relay logic; both add relay-based surface JSON-RPC calls.
  • manaflow-ai/cmux#4065: Related right-sidebar tool panel changes where file-explorer wiring lives.

Poem

🐰 I nibble on paths both near and far,
From zsh and bash I hear where shells are,
A whisper of cwd sent over the wire,
The file explorer follows the rabbit's small fire. ✨


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Cache Substitution Correctness ❌ Error Current implementation uses _cmux_relay_rpc_bg which always returns 0 (success) since _cmux_detach_bg backgrounds the RPC. This causes _CMUX_PWD_LAST_PWD cache to be updated before the RPC complete... Change _cmux_relay_rpc_bg to _cmux_relay_rpc in both cmux-bash-integration.bash and cmux-zsh-integration.zsh at lines 166 and 168 respectively to wait for RPC completion before caching.
Cmux No Hacky Sleeps ❌ Error Shell integration uses _cmux_relay_rpc_bg (async background) instead of _cmux_relay_rpc (sync), violating retry logic: cache updates before RPC completion, suppressing transient failures instea... Change _cmux_relay_rpc_bg to _cmux_relay_rpc in _cmux_report_pwd_via_relay (bash line 166, zsh line 168) to wait for RPC completion before caching.
Cmux Architecture Rethink ❌ Error The shell integration uses _cmux_relay_rpc_bg (async) instead of _cmux_relay_rpc (sync) per architectural review feedback, causing _CMUX_PWD_LAST_PWD cache to be set after spawn rather than RPC com... Replace _cmux_relay_rpc_bg with _cmux_relay_rpc in _cmux_report_pwd_via_relay (bash/zsh) to synchronously wait for RPC completion before caching the pwd, ensuring transient failures retry on the next prompt.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: fixing SSH Files explorer to use remote $HOME and follow remote cd changes.
Description check ✅ Passed The description comprehensively covers all required template sections with detailed explanations of root causes, implementation details, test coverage, and verification steps.
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 All production Swift changes maintain proper Swift 6 actor isolation: MainActor-decorated protocols/classes, Sendable enums, correct async dispatch crossing, and no shared mutable state without iso...
Cmux Swift Blocking Runtime ✅ Passed PR introduces no new blocking or timing-based synchronization primitives (semaphores, waits, sleeps, asyncAfter, manual locks). New code uses actor-isolated state, non-blocking async dispatch, and...
Cmux Expensive Synchronous Load ✅ Passed No expensive synchronous loaders were added to main actor or interactive paths. All new methods (controlSurfaceReportPWD, fileExplorerRemoteRootPath, PWD handler) perform only lightweight in-memory...
Cmux Algorithmic Complexity ✅ Passed The PR follows the existing algorithmic complexity patterns and introduces no violations. controlSurfaceReportPWD mirrors the established controlSurfaceReportTTY pattern, reusing the same pruneSurf...
Cmux Swift Concurrency ✅ Passed PR introduces no problematic Swift concurrency patterns: @Published property added to existing ObservableObject, new DispatchQueue.main.async calls crossing AppKit/JSON-RPC callback boundaries (all...
Cmux Swift @Concurrent ✅ Passed All new Swift methods are synchronous, properly isolated on @MainActor, perform UI-bound work, and contain no concurrency annotation violations against swift-concurrent-annotation.md rules.
Cmux Swift File And Package Boundaries ✅ Passed All PR changes comply with Swift file/package boundaries. New files (TerminalController+ControlSurfaceContext4.swift: 469 lines, ControlCommandCoordinator+Surface3.swift: 367 lines) are under 800-l...
Cmux Swift Logging ✅ Passed PR adds no new logging violations. All NSLog statements in modified production files are guarded by #if DEBUG. New files (TerminalController+ControlSurfaceContext4.swift, ControlSurfaceReportPWDRes...
Cmux User-Facing Error Privacy ✅ Passed SSH stderr shown in FileExplorerError.sshCommandFailed displays only the first non-empty line (e.g., "ls: cannot access '/path'"), filtered to prevent multi-line banners. No credentials, tokens,...
Cmux Full Internationalization ✅ Passed PR uses String(localized:defaultValue:) API for all user-facing Swift strings and includes matching en/ja/ko translations in Localizable.xcstrings for the new fileExplorer.error.sshFailedWithDetail...
Cmux Swiftui State Layout ✅ Passed Workspace is an existing @MainActor ObservableObject with 53+ @Published properties; currentDirectoryOrigin follows the same legacy pattern incidentally touched by this PR without introducing new a...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds no new NSWindow, NSPanel, NSWindowController, SwiftUI Window/WindowGroup. Changes are SSH file explorer infrastructure (JSON-RPC, state tracking, shell integration) with no standalone windo...
Cmux Source Artifacts ✅ Passed All 16 changed files are legitimate source artifacts: hand-written Swift source code, XCTest tests, shell integration scripts, localization catalogs, and Xcode config—none are generated artifacts,...
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes two layered root causes behind the SSH Files explorer mounting at the local $HOME and ignoring remote cd. A new CurrentDirectoryOrigin enum (localSeed / localKnown / remoteReport) gates fileExplorerRemoteRootPath so the SSH file-explorer call sites never forward an unconfirmed local path; a new JSON-RPC command surface.report_pwd closes the gap where _cmux_send_bg (Unix socket only) could not deliver cwd changes from the remote relay.

  • Origin tracking: Workspace.currentDirectoryOrigin is promoted to .remoteReport only on .liveReport sources; restore always downgrades back to .localSeed so stale persisted paths can't seed the SSH root on relaunch.
  • Relay cwd reporting: bash and zsh integrations gain _cmux_report_pwd_via_relay wired into the relay-only branch of _cmux_prompt_command / _cmux_precmd; the Swift RPC handler mirrors controlSurfaceReportTTY in shape and routing.
  • OSC 7 guard: local OSC 7 PWD events are now dropped for SSH workspaces, preventing the parent macOS shell's cwd from overwriting the remote-reported path.

Confidence Score: 5/5

Safe to merge; the fix is well-scoped, backward-compatible with old snapshots, and covered by targeted unit tests.

The two root causes are addressed at their origins rather than patched at the call sites, snapshot decoding is backward-compatible via an optional field with nil-as-localSeed semantics, the OSC 7 guard is narrowly targeted to SSH workspaces, and the known applyTabSelectionNow bypass is explicitly acknowledged and has no user-visible impact on the hot path.

The bash and zsh shell integration comments slightly overstate retry guarantees for socket-level failures, but this does not affect correctness on the normal flow.

Important Files Changed

Filename Overview
Sources/Workspace.swift Adds CurrentDirectoryOrigin enum and currentDirectoryOrigin tracking to correctly gate fileExplorerRemoteRootPath; restore path correctly downgrades .remoteReport to .localSeed; DEBUG-only forced-focus helper for tests is properly #if DEBUG guarded.
Sources/FileExplorerStore.swift SSH error now surfaces the first non-empty trimmed stderr line via String(format:) + sshFailedWithDetail key; full stderr is kept in the associated value; fallback to bare key when detail is empty is correct.
Sources/GhosttyTerminalView.swift New guard drops OSC 7 PWD events for SSH workspaces so the local parent shell's cwd never overwrites the remote-reported path; local workspaces are unaffected.
Sources/TerminalController+ControlSurfaceContext4.swift Adds controlSurfaceReportPWD mirroring the existing controlSurfaceReportTTY shape; routes through the public updateSurfaceDirectory which uses .liveReport source, correctly triggering currentDirectoryOrigin promotion.
Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift New surfaceReportPWD JSON-RPC handler faithfully mirrors surfaceReportTTY — param validation, surface resolution, and response shape are all consistent with the existing pattern.
Resources/shell-integration/cmux-bash-integration.bash Adds _cmux_report_pwd_via_relay and wires it into the relay-only branch of _cmux_prompt_command; caching comment slightly overstates retry guarantees for socket-level failures.
Resources/shell-integration/cmux-zsh-integration.zsh Same changes as bash integration, symmetrically applied; same misleading comment about socket-not-ready retry.
Sources/SessionPersistence.swift Adds optional currentDirectoryOrigin to SessionWorkspaceSnapshot with default nil for backward compatibility; restore path correctly coerces missing/remote values to .localSeed.
cmuxTests/FileExplorerSSHRootSeedingTests.swift Six focused tests cover: fresh-workspace origin, supplied-dir origin, live-report promotion + exposed path, legacy snapshot nil-decode, and both branches of SSH error formatting.

Sequence Diagram

sequenceDiagram
    participant Shell as Remote Shell (bash/zsh)
    participant Relay as cmux relay CLI
    participant RPC as ControlCommandCoordinator
    participant TC as TerminalController
    participant WS as Workspace
    participant FE as FileExplorerStore

    Note over Shell: user does cd /data/workspace
    Shell->>Shell: _cmux_precmd / _cmux_prompt_command
    Shell->>Relay: "surface.report_pwd {workspace_id, path} (bg)"
    Relay->>RPC: JSON-RPC surface.report_pwd
    RPC->>TC: controlSurfaceReportPWD(workspaceID, surfaceID, path)
    TC->>WS: tabManager.updateSurfaceDirectory(tabId, surfaceId, path)
    WS->>WS: updatePanelDirectory(source: .liveReport)
    WS->>WS: "currentDirectory = /data/workspace"
    WS->>WS: "currentDirectoryOrigin = .remoteReport"

    Note over FE: view re-renders, reads fileExplorerRemoteRootPath
    FE->>WS: fileExplorerRemoteRootPath?
    WS-->>FE: "/data/workspace (origin == .remoteReport)"
    FE->>FE: applyWorkspaceRoot(/data/workspace)

    Note over Shell: local macOS parent shell OSC 7 fires
    Shell->>WS: "OSC 7 pwd = /Users/mac-user"
    WS->>WS: isRemoteWorkspace? drop (no update)
Loading

Reviews (3): Last reviewed commit: "Address PR review feedback (#5360)" | Re-trigger Greptile

Comment thread Sources/FileExplorerStore.swift Outdated
Comment on lines +720 to +732
case .sshCommandFailed(let detail):
let trimmed = detail.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
return String(
localized: "fileExplorer.error.sshFailed",
defaultValue: "SSH command failed"
)
}
let format = String(
localized: "fileExplorer.error.sshFailedWithDetail",
defaultValue: "SSH command failed: %@"
)
return String(format: format, trimmed)

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.

P2 Raw SSH process stderr is now embedded verbatim in the user-visible errorDescription. SSH stderr can be multi-line (connection banners, host-key warnings, IP addresses) and will appear as-is in the Files panel error label. Per the cmux user-facing error rule, raw upstream error output belongs in sanitized logs; the user-facing copy should stay in cmux terms. The detail should be passed to a details/telemetry path and the public message kept as the bare localized string, or at minimum the first non-empty trimmed line should be extracted to avoid banner noise.

Suggested change
case .sshCommandFailed(let detail):
let trimmed = detail.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
return String(
localized: "fileExplorer.error.sshFailed",
defaultValue: "SSH command failed"
)
}
let format = String(
localized: "fileExplorer.error.sshFailedWithDetail",
defaultValue: "SSH command failed: %@"
)
return String(format: format, trimmed)
case .sshCommandFailed(let detail):
let trimmed = detail.trimmingCharacters(in: .whitespacesAndNewlines)
// Surface only the first non-empty line so multi-line SSH banners
// (host-key notices, connection warnings) don't appear verbatim in
// the UI. Full stderr belongs in sanitized logs.
let firstLine = trimmed
.components(separatedBy: .newlines)
.first(where: { !$0.trimmingCharacters(in: .whitespaces).isEmpty }) ?? ""
if firstLine.isEmpty {
return String(
localized: "fileExplorer.error.sshFailed",
defaultValue: "SSH command failed"
)
}
let format = String(
localized: "fileExplorer.error.sshFailedWithDetail",
defaultValue: "SSH command failed: %@"
)
return String(format: format, firstLine)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Applied in ee25eebdc. errorDescription now extracts the first non-empty line of the trimmed stderr; full stderr stays in the .sshCommandFailed(detail) associated value for callers that want it for logs/telemetry.

Comment on lines +131630 to +131652
"fileExplorer.error.sshFailedWithDetail": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "SSH command failed: %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "SSH コマンドに失敗しました: %@"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "SSH 명령에 실패했습니다: %@"
}
}
}
},

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.

P2 New xcstrings key missing catalog-supported locales

fileExplorer.error.sshFailedWithDetail is added with translations for en/ja/ko only, but Resources/Localizable.xcstrings already carries German (de) and Chinese Simplified (zh-Hans) entries (1,300+ each). Per the cmux full-internationalization rule, new string-catalog additions must include translated entries for every locale already supported by the touched catalog. The sibling fileExplorer.error.sshFailed has the same gap, but the new key introduced by this PR should have complete coverage.

Rule Used: Flag production user-facing text that is not fully... (source)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Skipping. The sibling key fileExplorer.error.sshFailed and the adjacent fileExplorer.status.sshHomeFailed (rendered side-by-side with this one in the Files panel) ship en/ja/ko only — going to 20 locales for one new key would be inconsistent with the catalog's actual coverage in this neighborhood. If the project wants a broader rollout, it should backfill the sibling keys at the same time as a separate localization sweep.

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

🤖 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/FileExplorerSSHRootSeedingTests.swift`:
- Around line 62-79: The test named
testLegacySnapshotMissingOriginDecodesAsLocalSeed currently only checks that
decoding yields a nil currentDirectoryOrigin on SessionWorkspaceSnapshot; either
rename the test to testLegacySnapshotMissingOriginDecodesAsNil to reflect that
it only asserts nil, or extend the test to complete the back-compat contract by
creating a Workspace (or calling the restore method used in production) from the
decoded snapshot and assert the resulting Workspace's currentDirectoryOrigin
equals .localSeed; locate the test function
testLegacySnapshotMissingOriginDecodesAsLocalSeed and update its name or add the
workspace restoration and final XCTAssert for .localSeed on the restored
object's currentDirectoryOrigin accordingly.

In `@Resources/shell-integration/cmux-bash-integration.bash`:
- Around line 1480-1483: In both affected files update the logic so the
last-reported PWD is only set after the relay dispatch succeeds: in
Resources/shell-integration/cmux-bash-integration.bash (lines 1480-1483) call
_cmux_report_pwd_via_relay "$relay_pwd" and, only if it returns success, then
set _CMUX_PWD_LAST_PWD="$relay_pwd" (i.e., move the assignment to run on
successful return of _cmux_report_pwd_via_relay); in
Resources/shell-integration/cmux-zsh-integration.zsh (lines 1672-1675) apply the
same pattern—invoke _cmux_report_pwd_via_relay "$relay_pwd" first and update
_CMUX_PWD_LAST_PWD="$relay_pwd" only when that call succeeds so failed
dispatches can be retried on the next prompt.

In `@Sources/Workspace.swift`:
- Around line 295-299: The code currently rehydrates currentDirectory and
currentDirectoryOrigin from a snapshot even when snapshot.currentDirectoryOrigin
== .remoteReport, which lets a stale remote cached path drive
fileExplorerRemoteRootPath; change the logic so you only accept the
normalizedCurrentDirectory from the snapshot if snapshot.currentDirectoryOrigin
is not .remoteReport (or if you also perform explicit freshness validation),
otherwise ignore the snapshot's normalizedCurrentDirectory and leave
currentDirectory/currentDirectoryOrigin at their safe defaults (or set origin to
.localSeed); update the block that references normalizedCurrentDirectory,
currentDirectory, and currentDirectoryOrigin (and the check of
snapshot.currentDirectoryOrigin) to implement this conditional acceptance of
remoteReport-derived paths.
🪄 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: fdbd35fb-7638-4cfb-b74a-eb7ce79c5ad5

📥 Commits

Reviewing files that changed from the base of the PR and between 1e3b103 and 31b18a7.

📒 Files selected for processing (16)
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceReportPWDResolution.swift
  • Resources/Localizable.xcstrings
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • Sources/ContentView.swift
  • Sources/FileExplorerStore.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/RightSidebarToolPanel.swift
  • Sources/SessionPersistence.swift
  • Sources/TerminalController+ControlSurfaceContext4.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/FileExplorerSSHRootSeedingTests.swift

Comment thread cmuxTests/FileExplorerSSHRootSeedingTests.swift Outdated
Comment thread Resources/shell-integration/cmux-bash-integration.bash
Comment thread Sources/Workspace.swift
- FileExplorerError.sshCommandFailed: surface only the first non-empty
  line of the SSH stderr in the user-visible message so multi-line SSH
  banners (host-key warnings, connection notices) don't appear verbatim
  in the Files panel error label. Full stderr is still passed in via
  the associated value for log/telemetry callers.

- restoreSessionSnapshot: never trust a persisted .remoteReport. The
  semantic of .remoteReport is "confirmed by a live remote shell", but a
  relaunched workspace has not re-confirmed anything yet — the remote
  shell may have started in a different cwd, or the persisted path may
  no longer exist. Coerce restored .remoteReport to .localSeed so the
  SSH file explorer falls back to remote $HOME via resolveRemoteHome on
  reconnect; the first .liveReport from the reconnected shell flips the
  origin back to .remoteReport.

- bash/zsh integration: cache _CMUX_PWD_LAST_PWD only after the relay
  dispatch succeeds. The previous unconditional cache suppressed retries
  when the relay CLI path or remote-relay socket detection transiently
  fails on the first prompt of a new shell.

- FileExplorerSSHRootSeedingTests: rename
  testLegacySnapshotMissingOriginDecodesAsLocalSeed to
  ...DecodesAsNil to accurately describe what's asserted (the decoder
  contract — Workspace.restoreSessionSnapshot's nil → .localSeed
  coercion is exercised end-to-end via the manual restore path).
@FunJim

FunJim commented Jun 13, 2026

Copy link
Copy Markdown
Author

Addressed review feedback in ee25eebdc:

  • (greptile P2, FileExplorerStore.swift) SSH stderr in the user-visible error is now trimmed to the first non-empty line so multi-line banners don't leak into the Files panel.
  • (coderabbit major, Workspace.swift) Restored .remoteReport is downgraded to .localSeed so a relaunched SSH workspace re-resolves remote $HOME instead of trusting a stale cached path; the first .liveReport after reconnect flips origin back.
  • (coderabbit major, shell integration) Bash + zsh now cache _CMUX_PWD_LAST_PWD only after a successful relay dispatch, so transient first-prompt failures retry on the next prompt instead of being suppressed.
  • (coderabbit nit, FileExplorerSSHRootSeedingTests.swift) Renamed testLegacySnapshotMissingOriginDecodesAsLocalSeed...DecodesAsNil to match the actual assertion (the decode contract; the trivial ?? .localSeed coercion is verified by inspection).
  • (greptile P2, locales) Skipped — the sibling fileExplorer.error.sshFailed and the adjacent fileExplorer.status.sshHomeFailed only carry en/ja/ko in this neighborhood; broader coverage should be a separate localization sweep over all three keys.

Regression test still green: xcodebuild test -only-testing:cmuxTests/FileExplorerSSHRootSeedingTests → 6/6 passes on the new tip.

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

736-740: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Use modern localization interpolation instead of String(format:).

Lines 736-740 fetch a localized format string then apply String(format:) separately. Per repo learning, when the format template is retrieved as a plain String (not String.LocalizationValue), prefer String.localizedStringWithFormat or the modern interpolation overload.

Recommend:

-            let format = String(
-                localized: "fileExplorer.error.sshFailedWithDetail",
-                defaultValue: "SSH command failed: %@"
-            )
-            return String(format: format, firstLine)
+            return String(
+                localized: "fileExplorer.error.sshFailedWithDetail",
+                defaultValue: "SSH command failed: \(firstLine)"
+            )

This uses the modern String.LocalizationValue interpolation, which the xcstrings runtime handles automatically.

🤖 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 736 - 740, Replace the separate
format lookup + String(format:) call with the modern localized interpolation
overload: instead of creating `format` and then `return String(format: format,
firstLine)`, return a single interpolated localized string such as `return
String(localized: "fileExplorer.error.sshFailedWithDetail", defaultValue: "SSH
command failed: \(firstLine)")`; this uses the `String(localized:...)`
interpolation path and avoids `String(format:)`.

Source: Learnings

🤖 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 736-740: Replace the separate format lookup + String(format:) call
with the modern localized interpolation overload: instead of creating `format`
and then `return String(format: format, firstLine)`, return a single
interpolated localized string such as `return String(localized:
"fileExplorer.error.sshFailedWithDetail", defaultValue: "SSH command failed:
\(firstLine)")`; this uses the `String(localized:...)` interpolation path and
avoids `String(format:)`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6f221c23-4470-472c-a5b2-7b4940fc94a9

📥 Commits

Reviewing files that changed from the base of the PR and between 31b18a7 and ee25eeb.

📒 Files selected for processing (5)
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • Sources/FileExplorerStore.swift
  • Sources/Workspace.swift
  • cmuxTests/FileExplorerSSHRootSeedingTests.swift

@FunJim FunJim force-pushed the fix-5360-ssh-files-explorer-root branch from ee25eeb to 3db84fa Compare June 14, 2026 09:00

@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 `@Resources/shell-integration/cmux-bash-integration.bash`:
- Around line 152-167: In the _cmux_report_pwd_via_relay function, replace the
call to _cmux_relay_rpc_bg with _cmux_relay_rpc on the last line. This change
ensures the RPC call completes synchronously before the function returns,
allowing the caller to cache _CMUX_PWD_LAST_PWD only after the RPC has actually
succeeded, rather than just after the background spawn returns. This prevents
suppressing retries on transient relay or server failures.
🪄 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: d1bb5e9a-9126-4c57-85cd-e731b07b9c63

📥 Commits

Reviewing files that changed from the base of the PR and between ee25eeb and 3db84fa.

📒 Files selected for processing (16)
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceReportPWDResolution.swift
  • Resources/Localizable.xcstrings
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • Sources/ContentView.swift
  • Sources/FileExplorerStore.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/RightSidebarToolPanel.swift
  • Sources/SessionPersistence.swift
  • Sources/TerminalController+ControlSurfaceContext4.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/FileExplorerSSHRootSeedingTests.swift

Comment on lines +152 to +167
_cmux_report_pwd_via_relay() {
_cmux_socket_uses_remote_relay || return 1
local workspace_id=""
workspace_id="$(_cmux_relay_workspace_id)" || return 1
local pwd_value="$1"
[[ -n "$pwd_value" ]] || return 1

local pwd_json params
pwd_json="$(_cmux_json_escape "$pwd_value")"
params="{\"workspace_id\":\"$workspace_id\",\"path\":\"$pwd_json\""
if [[ -n "$CMUX_PANEL_ID" ]]; then
params+=",\"surface_id\":\"$CMUX_PANEL_ID\""
fi
params+="}"
_cmux_relay_rpc_bg "surface.report_pwd" "$params"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the helper/caller flow is fire-and-forget today.

# 1) PWD helper + cache update callsites
rg -n -C2 '_cmux_report_pwd_via_relay|_cmux_relay_rpc_bg "surface.report_pwd"|_CMUX_PWD_LAST_PWD' \
  Resources/shell-integration/cmux-bash-integration.bash \
  Resources/shell-integration/cmux-zsh-integration.zsh

# 2) Show relay bg path returns after background launch (no success wait)
rg -n -C3 '_cmux_relay_rpc_bg|_cmux_detach_bg|&!| \& \)|\|\| true' \
  Resources/shell-integration/cmux-bash-integration.bash \
  Resources/shell-integration/cmux-zsh-integration.zsh

Repository: manaflow-ai/cmux

Length of output: 50372


🏁 Script executed:

#!/bin/bash
# Verify synchronous _cmux_relay_rpc exists and check for other _cmux_relay_rpc_bg patterns

# 1) Show _cmux_relay_rpc (synchronous) definition
rg -n -A5 '_cmux_relay_rpc\(\)' \
  Resources/shell-integration/cmux-bash-integration.bash \
  Resources/shell-integration/cmux-zsh-integration.zsh

# 2) Find all callers of _cmux_relay_rpc_bg to assess scope of issue
rg -n '_cmux_relay_rpc_bg' \
  Resources/shell-integration/cmux-bash-integration.bash \
  Resources/shell-integration/cmux-zsh-integration.zsh

Repository: manaflow-ai/cmux

Length of output: 1681


🏁 Script executed:

#!/bin/bash
# Check if _cmux_ports_kick_via_relay has the same caching issue

# 1) Show _cmux_ports_kick_via_relay definition + callers
rg -n -B2 -A10 '_cmux_ports_kick_via_relay' \
  Resources/shell-integration/cmux-bash-integration.bash \
  Resources/shell-integration/cmux-zsh-integration.zsh | head -80

# 2) Show caller context for both relay functions (check if caching follows return)
rg -n -B3 -A5 'if.*_cmux_report_pwd_via_relay|if.*_cmux_ports_kick_via_relay' \
  Resources/shell-integration/cmux-bash-integration.bash \
  Resources/shell-integration/cmux-zsh-integration.zsh

Repository: manaflow-ai/cmux

Length of output: 6646


Change _cmux_relay_rpc_bg to _cmux_relay_rpc in pwd-reporting helpers to wait for RPC completion before caching.

_cmux_report_pwd_via_relay currently calls _cmux_relay_rpc_bg, which backgrounds the RPC and returns immediately. The caller then caches _CMUX_PWD_LAST_PWD on return success, suppressing retries on transient relay/server failures. The comments at lines 1479–1481 (bash) and 1671–1673 (zsh) explicitly state the intent: "Cache _CMUX_PWD_LAST_PWD only after the dispatch succeeds" — but "succeeds" is being interpreted as "background spawn" rather than "RPC completed." Using the synchronous _cmux_relay_rpc variant instead ensures the RPC completes before cache update.

Suggested fix
--- a/Resources/shell-integration/cmux-bash-integration.bash
+++ b/Resources/shell-integration/cmux-bash-integration.bash
@@ -163,7 +163,7 @@ _cmux_report_pwd_via_relay() {
         params+=",\"surface_id\":\"$CMUX_PANEL_ID\""
     fi
     params+="}"
-    _cmux_relay_rpc_bg "surface.report_pwd" "$params"
+    _cmux_relay_rpc "surface.report_pwd" "$params"
 }
--- a/Resources/shell-integration/cmux-zsh-integration.zsh
+++ b/Resources/shell-integration/cmux-zsh-integration.zsh
@@ -165,7 +165,7 @@ _cmux_report_pwd_via_relay() {
         params+=",\"surface_id\":\"$CMUX_PANEL_ID\""
     fi
     params+="}"
-    _cmux_relay_rpc_bg "surface.report_pwd" "$params"
+    _cmux_relay_rpc "surface.report_pwd" "$params"
 }
🤖 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 `@Resources/shell-integration/cmux-bash-integration.bash` around lines 152 -
167, In the _cmux_report_pwd_via_relay function, replace the call to
_cmux_relay_rpc_bg with _cmux_relay_rpc on the last line. This change ensures
the RPC call completes synchronously before the function returns, allowing the
caller to cache _CMUX_PWD_LAST_PWD only after the RPC has actually succeeded,
rather than just after the background spawn returns. This prevents suppressing
retries on transient relay or server failures.

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.

SSH workspace Files explorer uses local $HOME as remote root → "SSH command failed"

1 participant