fix(macos): show gateway in devices list
This commit is contained in:
@@ -69,7 +69,7 @@
|
||||
- CLI onboarding: always prompt for WhatsApp `whatsapp.allowFrom` and print (optionally open) the Control UI URL when done.
|
||||
- CLI onboarding: detect gateway reachability and annotate Local/Remote choices (helps pick the right mode).
|
||||
- macOS settings: colorize provider status subtitles to distinguish healthy vs degraded states.
|
||||
- macOS menu: show multi-line gateway error details, avoid duplicate gateway status rows, suppress transient `cancelled` device refresh errors, and auto-recover the control channel on disconnect.
|
||||
- macOS menu: show multi-line gateway error details, add an always-visible gateway row, avoid duplicate gateway status rows, suppress transient `cancelled` device refresh errors, and auto-recover the control channel on disconnect.
|
||||
- macOS: log health refresh failures and recovery to make gateway issues easier to diagnose.
|
||||
- macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b
|
||||
- macOS packaging: move rpath config into swift build for reliability (#69) — thanks @petter-b
|
||||
|
||||
@@ -189,6 +189,12 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
||||
menu.insertItem(topSeparator, at: cursor)
|
||||
cursor += 1
|
||||
|
||||
if let gatewayEntry = self.gatewayEntry() {
|
||||
let gatewayItem = self.makeNodeItem(entry: gatewayEntry, width: width)
|
||||
menu.insertItem(gatewayItem, at: cursor)
|
||||
cursor += 1
|
||||
}
|
||||
|
||||
guard self.isControlChannelConnected else { return }
|
||||
|
||||
if let error = self.nodesStore.lastError?.nonEmpty {
|
||||
@@ -214,15 +220,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
||||
cursor += 1
|
||||
} else {
|
||||
for entry in entries.prefix(8) {
|
||||
let item = NSMenuItem()
|
||||
item.tag = self.nodesTag
|
||||
item.target = self
|
||||
item.action = #selector(self.copyNodeSummary(_:))
|
||||
item.representedObject = NodeMenuEntryFormatter.summaryText(entry)
|
||||
item.view = HighlightedMenuItemHostView(
|
||||
rootView: AnyView(NodeMenuRowView(entry: entry, width: width)),
|
||||
width: width)
|
||||
item.submenu = self.buildNodeSubmenu(entry: entry, width: width)
|
||||
let item = self.makeNodeItem(entry: entry, width: width)
|
||||
menu.insertItem(item, at: cursor)
|
||||
cursor += 1
|
||||
}
|
||||
@@ -250,6 +248,58 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
||||
return false
|
||||
}
|
||||
|
||||
private func gatewayEntry() -> NodeInfo? {
|
||||
let mode = AppStateStore.shared.connectionMode
|
||||
let isConnected = self.isControlChannelConnected
|
||||
let port = GatewayEnvironment.gatewayPort()
|
||||
var host: String?
|
||||
var platform: String?
|
||||
|
||||
switch mode {
|
||||
case .remote:
|
||||
platform = "remote"
|
||||
let target = AppStateStore.shared.remoteTarget
|
||||
if let parsed = CommandResolver.parseSSHTarget(target) {
|
||||
host = parsed.port == 22 ? parsed.host : "\(parsed.host):\(parsed.port)"
|
||||
} else {
|
||||
host = target.nonEmpty
|
||||
}
|
||||
case .local:
|
||||
platform = "local"
|
||||
host = "127.0.0.1:\(port)"
|
||||
case .unconfigured:
|
||||
platform = nil
|
||||
host = nil
|
||||
}
|
||||
|
||||
return NodeInfo(
|
||||
nodeId: "gateway",
|
||||
displayName: "Gateway",
|
||||
platform: platform,
|
||||
version: nil,
|
||||
deviceFamily: nil,
|
||||
modelIdentifier: nil,
|
||||
remoteIp: host,
|
||||
caps: nil,
|
||||
commands: nil,
|
||||
permissions: nil,
|
||||
paired: nil,
|
||||
connected: isConnected)
|
||||
}
|
||||
|
||||
private func makeNodeItem(entry: NodeInfo, width: CGFloat) -> NSMenuItem {
|
||||
let item = NSMenuItem()
|
||||
item.tag = self.nodesTag
|
||||
item.target = self
|
||||
item.action = #selector(self.copyNodeSummary(_:))
|
||||
item.representedObject = NodeMenuEntryFormatter.summaryText(entry)
|
||||
item.view = HighlightedMenuItemHostView(
|
||||
rootView: AnyView(NodeMenuRowView(entry: entry, width: width)),
|
||||
width: width)
|
||||
item.submenu = self.buildNodeSubmenu(entry: entry, width: width)
|
||||
return item
|
||||
}
|
||||
|
||||
private func makeMessageItem(text: String, symbolName: String, width: CGFloat) -> NSMenuItem {
|
||||
let view = AnyView(
|
||||
Label(text, systemImage: symbolName)
|
||||
|
||||
@@ -2,15 +2,30 @@ import AppKit
|
||||
import SwiftUI
|
||||
|
||||
struct NodeMenuEntryFormatter {
|
||||
static func isGateway(_ entry: NodeInfo) -> Bool {
|
||||
entry.nodeId == "gateway"
|
||||
}
|
||||
|
||||
static func isConnected(_ entry: NodeInfo) -> Bool {
|
||||
entry.isConnected
|
||||
}
|
||||
|
||||
static func primaryName(_ entry: NodeInfo) -> String {
|
||||
if self.isGateway(entry) {
|
||||
return entry.displayName?.nonEmpty ?? "Gateway"
|
||||
}
|
||||
entry.displayName?.nonEmpty ?? entry.nodeId
|
||||
}
|
||||
|
||||
static func summaryText(_ entry: NodeInfo) -> String {
|
||||
if self.isGateway(entry) {
|
||||
let role = self.roleText(entry)
|
||||
let name = self.primaryName(entry)
|
||||
var parts = ["\(name) · \(role)"]
|
||||
if let ip = entry.remoteIp?.nonEmpty { parts.append("host \(ip)") }
|
||||
if let platform = self.platformText(entry) { parts.append(platform) }
|
||||
return parts.joined(separator: " · ")
|
||||
}
|
||||
let name = self.primaryName(entry)
|
||||
var prefix = "Node: \(name)"
|
||||
if let ip = entry.remoteIp?.nonEmpty {
|
||||
@@ -112,6 +127,11 @@ struct NodeMenuEntryFormatter {
|
||||
}
|
||||
|
||||
static func leadingSymbol(_ entry: NodeInfo) -> String {
|
||||
if self.isGateway(entry) {
|
||||
return self.safeSystemSymbol(
|
||||
"antenna.radiowaves.left.and.right",
|
||||
fallback: "dot.radiowaves.left.and.right")
|
||||
}
|
||||
if let family = entry.deviceFamily?.lowercased() {
|
||||
if family.contains("mac") {
|
||||
return self.safeSystemSymbol("laptopcomputer", fallback: "laptopcomputer")
|
||||
|
||||
Reference in New Issue
Block a user