From 7991602816a6b60d6abab0b46f102155aa063824 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 24 Jun 2026 14:21:09 -0700 Subject: [PATCH 1/7] Add remote cwd relay regression tests --- cmuxTests/FishShellIntegrationTests.swift | 52 +++++++++++ cmuxTests/GhosttyConfigTests.swift | 103 ++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/cmuxTests/FishShellIntegrationTests.swift b/cmuxTests/FishShellIntegrationTests.swift index e01c21fcdcb0..ce74ae1f0f51 100644 --- a/cmuxTests/FishShellIntegrationTests.swift +++ b/cmuxTests/FishShellIntegrationTests.swift @@ -162,6 +162,58 @@ struct FishShellIntegrationTests { ) } + @Test(.enabled(if: fishExecutablePath != nil)) + func testFishIntegrationRelayPromptReportsPWD() throws { + _ = try requireFishExecutable() + let fileManager = FileManager.default + let root = fileManager.temporaryDirectory + .appendingPathComponent("cmux-fish-relay-pwd-\(UUID().uuidString)") + let binDir = root.appendingPathComponent("bin", isDirectory: true) + let remoteDirectory = root.appendingPathComponent("remote-cwd", isDirectory: true) + let logPath = root.appendingPathComponent("relay.log", isDirectory: false) + let cmuxPath = binDir.appendingPathComponent("cmux", isDirectory: false) + + try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true) + try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: root) } + + try writeExecutableShellFile( + at: cmuxPath, + body: """ + #!/bin/sh + printf '%s\\n' "$*" >> "\(logPath.path)" + """ + ) + + let result = try runInteractiveFish( + command: """ + printf '' > "\(logPath.path)" + cd "\(remoteDirectory.path)" + set -g _CMUX_TTY_REPORTED 1 + set -g _CMUX_PORTS_LAST_RUN (_cmux_now) + set -g _CMUX_PWD_LAST_PWD /tmp/local-launch + _cmux_prompt + for _cmux_i in (seq 1 20) + test -s "\(logPath.path)"; and break + sleep 0.05 + end + cat "\(logPath.path)" + """, + extraEnvironment: [ + "CMUX_SOCKET_PATH": "127.0.0.1:64011", + "CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111", + "CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222", + "CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222", + "CMUX_BUNDLED_CLI_PATH": cmuxPath.path, + ] + ) + + expectTrue( + result.stdout.contains(#"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","path":"\#(remoteDirectory.path)","surface_id":"22222222-2222-2222-2222-222222222222"}"#), + result.stdout + ) + } + @Test func testGeneratedFishBootstrapStagesIntegrationAndPreservesUserConfigHome() throws { let fileManager = FileManager.default diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index 12c40f7ee472..330f1d695f3d 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -5161,6 +5161,58 @@ final class ZshShellIntegrationHandoffTests: XCTestCase { ) } + func testShellIntegrationRelayPromptReportsPWDInZsh() throws { + let fileManager = FileManager.default + let root = fileManager.temporaryDirectory + .appendingPathComponent("cmux-zsh-relay-pwd-\(UUID().uuidString)") + let binDir = root.appendingPathComponent("bin", isDirectory: true) + let remoteDirectory = root.appendingPathComponent("remote-cwd", isDirectory: true) + let logPath = root.appendingPathComponent("relay.log", isDirectory: false) + + try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true) + try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: root) } + + try writeExecutableScript( + at: binDir.appendingPathComponent("cmux", isDirectory: false), + contents: """ + #!/bin/sh + printf '%s\\n' "$*" >> "\(logPath.path)" + exit 0 + """ + ) + + let output = try runInteractiveZsh( + cmuxLoadGhosttyIntegration: false, + cmuxLoadShellIntegration: true, + command: """ + : > "\(logPath.path)" + cd "\(remoteDirectory.path)" + _CMUX_TTY_REPORTED=1 + _CMUX_PORTS_LAST_RUN=$(_cmux_now) + _CMUX_PWD_LAST_PWD="/tmp/local-launch" + _cmux_precmd + repeat 20; do + [[ -s "\(logPath.path)" ]] && break + sleep 0.05 + done + cat "\(logPath.path)" + """, + extraEnvironment: [ + "PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin", + "CMUX_SOCKET_PATH": "127.0.0.1:64011", + "CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111", + "CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222", + "CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222", + ] + ) + + XCTAssertTrue( + output.contains(#"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","path":"\#(remoteDirectory.path)","surface_id":"22222222-2222-2222-2222-222222222222"}"#), + output + ) + } + func testShellIntegrationRelayReportTTYUsesWorkspaceIDInBash() throws { let fileManager = FileManager.default let root = fileManager.temporaryDirectory @@ -5203,6 +5255,57 @@ final class ZshShellIntegrationHandoffTests: XCTestCase { ) } + func testShellIntegrationRelayPromptReportsPWDInBash() throws { + let fileManager = FileManager.default + let root = fileManager.temporaryDirectory + .appendingPathComponent("cmux-bash-relay-pwd-\(UUID().uuidString)") + let binDir = root.appendingPathComponent("bin", isDirectory: true) + let remoteDirectory = root.appendingPathComponent("remote-cwd", isDirectory: true) + let logPath = root.appendingPathComponent("relay.log", isDirectory: false) + + try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true) + try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: root) } + + try writeExecutableScript( + at: binDir.appendingPathComponent("cmux", isDirectory: false), + contents: """ + #!/bin/sh + printf '%s\\n' "$*" >> "\(logPath.path)" + exit 0 + """ + ) + + let result = try runInteractiveBash( + cmuxLoadShellIntegration: true, + command: """ + : > "\(logPath.path)" + cd "\(remoteDirectory.path)" + _CMUX_TTY_REPORTED=1 + _CMUX_PORTS_LAST_RUN=$(_cmux_now) + _CMUX_PWD_LAST_PWD="/tmp/local-launch" + _cmux_prompt_command + for _cmux_i in $(seq 1 20); do + [ -s "\(logPath.path)" ] && break + sleep 0.05 + done + cat "\(logPath.path)" + """, + extraEnvironment: [ + "PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin", + "CMUX_SOCKET_PATH": "127.0.0.1:64011", + "CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111", + "CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222", + "CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222", + ] + ) + + XCTAssertTrue( + result.stdout.contains(#"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","path":"\#(remoteDirectory.path)","surface_id":"22222222-2222-2222-2222-222222222222"}"#), + result.stdout + ) + } + func testShellIntegrationRelayPreexecWorksBeforeSurfaceIDExistsInBash() throws { let fileManager = FileManager.default let root = fileManager.temporaryDirectory From f6650f74be5db6787323b6fd07a1dfd97e88c77b Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 24 Jun 2026 14:26:15 -0700 Subject: [PATCH 2/7] Report remote shell cwd through relay --- .../ControlCommandCoordinator+Surface.swift | 2 + .../ControlCommandCoordinator+Surface3.swift | 52 ++++++++++++++++++- .../Surface/ControlSurfaceContext.swift | 15 +++++- .../ControlSurfaceReportPWDResolution.swift | 23 ++++++++ .../ControlCommandContextTestStubs.swift | 6 +++ .../cmux-bash-integration.bash | 23 +++++++- .../cmux-zsh-integration.zsh | 23 +++++++- Resources/shell-integration/fish/config.fish | 26 ++++++++++ ...nalController+ControlSurfaceContext4.swift | 46 ++++++++++++++-- Sources/TerminalController.swift | 5 +- Sources/Workspace.swift | 29 +++++++++++ 11 files changed, 238 insertions(+), 12 deletions(-) create mode 100644 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceReportPWDResolution.swift diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift index 1969efa875ba..76915d25c0d9 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift @@ -51,6 +51,8 @@ extension ControlCommandCoordinator { return surfaceSendKey(request.params) case "surface.report_tty": return surfaceReportTTY(request.params) + case "surface.report_pwd": + return surfaceReportPWD(request.params) case "surface.report_shell_state": return surfaceReportShellState(request.params) case "surface.ports_kick": diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift index a711ff923eb5..09563ad41d53 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift @@ -1,8 +1,8 @@ internal import Foundation /// The surface-domain resume (`surface.resume.*`) and reporting -/// (`surface.report_tty` / `report_shell_state` / `ports_kick`) bodies, plus the -/// shared resume-binding payload helper, split out of +/// (`surface.report_tty` / `report_pwd` / `report_shell_state` / `ports_kick`) +/// bodies, plus the shared resume-binding payload helper, split out of /// `ControlCommandCoordinator+Surface.swift` to keep each file under the 500-line /// budget. See that file's doc comment for the domain overview. extension ControlCommandCoordinator { @@ -209,6 +209,54 @@ extension ControlCommandCoordinator { } } + // MARK: - report_pwd + + /// `surface.report_pwd` — record a surface's current working directory. + func surfaceReportPWD(_ params: [String: JSONValue]) -> ControlCallResult { + guard let workspaceID = uuid(params, "workspace_id") else { + return .err(code: "invalid_params", message: "Missing or invalid workspace_id", data: nil) + } + let requestedSurfaceID = uuid(params, "surface_id") + if hasNonNull(params, "surface_id"), requestedSurfaceID == nil { + return .err(code: "invalid_params", message: "Missing or invalid surface_id", data: nil) + } + let rawPath = rawString(params, "path") + ?? rawString(params, "directory") + ?? rawString(params, "cwd") + guard let path = rawPath?.trimmingCharacters(in: .whitespacesAndNewlines), !path.isEmpty else { + return .err(code: "invalid_params", message: "Missing path", data: nil) + } + + let resolution = context?.controlSurfaceReportPWD( + workspaceID: workspaceID, + requestedSurfaceID: requestedSurfaceID, + path: path + ) ?? .workspaceNotFound + let requestedSurfaceData = surfaceReportSurfaceFields( + workspaceID: workspaceID, + requestedSurfaceID: requestedSurfaceID + ) + switch resolution { + case .workspaceNotFound: + return .err(code: "not_found", message: "Workspace not found", data: .object(requestedSurfaceData)) + case .surfaceNotFound: + return .err(code: "not_found", message: "Surface not found", data: .object(requestedSurfaceData)) + case .pending: + var payload = requestedSurfaceData + payload["path"] = .string(path) + payload["pending"] = .bool(true) + return .ok(.object(payload)) + case .recorded(let surfaceID): + return .ok(.object([ + "workspace_id": .string(workspaceID.uuidString), + "workspace_ref": ref(.workspace, workspaceID), + "surface_id": .string(surfaceID.uuidString), + "surface_ref": ref(.surface, surfaceID), + "path": .string(path), + ])) + } + } + // MARK: - report_shell_state /// `surface.report_shell_state` — record reported shell-activity state. diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift index 1d60cbd0ec7b..41ef6ca3a7d7 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift @@ -282,7 +282,7 @@ public protocol ControlSurfaceContext: AnyObject { expectedSource: String? ) -> ControlSurfaceResumeResolution - // MARK: - report_tty / report_shell_state / ports_kick + // MARK: - report_tty / report_pwd / report_shell_state / ports_kick /// Records a reported TTY name for `surface.report_tty`. /// @@ -297,6 +297,19 @@ public protocol ControlSurfaceContext: AnyObject { ttyName: String ) -> ControlSurfaceReportTTYResolution + /// Records a reported current working directory for `surface.report_pwd`. + /// + /// - Parameters: + /// - workspaceID: The target workspace. + /// - requestedSurfaceID: The explicit `surface_id`, or `nil` to resolve. + /// - path: The reported (trimmed, non-empty) current working directory. + /// - Returns: The report resolution. + func controlSurfaceReportPWD( + workspaceID: UUID, + requestedSurfaceID: UUID?, + path: String + ) -> ControlSurfaceReportPWDResolution + /// Parses a raw shell-activity token via the app's /// `parseReportedShellActivityState`, returning the state's raw value (the /// coordinator rejects a `nil` result as `invalid_params`). diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceReportPWDResolution.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceReportPWDResolution.swift new file mode 100644 index 000000000000..1ade331fd7ff --- /dev/null +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceReportPWDResolution.swift @@ -0,0 +1,23 @@ +public import Foundation + +/// The outcome of `surface.report_pwd`, preserving the report commands' +/// distinct failures and recorded identity. +/// +/// The coordinator validates the params (workspace required, surface-if-present +/// must parse, path required) and mints refs; every case echoes the workspace id +/// plus the requested-or-resolved surface id and the path. The app records the +/// current working directory against the resolved surface and returns this. +public enum ControlSurfaceReportPWDResolution: Sendable, Equatable { + /// The workspace did not resolve (`not_found` / "Workspace not found", + /// `data` echoes the workspace + requested surface). + case workspaceNotFound + /// The surface did not resolve (`not_found` / "Surface not found", `data` + /// echoes the workspace + requested surface). + case surfaceNotFound + /// The workspace is a remote workspace with no surfaces yet, so the path was + /// remembered (`ok` with `pending: true`, echoing the requested surface). + case pending + /// The path was recorded. Carries the resolved surface id (the `.ok` payload + /// echoes the resolved surface, not the requested one). + case recorded(surfaceID: UUID) +} diff --git a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift index 624f90687059..32879dbb341d 100644 --- a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift +++ b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift @@ -468,6 +468,12 @@ extension ControlSurfaceContext { ttyName: String ) -> ControlSurfaceReportTTYResolution { .workspaceNotFound } + func controlSurfaceReportPWD( + workspaceID: UUID, + requestedSurfaceID: UUID?, + path: String + ) -> ControlSurfaceReportPWDResolution { .workspaceNotFound } + func controlSurfaceReportShellState( workspaceID: UUID, requestedSurfaceID: UUID?, diff --git a/Resources/shell-integration/cmux-bash-integration.bash b/Resources/shell-integration/cmux-bash-integration.bash index 65b39f7a91eb..b97c10e0935d 100644 --- a/Resources/shell-integration/cmux-bash-integration.bash +++ b/Resources/shell-integration/cmux-bash-integration.bash @@ -149,6 +149,23 @@ _cmux_report_tty_via_relay() { _cmux_relay_rpc "surface.report_tty" "$params" } +_cmux_report_pwd_via_relay() { + local pwd="$1" + _cmux_socket_uses_remote_relay || return 1 + [[ -n "$pwd" ]] || return 1 + local workspace_id="" + workspace_id="$(_cmux_relay_workspace_id)" || return 1 + + local pwd_json params + pwd_json="$(_cmux_json_escape "$pwd")" + 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" +} + _cmux_ports_kick_via_relay() { local reason="${1:-command}" _cmux_socket_uses_remote_relay || return 1 @@ -1502,7 +1519,12 @@ _cmux_prompt_command() { local now now="$(_cmux_now)" + local pwd="$PWD" if (( ! cmux_has_unix_socket )); then + if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then + _CMUX_PWD_LAST_PWD="$pwd" + _cmux_report_pwd_via_relay "$pwd" + fi if (( now - _CMUX_PORTS_LAST_RUN >= 10 )); then _cmux_ports_kick refresh fi @@ -1510,7 +1532,6 @@ _cmux_prompt_command() { fi [[ -n "$CMUX_PANEL_ID" ]] || return 0 - local pwd="$PWD" _cmux_set_git_active_pwd "$pwd" # Post-wake socket writes can occasionally leave a probe process wedged. diff --git a/Resources/shell-integration/cmux-zsh-integration.zsh b/Resources/shell-integration/cmux-zsh-integration.zsh index b09104898493..a731aea9c1b8 100644 --- a/Resources/shell-integration/cmux-zsh-integration.zsh +++ b/Resources/shell-integration/cmux-zsh-integration.zsh @@ -151,6 +151,23 @@ _cmux_report_tty_via_relay() { _cmux_relay_rpc "surface.report_tty" "$params" } +_cmux_report_pwd_via_relay() { + local pwd="$1" + _cmux_socket_uses_remote_relay || return 1 + [[ -n "$pwd" ]] || return 1 + local workspace_id="" + workspace_id="$(_cmux_relay_workspace_id)" || return 1 + + local pwd_json params + pwd_json="$(_cmux_json_escape "$pwd")" + 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" +} + _cmux_ports_kick_via_relay() { local reason="${1:-command}" _cmux_socket_uses_remote_relay || return 1 @@ -1689,12 +1706,17 @@ _cmux_precmd() { local now="$(_cmux_now)" local cmd_start="$_CMUX_CMD_START" _CMUX_CMD_START=0 + local pwd="$PWD" local cmd_dur=0 if [[ -n "$cmd_start" && "$cmd_start" != 0 ]]; then cmd_dur=$(( now - cmd_start )) fi if (( ! cmux_has_unix_socket )); then + if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then + _CMUX_PWD_LAST_PWD="$pwd" + _cmux_report_pwd_via_relay "$pwd" + fi if (( cmd_dur >= 2 || now - _CMUX_PORTS_LAST_RUN >= 10 )); then _cmux_ports_kick refresh fi @@ -1702,7 +1724,6 @@ _cmux_precmd() { fi [[ -n "$CMUX_PANEL_ID" ]] || return 0 - local pwd="$PWD" _cmux_set_git_active_pwd "$pwd" _cmux_prompt_wrap_guard "$cmd_start" "$pwd" diff --git a/Resources/shell-integration/fish/config.fish b/Resources/shell-integration/fish/config.fish index f6ae5194276a..3177c1c83ebf 100644 --- a/Resources/shell-integration/fish/config.fish +++ b/Resources/shell-integration/fish/config.fish @@ -20,6 +20,7 @@ if test "$_cmux_integration_enabled" != 0 set -g _CMUX_PORTS_LAST_RUN 0 set -g _CMUX_TTY_NAME "" set -g _CMUX_TTY_REPORTED 0 + set -g _CMUX_PWD_LAST_PWD "" function _cmux_now if test -n "$EPOCHSECONDS" @@ -107,6 +108,19 @@ if test "$_cmux_integration_enabled" != 0 _cmux_relay_rpc_bg surface.report_tty "$params" end + function _cmux_report_pwd_via_relay --argument-names pwd + _cmux_socket_uses_remote_relay; or return 1 + test -n "$pwd"; or return 1 + set -l workspace_id (_cmux_relay_workspace_id); or return 1 + set -l pwd_json (_cmux_json_escape "$pwd") + set -l params "{\"workspace_id\":\"$workspace_id\",\"path\":\"$pwd_json\"" + if test -n "$CMUX_PANEL_ID" + set params "$params,\"surface_id\":\"$CMUX_PANEL_ID\"" + end + set params "$params}" + _cmux_relay_rpc_bg surface.report_pwd "$params" + end + function _cmux_ports_kick_via_relay --argument-names reason _cmux_socket_uses_remote_relay; or return 1 set -l workspace_id (_cmux_relay_workspace_id); or return 1 @@ -281,6 +295,18 @@ if test "$_cmux_integration_enabled" != 0 _cmux_reset_terminal_keyboard_protocols _cmux_report_tty_once _cmux_report_shell_activity_state prompt + set -l pwd "$PWD" + if test "$pwd" != "$_CMUX_PWD_LAST_PWD" + set -g _CMUX_PWD_LAST_PWD "$pwd" + if _cmux_socket_is_unix + if test -n "$CMUX_TAB_ID"; and test -n "$CMUX_PANEL_ID" + set -l qpwd (string replace -a '"' '\"' -- "$pwd") + _cmux_send_bg "report_pwd \"$qpwd\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID" + end + else + _cmux_report_pwd_via_relay "$pwd" + end + end set -l now (_cmux_now) if test (math "$now - $_CMUX_PORTS_LAST_RUN") -ge 5 _cmux_ports_kick refresh diff --git a/Sources/TerminalController+ControlSurfaceContext4.swift b/Sources/TerminalController+ControlSurfaceContext4.swift index 063ebe55bd37..dda206cb984c 100644 --- a/Sources/TerminalController+ControlSurfaceContext4.swift +++ b/Sources/TerminalController+ControlSurfaceContext4.swift @@ -6,11 +6,11 @@ import Foundation import CmuxWorkspaces /// The surface-domain resume (`resume.set` / `.get` / `.clear`) and reporting -/// (`report_tty` / `report_shell_state` / `ports_kick`) witnesses, plus the token -/// parsers. Split out of `TerminalController+ControlSurfaceContext` to keep the -/// conformance readable; see that file's doc comment for the overview. The blocking -/// approval `NSAlert` and its `String(localized:)` calls resolve here, in the app -/// bundle, so translations survive. +/// (`report_tty` / `report_pwd` / `report_shell_state` / `ports_kick`) witnesses, +/// plus the token parsers. Split out of `TerminalController+ControlSurfaceContext` +/// to keep the conformance readable; see that file's doc comment for the overview. +/// The blocking approval `NSAlert` and its `String(localized:)` calls resolve +/// here, in the app bundle, so translations survive. extension TerminalController { // MARK: - resume target resolution (twin of v2ResolveSurfaceResumeTarget) @@ -315,6 +315,42 @@ extension TerminalController { return .recorded(surfaceID: surfaceId) } + // MARK: - report_pwd + + func controlSurfaceReportPWD( + workspaceID: UUID, + requestedSurfaceID: UUID?, + path: String + ) -> ControlSurfaceReportPWDResolution { + guard let tab = controlTabForSidebarMutation(id: workspaceID) else { + return .workspaceNotFound + } + let validSurfaceIds = Set(tab.panels.keys) + tab.pruneSurfaceMetadata(validSurfaceIds: validSurfaceIds) + + let surfaceId = controlResolveReportedSurfaceId( + in: tab, + requestedSurfaceId: requestedSurfaceID, + validSurfaceIds: validSurfaceIds + ) + guard let surfaceId, validSurfaceIds.contains(surfaceId) else { + if tab.isRemoteWorkspace, validSurfaceIds.isEmpty { + tab.rememberPendingRemoteSurfacePWD(path, requestedSurfaceId: requestedSurfaceID) + return .pending + } + return .surfaceNotFound + } + + if tab.isRemoteWorkspace { + _ = tab.updatePanelDirectory(panelId: surfaceId, directory: path) + } else if let tabManager = AppDelegate.shared?.tabManagerFor(tabId: workspaceID) ?? tabManager { + tabManager.updateSurfaceDirectory(tabId: workspaceID, surfaceId: surfaceId, directory: path) + } else { + _ = tab.updatePanelDirectory(panelId: surfaceId, directory: path) + } + return .recorded(surfaceID: surfaceId) + } + // MARK: - report_shell_state func controlSurfaceReportShellState( diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 18cad89eae4c..07082cf8ae19 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -1839,8 +1839,8 @@ class TerminalController { // still-shared v2SurfaceSplitOff) handled by ControlCommandCoordinator too. // surface.refresh/health/resume.set/get/clear, debug.terminals (forwards to the // still-shared v2DebugTerminals), surface.send_text/send_key/report_tty/ - // report_shell_state/ports_kick/clear_history/trigger_flash, and surface.read_text - // handled by ControlCommandCoordinator. + // report_pwd/report_shell_state/ports_kick/clear_history/trigger_flash, and + // surface.read_text handled by ControlCommandCoordinator. // Panes // pane.* handled by ControlCommandCoordinator. @@ -2059,6 +2059,7 @@ class TerminalController { "surface.send_text", "surface.send_key", "surface.report_tty", + "surface.report_pwd", "surface.report_shell_state", "surface.ports_kick", "surface.read_text", diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 2e0d51a11257..87012e8fd99a 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -3431,6 +3431,8 @@ final class Workspace: Identifiable, ObservableObject { private var pendingRemoteSurfaceTTYSurfaceId: UUID? private var pendingRemoteSurfacePortKickReason: PortScanKickReason? private var pendingRemoteSurfacePortKickSurfaceId: UUID? + private var pendingRemoteSurfacePWD: String? + private var pendingRemoteSurfacePWDSurfaceId: UUID? // When the last live remote terminal is detached out, the source workspace may be // closed immediately after the move succeeds. That teardown must not shut down the // shared SSH control master that is still serving the moved terminal. @@ -5730,6 +5732,8 @@ final class Workspace: Identifiable, ObservableObject { pendingRemoteSurfaceTTYSurfaceId = nil pendingRemoteSurfacePortKickReason = nil pendingRemoteSurfacePortKickSurfaceId = nil + pendingRemoteSurfacePWD = nil + pendingRemoteSurfacePWDSurfaceId = nil clearRemoteDetectedSurfacePorts() remoteDetectedPorts = [] remoteForwardedPorts = [] @@ -5801,6 +5805,7 @@ final class Workspace: Identifiable, ObservableObject { } guard activeRemoteTerminalSurfaceIds.insert(panelId).inserted else { return } activeRemoteTerminalSessionCount = activeRemoteTerminalSurfaceIds.count + _ = applyPendingRemoteSurfacePWDIfNeeded(to: panelId) applyPendingRemoteSurfaceTTYIfNeeded(to: panelId) _ = applyPendingRemoteSurfacePortKickIfNeeded(to: panelId) } @@ -6270,6 +6275,30 @@ final class Workspace: Identifiable, ObservableObject { pendingRemoteSurfacePortKickSurfaceId = requestedSurfaceId } + @MainActor + func rememberPendingRemoteSurfacePWD(_ path: String, requestedSurfaceId: UUID?) { + let trimmedPath = path.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedPath.isEmpty else { return } + pendingRemoteSurfacePWD = trimmedPath + pendingRemoteSurfacePWDSurfaceId = requestedSurfaceId + } + + @MainActor + @discardableResult + private func applyPendingRemoteSurfacePWDIfNeeded(to panelId: UUID) -> Bool { + guard let path = pendingRemoteSurfacePWD?.trimmingCharacters(in: .whitespacesAndNewlines), + !path.isEmpty else { + return false + } + if let requestedSurfaceId = pendingRemoteSurfacePWDSurfaceId, + requestedSurfaceId != panelId { + return false + } + pendingRemoteSurfacePWD = nil + pendingRemoteSurfacePWDSurfaceId = nil + return updatePanelDirectory(panelId: panelId, directory: path) + } + @MainActor private func applyPendingRemoteSurfaceTTYIfNeeded(to panelId: UUID) { guard let ttyName = pendingRemoteSurfaceTTYName?.trimmingCharacters(in: .whitespacesAndNewlines), From 53cddc1f4e96f07afa418f1fb2264125f2e552cd Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 24 Jun 2026 14:41:08 -0700 Subject: [PATCH 3/7] Address remote cwd PR feedback --- .../ControlCommandCoordinator+Surface.swift | 6 +- .../ControlCommandContextTestStubs.swift | 15 +- Resources/shell-integration/fish/config.fish | 2 +- Sources/AppDelegate.swift | 1 - Sources/TerminalController.swift | 7 +- Sources/Workspace.swift | 13 +- cmux.xcodeproj/project.pbxproj | 4 + cmuxTests/GhosttyConfigTests.swift | 103 -------- cmuxTests/RemoteShellCWDRelayTests.swift | 226 ++++++++++++++++++ 9 files changed, 249 insertions(+), 128 deletions(-) create mode 100644 cmuxTests/RemoteShellCWDRelayTests.swift diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift index 76915d25c0d9..4e958f28ed2c 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface.swift @@ -49,10 +49,8 @@ extension ControlCommandCoordinator { return surfaceSendText(request.params) case "surface.send_key": return surfaceSendKey(request.params) - case "surface.report_tty": - return surfaceReportTTY(request.params) - case "surface.report_pwd": - return surfaceReportPWD(request.params) + case "surface.report_tty": return surfaceReportTTY(request.params) + case "surface.report_pwd": return surfaceReportPWD(request.params) case "surface.report_shell_state": return surfaceReportShellState(request.params) case "surface.ports_kick": diff --git a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift index 32879dbb341d..6230513a5102 100644 --- a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift +++ b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift @@ -462,17 +462,10 @@ extension ControlSurfaceContext { func controlSurfaceParseShellActivityState(_ rawState: String) -> String? { nil } func controlSurfaceParsePortScanKickReason(_ rawReason: String) -> String? { nil } - func controlSurfaceReportTTY( - workspaceID: UUID, - requestedSurfaceID: UUID?, - ttyName: String - ) -> ControlSurfaceReportTTYResolution { .workspaceNotFound } - - func controlSurfaceReportPWD( - workspaceID: UUID, - requestedSurfaceID: UUID?, - path: String - ) -> ControlSurfaceReportPWDResolution { .workspaceNotFound } + func controlSurfaceReportTTY(workspaceID: UUID, requestedSurfaceID: UUID?, ttyName: String) + -> ControlSurfaceReportTTYResolution { .workspaceNotFound } + func controlSurfaceReportPWD(workspaceID: UUID, requestedSurfaceID: UUID?, path: String) + -> ControlSurfaceReportPWDResolution { .workspaceNotFound } func controlSurfaceReportShellState( workspaceID: UUID, diff --git a/Resources/shell-integration/fish/config.fish b/Resources/shell-integration/fish/config.fish index 3177c1c83ebf..32c79dab7c64 100644 --- a/Resources/shell-integration/fish/config.fish +++ b/Resources/shell-integration/fish/config.fish @@ -300,7 +300,7 @@ if test "$_cmux_integration_enabled" != 0 set -g _CMUX_PWD_LAST_PWD "$pwd" if _cmux_socket_is_unix if test -n "$CMUX_TAB_ID"; and test -n "$CMUX_PANEL_ID" - set -l qpwd (string replace -a '"' '\"' -- "$pwd") + set -l qpwd (_cmux_json_escape "$pwd") _cmux_send_bg "report_pwd \"$qpwd\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID" end else diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 1d07c72cc84b..a3911862bfdd 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -6259,7 +6259,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent arguments.append(contentsOf: ["--session", sessionId]) } process.arguments = arguments - process.currentDirectoryURL = URL(fileURLWithPath: cwd, isDirectory: true) var environment = ProcessInfo.processInfo.environment environment["CMUX_SOCKET_PATH"] = socketPath environment["CMUX_BUNDLED_CLI_PATH"] = cliURL.path diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 07082cf8ae19..915b2bd88897 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -1837,10 +1837,9 @@ class TerminalController { // still-shared v2SurfaceMove). surface.action/tab.action and // surface.drag_to_split/surface.split_off (the latter forwarding to the // still-shared v2SurfaceSplitOff) handled by ControlCommandCoordinator too. - // surface.refresh/health/resume.set/get/clear, debug.terminals (forwards to the - // still-shared v2DebugTerminals), surface.send_text/send_key/report_tty/ - // report_pwd/report_shell_state/ports_kick/clear_history/trigger_flash, and - // surface.read_text handled by ControlCommandCoordinator. + // surface.refresh/health/resume.set/get/clear, debug.terminals, surface.send_text/ + // send_key/report_tty/report_pwd/report_shell_state/ports_kick/clear_history/ + // trigger_flash/read_text handled by ControlCommandCoordinator. // Panes // pane.* handled by ControlCommandCoordinator. diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 87012e8fd99a..5a164c092be5 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -2545,6 +2545,10 @@ final class Workspace: Identifiable, ObservableObject { private func scheduleExtensionSidebarProjectRootRefresh(for directory: String) { extensionSidebarProjectRootRefreshID &+= 1 let refreshID = extensionSidebarProjectRootRefreshID + guard !isRemoteWorkspace else { + extensionSidebarProjectRootPath = nil + return + } let trimmedDirectory = directory.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedDirectory.isEmpty else { extensionSidebarProjectRootPath = nil @@ -4459,13 +4463,13 @@ final class Workspace: Identifiable, ObservableObject { } private func configTrackingDirectory(for panelId: UUID?) -> String? { - // A remote tmux mirror's directories are paths on the REMOTE host. + // A remote workspace's directories are paths on the REMOTE host. // Feeding one into local cmux.json tracking makes CmuxConfigStore walk // the ancestor chain with FileManager.fileExists on the main thread, // and stat'ing e.g. /home/… locally blocks on the autofs automounter // for hundreds of ms (measured via sample during tab-reveal stalls). // No local per-directory config can apply to a remote path — track none. - if isRemoteTmuxMirror { return nil } + if isRemoteWorkspace { return nil } if let panelId { for candidate in [ panelDirectories[panelId], @@ -4504,8 +4508,9 @@ final class Workspace: Identifiable, ObservableObject { } // Update current directory if this is the focused panel if panelId == focusedPanelId { - if surfaceTabBarDirectory != trimmed { - surfaceTabBarDirectory = trimmed + let nextSurfaceTabBarDirectory = configTrackingDirectory(for: panelId) + if surfaceTabBarDirectory != nextSurfaceTabBarDirectory { + surfaceTabBarDirectory = nextSurfaceTabBarDirectory } if currentDirectory != trimmed { currentDirectory = trimmed diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index ecd1f57c0ec9..3aa3cb151eb9 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -675,6 +675,7 @@ REE0CA0000000000000000A2 /* RemotesClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = REE0CA0000000000000000A1 /* RemotesClient.swift */; }; REE0CA0000000000000000B2 /* RemotesClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = REE0CA0000000000000000B1 /* RemotesClientTests.swift */; }; EE30D6000000000000000006 /* RemoteSessionStrings+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE30D6000000000000000005 /* RemoteSessionStrings+App.swift */; }; + C0F674600000000000000001 /* RemoteShellCWDRelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F674600000000000000002 /* RemoteShellCWDRelayTests.swift */; }; E4321000E4321000E4321001 /* RemoteShellSessionParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4321000E4321000E4321002 /* RemoteShellSessionParsing.swift */; }; 0A17C0DE0A17C0DE0A17C002 /* RemoteTmuxAttachOutcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A17C0DE0A17C0DE0A17C001 /* RemoteTmuxAttachOutcome.swift */; }; 0A17C0DE0A17C0DE0A17C004 /* RemoteTmuxAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A17C0DE0A17C0DE0A17C003 /* RemoteTmuxAuthTests.swift */; }; @@ -1756,6 +1757,7 @@ REE0CA0000000000000000A1 /* RemotesClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RemotesClient.swift; sourceTree = ""; }; REE0CA0000000000000000B1 /* RemotesClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotesClientTests.swift; sourceTree = ""; }; EE30D6000000000000000005 /* RemoteSessionStrings+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RemoteSessionStrings+App.swift"; sourceTree = ""; }; + C0F674600000000000000002 /* RemoteShellCWDRelayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShellCWDRelayTests.swift; sourceTree = ""; }; E4321000E4321000E4321002 /* RemoteShellSessionParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShellSessionParsing.swift; sourceTree = ""; }; 0A17C0DE0A17C0DE0A17C001 /* RemoteTmuxAttachOutcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteTmuxAttachOutcome.swift; sourceTree = ""; }; 0A17C0DE0A17C0DE0A17C003 /* RemoteTmuxAuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteTmuxAuthTests.swift; sourceTree = ""; }; @@ -3060,6 +3062,7 @@ C0F15A000000000000000002 /* FishShellIntegrationTests.swift */, C0F16A000000000000000002 /* ShellStartupMatrixTests.swift */, C0F16C000000000000000002 /* ShellStartupMissingBundleTests.swift */, + C0F674600000000000000002 /* RemoteShellCWDRelayTests.swift */, F4000001A1B2C3D4E5F60718 /* GhosttyConfigTests.swift */, B8E9500A0000000000000002 /* PostHogAnalyticsPropertiesTests.swift */, 604100010000000000000002 /* GhosttyDrawableSizeRetryTests.swift */, @@ -4664,6 +4667,7 @@ A64610020000000000000001 /* QuitConfirmationAlertPresenterTests.swift in Sources */, A6D1F0400000000000000002 /* ReleasingWindowControllerTests.swift in Sources */, REE0CA0000000000000000B2 /* RemotesClientTests.swift in Sources */, + C0F674600000000000000001 /* RemoteShellCWDRelayTests.swift in Sources */, 0A17C0DE0A17C0DE0A17C004 /* RemoteTmuxAuthTests.swift in Sources */, 5F5553CA5553CA5553CA0001 /* RemoteTmuxCapabilitiesTests.swift in Sources */, B2FDE62450514C4C27FBD8F1 /* RemoteTmuxControlParserTests.swift in Sources */, diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index 330f1d695f3d..12c40f7ee472 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -5161,58 +5161,6 @@ final class ZshShellIntegrationHandoffTests: XCTestCase { ) } - func testShellIntegrationRelayPromptReportsPWDInZsh() throws { - let fileManager = FileManager.default - let root = fileManager.temporaryDirectory - .appendingPathComponent("cmux-zsh-relay-pwd-\(UUID().uuidString)") - let binDir = root.appendingPathComponent("bin", isDirectory: true) - let remoteDirectory = root.appendingPathComponent("remote-cwd", isDirectory: true) - let logPath = root.appendingPathComponent("relay.log", isDirectory: false) - - try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true) - try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true) - defer { try? fileManager.removeItem(at: root) } - - try writeExecutableScript( - at: binDir.appendingPathComponent("cmux", isDirectory: false), - contents: """ - #!/bin/sh - printf '%s\\n' "$*" >> "\(logPath.path)" - exit 0 - """ - ) - - let output = try runInteractiveZsh( - cmuxLoadGhosttyIntegration: false, - cmuxLoadShellIntegration: true, - command: """ - : > "\(logPath.path)" - cd "\(remoteDirectory.path)" - _CMUX_TTY_REPORTED=1 - _CMUX_PORTS_LAST_RUN=$(_cmux_now) - _CMUX_PWD_LAST_PWD="/tmp/local-launch" - _cmux_precmd - repeat 20; do - [[ -s "\(logPath.path)" ]] && break - sleep 0.05 - done - cat "\(logPath.path)" - """, - extraEnvironment: [ - "PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin", - "CMUX_SOCKET_PATH": "127.0.0.1:64011", - "CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111", - "CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222", - "CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222", - ] - ) - - XCTAssertTrue( - output.contains(#"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","path":"\#(remoteDirectory.path)","surface_id":"22222222-2222-2222-2222-222222222222"}"#), - output - ) - } - func testShellIntegrationRelayReportTTYUsesWorkspaceIDInBash() throws { let fileManager = FileManager.default let root = fileManager.temporaryDirectory @@ -5255,57 +5203,6 @@ final class ZshShellIntegrationHandoffTests: XCTestCase { ) } - func testShellIntegrationRelayPromptReportsPWDInBash() throws { - let fileManager = FileManager.default - let root = fileManager.temporaryDirectory - .appendingPathComponent("cmux-bash-relay-pwd-\(UUID().uuidString)") - let binDir = root.appendingPathComponent("bin", isDirectory: true) - let remoteDirectory = root.appendingPathComponent("remote-cwd", isDirectory: true) - let logPath = root.appendingPathComponent("relay.log", isDirectory: false) - - try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true) - try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true) - defer { try? fileManager.removeItem(at: root) } - - try writeExecutableScript( - at: binDir.appendingPathComponent("cmux", isDirectory: false), - contents: """ - #!/bin/sh - printf '%s\\n' "$*" >> "\(logPath.path)" - exit 0 - """ - ) - - let result = try runInteractiveBash( - cmuxLoadShellIntegration: true, - command: """ - : > "\(logPath.path)" - cd "\(remoteDirectory.path)" - _CMUX_TTY_REPORTED=1 - _CMUX_PORTS_LAST_RUN=$(_cmux_now) - _CMUX_PWD_LAST_PWD="/tmp/local-launch" - _cmux_prompt_command - for _cmux_i in $(seq 1 20); do - [ -s "\(logPath.path)" ] && break - sleep 0.05 - done - cat "\(logPath.path)" - """, - extraEnvironment: [ - "PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin", - "CMUX_SOCKET_PATH": "127.0.0.1:64011", - "CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111", - "CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222", - "CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222", - ] - ) - - XCTAssertTrue( - result.stdout.contains(#"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","path":"\#(remoteDirectory.path)","surface_id":"22222222-2222-2222-2222-222222222222"}"#), - result.stdout - ) - } - func testShellIntegrationRelayPreexecWorksBeforeSurfaceIDExistsInBash() throws { let fileManager = FileManager.default let root = fileManager.temporaryDirectory diff --git a/cmuxTests/RemoteShellCWDRelayTests.swift b/cmuxTests/RemoteShellCWDRelayTests.swift new file mode 100644 index 000000000000..dc30c5ebf0a5 --- /dev/null +++ b/cmuxTests/RemoteShellCWDRelayTests.swift @@ -0,0 +1,226 @@ +import Foundation +import Testing + +@Suite(.serialized) +struct RemoteShellCWDRelayTests { + @Test + func zshRelayPromptReportsRemotePWD() throws { + let fileManager = FileManager.default + let root = fileManager.temporaryDirectory + .appendingPathComponent("cmux-zsh-relay-pwd-\(UUID().uuidString)") + let binDir = root.appendingPathComponent("bin", isDirectory: true) + let remoteDirectory = root.appendingPathComponent("remote-cwd", isDirectory: true) + let logPath = root.appendingPathComponent("relay.log", isDirectory: false) + + try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true) + try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: root) } + + try writeExecutableScript( + at: binDir.appendingPathComponent("cmux", isDirectory: false), + contents: """ + #!/bin/sh + printf '%s\\n' "$*" >> "\(logPath.path)" + exit 0 + """ + ) + + let output = try runInteractiveZsh( + command: """ + : > "\(logPath.path)" + cd "\(remoteDirectory.path)" + _CMUX_TTY_REPORTED=1 + _CMUX_PORTS_LAST_RUN=$(_cmux_now) + _CMUX_PWD_LAST_PWD="/tmp/local-launch" + _cmux_precmd + repeat 20; do + [[ -s "\(logPath.path)" ]] && break + sleep 0.05 + done + cat "\(logPath.path)" + """, + extraEnvironment: [ + "PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin", + "CMUX_SOCKET_PATH": "127.0.0.1:64011", + "CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111", + "CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222", + "CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222", + ] + ) + + let expected = #"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","path":"\#(remoteDirectory.path)","surface_id":"22222222-2222-2222-2222-222222222222"}"# + #expect(output.contains(expected), output) + } + + @Test + func bashRelayPromptReportsRemotePWD() throws { + let fileManager = FileManager.default + let root = fileManager.temporaryDirectory + .appendingPathComponent("cmux-bash-relay-pwd-\(UUID().uuidString)") + let binDir = root.appendingPathComponent("bin", isDirectory: true) + let remoteDirectory = root.appendingPathComponent("remote-cwd", isDirectory: true) + let logPath = root.appendingPathComponent("relay.log", isDirectory: false) + + try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true) + try fileManager.createDirectory(at: remoteDirectory, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: root) } + + try writeExecutableScript( + at: binDir.appendingPathComponent("cmux", isDirectory: false), + contents: """ + #!/bin/sh + printf '%s\\n' "$*" >> "\(logPath.path)" + exit 0 + """ + ) + + let result = try runInteractiveBash( + command: """ + : > "\(logPath.path)" + cd "\(remoteDirectory.path)" + _CMUX_TTY_REPORTED=1 + _CMUX_PORTS_LAST_RUN=$(_cmux_now) + _CMUX_PWD_LAST_PWD="/tmp/local-launch" + _cmux_prompt_command + for _cmux_i in $(seq 1 20); do + [ -s "\(logPath.path)" ] && break + sleep 0.05 + done + cat "\(logPath.path)" + """, + extraEnvironment: [ + "PATH": "\(binDir.path):/usr/bin:/bin:/usr/sbin:/sbin", + "CMUX_SOCKET_PATH": "127.0.0.1:64011", + "CMUX_WORKSPACE_ID": "11111111-1111-1111-1111-111111111111", + "CMUX_TAB_ID": "22222222-2222-2222-2222-222222222222", + "CMUX_PANEL_ID": "22222222-2222-2222-2222-222222222222", + ] + ) + + let expected = #"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","path":"\#(remoteDirectory.path)","surface_id":"22222222-2222-2222-2222-222222222222"}"# + #expect(result.stdout.contains(expected), result.stdout) + } + + private func runInteractiveZsh( + command: String, + extraEnvironment: [String: String] + ) throws -> String { + let fileManager = FileManager.default + let root = fileManager.temporaryDirectory + .appendingPathComponent("cmux-zsh-shell-integration-\(UUID().uuidString)") + try fileManager.createDirectory(at: root, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: root) } + + let userZdotdir = root.appendingPathComponent("zdotdir") + try fileManager.createDirectory(at: userZdotdir, withIntermediateDirectories: true) + var userZshEnvFileContents = "\n" + if let path = extraEnvironment["PATH"] { + let escaped = path.replacingOccurrences(of: "\"", with: "\\\"") + userZshEnvFileContents = "export PATH=\"\(escaped)\"\n" + } + try userZshEnvFileContents.write( + to: userZdotdir.appendingPathComponent(".zshenv"), + atomically: true, + encoding: .utf8 + ) + + let repoRoot = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + let cmuxZdotdir = repoRoot.appendingPathComponent("Resources/shell-integration") + let ghosttyResources = repoRoot.appendingPathComponent("ghostty/src") + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/bin/zsh") + process.arguments = ["-i", "-c", command] + process.environment = [ + "HOME": root.path, + "TERM": "xterm-256color", + "SHELL": "/bin/zsh", + "USER": NSUserName(), + "ZDOTDIR": cmuxZdotdir.path, + "CMUX_ZSH_ZDOTDIR": userZdotdir.path, + "CMUX_SHELL_INTEGRATION": "1", + "CMUX_SHELL_INTEGRATION_DIR": cmuxZdotdir.path, + "GHOSTTY_RESOURCES_DIR": ghosttyResources.path, + ] + for (key, value) in extraEnvironment { + process.environment?[key] = value + } + + let output = try runProcess(process) + #expect(output.status == 0, output.stderr) + return output.stdout.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private func runInteractiveBash( + command: String, + extraEnvironment: [String: String] + ) throws -> (stdout: String, stderr: String) { + let fileManager = FileManager.default + let root = fileManager.temporaryDirectory + .appendingPathComponent("cmux-bash-shell-integration-\(UUID().uuidString)") + try fileManager.createDirectory(at: root, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: root) } + + let repoRoot = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + let integrationPath = repoRoot.appendingPathComponent("Resources/shell-integration/cmux-bash-integration.bash") + let rcfilePath = root.appendingPathComponent(".bashrc") + try ". \"\(integrationPath.path)\"\n".write(to: rcfilePath, atomically: true, encoding: .utf8) + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/bin/bash") + process.arguments = ["--noprofile", "--rcfile", rcfilePath.path, "-i", "-c", command] + process.environment = [ + "HOME": root.path, + "TERM": "xterm-256color", + "SHELL": "/bin/bash", + "USER": NSUserName(), + ] + for (key, value) in extraEnvironment { + process.environment?[key] = value + } + + let output = try runProcess(process) + #expect(output.status == 0, output.stderr) + return ( + stdout: output.stdout.trimmingCharacters(in: .whitespacesAndNewlines), + stderr: output.stderr.trimmingCharacters(in: .whitespacesAndNewlines) + ) + } + + private func runProcess(_ process: Process) throws -> (status: Int32, stdout: String, stderr: String) { + let stdout = Pipe() + let stderr = Pipe() + process.standardOutput = stdout + process.standardError = stderr + + try process.run() + let deadline = Date.now.addingTimeInterval(5) + while process.isRunning && Date.now < deadline { + _ = RunLoop.current.run(mode: .default, before: Date.now.addingTimeInterval(0.01)) + } + if process.isRunning { + process.terminate() + process.waitUntilExit() + throw NSError( + domain: "RemoteShellCWDRelayTests", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Timed out waiting for shell to exit"] + ) + } + + return ( + status: process.terminationStatus, + stdout: String(data: stdout.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "", + stderr: String(data: stderr.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" + ) + } + + private func writeExecutableScript(at url: URL, contents: String) throws { + try contents.write(to: url, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path) + } +} From da8cf667195aac4dafb9aac6089b74ece641ef15 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 24 Jun 2026 14:44:17 -0700 Subject: [PATCH 4/7] Address remote cwd review feedback --- .../ControlCommandCoordinator+Surface3.swift | 12 ++-- ...ontrolCommandCoordinatorSurfaceTests.swift | 55 +++++++++++++++++++ .../cmux-bash-integration.bash | 3 +- .../cmux-zsh-integration.zsh | 3 +- Resources/shell-integration/fish/config.fish | 9 +-- Sources/Workspace.swift | 9 ++- 6 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift index 09563ad41d53..30f89a9ad5ae 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift @@ -220,12 +220,16 @@ extension ControlCommandCoordinator { if hasNonNull(params, "surface_id"), requestedSurfaceID == nil { return .err(code: "invalid_params", message: "Missing or invalid surface_id", data: nil) } - let rawPath = rawString(params, "path") - ?? rawString(params, "directory") - ?? rawString(params, "cwd") - guard let path = rawPath?.trimmingCharacters(in: .whitespacesAndNewlines), !path.isEmpty else { + // Accept compatibility aliases, but require one exact cwd value. + let candidatePaths = ["path", "directory", "cwd"] + .compactMap { rawString(params, $0) } + .filter { $0.rangeOfCharacter(from: .whitespacesAndNewlines.inverted) != nil } + guard let path = candidatePaths.first else { return .err(code: "invalid_params", message: "Missing path", data: nil) } + guard candidatePaths.allSatisfy({ $0 == path }) else { + return .err(code: "invalid_params", message: "Conflicting path parameters", data: nil) + } let resolution = context?.controlSurfaceReportPWD( workspaceID: workspaceID, diff --git a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift new file mode 100644 index 000000000000..212158737ce0 --- /dev/null +++ b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift @@ -0,0 +1,55 @@ +import Foundation +import Testing +@testable import CmuxControlSocket + +@MainActor +private final class FakeSurfaceControlCommandContext: ControlCommandContext { + var reportPWDResolution: ControlSurfaceReportPWDResolution = .recorded(surfaceID: UUID()) + var reportedPWD: (workspaceID: UUID, requestedSurfaceID: UUID?, path: String)? + + func controlSurfaceReportPWD( + workspaceID: UUID, + requestedSurfaceID: UUID?, + path: String + ) -> ControlSurfaceReportPWDResolution { + reportedPWD = (workspaceID, requestedSurfaceID, path) + return reportPWDResolution + } +} + +@MainActor +@Suite("ControlCommandCoordinator surface domain") +struct ControlCommandCoordinatorSurfaceTests { + private func makeCoordinator() -> (ControlCommandCoordinator, FakeSurfaceControlCommandContext) { + let context = FakeSurfaceControlCommandContext() + return (ControlCommandCoordinator(context: context), context) + } + + private func request(_ params: [String: JSONValue]) -> ControlRequest { + ControlRequest(id: .int(1), method: "surface.report_pwd", params: params) + } + + @Test func reportPWDRejectsConflictingPathAliases() { + let (coordinator, context) = makeCoordinator() + let workspaceID = UUID() + let result = coordinator.handle(request([ + "workspace_id": .string(workspaceID.uuidString), + "path": .string("/srv/work/bar"), + "cwd": .string("/srv/work/other"), + ])) + + #expect(result == .err(code: "invalid_params", message: "Conflicting path parameters", data: nil)) + #expect(context.reportedPWD?.path == nil) + } + + @Test func reportPWDPreservesExactPathWhitespace() { + let (coordinator, context) = makeCoordinator() + let workspaceID = UUID() + _ = coordinator.handle(request([ + "workspace_id": .string(workspaceID.uuidString), + "path": .string("/srv/work/bar "), + ])) + + #expect(context.reportedPWD?.path == "/srv/work/bar ") + } +} diff --git a/Resources/shell-integration/cmux-bash-integration.bash b/Resources/shell-integration/cmux-bash-integration.bash index b97c10e0935d..938a488cf06d 100644 --- a/Resources/shell-integration/cmux-bash-integration.bash +++ b/Resources/shell-integration/cmux-bash-integration.bash @@ -1522,8 +1522,7 @@ _cmux_prompt_command() { local pwd="$PWD" if (( ! cmux_has_unix_socket )); then if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then - _CMUX_PWD_LAST_PWD="$pwd" - _cmux_report_pwd_via_relay "$pwd" + _cmux_report_pwd_via_relay "$pwd" && _CMUX_PWD_LAST_PWD="$pwd" fi if (( now - _CMUX_PORTS_LAST_RUN >= 10 )); then _cmux_ports_kick refresh diff --git a/Resources/shell-integration/cmux-zsh-integration.zsh b/Resources/shell-integration/cmux-zsh-integration.zsh index a731aea9c1b8..c1a6e0963569 100644 --- a/Resources/shell-integration/cmux-zsh-integration.zsh +++ b/Resources/shell-integration/cmux-zsh-integration.zsh @@ -1714,8 +1714,7 @@ _cmux_precmd() { if (( ! cmux_has_unix_socket )); then if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then - _CMUX_PWD_LAST_PWD="$pwd" - _cmux_report_pwd_via_relay "$pwd" + _cmux_report_pwd_via_relay "$pwd" && _CMUX_PWD_LAST_PWD="$pwd" fi if (( cmd_dur >= 2 || now - _CMUX_PORTS_LAST_RUN >= 10 )); then _cmux_ports_kick refresh diff --git a/Resources/shell-integration/fish/config.fish b/Resources/shell-integration/fish/config.fish index 32c79dab7c64..2e06f042b99e 100644 --- a/Resources/shell-integration/fish/config.fish +++ b/Resources/shell-integration/fish/config.fish @@ -297,14 +297,15 @@ if test "$_cmux_integration_enabled" != 0 _cmux_report_shell_activity_state prompt set -l pwd "$PWD" if test "$pwd" != "$_CMUX_PWD_LAST_PWD" - set -g _CMUX_PWD_LAST_PWD "$pwd" if _cmux_socket_is_unix if test -n "$CMUX_TAB_ID"; and test -n "$CMUX_PANEL_ID" set -l qpwd (_cmux_json_escape "$pwd") - _cmux_send_bg "report_pwd \"$qpwd\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID" + if _cmux_send_bg "report_pwd \"$qpwd\" --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID" + set -g _CMUX_PWD_LAST_PWD "$pwd" + end end - else - _cmux_report_pwd_via_relay "$pwd" + else if _cmux_report_pwd_via_relay "$pwd" + set -g _CMUX_PWD_LAST_PWD "$pwd" end end set -l now (_cmux_now) diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 5a164c092be5..a10d01af9e65 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -6282,17 +6282,16 @@ final class Workspace: Identifiable, ObservableObject { @MainActor func rememberPendingRemoteSurfacePWD(_ path: String, requestedSurfaceId: UUID?) { - let trimmedPath = path.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmedPath.isEmpty else { return } - pendingRemoteSurfacePWD = trimmedPath + guard path.rangeOfCharacter(from: .whitespacesAndNewlines.inverted) != nil else { return } + pendingRemoteSurfacePWD = path pendingRemoteSurfacePWDSurfaceId = requestedSurfaceId } @MainActor @discardableResult private func applyPendingRemoteSurfacePWDIfNeeded(to panelId: UUID) -> Bool { - guard let path = pendingRemoteSurfacePWD?.trimmingCharacters(in: .whitespacesAndNewlines), - !path.isEmpty else { + guard let path = pendingRemoteSurfacePWD, + path.rangeOfCharacter(from: .whitespacesAndNewlines.inverted) != nil else { return false } if let requestedSurfaceId = pendingRemoteSurfacePWDSurfaceId, From da3b3c71e1708d59c3fbab14f0cbeed25bae80a1 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 24 Jun 2026 14:54:22 -0700 Subject: [PATCH 5/7] Avoid remote cwd config hook scans --- Sources/Feed/FeedCoordinator.swift | 2 +- Sources/TerminalNotificationStore.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Feed/FeedCoordinator.swift b/Sources/Feed/FeedCoordinator.swift index 2d0ae80a7521..90d60c2fb993 100644 --- a/Sources/Feed/FeedCoordinator.swift +++ b/Sources/Feed/FeedCoordinator.swift @@ -1078,7 +1078,7 @@ private func makeFeedNotificationPolicyContext( ), effects: effects ), - hooks: context?.cmuxConfigStore?.notificationHooks(startingFrom: cwd) ?? [], + hooks: context?.cmuxConfigStore?.notificationHooks(startingFrom: workspace?.isRemoteWorkspace == true ? workspace?.surfaceTabBarDirectory : (normalizedFeedNotificationCWD(event.cwd) ?? workspace?.surfaceTabBarDirectory)) ?? [], globalConfigPath: context?.cmuxConfigStore?.globalConfigPath ) } diff --git a/Sources/TerminalNotificationStore.swift b/Sources/TerminalNotificationStore.swift index bba05a9cf572..f61ca4b55bf6 100644 --- a/Sources/TerminalNotificationStore.swift +++ b/Sources/TerminalNotificationStore.swift @@ -1054,7 +1054,7 @@ final class TerminalNotificationStore: ObservableObject { isAppFocused: isAppFocused, isFocusedPanel: isFocusedPanel ), - hooks: cmuxConfigStore?.notificationHooks(startingFrom: cwd) ?? [], + hooks: cmuxConfigStore?.notificationHooks(startingFrom: workspace?.isRemoteWorkspace == true ? workspace?.surfaceTabBarDirectory : (workspace?.surfaceTabBarDirectory ?? cwd)) ?? [], globalConfigPath: cmuxConfigStore?.globalConfigPath ) } From 8af450dcc5edf51c582a04911097052fff64a9e4 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 24 Jun 2026 15:18:56 -0700 Subject: [PATCH 6/7] Fix remote cwd test compile --- .../ControlCommandCoordinatorSurfaceTests.swift | 15 --------------- .../FakeSurfaceControlCommandContext.swift | 17 +++++++++++++++++ cmuxTests/RemoteShellCWDRelayTests.swift | 8 ++++---- 3 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/FakeSurfaceControlCommandContext.swift diff --git a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift index 212158737ce0..a9bfd9f5544a 100644 --- a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift +++ b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift @@ -2,21 +2,6 @@ import Foundation import Testing @testable import CmuxControlSocket -@MainActor -private final class FakeSurfaceControlCommandContext: ControlCommandContext { - var reportPWDResolution: ControlSurfaceReportPWDResolution = .recorded(surfaceID: UUID()) - var reportedPWD: (workspaceID: UUID, requestedSurfaceID: UUID?, path: String)? - - func controlSurfaceReportPWD( - workspaceID: UUID, - requestedSurfaceID: UUID?, - path: String - ) -> ControlSurfaceReportPWDResolution { - reportedPWD = (workspaceID, requestedSurfaceID, path) - return reportPWDResolution - } -} - @MainActor @Suite("ControlCommandCoordinator surface domain") struct ControlCommandCoordinatorSurfaceTests { diff --git a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/FakeSurfaceControlCommandContext.swift b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/FakeSurfaceControlCommandContext.swift new file mode 100644 index 000000000000..545726bfa574 --- /dev/null +++ b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/FakeSurfaceControlCommandContext.swift @@ -0,0 +1,17 @@ +import Foundation +@testable import CmuxControlSocket + +@MainActor +final class FakeSurfaceControlCommandContext: ControlCommandContext { + var reportPWDResolution: ControlSurfaceReportPWDResolution = .recorded(surfaceID: UUID()) + var reportedPWD: (workspaceID: UUID, requestedSurfaceID: UUID?, path: String)? + + func controlSurfaceReportPWD( + workspaceID: UUID, + requestedSurfaceID: UUID?, + path: String + ) -> ControlSurfaceReportPWDResolution { + reportedPWD = (workspaceID, requestedSurfaceID, path) + return reportPWDResolution + } +} diff --git a/cmuxTests/RemoteShellCWDRelayTests.swift b/cmuxTests/RemoteShellCWDRelayTests.swift index dc30c5ebf0a5..ee55491ddd4e 100644 --- a/cmuxTests/RemoteShellCWDRelayTests.swift +++ b/cmuxTests/RemoteShellCWDRelayTests.swift @@ -49,7 +49,7 @@ struct RemoteShellCWDRelayTests { ) let expected = #"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","path":"\#(remoteDirectory.path)","surface_id":"22222222-2222-2222-2222-222222222222"}"# - #expect(output.contains(expected), output) + #expect(output.contains(expected), Comment(rawValue: output)) } @Test @@ -98,7 +98,7 @@ struct RemoteShellCWDRelayTests { ) let expected = #"rpc surface.report_pwd {"workspace_id":"11111111-1111-1111-1111-111111111111","path":"\#(remoteDirectory.path)","surface_id":"22222222-2222-2222-2222-222222222222"}"# - #expect(result.stdout.contains(expected), result.stdout) + #expect(result.stdout.contains(expected), Comment(rawValue: result.stdout)) } private func runInteractiveZsh( @@ -149,7 +149,7 @@ struct RemoteShellCWDRelayTests { } let output = try runProcess(process) - #expect(output.status == 0, output.stderr) + #expect(output.status == 0, Comment(rawValue: output.stderr)) return output.stdout.trimmingCharacters(in: .whitespacesAndNewlines) } @@ -184,7 +184,7 @@ struct RemoteShellCWDRelayTests { } let output = try runProcess(process) - #expect(output.status == 0, output.stderr) + #expect(output.status == 0, Comment(rawValue: output.stderr)) return ( stdout: output.stdout.trimmingCharacters(in: .whitespacesAndNewlines), stderr: output.stderr.trimmingCharacters(in: .whitespacesAndNewlines) From af708de64f5b6ed01092a2c11dd4b08a071df403 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Wed, 24 Jun 2026 15:23:58 -0700 Subject: [PATCH 7/7] Make remote notification hook root explicit --- Sources/Feed/FeedCoordinator.swift | 2 +- Sources/TerminalNotificationStore.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Feed/FeedCoordinator.swift b/Sources/Feed/FeedCoordinator.swift index 90d60c2fb993..ecdc1988f6d2 100644 --- a/Sources/Feed/FeedCoordinator.swift +++ b/Sources/Feed/FeedCoordinator.swift @@ -1078,7 +1078,7 @@ private func makeFeedNotificationPolicyContext( ), effects: effects ), - hooks: context?.cmuxConfigStore?.notificationHooks(startingFrom: workspace?.isRemoteWorkspace == true ? workspace?.surfaceTabBarDirectory : (normalizedFeedNotificationCWD(event.cwd) ?? workspace?.surfaceTabBarDirectory)) ?? [], + hooks: context?.cmuxConfigStore?.notificationHooks(startingFrom: workspace?.isRemoteWorkspace == true ? nil : (normalizedFeedNotificationCWD(event.cwd) ?? workspace?.surfaceTabBarDirectory)) ?? [], globalConfigPath: context?.cmuxConfigStore?.globalConfigPath ) } diff --git a/Sources/TerminalNotificationStore.swift b/Sources/TerminalNotificationStore.swift index f61ca4b55bf6..756f0825c2d2 100644 --- a/Sources/TerminalNotificationStore.swift +++ b/Sources/TerminalNotificationStore.swift @@ -1054,7 +1054,7 @@ final class TerminalNotificationStore: ObservableObject { isAppFocused: isAppFocused, isFocusedPanel: isFocusedPanel ), - hooks: cmuxConfigStore?.notificationHooks(startingFrom: workspace?.isRemoteWorkspace == true ? workspace?.surfaceTabBarDirectory : (workspace?.surfaceTabBarDirectory ?? cwd)) ?? [], + hooks: cmuxConfigStore?.notificationHooks(startingFrom: workspace?.isRemoteWorkspace == true ? nil : cwd) ?? [], globalConfigPath: cmuxConfigStore?.globalConfigPath ) }