From 9c9e04c5a0c74c87cf8f59bcf208d9f2c1f4febf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 7 Dec 2025 15:00:02 +0100 Subject: [PATCH] debug: add voice forward test button --- .../macos/Sources/Clawdis/DebugSettings.swift | 52 +++++++++++++++++++ .../Sources/Clawdis/VoiceWakeForwarder.swift | 26 ++++++---- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift index db596b53e..376c4e6fc 100644 --- a/apps/macos/Sources/Clawdis/DebugSettings.swift +++ b/apps/macos/Sources/Clawdis/DebugSettings.swift @@ -12,6 +12,9 @@ struct DebugSettings: View { @State private var relayRootInput: String = RelayProcessManager.shared.projectRootPath() @State private var sessionStorePath: String = SessionLoader.defaultStorePath @State private var sessionStoreSaveError: String? + @State private var debugSendInFlight = false + @State private var debugSendStatus: String? + @State private var debugSendError: String? var body: some View { ScrollView(.vertical) { @@ -129,6 +132,31 @@ struct DebugSettings: View { Task { _ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil) } } .buttonStyle(.bordered) + VStack(alignment: .leading, spacing: 6) { + Button { + Task { await self.sendVoiceDebug() } + } label: { + Label( + self.debugSendInFlight ? "Sending debug voice…" : "Send debug voice via forwarder", + systemImage: self.debugSendInFlight ? "bolt.horizontal.circle" : "waveform") + } + .buttonStyle(.borderedProminent) + .disabled(self.debugSendInFlight) + + if let debugSendStatus { + Text(debugSendStatus) + .font(.caption) + .foregroundStyle(.secondary) + } else if let debugSendError { + Text(debugSendError) + .font(.caption) + .foregroundStyle(.red) + } else { + Text("Sends the same command path as Voice Wake (ssh target + clawdis-mac agent → rpc → node cli → p-agent → WhatsApp).") + .font(.caption) + .foregroundStyle(.secondary) + } + } HStack { Button("Restart app") { self.relaunch() } Button("Reveal app in Finder") { self.revealApp() } @@ -211,6 +239,30 @@ struct DebugSettings: View { } } + private func sendVoiceDebug() async { + await MainActor.run { + self.debugSendInFlight = true + self.debugSendError = nil + self.debugSendStatus = nil + } + + let message = "This is a debug test from the Mac app. Reply with \"Debug test works (and a funny pun)\" if you received that." + let config = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig } + let result = await VoiceWakeForwarder.forward(transcript: message, config: config) + + await MainActor.run { + self.debugSendInFlight = false + switch result { + case .success: + self.debugSendStatus = "Sent via \(config.target). Await WhatsApp reply." + self.debugSendError = nil + case let .failure(error): + self.debugSendStatus = nil + self.debugSendError = error.localizedDescription + } + } + } + private func relaunch() { let url = Bundle.main.bundleURL let task = Process() diff --git a/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift b/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift index 72550ee0e..eb0ef93f6 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift @@ -86,6 +86,7 @@ enum VoiceWakeForwarder { case launchFailed(String) case nonZeroExit(Int32, String) case cliMissingOrFailed(Int32, String) + case disabled var errorDescription: String? { switch self { @@ -101,16 +102,18 @@ enum VoiceWakeForwarder { return clipped.isEmpty ? "clawdis-mac failed on remote (code \(code))" : "clawdis-mac failed on remote (code \(code)): \(clipped)" + case .disabled: return "Voice wake forwarding disabled" } } } - static func forward(transcript: String, config: VoiceWakeForwardConfig) async { - guard config.enabled else { return } + @discardableResult + static func forward(transcript: String, config: VoiceWakeForwardConfig) async -> Result { + guard config.enabled else { return .failure(.disabled) } let destination = config.target.trimmingCharacters(in: .whitespacesAndNewlines) guard let parsed = self.parse(target: destination) else { self.logger.error("voice wake forward skipped: host missing") - return + return .failure(.invalidTarget) } let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host @@ -144,7 +147,7 @@ enum VoiceWakeForwarder { try process.run() } catch { self.logger.error("voice wake forward failed to start ssh: \(error.localizedDescription, privacy: .public)") - return + return .failure(.launchFailed(error.localizedDescription)) } if let data = transcript.data(using: .utf8) { @@ -155,12 +158,17 @@ enum VoiceWakeForwarder { let out = await self.wait(process, timeout: config.timeout) if process.terminationStatus == 0 { self.logger.info("voice wake forward ok host=\(userHost, privacy: .public)") - } else { - // surface the failure instead of being silent - let clipped = out.prefix(240) - self.logger.error( - "voice wake forward failed exit=\(process.terminationStatus) host=\(userHost, privacy: .public) out=\(clipped, privacy: .public)") + return .success(()) } + + // surface the failure instead of being silent + let clipped = out.prefix(240) + self.logger.error( + "voice wake forward failed exit=\(process.terminationStatus) host=\(userHost, privacy: .public) out=\(clipped, privacy: .public)") + if process.terminationStatus == 127 { + return .failure(.cliMissingOrFailed(process.terminationStatus, out)) + } + return .failure(.nonZeroExit(process.terminationStatus, out)) } static func checkConnection(config: VoiceWakeForwardConfig) async -> Result {