From 0e40779bc9d682a3683fb282f73674f14aa86245 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:41:15 -0800 Subject: [PATCH] Show SSH copy-error menu for status-only error workspaces --- Sources/ContentView.swift | 78 +++++++++++++++---- cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 21 +++++ 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index bf36864a..e64d56d8 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -2240,6 +2240,37 @@ enum SidebarRemoteErrorCopySupport { "\(index + 1). \(entry.workspaceTitle) (\(entry.target)): \(entry.detail)" }.joined(separator: "\n") } + + static func parsedTargetAndDetail(from statusValue: String, fallbackTarget: String? = nil) -> (target: String, detail: String)? { + let trimmed = statusValue.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty, trimmed.hasPrefix("SSH error") else { return nil } + + let normalizedFallbackTarget: String? = { + guard let fallbackTarget else { return nil } + let trimmedFallback = fallbackTarget.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedFallback.isEmpty ? nil : trimmedFallback + }() + + if let separator = trimmed.range(of: ": ") { + let prefix = String(trimmed[.. 1 ? "Reconnect Workspaces" : "Reconnect Workspace" let disconnectLabel = remoteTargetWorkspaces.count > 1 ? "Disconnect Workspaces" : "Disconnect Workspace" let shouldPin = !tab.isPinned @@ -2572,22 +2603,24 @@ private struct TabItemView: View { } } - if !remoteTargetWorkspaces.isEmpty { + if !remoteTargetWorkspaces.isEmpty || !remoteWorkspaceErrors.isEmpty { Divider() - Button(reconnectLabel) { - for workspace in remoteTargetWorkspaces { - workspace.reconnectRemoteConnection() + if !remoteTargetWorkspaces.isEmpty { + Button(reconnectLabel) { + for workspace in remoteTargetWorkspaces { + workspace.reconnectRemoteConnection() + } } - } - .disabled(remoteTargetWorkspaces.allSatisfy { $0.remoteConnectionState == .connecting }) + .disabled(remoteTargetWorkspaces.allSatisfy { $0.remoteConnectionState == .connecting }) - Button(disconnectLabel) { - for workspace in remoteTargetWorkspaces { - workspace.disconnectRemoteConnection(clearConfiguration: false) + Button(disconnectLabel) { + for workspace in remoteTargetWorkspaces { + workspace.disconnectRemoteConnection(clearConfiguration: false) + } } + .disabled(remoteTargetWorkspaces.allSatisfy { $0.remoteConnectionState == .disconnected }) } - .disabled(remoteTargetWorkspaces.allSatisfy { $0.remoteConnectionState == .disconnected }) if let copyErrorLabel = SidebarRemoteErrorCopySupport.menuLabel(for: remoteWorkspaceErrors), let copyErrorText = SidebarRemoteErrorCopySupport.clipboardText(for: remoteWorkspaceErrors) { @@ -2789,15 +2822,28 @@ private struct TabItemView: View { private func remoteErrorCopyEntries(in workspaces: [Tab]) -> [SidebarRemoteErrorCopyEntry] { workspaces.compactMap { workspace in - guard workspace.remoteConnectionState == .error else { return nil } - guard let detail = workspace.remoteConnectionDetail?.trimmingCharacters(in: .whitespacesAndNewlines), - !detail.isEmpty else { + if workspace.remoteConnectionState == .error, + let detail = workspace.remoteConnectionDetail?.trimmingCharacters(in: .whitespacesAndNewlines), + !detail.isEmpty { + return SidebarRemoteErrorCopyEntry( + workspaceTitle: workspace.title, + target: workspace.remoteDisplayTarget ?? "remote host", + detail: detail + ) + } + + guard let statusValue = workspace.statusEntries["remote.error"]?.value, + let parsed = SidebarRemoteErrorCopySupport.parsedTargetAndDetail( + from: statusValue, + fallbackTarget: workspace.remoteDisplayTarget + ) else { return nil } + return SidebarRemoteErrorCopyEntry( workspaceTitle: workspace.title, - target: workspace.remoteDisplayTarget ?? "remote host", - detail: detail + target: parsed.target, + detail: parsed.detail ) } } diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 5a674736..0e72f575 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -2725,6 +2725,27 @@ final class SidebarRemoteErrorCopySupportTests: XCTestCase { """ ) } + + func testParsedTargetAndDetailParsesCanonicalStatusValue() { + let parsed = SidebarRemoteErrorCopySupport.parsedTargetAndDetail( + from: "SSH error (devbox:22): failed to bootstrap daemon" + ) + XCTAssertEqual(parsed?.target, "devbox:22") + XCTAssertEqual(parsed?.detail, "failed to bootstrap daemon") + } + + func testParsedTargetAndDetailUsesFallbackTargetWhenStatusOmitsTarget() { + let parsed = SidebarRemoteErrorCopySupport.parsedTargetAndDetail( + from: "SSH error: connection refused", + fallbackTarget: "fallback-host" + ) + XCTAssertEqual(parsed?.target, "fallback-host") + XCTAssertEqual(parsed?.detail, "connection refused") + } + + func testParsedTargetAndDetailIgnoresNonSSHStatusValues() { + XCTAssertNil(SidebarRemoteErrorCopySupport.parsedTargetAndDetail(from: "All good")) + } } final class WorkspaceReorderTests: XCTestCase {