From 3a42979e53c19f9524cfc94b458277c2c3b2d720 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 03:20:52 +0100 Subject: [PATCH] Voice wake: log overlay lifecycle and enforce PTT cooldown --- .../Sources/Clawdis/VoicePushToTalk.swift | 7 ++---- .../Sources/Clawdis/VoiceWakeChime.swift | 3 +++ .../Sources/Clawdis/VoiceWakeOverlay.swift | 22 +++++++++---------- .../Sources/Clawdis/VoiceWakeRuntime.swift | 4 ++++ 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift index b352fd469..0a1d86100 100644 --- a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift +++ b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift @@ -156,11 +156,8 @@ actor VoicePushToTalk { self.triggerChimePlayed = false // Resume the wake-word runtime after push-to-talk finishes. - _ = await MainActor.run { - Task { - await VoiceWakeRuntime.shared.refresh(state: AppStateStore.shared) - } - } + await VoiceWakeRuntime.shared.applyPushToTalkCooldown() + _ = await MainActor.run { Task { await VoiceWakeRuntime.shared.refresh(state: AppStateStore.shared) } } } // MARK: - Private diff --git a/apps/macos/Sources/Clawdis/VoiceWakeChime.swift b/apps/macos/Sources/Clawdis/VoiceWakeChime.swift index de57ecdd4..e2d9c6c18 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeChime.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeChime.swift @@ -1,5 +1,6 @@ import AppKit import Foundation +import OSLog enum VoiceWakeChime: Codable, Equatable, Sendable { case none @@ -44,11 +45,13 @@ struct VoiceWakeChimeCatalog { @MainActor enum VoiceWakeChimePlayer { + private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "voicewake.chime") private static var lastSound: NSSound? @MainActor static func play(_ chime: VoiceWakeChime) { guard let sound = self.sound(for: chime) else { return } + self.logger.log(level: .info, "chime play type=\(String(describing: chime), privacy: .public) name=\(sound.name ?? "", privacy: .public)") self.lastSound = sound sound.stop() sound.play() diff --git a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift index f85fdb04a..1baf4a26e 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift @@ -39,7 +39,7 @@ final class VoiceWakeOverlayController: ObservableObject { let closeOverflow: CGFloat = 10 func showPartial(transcript: String, attributed: NSAttributedString? = nil) { - self.logger.debug("overlay showPartial len=\(transcript.count, privacy: .public) visible=\(self.model.isVisible, privacy: .public) isFinal=false") + self.logger.log(level: .info, "overlay showPartial len=\(transcript.count, privacy: .public) visible=\(self.model.isVisible, privacy: .public) isFinal=false") self.autoSendTask?.cancel() self.forwardConfig = nil self.model.text = transcript @@ -60,7 +60,7 @@ final class VoiceWakeOverlayController: ObservableObject { sendChime: VoiceWakeChime = .none, attributed: NSAttributedString? = nil) { - self.logger.debug("overlay presentFinal len=\(transcript.count, privacy: .public) autoSendAfter=\(delay ?? -1, privacy: .public) forwardEnabled=\(forwardConfig.enabled, privacy: .public)") + self.logger.log(level: .info, "overlay presentFinal len=\(transcript.count, privacy: .public) autoSendAfter=\(delay ?? -1, privacy: .public) forwardEnabled=\(forwardConfig.enabled, privacy: .public)") self.autoSendTask?.cancel() self.forwardConfig = forwardConfig self.model.text = transcript @@ -101,31 +101,31 @@ final class VoiceWakeOverlayController: ObservableObject { } func sendNow(sendChime: VoiceWakeChime = .none) { - self.logger.debug("overlay sendNow called isSending=\(self.model.isSending, privacy: .public) forwardEnabled=\(self.model.forwardEnabled, privacy: .public) textLen=\(self.model.text.count, privacy: .public)") + self.logger.log(level: .info, "overlay sendNow called isSending=\(self.model.isSending, privacy: .public) forwardEnabled=\(self.model.forwardEnabled, privacy: .public) textLen=\(self.model.text.count, privacy: .public)") self.autoSendTask?.cancel() self.autoSendTask = nil if self.model.isSending { return } self.model.isEditing = false guard let forwardConfig, forwardConfig.enabled else { - self.logger.debug("overlay sendNow disabled -> dismiss") + self.logger.log(level: .info, "overlay sendNow disabled -> dismiss") self.dismiss(reason: .explicit) return } let text = self.model.text.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { - self.logger.debug("overlay sendNow empty -> dismiss") + self.logger.log(level: .info, "overlay sendNow empty -> dismiss") self.dismiss(reason: .empty) return } if sendChime != .none { - self.logger.debug("overlay sendNow playing sendChime=\(String(describing: sendChime), privacy: .public)") + self.logger.log(level: .info, "overlay sendNow playing sendChime=\(String(describing: sendChime), privacy: .public)") VoiceWakeChimePlayer.play(sendChime) } self.model.isSending = true let payload = VoiceWakeForwarder.prefixedTranscript(text) - self.logger.debug("overlay sendNow forwarding len=\(payload.count, privacy: .public) target=\(forwardConfig.target, privacy: .public)") + self.logger.log(level: .info, "overlay sendNow forwarding len=\(payload.count, privacy: .public) target=\(forwardConfig.target, privacy: .public)") Task.detached { await VoiceWakeForwarder.forward(transcript: payload, config: forwardConfig) } @@ -135,7 +135,7 @@ final class VoiceWakeOverlayController: ObservableObject { } func dismiss(reason: DismissReason = .explicit, outcome: SendOutcome = .empty) { - self.logger.debug("overlay dismiss reason=\(String(describing: reason), privacy: .public) outcome=\(String(describing: outcome), privacy: .public) visible=\(self.model.isVisible, privacy: .public) sending=\(self.model.isSending, privacy: .public)") + self.logger.log(level: .info, "overlay dismiss reason=\(String(describing: reason), privacy: .public) outcome=\(String(describing: outcome), privacy: .public) visible=\(self.model.isVisible, privacy: .public) sending=\(self.model.isSending, privacy: .public)") self.autoSendTask?.cancel() self.model.isSending = false self.model.isEditing = false @@ -180,7 +180,7 @@ final class VoiceWakeOverlayController: ObservableObject { guard let window else { return } if !self.model.isVisible { self.model.isVisible = true - self.logger.debug("overlay present windowShown textLen=\(self.model.text.count, privacy: .public)") + self.logger.log(level: .info, "overlay present windowShown textLen=\(self.model.text.count, privacy: .public)") // Keep the status item in “listening” mode until we explicitly dismiss the overlay. AppStateStore.shared.triggerVoiceEars(ttl: nil) let start = target.offsetBy(dx: 0, dy: -6) @@ -293,7 +293,7 @@ final class VoiceWakeOverlayController: ObservableObject { } private func scheduleAutoSend(after delay: TimeInterval, sendChime: VoiceWakeChime) { - self.logger.debug("overlay scheduleAutoSend after=\(delay, privacy: .public) sendChime=\(String(describing: sendChime), privacy: .public)") + self.logger.log(level: .info, "overlay scheduleAutoSend after=\(delay, privacy: .public) sendChime=\(String(describing: sendChime), privacy: .public)") self.autoSendTask?.cancel() self.autoSendTask = Task { [weak self, sendChime] in let nanos = UInt64(max(0, delay) * 1_000_000_000) @@ -301,7 +301,7 @@ final class VoiceWakeOverlayController: ObservableObject { guard !Task.isCancelled else { return } await MainActor.run { guard let self else { return } - self.logger.debug("overlay autoSend firing") + self.logger.log(level: .info, "overlay autoSend firing") self.sendNow(sendChime: sendChime) self.autoSendTask = nil } diff --git a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift index 326bcbf2a..e35f63236 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift @@ -375,6 +375,10 @@ actor VoiceWakeRuntime { } } + func applyPushToTalkCooldown() { + self.cooldownUntil = Date().addingTimeInterval(self.debounceAfterSend) + } + func pauseForPushToTalk() { self.listeningState = .pushToTalk self.stop(dismissOverlay: false)