diff --git a/apps/macos/Sources/Clawdis/DebugActions.swift b/apps/macos/Sources/Clawdis/DebugActions.swift new file mode 100644 index 000000000..e0fd02e90 --- /dev/null +++ b/apps/macos/Sources/Clawdis/DebugActions.swift @@ -0,0 +1,104 @@ +import AppKit +import Foundation +import SwiftUI + +enum DebugActions { + @MainActor + static func openAgentEventsWindow() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 620, height: 420), + styleMask: [.titled, .closable, .miniaturizable, .resizable], + backing: .buffered, + defer: false) + window.title = "Agent Events" + window.isReleasedWhenClosed = false + window.contentView = NSHostingView(rootView: AgentEventsWindow()) + window.center() + window.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } + + @MainActor + static func openLog() { + let path = self.pinoLogPath() + let url = URL(fileURLWithPath: path) + guard FileManager.default.fileExists(atPath: path) else { + let alert = NSAlert() + alert.messageText = "Log file not found" + alert.informativeText = path + alert.runModal() + return + } + NSWorkspace.shared.activateFileViewerSelecting([url]) + } + + static func sendTestNotification() async { + _ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil) + } + + static func sendDebugVoice() async -> Result { + 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 shouldForward = config.enabled + + if shouldForward { + let result = await VoiceWakeForwarder.forward(transcript: message, config: config) + switch result { + case .success: + return .success("Forwarded. Await reply.") + case let .failure(error): + let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) + return .failure("Forward failed: \(detail)") + } + } + + do { + let status = await AgentRPC.shared.status() + if !status.ok { + try await AgentRPC.shared.start() + } + + let rpcResult = await AgentRPC.shared.send( + text: message, + thinking: "low", + session: "main", + deliver: true, + to: nil) + + if rpcResult.ok { + return .success("Sent locally via voice wake path.") + } else { + let reason = rpcResult.error?.trimmingCharacters(in: .whitespacesAndNewlines) + let detail = (reason?.isEmpty == false) + ? reason! + : "No error returned. Check /tmp/clawdis.log or rpc output." + return .failure("Local send failed: \(detail)") + } + } catch { + let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) + return .failure("Local send failed: \(detail)") + } + } + + static func restartGateway() { + Task { @MainActor in + RelayProcessManager.shared.stop() + try? await Task.sleep(nanoseconds: 300_000_000) + RelayProcessManager.shared.setActive(true) + } + } + + private static func pinoLogPath() -> String { + let df = DateFormatter() + df.calendar = Calendar(identifier: .iso8601) + df.locale = Locale(identifier: "en_US_POSIX") + df.dateFormat = "yyyy-MM-dd" + let today = df.string(from: Date()) + let rolling = URL(fileURLWithPath: "/tmp/clawdis/clawdis-\(today).log").path + if FileManager.default.fileExists(atPath: rolling) { return rolling } + return "/tmp/clawdis.log" + } +} diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift index 7d382acee..5634af4e3 100644 --- a/apps/macos/Sources/Clawdis/DebugSettings.swift +++ b/apps/macos/Sources/Clawdis/DebugSettings.swift @@ -154,22 +154,11 @@ struct DebugSettings: View { } } Button("Send Test Notification") { - Task { - _ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil) - } + Task { await DebugActions.sendTestNotification() } } .buttonStyle(.bordered) Button("Open Agent Events") { - let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 620, height: 420), - styleMask: [.titled, .closable, .miniaturizable, .resizable], - backing: .buffered, - defer: false) - window.title = "Agent Events" - window.isReleasedWhenClosed = false - window.contentView = NSHostingView(rootView: AgentEventsWindow()) - window.center() - window.makeKeyAndOrderFront(nil) + DebugActions.openAgentEventsWindow() } .buttonStyle(.borderedProminent) VStack(alignment: .leading, spacing: 6) { @@ -206,7 +195,7 @@ struct DebugSettings: View { HStack { Button("Restart app") { self.relaunch() } Button("Reveal app in Finder") { self.revealApp() } - Button("Restart Gateway") { self.restartRelay() } + Button("Restart Gateway") { DebugActions.restartGateway() } } .buttonStyle(.bordered) Spacer(minLength: 8) @@ -221,39 +210,6 @@ struct DebugSettings: View { } } - private var pinoLogPath: String { - let df = DateFormatter() - df.calendar = Calendar(identifier: .iso8601) - df.locale = Locale(identifier: "en_US_POSIX") - df.dateFormat = "yyyy-MM-dd" - let today = df.string(from: Date()) - // Prefer rolling log; fall back to legacy single-file path. - let rolling = URL(fileURLWithPath: "/tmp/clawdis/clawdis-\(today).log").path - if FileManager.default.fileExists(atPath: rolling) { return rolling } - return "/tmp/clawdis.log" - } - - private func openLog() { - let path = self.pinoLogPath - let url = URL(fileURLWithPath: path) - if !FileManager.default.fileExists(atPath: path) { - let alert = NSAlert() - alert.messageText = "Log file not found" - alert.informativeText = path - alert.runModal() - return - } - NSWorkspace.shared.activateFileViewerSelecting([url]) - } - - private func restartRelay() { - Task { @MainActor in - self.relayManager.stop() - try? await Task.sleep(nanoseconds: 300_000_000) - self.relayManager.setActive(true) - } - } - private func chooseCatalogFile() { let panel = NSOpenPanel() panel.title = "Select models.generated.ts" @@ -292,62 +248,16 @@ struct DebugSettings: View { 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 shouldForward = config.enabled + let result = await DebugActions.sendDebugVoice() - if shouldForward { - let result = await VoiceWakeForwarder.forward(transcript: message, config: config) - await MainActor.run { - self.debugSendInFlight = false - switch result { - case .success: - self.debugSendStatus = "Forwarded. Await reply." - self.debugSendError = nil - case let .failure(error): - let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) - self.debugSendStatus = "Forward failed: \(detail)" - self.debugSendError = nil - } - } - return - } - - do { - let status = await AgentRPC.shared.status() - if !status.ok { - try await AgentRPC.shared.start() - } - - let rpcResult = await AgentRPC.shared.send( - text: message, - thinking: "low", - session: "main", - deliver: true, - to: nil) - - await MainActor.run { - self.debugSendInFlight = false - if rpcResult.ok { - self.debugSendStatus = "Sent locally via voice wake path." - self.debugSendError = nil - } else { - let reason = rpcResult.error?.trimmingCharacters(in: .whitespacesAndNewlines) - let detail = (reason?.isEmpty == false) - ? reason! - : "No error returned. Check /tmp/clawdis.log or rpc output." - self.debugSendStatus = "Local send failed: \(detail)" - self.debugSendError = nil - } - } - } catch { - await MainActor.run { - self.debugSendInFlight = false - let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) - self.debugSendStatus = "Local send failed: \(detail)" + await MainActor.run { + self.debugSendInFlight = false + switch result { + case let .success(message): + self.debugSendStatus = message + self.debugSendError = nil + case let .failure(message): + self.debugSendStatus = message self.debugSendError = nil } } diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index f8431de6c..d2a7d16fe 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -41,6 +41,20 @@ struct MenuContent: View { if let updater, updater.isAvailable { Button("Check for Updates…") { updater.checkForUpdates(nil) } } + if self.state.debugPaneEnabled { + Menu("Debug") { + Button("Open Agent Events…") { DebugActions.openAgentEventsWindow() } + Button("Open Log") { DebugActions.openLog() } + Button("Send Debug Voice Text") { + Task { _ = await DebugActions.sendDebugVoice() } + } + Button("Send Test Notification") { + Task { await DebugActions.sendTestNotification() } + } + Divider() + Button("Restart Gateway") { DebugActions.restartGateway() } + } + } Button("Quit") { NSApplication.shared.terminate(nil) } } .task(id: self.state.swabbleEnabled) {