Menu: add more debug utilities
This commit is contained in:
@@ -3,6 +3,8 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum DebugActions {
|
enum DebugActions {
|
||||||
|
private static let verboseDefaultsKey = "clawdis.debug.verboseMain"
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func openAgentEventsWindow() {
|
static func openAgentEventsWindow() {
|
||||||
let window = NSWindow(
|
let window = NSWindow(
|
||||||
@@ -32,6 +34,25 @@ enum DebugActions {
|
|||||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
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 {
|
static func sendTestNotification() async {
|
||||||
_ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil)
|
_ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil)
|
||||||
}
|
}
|
||||||
@@ -101,6 +122,67 @@ enum DebugActions {
|
|||||||
if FileManager.default.fileExists(atPath: rolling) { return rolling }
|
if FileManager.default.fileExists(atPath: rolling) { return rolling }
|
||||||
return "/tmp/clawdis.log"
|
return "/tmp/clawdis.log"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
static func runHealthCheckNow() async {
|
||||||
|
await HealthStore.shared.refresh(onDemand: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func sendTestHeartbeat() async -> Result<ControlHeartbeatEvent?, String> {
|
||||||
|
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 {
|
enum DebugActionError: LocalizedError {
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ struct DebugSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Button("Restart app") { self.relaunch() }
|
Button("Restart app") { DebugActions.restartApp() }
|
||||||
Button("Reveal app in Finder") { self.revealApp() }
|
Button("Reveal app in Finder") { self.revealApp() }
|
||||||
Button("Restart Gateway") { DebugActions.restartGateway() }
|
Button("Restart Gateway") { DebugActions.restartGateway() }
|
||||||
Button("Clear log") { GatewayProcessManager.shared.clearLog() }
|
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() {
|
private func revealApp() {
|
||||||
let url = Bundle.main.bundleURL
|
let url = Bundle.main.bundleURL
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
NSWorkspace.shared.activateFileViewerSelecting([url])
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ struct MenuContent: View {
|
|||||||
self.voiceWakeMicMenu
|
self.voiceWakeMicMenu
|
||||||
}
|
}
|
||||||
if AppStateStore.webChatEnabled {
|
if AppStateStore.webChatEnabled {
|
||||||
Button("Open Chat") { WebChatManager.shared.show(sessionKey: self.primarySessionKey()) }
|
Button("Open Chat") { WebChatManager.shared.show(sessionKey: WebChatManager.shared.preferredSessionKey()) }
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Button("Settings…") { self.open(tab: .general) }
|
Button("Settings…") { self.open(tab: .general) }
|
||||||
@@ -43,6 +43,21 @@ struct MenuContent: View {
|
|||||||
}
|
}
|
||||||
if self.state.debugPaneEnabled {
|
if self.state.debugPaneEnabled {
|
||||||
Menu("Debug") {
|
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 Agent Events…") { DebugActions.openAgentEventsWindow() }
|
||||||
Button("Open Log") { DebugActions.openLog() }
|
Button("Open Log") { DebugActions.openLog() }
|
||||||
Button("Send Debug Voice Text") {
|
Button("Send Debug Voice Text") {
|
||||||
@@ -53,6 +68,7 @@ struct MenuContent: View {
|
|||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Button("Restart Gateway") { DebugActions.restartGateway() }
|
Button("Restart Gateway") { DebugActions.restartGateway() }
|
||||||
|
Button("Restart App") { DebugActions.restartApp() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button("Quit") { NSApplication.shared.terminate(nil) }
|
Button("Quit") { NSApplication.shared.terminate(nil) }
|
||||||
@@ -276,24 +292,6 @@ struct MenuContent: View {
|
|||||||
self.loadingMics = false
|
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 {
|
private struct AudioInputDevice: Identifiable, Equatable {
|
||||||
let uid: String
|
let uid: String
|
||||||
let name: String
|
let name: String
|
||||||
|
|||||||
Reference in New Issue
Block a user