diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f48186b..6bc7610cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - System prompt now tags allowlisted owner numbers as the user identity to avoid mistaken “friend” assumptions. - LM Studio/Ollama replies now require tags; streaming ignores content until begins. - `process log` pagination is now line-based (omit `offset` to grab the last N lines). +- macOS WebChat: assistant bubbles now update correctly when toggling light/dark mode. - macOS: avoid spawning a duplicate gateway process when an external listener already exists. - Node bridge: when binding to a non-loopback host (e.g. Tailnet IP), also listens on `127.0.0.1` for local connections (without creating duplicate loopback listeners for `0.0.0.0`/`127.0.0.1` binds). - UI perf: pause repeat animations when scenes are inactive (typing dots, onboarding glow, iOS status pulse), throttle voice overlay level updates, and reduce overlay focus churn. diff --git a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatTheme.swift b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatTheme.swift index c676efd58..c6e00045b 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatTheme.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatTheme.swift @@ -6,7 +6,39 @@ import AppKit import UIKit #endif +#if os(macOS) +private extension NSAppearance { + var isDarkAqua: Bool { + self.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + } +} +#endif + enum ClawdisChatTheme { + #if os(macOS) + static func resolvedAssistantBubbleColor(for appearance: NSAppearance) -> NSColor { + // NSColor semantic colors don't reliably resolve for arbitrary NSAppearance in SwiftPM. + // Use explicit light/dark values so the bubble updates when the system appearance flips. + appearance.isDarkAqua + ? NSColor(calibratedWhite: 0.18, alpha: 0.88) + : NSColor(calibratedWhite: 0.94, alpha: 0.92) + } + + static func resolvedOnboardingAssistantBubbleColor(for appearance: NSAppearance) -> NSColor { + appearance.isDarkAqua + ? NSColor(calibratedWhite: 0.20, alpha: 0.94) + : NSColor(calibratedWhite: 0.97, alpha: 0.98) + } + + static let assistantBubbleDynamicNSColor = NSColor( + name: NSColor.Name("ClawdisChatTheme.assistantBubble"), + dynamicProvider: resolvedAssistantBubbleColor(for:)) + + static let onboardingAssistantBubbleDynamicNSColor = NSColor( + name: NSColor.Name("ClawdisChatTheme.onboardingAssistantBubble"), + dynamicProvider: resolvedOnboardingAssistantBubbleColor(for:)) + #endif + static var surface: Color { #if os(macOS) Color(nsColor: .windowBackgroundColor) @@ -78,9 +110,7 @@ enum ClawdisChatTheme { static var assistantBubble: Color { #if os(macOS) - let base = NSColor.controlBackgroundColor - let blended = base.blended(withFraction: 0.18, of: .white) ?? base - return Color(nsColor: blended).opacity(0.88) + Color(nsColor: self.assistantBubbleDynamicNSColor) #else Color(uiColor: .secondarySystemBackground) #endif @@ -88,9 +118,7 @@ enum ClawdisChatTheme { static var onboardingAssistantBubble: Color { #if os(macOS) - let base = NSColor.controlBackgroundColor - let blended = base.blended(withFraction: 0.22, of: .white) ?? base - return Color(nsColor: blended) + Color(nsColor: self.onboardingAssistantBubbleDynamicNSColor) #else Color(uiColor: .secondarySystemBackground) #endif diff --git a/apps/shared/ClawdisKit/Tests/ClawdisKitTests/ChatThemeTests.swift b/apps/shared/ClawdisKit/Tests/ClawdisKitTests/ChatThemeTests.swift new file mode 100644 index 000000000..26352a411 --- /dev/null +++ b/apps/shared/ClawdisKit/Tests/ClawdisKitTests/ChatThemeTests.swift @@ -0,0 +1,29 @@ +@testable import ClawdisChatUI +import Foundation +import Testing + +#if os(macOS) +import AppKit +#endif + +#if os(macOS) +private func luminance(_ color: NSColor) throws -> CGFloat { + let rgb = try #require(color.usingColorSpace(.deviceRGB)) + return 0.2126 * rgb.redComponent + 0.7152 * rgb.greenComponent + 0.0722 * rgb.blueComponent +} +#endif + +@Suite struct ChatThemeTests { + @Test func assistantBubbleResolvesForLightAndDark() throws { + #if os(macOS) + let lightAppearance = try #require(NSAppearance(named: .aqua)) + let darkAppearance = try #require(NSAppearance(named: .darkAqua)) + + let lightResolved = ClawdisChatTheme.resolvedAssistantBubbleColor(for: lightAppearance) + let darkResolved = ClawdisChatTheme.resolvedAssistantBubbleColor(for: darkAppearance) + #expect(try luminance(lightResolved) > luminance(darkResolved)) + #else + #expect(Bool(true)) + #endif + } +}