debug: add voice forward test button
This commit is contained in:
@@ -12,6 +12,9 @@ struct DebugSettings: View {
|
|||||||
@State private var relayRootInput: String = RelayProcessManager.shared.projectRootPath()
|
@State private var relayRootInput: String = RelayProcessManager.shared.projectRootPath()
|
||||||
@State private var sessionStorePath: String = SessionLoader.defaultStorePath
|
@State private var sessionStorePath: String = SessionLoader.defaultStorePath
|
||||||
@State private var sessionStoreSaveError: String?
|
@State private var sessionStoreSaveError: String?
|
||||||
|
@State private var debugSendInFlight = false
|
||||||
|
@State private var debugSendStatus: String?
|
||||||
|
@State private var debugSendError: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
@@ -129,6 +132,31 @@ struct DebugSettings: View {
|
|||||||
Task { _ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil) }
|
Task { _ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil) }
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.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 {
|
HStack {
|
||||||
Button("Restart app") { self.relaunch() }
|
Button("Restart app") { self.relaunch() }
|
||||||
Button("Reveal app in Finder") { self.revealApp() }
|
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() {
|
private func relaunch() {
|
||||||
let url = Bundle.main.bundleURL
|
let url = Bundle.main.bundleURL
|
||||||
let task = Process()
|
let task = Process()
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ enum VoiceWakeForwarder {
|
|||||||
case launchFailed(String)
|
case launchFailed(String)
|
||||||
case nonZeroExit(Int32, String)
|
case nonZeroExit(Int32, String)
|
||||||
case cliMissingOrFailed(Int32, String)
|
case cliMissingOrFailed(Int32, String)
|
||||||
|
case disabled
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -101,16 +102,18 @@ enum VoiceWakeForwarder {
|
|||||||
return clipped.isEmpty
|
return clipped.isEmpty
|
||||||
? "clawdis-mac failed on remote (code \(code))"
|
? "clawdis-mac failed on remote (code \(code))"
|
||||||
: "clawdis-mac failed on remote (code \(code)): \(clipped)"
|
: "clawdis-mac failed on remote (code \(code)): \(clipped)"
|
||||||
|
case .disabled: return "Voice wake forwarding disabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func forward(transcript: String, config: VoiceWakeForwardConfig) async {
|
@discardableResult
|
||||||
guard config.enabled else { return }
|
static func forward(transcript: String, config: VoiceWakeForwardConfig) async -> Result<Void, VoiceWakeForwardError> {
|
||||||
|
guard config.enabled else { return .failure(.disabled) }
|
||||||
let destination = config.target.trimmingCharacters(in: .whitespacesAndNewlines)
|
let destination = config.target.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard let parsed = self.parse(target: destination) else {
|
guard let parsed = self.parse(target: destination) else {
|
||||||
self.logger.error("voice wake forward skipped: host missing")
|
self.logger.error("voice wake forward skipped: host missing")
|
||||||
return
|
return .failure(.invalidTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host
|
let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host
|
||||||
@@ -144,7 +147,7 @@ enum VoiceWakeForwarder {
|
|||||||
try process.run()
|
try process.run()
|
||||||
} catch {
|
} catch {
|
||||||
self.logger.error("voice wake forward failed to start ssh: \(error.localizedDescription, privacy: .public)")
|
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) {
|
if let data = transcript.data(using: .utf8) {
|
||||||
@@ -155,12 +158,17 @@ enum VoiceWakeForwarder {
|
|||||||
let out = await self.wait(process, timeout: config.timeout)
|
let out = await self.wait(process, timeout: config.timeout)
|
||||||
if process.terminationStatus == 0 {
|
if process.terminationStatus == 0 {
|
||||||
self.logger.info("voice wake forward ok host=\(userHost, privacy: .public)")
|
self.logger.info("voice wake forward ok host=\(userHost, privacy: .public)")
|
||||||
} else {
|
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)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<Void, VoiceWakeForwardError> {
|
static func checkConnection(config: VoiceWakeForwardConfig) async -> Result<Void, VoiceWakeForwardError> {
|
||||||
|
|||||||
Reference in New Issue
Block a user