From 469c8a1a4b1612e79da4439679632f9de5819dbf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 22 Dec 2025 21:13:33 +0100 Subject: [PATCH] fix(mac): show disconnected sessions + sleeping eyes --- .../Sources/Clawdis/CritterStatusLabel.swift | 59 ++++++++++++++----- .../Sources/Clawdis/MenuContentView.swift | 19 ++++-- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/apps/macos/Sources/Clawdis/CritterStatusLabel.swift b/apps/macos/Sources/Clawdis/CritterStatusLabel.swift index 2ccddb797..6ba049559 100644 --- a/apps/macos/Sources/Clawdis/CritterStatusLabel.swift +++ b/apps/macos/Sources/Clawdis/CritterStatusLabel.swift @@ -137,7 +137,7 @@ struct CritterStatusLabel: View { } if self.isSleeping { - return Image(nsImage: CritterIconRenderer.makeIcon(blink: 1, badge: nil)) + return Image(nsImage: CritterIconRenderer.makeIcon(blink: 1, eyesClosedLines: true, badge: nil)) } return Image(nsImage: CritterIconRenderer.makeIcon( @@ -268,6 +268,7 @@ enum CritterIconRenderer { earWiggle: CGFloat = 0, earScale: CGFloat = 1, earHoles: Bool = false, + eyesClosedLines: Bool = false, badge: Badge? = nil) -> NSImage { // Force a 36×36px backing store (2× for the 18pt logical canvas) so the menu bar icon stays crisp on Retina. @@ -333,9 +334,7 @@ enum CritterIconRenderer { let legLift = snapY(legH * 0.35 * legWiggle) let legYBase = snapY(bodyY - legH + h * 0.05) - let eyeOpen = max(0.05, 1 - blink) let eyeW = snapX(bodyW * 0.2) - let eyeH = snapY(bodyH * 0.26 * eyeOpen) let eyeY = snapY(bodyY + bodyH * 0.56) let eyeOffset = snapX(bodyW * 0.24) @@ -405,20 +404,50 @@ enum CritterIconRenderer { transform: nil)) } - let left = CGMutablePath() - left.move(to: CGPoint(x: snapX(leftCenter.x - eyeW / 2), y: snapY(leftCenter.y - eyeH))) - left.addLine(to: CGPoint(x: snapX(leftCenter.x + eyeW / 2), y: snapY(leftCenter.y))) - left.addLine(to: CGPoint(x: snapX(leftCenter.x - eyeW / 2), y: snapY(leftCenter.y + eyeH))) - left.closeSubpath() + if eyesClosedLines { + let lineW = snapX(eyeW * 0.95) + let lineH = snapY(max(stepY * 2, bodyH * 0.06)) + let corner = snapX(lineH * 0.6) + let leftRect = CGRect( + x: snapX(leftCenter.x - lineW / 2), + y: snapY(leftCenter.y - lineH / 2), + width: lineW, + height: lineH) + let rightRect = CGRect( + x: snapX(rightCenter.x - lineW / 2), + y: snapY(rightCenter.y - lineH / 2), + width: lineW, + height: lineH) + context.cgContext.addPath(CGPath( + roundedRect: leftRect, + cornerWidth: corner, + cornerHeight: corner, + transform: nil)) + context.cgContext.addPath(CGPath( + roundedRect: rightRect, + cornerWidth: corner, + cornerHeight: corner, + transform: nil)) + } else { + let eyeOpen = max(0.05, 1 - blink) + let eyeH = snapY(bodyH * 0.26 * eyeOpen) - let right = CGMutablePath() - right.move(to: CGPoint(x: snapX(rightCenter.x + eyeW / 2), y: snapY(rightCenter.y - eyeH))) - right.addLine(to: CGPoint(x: snapX(rightCenter.x - eyeW / 2), y: snapY(rightCenter.y))) - right.addLine(to: CGPoint(x: snapX(rightCenter.x + eyeW / 2), y: snapY(rightCenter.y + eyeH))) - right.closeSubpath() + let left = CGMutablePath() + left.move(to: CGPoint(x: snapX(leftCenter.x - eyeW / 2), y: snapY(leftCenter.y - eyeH))) + left.addLine(to: CGPoint(x: snapX(leftCenter.x + eyeW / 2), y: snapY(leftCenter.y))) + left.addLine(to: CGPoint(x: snapX(leftCenter.x - eyeW / 2), y: snapY(leftCenter.y + eyeH))) + left.closeSubpath() + + let right = CGMutablePath() + right.move(to: CGPoint(x: snapX(rightCenter.x + eyeW / 2), y: snapY(rightCenter.y - eyeH))) + right.addLine(to: CGPoint(x: snapX(rightCenter.x - eyeW / 2), y: snapY(rightCenter.y))) + right.addLine(to: CGPoint(x: snapX(rightCenter.x + eyeW / 2), y: snapY(rightCenter.y + eyeH))) + right.closeSubpath() + + context.cgContext.addPath(left) + context.cgContext.addPath(right) + } - context.cgContext.addPath(left) - context.cgContext.addPath(right) context.cgContext.fillPath() context.cgContext.restoreGState() diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index e38db2ad8..f070dfd77 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -411,6 +411,16 @@ struct MenuContent: View { self.sessionLoading = true self.sessionErrorText = nil + if case .connected = self.controlChannel.state { + // ok + } else { + self.sessionStorePath = nil + self.sessionMenu = [] + self.sessionErrorText = "No connection to gateway" + self.sessionLoading = false + return + } + do { let snapshot = try await SessionLoader.loadSnapshot(limit: 32) self.sessionStorePath = snapshot.storePath @@ -426,7 +436,8 @@ struct MenuContent: View { 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.sessionStorePath = nil + self.sessionMenu = [] self.sessionErrorText = self.compactSessionError(error) } @@ -437,12 +448,12 @@ struct MenuContent: View { if let loadError = error as? SessionLoadError { switch loadError { case .gatewayUnavailable: - return "Sessions unavailable — gateway unreachable" + return "No connection to gateway" case .decodeFailed: - return "Sessions unavailable — invalid payload" + return "Sessions unavailable" } } - return "Sessions unavailable" + return "No connection to gateway" } private func open(tab: SettingsTab) {