diff --git a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift index cc8314f78..57f3fd1d4 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift @@ -144,8 +144,9 @@ actor VoiceWakeRuntime { if !transcript.isEmpty { self.lastHeard = now if self.isCapturing { - self.capturedTranscript = Self.trimmedAfterTrigger(transcript, triggers: config.triggers) - self.updateHeardBeyondTrigger(with: transcript) + let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers) + self.capturedTranscript = trimmed + self.updateHeardBeyondTrigger(withTrimmed: trimmed) let snapshot = self.capturedTranscript await MainActor.run { VoiceWakeOverlayController.shared.showPartial(transcript: snapshot) @@ -176,10 +177,11 @@ actor VoiceWakeRuntime { private func beginCapture(transcript: String, config: RuntimeConfig) async { self.isCapturing = true - self.capturedTranscript = Self.trimmedAfterTrigger(transcript, triggers: config.triggers) + let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers) + self.capturedTranscript = trimmed self.captureStartedAt = Date() self.cooldownUntil = nil - self.heardBeyondTrigger = self.textHasBeyondTriggerContent(transcript) + self.heardBeyondTrigger = !trimmed.isEmpty let snapshot = self.capturedTranscript await MainActor.run { @@ -236,15 +238,8 @@ actor VoiceWakeRuntime { await MainActor.run { AppStateStore.shared.stopVoiceEars() } - guard !finalTranscript.isEmpty else { - await MainActor.run { VoiceWakeOverlayController.shared.dismiss(reason: .empty) } - self.cooldownUntil = Date().addingTimeInterval(self.debounceAfterSend) - self.restartRecognizer() - return - } - let forwardConfig = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig } - let delay: TimeInterval = heardBeyondTrigger ? 1.0 : 3.0 + let delay: TimeInterval = (heardBeyondTrigger && !finalTranscript.isEmpty) ? 1.0 : 3.0 await MainActor.run { VoiceWakeOverlayController.shared.presentFinal( transcript: finalTranscript, @@ -265,13 +260,8 @@ actor VoiceWakeRuntime { } } - private func textHasBeyondTriggerContent(_ text: String) -> Bool { - let words = text.split(whereSeparator: { $0.isWhitespace }) - return words.count > 1 - } - - private func updateHeardBeyondTrigger(with transcript: String) { - if !self.heardBeyondTrigger, self.textHasBeyondTriggerContent(transcript) { + private func updateHeardBeyondTrigger(withTrimmed trimmed: String) { + if !self.heardBeyondTrigger, !trimmed.isEmpty { self.heardBeyondTrigger = true } } @@ -292,6 +282,10 @@ actor VoiceWakeRuntime { static func _testTrimmedAfterTrigger(_ text: String, triggers: [String]) -> String { self.trimmedAfterTrigger(text, triggers: triggers) } + + static func _testHasContentAfterTrigger(_ text: String, triggers: [String]) -> Bool { + !self.trimmedAfterTrigger(text, triggers: triggers).isEmpty + } #endif #if DEBUG diff --git a/apps/macos/Tests/ClawdisIPCTests/VoiceWakeRuntimeTests.swift b/apps/macos/Tests/ClawdisIPCTests/VoiceWakeRuntimeTests.swift index 39385bd1b..58beb1740 100644 --- a/apps/macos/Tests/ClawdisIPCTests/VoiceWakeRuntimeTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/VoiceWakeRuntimeTests.swift @@ -35,4 +35,16 @@ import Testing let text = "hello buddy this is after trigger claude also here" #expect(VoiceWakeRuntime._testTrimmedAfterTrigger(text, triggers: triggers) == "this is after trigger claude also here") } + + @Test func hasContentAfterTriggerFalseWhenOnlyTrigger() { + let triggers = ["clawd"] + let text = "hey clawd" + #expect(!VoiceWakeRuntime._testHasContentAfterTrigger(text, triggers: triggers)) + } + + @Test func hasContentAfterTriggerTrueWhenSpeechContinues() { + let triggers = ["claude"] + let text = "claude write a note" + #expect(VoiceWakeRuntime._testHasContentAfterTrigger(text, triggers: triggers)) + } }