fix(mac): sessions error UI + sleeping icon

This commit is contained in:
Peter Steinberger
2025-12-22 21:02:26 +01:00
parent a11a204b8e
commit 9d47b15575
5 changed files with 178 additions and 44 deletions

View File

@@ -18,6 +18,8 @@ struct MenuContent: View {
@State private var loadingMics = false
@State private var sessionMenu: [SessionRow] = []
@State private var sessionStorePath: String?
@State private var sessionLoading = true
@State private var sessionErrorText: String?
@State private var browserControlEnabled = true
private let sessionMenuItemWidth: CGFloat = 320
private let sessionMenuActiveWindowSeconds: TimeInterval = 24 * 60 * 60
@@ -31,7 +33,9 @@ struct MenuContent: View {
}
}
.disabled(self.state.connectionMode == .unconfigured)
self.sessionsSection
Divider()
Toggle(isOn: self.heartbeatsBinding) {
VStack(alignment: .leading, spacing: 2) {
@@ -196,12 +200,39 @@ struct MenuContent: View {
private var sessionsSection: some View {
Group {
Divider()
MenuHostedItem(
width: self.sessionMenuItemWidth,
rootView: AnyView(MenuSessionsHeaderView(
count: self.sessionMenu.count,
statusText: self.sessionLoading
? "Loading sessions…"
: (self.sessionMenu.isEmpty ? nil : self.sessionErrorText))))
.disabled(true)
if self.sessionMenu.isEmpty {
Text("No active sessions")
.font(.caption)
.foregroundStyle(.secondary)
if self.sessionMenu.isEmpty, !self.sessionLoading, let error = self.sessionErrorText, !error.isEmpty {
MenuHostedItem(
width: self.sessionMenuItemWidth,
rootView: AnyView(
Label(error, systemImage: "exclamationmark.triangle")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.padding(.leading, 20)
.padding(.trailing, 10)
.padding(.vertical, 6)
.frame(minWidth: 300, alignment: .leading)))
.disabled(true)
} else if self.sessionMenu.isEmpty, !self.sessionLoading, self.sessionErrorText == nil {
MenuHostedItem(
width: self.sessionMenuItemWidth,
rootView: AnyView(Text("No active sessions")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, 20)
.padding(.trailing, 10)
.padding(.vertical, 6)
.frame(minWidth: 300, alignment: .leading)))
.disabled(true)
} else {
ForEach(self.sessionMenu) { row in
@@ -375,6 +406,45 @@ struct MenuContent: View {
}
}
@MainActor
private func reloadSessionMenu() async {
self.sessionLoading = true
self.sessionErrorText = nil
do {
let snapshot = try await SessionLoader.loadSnapshot(limit: 32)
self.sessionStorePath = snapshot.storePath
let now = Date()
let active = snapshot.rows.filter { row in
if row.key == "main" { return true }
guard let updatedAt = row.updatedAt else { return false }
return now.timeIntervalSince(updatedAt) <= self.sessionMenuActiveWindowSeconds
}
self.sessionMenu = active.sorted { lhs, rhs in
if lhs.key == "main" { return true }
if rhs.key == "main" { return false }
return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
}
} catch {
// Keep the previous snapshot (if any) so the menu doesn't go empty while the gateway is flaky.
self.sessionErrorText = self.compactSessionError(error)
}
self.sessionLoading = false
}
private func compactSessionError(_ error: Error) -> String {
if let loadError = error as? SessionLoadError {
switch loadError {
case .gatewayUnavailable:
return "Sessions unavailable — gateway unreachable"
case .decodeFailed:
return "Sessions unavailable — invalid payload"
}
}
return "Sessions unavailable"
}
private func open(tab: SettingsTab) {
SettingsTabRouter.request(tab)
NSApp.activate(ignoringOtherApps: true)
@@ -561,28 +631,6 @@ struct MenuContent: View {
return "System default"
}
@MainActor
private func reloadSessionMenu() async {
do {
let snapshot = try await SessionLoader.loadSnapshot(limit: 32)
self.sessionStorePath = snapshot.storePath
let now = Date()
let active = snapshot.rows.filter { row in
if row.key == "main" { return true }
guard let updatedAt = row.updatedAt else { return false }
return now.timeIntervalSince(updatedAt) <= self.sessionMenuActiveWindowSeconds
}
self.sessionMenu = active.sorted { lhs, rhs in
if lhs.key == "main" { return true }
if rhs.key == "main" { return false }
return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
}
} catch {
self.sessionStorePath = nil
self.sessionMenu = []
}
}
@MainActor
private func loadMicrophones(force: Bool = false) async {
guard self.showVoiceWakeMicPicker else {