From 1a0e57d926fe3c67b2adadb5be17dd66ae7ef241 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 21:06:21 +0100 Subject: [PATCH] Menu: add more debug utilities --- apps/macos/Sources/Clawdis/DebugActions.swift | 82 +++++++++++++++++++ .../macos/Sources/Clawdis/DebugSettings.swift | 15 +--- .../Sources/Clawdis/MenuContentView.swift | 36 ++++---- 3 files changed, 100 insertions(+), 33 deletions(-) diff --git a/apps/macos/Sources/Clawdis/DebugActions.swift b/apps/macos/Sources/Clawdis/DebugActions.swift index 7213732b6..0ba08aa30 100644 --- a/apps/macos/Sources/Clawdis/DebugActions.swift +++ b/apps/macos/Sources/Clawdis/DebugActions.swift @@ -3,6 +3,8 @@ import Foundation import SwiftUI enum DebugActions { + private static let verboseDefaultsKey = "clawdis.debug.verboseMain" + @MainActor static func openAgentEventsWindow() { let window = NSWindow( @@ -32,6 +34,25 @@ enum DebugActions { NSWorkspace.shared.activateFileViewerSelecting([url]) } + @MainActor + static func openConfigFolder() { + let url = FileManager.default + .homeDirectoryForCurrentUser + .appendingPathComponent(".clawdis", isDirectory: true) + NSWorkspace.shared.activateFileViewerSelecting([url]) + } + + @MainActor + static func openSessionStore() { + let path = self.resolveSessionStorePath() + let url = URL(fileURLWithPath: path) + if FileManager.default.fileExists(atPath: path) { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } else { + NSWorkspace.shared.open(url.deletingLastPathComponent()) + } + } + static func sendTestNotification() async { _ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil) } @@ -101,6 +122,67 @@ enum DebugActions { if FileManager.default.fileExists(atPath: rolling) { return rolling } return "/tmp/clawdis.log" } + + @MainActor + static func runHealthCheckNow() async { + await HealthStore.shared.refresh(onDemand: true) + } + + static func sendTestHeartbeat() async -> Result { + do { + _ = await AgentRPC.shared.setHeartbeatsEnabled(true) + try await ControlChannel.shared.configure() + let data = try await ControlChannel.shared.request(method: "last-heartbeat") + if let evt = try? JSONDecoder().decode(ControlHeartbeatEvent.self, from: data) { + return .success(evt) + } + return .success(nil) + } catch { + return .failure(error.localizedDescription) + } + } + + static var verboseLoggingEnabledMain: Bool { + UserDefaults.standard.bool(forKey: self.verboseDefaultsKey) + } + + static func toggleVerboseLoggingMain() async -> Bool { + let newValue = !self.verboseLoggingEnabledMain + UserDefaults.standard.set(newValue, forKey: self.verboseDefaultsKey) + try? await ControlChannel.shared.request( + method: "system-event", + params: ["text": AnyHashable("verbose-main:\(newValue ? "on" : "off")")]) + return newValue + } + + @MainActor + static func restartApp() { + let url = Bundle.main.bundleURL + let task = Process() + task.launchPath = "/usr/bin/open" + task.arguments = [url.path] + try? task.run() + task.waitUntilExit() + NSApp.terminate(nil) + } + + private static func resolveSessionStorePath() -> String { + let defaultPath = SessionLoader.defaultStorePath + let configURL = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".clawdis/clawdis.json") + guard + let data = try? Data(contentsOf: configURL), + let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let inbound = parsed["inbound"] as? [String: Any], + let reply = inbound["reply"] as? [String: Any], + let session = reply["session"] as? [String: Any], + let path = session["store"] as? String, + !path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + else { + return defaultPath + } + return path + } } enum DebugActionError: LocalizedError { diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift index b3913fe9b..9730330d1 100644 --- a/apps/macos/Sources/Clawdis/DebugSettings.swift +++ b/apps/macos/Sources/Clawdis/DebugSettings.swift @@ -194,7 +194,7 @@ struct DebugSettings: View { } } HStack { - Button("Restart app") { self.relaunch() } + Button("Restart app") { DebugActions.restartApp() } Button("Reveal app in Finder") { self.revealApp() } Button("Restart Gateway") { DebugActions.restartGateway() } Button("Clear log") { GatewayProcessManager.shared.clearLog() } @@ -266,19 +266,6 @@ struct DebugSettings: View { } } - private func relaunch() { - let url = Bundle.main.bundleURL - let task = Process() - task.launchPath = "/bin/sh" - task.arguments = ["-c", "sleep 0.3; open -n \"\(url.path)\""] - task.standardOutput = nil - task.standardError = nil - task.standardInput = nil - try? task.run() - // Terminate current instance; spawned shell re-opens after a short delay. - NSApp.terminate(nil) - } - private func revealApp() { let url = Bundle.main.bundleURL NSWorkspace.shared.activateFileViewerSelecting([url]) diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index 8032b50df..79c4c2928 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -32,7 +32,7 @@ struct MenuContent: View { self.voiceWakeMicMenu } if AppStateStore.webChatEnabled { - Button("Open Chat") { WebChatManager.shared.show(sessionKey: self.primarySessionKey()) } + Button("Open Chat") { WebChatManager.shared.show(sessionKey: WebChatManager.shared.preferredSessionKey()) } } Divider() Button("Settings…") { self.open(tab: .general) } @@ -43,6 +43,21 @@ struct MenuContent: View { } if self.state.debugPaneEnabled { Menu("Debug") { + Button("Open Config Folder") { DebugActions.openConfigFolder() } + Button("Run Health Check Now") { + Task { await DebugActions.runHealthCheckNow() } + } + Button("Send Test Heartbeat") { + Task { _ = await DebugActions.sendTestHeartbeat() } + } + Button(DebugActions.verboseLoggingEnabledMain + ? "Verbose Logging (Main): On" + : "Verbose Logging (Main): Off") + { + Task { _ = await DebugActions.toggleVerboseLoggingMain() } + } + Button("Open Session Store") { DebugActions.openSessionStore() } + Divider() Button("Open Agent Events…") { DebugActions.openAgentEventsWindow() } Button("Open Log") { DebugActions.openLog() } Button("Send Debug Voice Text") { @@ -53,6 +68,7 @@ struct MenuContent: View { } Divider() Button("Restart Gateway") { DebugActions.restartGateway() } + Button("Restart App") { DebugActions.restartApp() } } } Button("Quit") { NSApplication.shared.terminate(nil) } @@ -276,24 +292,6 @@ struct MenuContent: View { self.loadingMics = false } - private func primarySessionKey() -> String { - // Prefer canonical main session; fall back to most recent. - let storePath = SessionLoader.defaultStorePath - if let data = try? Data(contentsOf: URL(fileURLWithPath: storePath)), - let decoded = try? JSONDecoder().decode([String: SessionEntryRecord].self, from: data) - { - if decoded.keys.contains("main") { return "main" } - - let sorted = decoded.sorted { a, b -> Bool in - let lhs = a.value.updatedAt ?? 0 - let rhs = b.value.updatedAt ?? 0 - return lhs > rhs - } - if let first = sorted.first { return first.key } - } - return "+1003" - } - private struct AudioInputDevice: Identifiable, Equatable { let uid: String let name: String