fix(webchat): refresh bubbles on theme change
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
- System prompt now tags allowlisted owner numbers as the user identity to avoid mistaken “friend” assumptions.
|
- System prompt now tags allowlisted owner numbers as the user identity to avoid mistaken “friend” assumptions.
|
||||||
- LM Studio/Ollama replies now require <final> tags; streaming ignores content until <final> begins.
|
- LM Studio/Ollama replies now require <final> tags; streaming ignores content until <final> begins.
|
||||||
- `process log` pagination is now line-based (omit `offset` to grab the last N lines).
|
- `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.
|
- 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).
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -6,7 +6,39 @@ import AppKit
|
|||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
private extension NSAppearance {
|
||||||
|
var isDarkAqua: Bool {
|
||||||
|
self.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
enum ClawdisChatTheme {
|
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 {
|
static var surface: Color {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Color(nsColor: .windowBackgroundColor)
|
Color(nsColor: .windowBackgroundColor)
|
||||||
@@ -78,9 +110,7 @@ enum ClawdisChatTheme {
|
|||||||
|
|
||||||
static var assistantBubble: Color {
|
static var assistantBubble: Color {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
let base = NSColor.controlBackgroundColor
|
Color(nsColor: self.assistantBubbleDynamicNSColor)
|
||||||
let blended = base.blended(withFraction: 0.18, of: .white) ?? base
|
|
||||||
return Color(nsColor: blended).opacity(0.88)
|
|
||||||
#else
|
#else
|
||||||
Color(uiColor: .secondarySystemBackground)
|
Color(uiColor: .secondarySystemBackground)
|
||||||
#endif
|
#endif
|
||||||
@@ -88,9 +118,7 @@ enum ClawdisChatTheme {
|
|||||||
|
|
||||||
static var onboardingAssistantBubble: Color {
|
static var onboardingAssistantBubble: Color {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
let base = NSColor.controlBackgroundColor
|
Color(nsColor: self.onboardingAssistantBubbleDynamicNSColor)
|
||||||
let blended = base.blended(withFraction: 0.22, of: .white) ?? base
|
|
||||||
return Color(nsColor: blended)
|
|
||||||
#else
|
#else
|
||||||
Color(uiColor: .secondarySystemBackground)
|
Color(uiColor: .secondarySystemBackground)
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user