From a7617e4d79ae7cf48ce4e5d39cfcb74a4873e1bd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 30 Dec 2025 06:47:35 +0100 Subject: [PATCH] fix(ui): refine talk overlays --- CHANGELOG.md | 4 ++++ .../java/com/steipete/clawdis/node/ui/TalkOrbOverlay.kt | 2 +- apps/ios/Sources/Voice/TalkOrbOverlay.swift | 8 ++++---- apps/macos/Sources/Clawdis/TalkOverlay.swift | 7 ++++--- apps/macos/Sources/Clawdis/TalkOverlayView.swift | 6 +++--- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4168f520c..a4ab83493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ - macOS Talk Mode: fix audio stop ordering so disabling Talk Mode always stops in-flight playback. - macOS Talk Mode: throttle audio-level updates (avoid per-buffer task creation) to reduce CPU/task churn. - macOS Talk Mode: increase overlay window size so wave rings don’t clip; close button is hover-only and closer to the orb. +- Talk Mode: align to the gateway’s main session key and fall back to history polling when chat events drop (prevents stuck “thinking” / missing messages). +- Talk Mode: treat history timestamps as seconds or milliseconds to avoid stale assistant picks (macOS/iOS/Android). +- Chat UI: dedupe identical history messages to avoid duplicate bubbles. +- Chat UI: user bubbles use `ui.seamColor` (fallback to a calmer default blue). - Talk Mode: wait for chat history to surface the assistant reply before starting TTS (macOS/iOS/Android). - iOS Talk Mode: fix chat completion wait to time out even if no events arrive (prevents “Thinking…” hangs). - iOS Talk Mode: keep recognition running during playback to support interrupt-on-speech. diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/TalkOrbOverlay.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/TalkOrbOverlay.kt index 38b8e819e..11b7a3176 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/TalkOrbOverlay.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/TalkOrbOverlay.kt @@ -62,7 +62,7 @@ fun TalkOrbOverlay( verticalArrangement = Arrangement.spacedBy(12.dp), ) { Box(contentAlignment = Alignment.Center) { - Canvas(modifier = Modifier.size(240.dp)) { + Canvas(modifier = Modifier.size(300.dp)) { val center = this.center val baseRadius = size.minDimension * 0.27f diff --git a/apps/ios/Sources/Voice/TalkOrbOverlay.swift b/apps/ios/Sources/Voice/TalkOrbOverlay.swift index 3d7907c6d..27bfbf4dc 100644 --- a/apps/ios/Sources/Voice/TalkOrbOverlay.swift +++ b/apps/ios/Sources/Voice/TalkOrbOverlay.swift @@ -12,14 +12,14 @@ struct TalkOrbOverlay: View { ZStack { Circle() .stroke(seam.opacity(0.26), lineWidth: 2) - .frame(width: 220, height: 220) + .frame(width: 280, height: 280) .scaleEffect(self.pulse ? 1.15 : 0.96) .opacity(self.pulse ? 0.0 : 1.0) .animation(.easeOut(duration: 1.3).repeatForever(autoreverses: false), value: self.pulse) Circle() .stroke(seam.opacity(0.18), lineWidth: 2) - .frame(width: 220, height: 220) + .frame(width: 280, height: 280) .scaleEffect(self.pulse ? 1.45 : 1.02) .opacity(self.pulse ? 0.0 : 0.9) .animation(.easeOut(duration: 1.9).repeatForever(autoreverses: false).delay(0.2), value: self.pulse) @@ -34,8 +34,8 @@ struct TalkOrbOverlay: View { ], center: .center, startRadius: 1, - endRadius: 92)) - .frame(width: 136, height: 136) + endRadius: 112)) + .frame(width: 168, height: 168) .overlay( Circle() .stroke(seam.opacity(0.35), lineWidth: 1)) diff --git a/apps/macos/Sources/Clawdis/TalkOverlay.swift b/apps/macos/Sources/Clawdis/TalkOverlay.swift index 3031cb97e..bbddd2a77 100644 --- a/apps/macos/Sources/Clawdis/TalkOverlay.swift +++ b/apps/macos/Sources/Clawdis/TalkOverlay.swift @@ -7,7 +7,8 @@ import SwiftUI @Observable final class TalkOverlayController { static let shared = TalkOverlayController() - static let overlaySize: CGFloat = 320 + static let overlaySize: CGFloat = 360 + static let windowInset: CGFloat = 88 private let logger = Logger(subsystem: "com.steipete.clawdis", category: "talk.overlay") @@ -110,8 +111,8 @@ final class TalkOverlayController { let size = NSSize(width: Self.overlaySize, height: Self.overlaySize) let visible = screen.visibleFrame let origin = CGPoint( - x: visible.maxX - size.width - self.padding, - y: visible.maxY - size.height - self.padding) + x: visible.maxX - size.width - self.padding + Self.windowInset, + y: visible.maxY - size.height - self.padding + Self.windowInset) return NSRect(origin: origin, size: size) } } diff --git a/apps/macos/Sources/Clawdis/TalkOverlayView.swift b/apps/macos/Sources/Clawdis/TalkOverlayView.swift index 5611f3d1f..e9b3091d5 100644 --- a/apps/macos/Sources/Clawdis/TalkOverlayView.swift +++ b/apps/macos/Sources/Clawdis/TalkOverlayView.swift @@ -12,8 +12,8 @@ struct TalkOverlayView: View { level: self.controller.model.level, accent: self.seamColor) .frame(width: 96, height: 96) - .padding(.top, 6) - .padding(.trailing, 6) + .padding(.top, 6 + TalkOverlayController.windowInset) + .padding(.trailing, 6 + TalkOverlayController.windowInset) .contentShape(Circle()) .onTapGesture { TalkModeController.shared.stopSpeaking(reason: .userTap) @@ -31,7 +31,7 @@ struct TalkOverlayView: View { } .buttonStyle(.plain) .contentShape(Circle()) - .offset(x: -7, y: -7) + .offset(x: -5, y: -5) .opacity(self.hoveringWindow ? 1 : 0) .animation(.easeOut(duration: 0.12), value: self.hoveringWindow) .allowsHitTesting(self.hoveringWindow)