mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 06:16:07 +00:00
283 lines
11 KiB
Swift
283 lines
11 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import SwiftUI
|
|
|
|
enum DebugActions {
|
|
private static let verboseDefaultsKey = "openclaw.debug.verboseMain"
|
|
private static let onboardingSeenKey = "openclaw.onboardingSeen"
|
|
|
|
@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().fileExists(atPath: path) else {
|
|
let alert = NSAlert()
|
|
alert.messageText = "Log file not found"
|
|
alert.informativeText = path
|
|
alert.runModal()
|
|
return
|
|
}
|
|
NSWorkspace.shared.activateFileViewerSelecting([url])
|
|
}
|
|
|
|
@MainActor
|
|
static func openConfigFolder() {
|
|
let url = OpenClawPaths.stateDirURL
|
|
NSWorkspace.shared.activateFileViewerSelecting([url])
|
|
}
|
|
|
|
@MainActor
|
|
static func openSessionStore() {
|
|
if AppStateStore.shared.connectionMode == .remote {
|
|
let alert = NSAlert()
|
|
alert.messageText = "Remote mode"
|
|
alert.informativeText = "Session store lives on the gateway host in remote mode."
|
|
alert.runModal()
|
|
return
|
|
}
|
|
let path = self.resolveSessionStorePath()
|
|
let url = URL(fileURLWithPath: path)
|
|
if FileManager().fileExists(atPath: path) {
|
|
NSWorkspace.shared.activateFileViewerSelecting([url])
|
|
} else {
|
|
NSWorkspace.shared.open(url.deletingLastPathComponent())
|
|
}
|
|
}
|
|
|
|
static func sendTestNotification() async {
|
|
_ = await NotificationManager().send(title: "OpenClaw", body: "Test notification", sound: nil)
|
|
}
|
|
|
|
static func sendDebugVoice() async -> Result<String, DebugActionError> {
|
|
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 result = await VoiceWakeForwarder.forward(transcript: message)
|
|
switch result {
|
|
case .success:
|
|
return .success("Sent. Await reply.")
|
|
case let .failure(error):
|
|
let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return .failure(.message("Send failed: \(detail)"))
|
|
}
|
|
}
|
|
|
|
static func restartGateway() {
|
|
Task { @MainActor in
|
|
switch AppStateStore.shared.connectionMode {
|
|
case .local:
|
|
GatewayProcessManager.shared.stop()
|
|
// Kick the control channel + health check so the UI recovers immediately.
|
|
await GatewayConnection.shared.shutdown()
|
|
try? await Task.sleep(nanoseconds: 300_000_000)
|
|
GatewayProcessManager.shared.setActive(true)
|
|
Task { try? await ControlChannel.shared.configure(mode: .local) }
|
|
Task { await HealthStore.shared.refresh(onDemand: true) }
|
|
|
|
case .remote:
|
|
// In remote mode, there is no local gateway to restart. "Restart Gateway" should
|
|
// reset the SSH control tunnel + reconnect so the menu recovers.
|
|
await RemoteTunnelManager.shared.stopAll()
|
|
await GatewayConnection.shared.shutdown()
|
|
do {
|
|
_ = try await RemoteTunnelManager.shared.ensureControlTunnel()
|
|
let settings = CommandResolver.connectionSettings()
|
|
try await ControlChannel.shared.configure(mode: .remote(
|
|
target: settings.target,
|
|
identity: settings.identity))
|
|
} catch {
|
|
// ControlChannel will surface a degraded state; also refresh health to update the menu text.
|
|
Task { await HealthStore.shared.refresh(onDemand: true) }
|
|
}
|
|
|
|
case .unconfigured:
|
|
await GatewayConnection.shared.shutdown()
|
|
await ControlChannel.shared.disconnect()
|
|
}
|
|
}
|
|
}
|
|
|
|
static func resetGatewayTunnel() async -> Result<String, DebugActionError> {
|
|
let mode = CommandResolver.connectionSettings().mode
|
|
guard mode == .remote else {
|
|
return .failure(.message("Remote mode is not enabled."))
|
|
}
|
|
await RemoteTunnelManager.shared.stopAll()
|
|
await GatewayConnection.shared.shutdown()
|
|
do {
|
|
_ = try await RemoteTunnelManager.shared.ensureControlTunnel()
|
|
let settings = CommandResolver.connectionSettings()
|
|
try await ControlChannel.shared.configure(mode: .remote(
|
|
target: settings.target,
|
|
identity: settings.identity))
|
|
await HealthStore.shared.refresh(onDemand: true)
|
|
return .success("SSH tunnel reset.")
|
|
} catch {
|
|
Task { await HealthStore.shared.refresh(onDemand: true) }
|
|
return .failure(.message(error.localizedDescription))
|
|
}
|
|
}
|
|
|
|
static func pinoLogPath() -> String {
|
|
LogLocator.bestLogFile()?.path ?? LogLocator.launchdLogPath
|
|
}
|
|
|
|
@MainActor
|
|
static func runHealthCheckNow() async {
|
|
await HealthStore.shared.refresh(onDemand: true)
|
|
}
|
|
|
|
static func sendTestHeartbeat() async -> Result<ControlHeartbeatEvent?, Error> {
|
|
do {
|
|
_ = await GatewayConnection.shared.setHeartbeatsEnabled(true)
|
|
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)
|
|
}
|
|
}
|
|
|
|
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()
|
|
// Relaunch shortly after this instance exits so we get a true restart even in debug.
|
|
task.launchPath = "/bin/sh"
|
|
task.arguments = ["-c", "sleep 0.2; open -n \"$1\"", "_", url.path]
|
|
try? task.run()
|
|
NSApp.terminate(nil)
|
|
}
|
|
|
|
@MainActor
|
|
static func restartOnboarding() {
|
|
UserDefaults.standard.set(false, forKey: self.onboardingSeenKey)
|
|
UserDefaults.standard.set(0, forKey: onboardingVersionKey)
|
|
AppStateStore.shared.onboardingSeen = false
|
|
OnboardingController.shared.restart()
|
|
}
|
|
|
|
@MainActor
|
|
private static func resolveSessionStorePath() -> String {
|
|
let defaultPath = SessionLoader.defaultStorePath
|
|
let configURL = OpenClawPaths.configURL
|
|
guard
|
|
let data = try? Data(contentsOf: configURL),
|
|
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let session = parsed["session"] as? [String: Any],
|
|
let path = session["store"] as? String,
|
|
!path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
else {
|
|
return defaultPath
|
|
}
|
|
return path
|
|
}
|
|
|
|
// MARK: - Port diagnostics
|
|
|
|
typealias PortListener = PortGuardian.ReportListener
|
|
typealias PortReport = PortGuardian.PortReport
|
|
|
|
static func checkGatewayPorts() async -> [PortReport] {
|
|
let mode = CommandResolver.connectionSettings().mode
|
|
return await PortGuardian.shared.diagnose(mode: mode)
|
|
}
|
|
|
|
static func killProcess(_ pid: Int) async -> Result<Void, DebugActionError> {
|
|
let primary = await ShellExecutor.run(command: ["kill", "-TERM", "\(pid)"], cwd: nil, env: nil, timeout: 2)
|
|
if primary.ok { return .success(()) }
|
|
let force = await ShellExecutor.run(command: ["kill", "-KILL", "\(pid)"], cwd: nil, env: nil, timeout: 2)
|
|
if force.ok { return .success(()) }
|
|
let detail = force.message ?? primary.message ?? "kill failed"
|
|
return .failure(.message(detail))
|
|
}
|
|
|
|
#if DEBUG
|
|
@MainActor
|
|
static func showPairingPanelDemo() {
|
|
let now = Date()
|
|
PairingApprovalCenter.shared.injectDemoCards([
|
|
PairingApprovalCenter.Card(
|
|
kind: .node,
|
|
requestId: "demo-node-1",
|
|
subjectId: "19cec1c3301a7469d4fd71f5f81339508390dadda91b34aee15faf2849dccdc7",
|
|
displayName: "Peter's MacBook Pro",
|
|
platform: "macos 26.5",
|
|
deviceFamily: "Mac",
|
|
modelIdentifier: "MacBookPro18,3",
|
|
version: "2026.6.11",
|
|
coreVersion: "2026.6.10",
|
|
remoteIp: "192.168.1.42",
|
|
role: nil,
|
|
scopes: [],
|
|
caps: ["screen", "camera", "file"],
|
|
commands: ["system.run", "system.notify"],
|
|
isRepair: false,
|
|
previouslyPaired: false,
|
|
requestedAt: now.addingTimeInterval(-45)),
|
|
PairingApprovalCenter.Card(
|
|
kind: .device,
|
|
requestId: "demo-device-1",
|
|
subjectId: "4a865684dbfa7b7937bd333813476ca88b672c2d02ad08fc52b80d88af4e82bd",
|
|
displayName: "OpenClaw iPhone",
|
|
platform: "ios 26.4",
|
|
deviceFamily: nil,
|
|
modelIdentifier: nil,
|
|
version: nil,
|
|
coreVersion: nil,
|
|
remoteIp: "192.168.1.87",
|
|
role: "operator",
|
|
scopes: ["operator.read", "operator.write", "operator.approvals"],
|
|
caps: [],
|
|
commands: [],
|
|
isRepair: true,
|
|
previouslyPaired: true,
|
|
requestedAt: now.addingTimeInterval(-190)),
|
|
])
|
|
}
|
|
#endif
|
|
}
|
|
|
|
enum DebugActionError: LocalizedError {
|
|
case message(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case let .message(text):
|
|
text
|
|
}
|
|
}
|
|
}
|