From 819ceb8ebb54311104bca8bc64bd6d6ec0b73d89 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Thu, 26 Mar 2026 15:49:38 -0700 Subject: [PATCH] feat(sidebar): make listening ports clickable to open in browser (#1844) * feat(sidebar): make listening ports clickable to open in browser Wrap each sidebar port in a Button that opens http://localhost:{port} in the cmux built-in browser (or system browser as fallback), matching the existing PR link click behavior. Fixes #1602 Co-Authored-By: Claude Opus 4.6 * fix: address bot review feedback on port clickability - Localize port label text with String(localized:) instead of bare literal - Add sidebar.port.label and sidebar.port.openTooltip keys to Localizable.xcstrings with English and Japanese translations - Respect openSidebarPullRequestLinksInCmuxBrowser user preference in openPortLink, matching the openPullRequestLink pattern exactly Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- Resources/Localizable.xcstrings | 34 +++++++++++++++++++++++++++++ Sources/ContentView.swift | 38 ++++++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 14de005d..9dbe7308 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -64785,6 +64785,40 @@ } } }, + "sidebar.port.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": ":%lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": ":%lld" + } + } + } + }, + "sidebar.port.openTooltip": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open localhost:%lld" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "localhost:%lldを開く" + } + } + } + }, "sidebar.pullRequest.openTooltip": { "extractionState": "manual", "localizations": { diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 9065d597..046ce785 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -11486,11 +11486,22 @@ private struct TabItemView: View, Equatable { // Ports row if detailVisibility.showsPorts, !tab.listeningPorts.isEmpty { - Text(tab.listeningPorts.map { ":\($0)" }.joined(separator: ", ")) - .font(.system(size: 10, design: .monospaced)) - .foregroundColor(activeSecondaryColor(0.75)) - .lineLimit(1) - .truncationMode(.tail) + HStack(spacing: 4) { + ForEach(tab.listeningPorts, id: \.self) { port in + Button(action: { + openPortLink(port) + }) { + Text(String(localized: "sidebar.port.label", defaultValue: ":\(port)")) + .underline() + } + .buttonStyle(.plain) + .safeHelp(String(localized: "sidebar.port.openTooltip", defaultValue: "Open localhost:\(port)")) + } + Spacer(minLength: 0) + } + .font(.system(size: 10, design: .monospaced)) + .foregroundColor(activeSecondaryColor(0.75)) + .lineLimit(1) } } .animation(.easeInOut(duration: 0.2), value: tab.logEntries.count) @@ -12206,6 +12217,23 @@ private struct TabItemView: View, Equatable { NSWorkspace.shared.open(url) } + private func openPortLink(_ port: Int) { + guard let url = URL(string: "http://localhost:\(port)") else { return } + updateSelection() + if openSidebarPullRequestLinksInCmuxBrowser { + if tabManager.openBrowser( + inWorkspace: tab.id, + url: url, + preferSplitRight: true, + insertAtEnd: true + ) == nil { + NSWorkspace.shared.open(url) + } + return + } + NSWorkspace.shared.open(url) + } + private func pullRequestStatusLabel( _ status: SidebarPullRequestStatus, checks _: SidebarPullRequestChecksStatus?