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: 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).
|
- 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 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: 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 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
|
- 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)
|
menu.insertItem(topSeparator, at: cursor)
|
||||||
cursor += 1
|
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 }
|
guard self.isControlChannelConnected else { return }
|
||||||
|
|
||||||
if let error = self.nodesStore.lastError?.nonEmpty {
|
if let error = self.nodesStore.lastError?.nonEmpty {
|
||||||
@@ -214,15 +220,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
|||||||
cursor += 1
|
cursor += 1
|
||||||
} else {
|
} else {
|
||||||
for entry in entries.prefix(8) {
|
for entry in entries.prefix(8) {
|
||||||
let item = NSMenuItem()
|
let item = self.makeNodeItem(entry: entry, width: width)
|
||||||
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)
|
|
||||||
menu.insertItem(item, at: cursor)
|
menu.insertItem(item, at: cursor)
|
||||||
cursor += 1
|
cursor += 1
|
||||||
}
|
}
|
||||||
@@ -250,6 +248,58 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
|||||||
return false
|
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 {
|
private func makeMessageItem(text: String, symbolName: String, width: CGFloat) -> NSMenuItem {
|
||||||
let view = AnyView(
|
let view = AnyView(
|
||||||
Label(text, systemImage: symbolName)
|
Label(text, systemImage: symbolName)
|
||||||
|
|||||||
@@ -2,15 +2,30 @@ import AppKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct NodeMenuEntryFormatter {
|
struct NodeMenuEntryFormatter {
|
||||||
|
static func isGateway(_ entry: NodeInfo) -> Bool {
|
||||||
|
entry.nodeId == "gateway"
|
||||||
|
}
|
||||||
|
|
||||||
static func isConnected(_ entry: NodeInfo) -> Bool {
|
static func isConnected(_ entry: NodeInfo) -> Bool {
|
||||||
entry.isConnected
|
entry.isConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
static func primaryName(_ entry: NodeInfo) -> String {
|
static func primaryName(_ entry: NodeInfo) -> String {
|
||||||
|
if self.isGateway(entry) {
|
||||||
|
return entry.displayName?.nonEmpty ?? "Gateway"
|
||||||
|
}
|
||||||
entry.displayName?.nonEmpty ?? entry.nodeId
|
entry.displayName?.nonEmpty ?? entry.nodeId
|
||||||
}
|
}
|
||||||
|
|
||||||
static func summaryText(_ entry: NodeInfo) -> String {
|
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)
|
let name = self.primaryName(entry)
|
||||||
var prefix = "Node: \(name)"
|
var prefix = "Node: \(name)"
|
||||||
if let ip = entry.remoteIp?.nonEmpty {
|
if let ip = entry.remoteIp?.nonEmpty {
|
||||||
@@ -112,6 +127,11 @@ struct NodeMenuEntryFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func leadingSymbol(_ entry: NodeInfo) -> String {
|
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 let family = entry.deviceFamily?.lowercased() {
|
||||||
if family.contains("mac") {
|
if family.contains("mac") {
|
||||||
return self.safeSystemSymbol("laptopcomputer", fallback: "laptopcomputer")
|
return self.safeSystemSymbol("laptopcomputer", fallback: "laptopcomputer")
|
||||||
|
|||||||
Reference in New Issue
Block a user