Files
openclaw/apps/macos/Sources/OpenClaw/DebugSettings.swift
Peter Steinberger bb46b79d3c refactor: internalize OpenClaw agent runtime (#85341)
* refactor: extract agent core package

Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts.

* refactor: extract shared llm runtime

Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout.

* refactor: remove pi runtime internals

Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code.

* refactor: tighten agent session runtime

Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts.

* refactor: remove static model and pi auth paths

Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities.

* refactor: remove legacy provider compat paths

* docs: remove agent parity notes

* fix: skip provider wildcard metadata parsing

* refactor: share session extension sdk loading

* refactor: inline acpx proxy error formatter

* refactor: fold edit recovery into edit tool

* fix: accept extension batch separator

* test: align startup provider plugin expectations

* fix: restore provider-scoped release discovery

* test: align static asset packaging expectations

* fix: run static provider catalogs during scoped discovery

* fix: add provider entry catalogs for scoped live discovery

* fix: load lightweight provider catalog entries

* fix: refresh provider-scoped plugin metadata

* fix: keep provider catalog entries on release live path

* fix: keep static manifest models in release live checks

* fix: harden release model discovery

* fix: reduce OpenAI live cache probe reasoning

* fix: disable OpenAI cache probe reasoning

* ci: extend OpenAI gateway live timeout

* fix: extend live gateway model budget

* fix: stabilize release validation regressions

* fix: honor provider aliases in model rows

* fix: stabilize release validation lanes

* fix: stabilize release memory qa

* ci: stabilize release validation lanes

* ci: prefer ipv4 for live docker node calls

* fix: restore shared tool-call stream wrapper

* ci: remove legacy pi test shard alias

* fix: clean up embedded agent test drift

* fix: stabilize runtime alias status

* fix: clean up embedded agent ci drift

* fix: restore release ci invariants

* fix: clean up post-rebase runtime drift

* fix: restore release ci checks

* fix: restore release ci after rebase

* fix: remove stale pi runtime path

* test: align compaction runtime expectations

* test: update plugin prerelease expectations

* fix: handle claude live tool approvals

* fix: stabilize release validation gates

* fix: finish agent runtime import

* test: finish post-rebase agent runtime mocks

* fix: keep codex compaction native

* fix: stabilize codex app-server hook tests

* test: isolate codex diagnostic active run

* test: remove codex diagnostic completion race

# Conflicts:
#	extensions/codex/src/app-server/run-attempt.test.ts

* ci: fix full release manifest performance run id

* refactor: narrow llm plugin sdk boundary

* chore: drop generated google boundary stamps

* fix: repair rebase fallout

* fix: clean up rebased runtime references

* fix: decode codex jwt payloads as base64url

* fix: preserve shipped pi runtime alias

* fix: add scoped sdk virtual modules

* fix: decode llm codex oauth jwt as base64url

* fix: avoid stale vertex adc negative cache

* fix: harden tool arg decoding and codeql path

* fix: keep vertex adc negative checks live

* refactor: consolidate codex jwt and edit helpers

* fix: await codex oauth node runtime imports

* fix: preserve sdk tool and notice contracts

* fix: preserve shipped compat config boundaries

* fix: align codex oauth callback host

* fix: terminate agent-core loop streams on failure

* fix: keep codex oauth callback alive during fallback

* ci: include session tools in critical codeql scans

* fix: keep Cloudflare Anthropic provider auth header

* docs: redirect legacy pi runtime pages

* fix: honor bundled web provider compat discovery

* fix: protect session output spill files

* fix: keep legacy agent dir env blocked

* fix: contain auto-discovered skill symlinks

* fix: harden agent core sdk proxy surfaces

* fix: restore approval reaction sdk compat

* fix: keep live docker runs bounded

* fix: keep codex oauth redirect host aligned

* fix: resolve post-rebase agent runtime drift

* fix: redact anthropic oauth parse failures

* fix: preserve responses strict tool shaping

* fix: repair agent runtime rebase cleanup

* docs: redirect retired parity pages

* fix: bound auto-discovered resources to roots

* fix: repair post-rebase agent test drift

* fix: preserve bundled provider allowlist migration

* fix: preserve manifest-owned provider aliases

* fix: declare photon image dependency

* fix: keep provider headers out of proxy body

* fix: preserve shipped env aliases

* fix: refresh control ui i18n generated state

* fix: quote read fallback paths

* fix: preview edits through configured backend

* test: satisfy core test typecheck

* fix: preserve ZAI usage auth fallback

* test: repair codex diagnostic test

* fix: repair agent runtime rebase drift

* test: finish embedded runner import rename

* fix: repair agent runtime rebase integrations

* test: align compaction oauth fallback expectations

* fix: allow sdk-auth session models

* fix: update doctor tool schema import

* fix: preserve bedrock plugin region

* fix: stream harmony-like prose immediately

* ci: include session runtime in codeql shards

* fix: repair latest rebase integrations

* fix: honor explicit codex websocket transport

* fix: keep openai-compatible credentials provider-scoped

* fix: refresh sdk api baseline after rebase

* fix: route cli runtime aliases through openclaw harness

* test: rename stale harness mock expectation

* test: rename embedded agent overflow calls

* test: clean embedded auth test wording

* test: use openclaw stream types in deepinfra cache test

* fix: refresh sdk api baseline on latest main

* fix: honor bundled discovery compat allowlists

* fix: refresh sdk api baseline after latest rebase

* fix: remove stale rebase imports

* test: rename stale model catalog mock

* test: mock renamed doctor runtime modules

* fix: map canonical kimi env auth

* fix: use internal model registry in bench script

* fix: migrate deepinfra provider catalog entry

* fix: enforce builtin tool suppression

* fix: route compaction auth and proxy payloads safely

* refactor: prune unused llm registry leftovers

* test: update codex hooks session import

* test: fix model picker ci coverage

* test: align model picker auth mock types
2026-05-27 19:24:04 +01:00

1018 lines
41 KiB
Swift

import AppKit
import Observation
import SwiftUI
struct DebugSettings: View {
@Bindable var state: AppState
private let isPreview = ProcessInfo.processInfo.isPreview
private let labelColumnWidth: CGFloat = 140
@AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue
@AppStorage(canvasEnabledKey) private var canvasEnabled: Bool = true
private let gatewayManager = GatewayProcessManager.shared
private let healthStore = HealthStore.shared
@State private var launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled()
@State private var launchAgentWriteError: String?
@State private var gatewayRootInput: String = GatewayProcessManager.shared.projectRootPath()
@State private var sessionStorePath: String = SessionLoader.defaultStorePath
@State private var sessionStoreSaveError: String?
@State private var debugSendInFlight = false
@State private var debugSendStatus: String?
@State private var debugSendError: String?
@State private var portCheckInFlight = false
@State private var portReports: [DebugActions.PortReport] = []
@State private var portKillStatus: String?
@State private var tunnelResetInFlight = false
@State private var tunnelResetStatus: String?
@State private var pendingKill: DebugActions.PortListener?
@AppStorage(debugFileLogEnabledKey) private var diagnosticsFileLogEnabled: Bool = false
@AppStorage(appLogLevelKey) private var appLogLevelRaw: String = AppLogLevel.default.rawValue
@State private var canvasSessionKey: String = "main"
@State private var canvasStatus: String?
@State private var canvasError: String?
@State private var canvasEvalJS: String = "document.title"
@State private var canvasEvalResult: String?
@State private var canvasSnapshotPath: String?
init(state: AppState = AppStateStore.shared) {
self.state = state
}
var body: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 14) {
self.header
self.overviewSection
self.launchdSection
self.appInfoSection
self.gatewaySection
self.logsSection
self.portsSection
self.pathsSection
self.quickActionsSection
self.canvasSection
self.experimentsSection
Spacer(minLength: 0)
}
.settingsDetailContent()
.groupBoxStyle(PlainSettingsGroupBoxStyle())
}
.task {
guard !self.isPreview else { return }
self.loadSessionStorePath()
}
.alert(item: self.$pendingKill) { listener in
Alert(
title: Text("Kill \(listener.command) (\(listener.pid))?"),
message: Text("This process looks expected for the current mode. Kill anyway?"),
primaryButton: .destructive(Text("Kill")) {
Task { await self.killConfirmed(listener.pid) }
},
secondaryButton: .cancel())
}
}
private var launchdSection: some View {
GroupBox("Gateway startup") {
VStack(alignment: .leading, spacing: 8) {
Toggle("Attach only (skip launchd install)", isOn: self.$launchAgentWriteDisabled)
.onChange(of: self.launchAgentWriteDisabled) { _, newValue in
self.launchAgentWriteError = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(newValue)
if self.launchAgentWriteError != nil {
self.launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled()
return
}
}
Text(
"When enabled, OpenClaw won't install or manage \(gatewayLaunchdLabel). " +
"It will only attach to an existing Gateway.")
.font(.caption)
.foregroundStyle(.secondary)
if let launchAgentWriteError {
Text(launchAgentWriteError)
.font(.caption)
.foregroundStyle(.red)
}
}
}
}
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Debug")
.font(.title3.weight(.semibold))
Text("Tools for diagnosing local issues (Gateway, ports, logs, Canvas).")
.font(.callout)
.foregroundStyle(.secondary)
}
}
private var overviewSection: some View {
HStack(spacing: 12) {
DebugMetricCard(
title: "App Health",
value: self.healthStore.state.debugTitle,
icon: "heart.text.square",
tint: self.healthStore.state.tint,
subtitle: self.healthStore.summaryLine)
DebugMetricCard(
title: "Gateway",
value: self.gatewayManager.status.label,
icon: "antenna.radiowaves.left.and.right",
tint: self.gatewayManager.status.debugTint,
subtitle: self.canRestartGateway ? "Local process" : "Remote connection")
DebugMetricCard(
title: "App PID",
value: "\(ProcessInfo.processInfo.processIdentifier)",
icon: "number.square",
tint: .blue,
subtitle: Bundle.main.bundleURL.lastPathComponent)
}
}
private func gridLabel(_ text: String) -> some View {
Text(text)
.foregroundStyle(.secondary)
.frame(width: self.labelColumnWidth, alignment: .leading)
}
private var appInfoSection: some View {
GroupBox("App") {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Health")
HStack(spacing: 8) {
Circle().fill(self.healthStore.state.tint).frame(width: 10, height: 10)
Text(self.healthStore.summaryLine)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
GridRow {
self.gridLabel("CLI")
let loc = CLIInstaller.installedLocation()
Text(loc ?? "missing")
.font(.caption.monospaced())
.foregroundStyle(loc == nil ? Color.red : Color.secondary)
.textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
}
GridRow {
self.gridLabel("PID")
Text("\(ProcessInfo.processInfo.processIdentifier)")
}
GridRow {
self.gridLabel("Binary path")
Text(Bundle.main.bundlePath)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
}
}
}
}
private var gatewaySection: some View {
GroupBox("Gateway") {
VStack(alignment: .leading, spacing: 10) {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Status")
HStack(spacing: 8) {
Text(self.gatewayManager.status.label)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
let key = DeepLinkHandler.currentKey()
HStack(spacing: 8) {
Text("Key")
.foregroundStyle(.secondary)
.frame(width: self.labelColumnWidth, alignment: .leading)
Text(key)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
Button("Copy") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(key, forType: .string)
}
.buttonStyle(.bordered)
Button("Copy sample URL") {
let msg = "Hello from deep link"
let encoded = msg.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? msg
let url = "openclaw://agent?message=\(encoded)&key=\(key)"
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(url, forType: .string)
}
.buttonStyle(.bordered)
Spacer(minLength: 0)
}
Text("Deep links (openclaw://…) are always enabled; the key controls unattended runs.")
.font(.caption2)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 6) {
Text("Stdout / stderr")
.font(.caption.weight(.semibold))
ScrollView {
Text(self.gatewayManager.log.isEmpty ? "" : self.gatewayManager.log)
.font(.caption.monospaced())
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
.frame(height: 130)
.background(.black.opacity(0.12), in: RoundedRectangle(cornerRadius: 8, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(.white.opacity(0.06))
}
HStack(spacing: 8) {
if self.canRestartGateway {
Button("Restart Gateway") { DebugActions.restartGateway() }
}
Button("Clear log") { GatewayProcessManager.shared.clearLog() }
Spacer(minLength: 0)
}
.buttonStyle(.bordered)
}
}
}
}
private var logsSection: some View {
GroupBox("Logs") {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Pino log")
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
Button("Open") { DebugActions.openLog() }
.buttonStyle(.bordered)
Text(DebugActions.pinoLogPath())
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
}
}
}
GridRow {
self.gridLabel("App logging")
VStack(alignment: .leading, spacing: 8) {
Picker("Verbosity", selection: self.$appLogLevelRaw) {
ForEach(AppLogLevel.allCases) { level in
Text(level.title).tag(level.rawValue)
}
}
.pickerStyle(.menu)
.labelsHidden()
.help("Controls the macOS app log verbosity.")
Toggle("Write rolling diagnostics log (JSONL)", isOn: self.$diagnosticsFileLogEnabled)
.toggleStyle(.checkbox)
.help(
"Writes a rotating, local-only log under ~/Library/Logs/OpenClaw/. " +
"Enable only while actively debugging.")
HStack(spacing: 8) {
Button("Open folder") {
NSWorkspace.shared.open(DiagnosticsFileLog.logDirectoryURL())
}
.buttonStyle(.bordered)
Button("Clear") {
Task { try? await DiagnosticsFileLog.shared.clear() }
}
.buttonStyle(.bordered)
}
Text(DiagnosticsFileLog.logFileURL().path)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
}
}
}
}
}
private var portsSection: some View {
GroupBox("Ports") {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 8) {
Text("Port diagnostics")
.font(.caption.weight(.semibold))
if self.portCheckInFlight { ProgressView().controlSize(.small) }
Spacer()
Button("Check gateway ports") {
Task { await self.runPortCheck() }
}
.buttonStyle(.borderedProminent)
.disabled(self.portCheckInFlight)
Button("Reset SSH tunnel") {
Task { await self.resetGatewayTunnel() }
}
.buttonStyle(.bordered)
.disabled(self.tunnelResetInFlight || !self.isRemoteMode)
}
if let portKillStatus {
Text(portKillStatus)
.font(.caption2)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
if let tunnelResetStatus {
Text(tunnelResetStatus)
.font(.caption2)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
if self.portReports.isEmpty, !self.portCheckInFlight {
Text("Check which process owns \(GatewayEnvironment.gatewayPort()) and suggest fixes.")
.font(.caption2)
.foregroundStyle(.secondary)
} else {
ForEach(self.portReports) { report in
VStack(alignment: .leading, spacing: 4) {
Text("Port \(report.port)")
.font(.footnote.weight(.semibold))
Text(report.summary)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
ForEach(report.listeners) { listener in
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 8) {
Text("\(listener.command) (\(listener.pid))")
.font(.caption.monospaced())
.foregroundStyle(listener.expected ? .secondary : Color.red)
.lineLimit(1)
Spacer()
Button("Kill") {
self.requestKill(listener)
}
.buttonStyle(.bordered)
}
Text(listener.fullCommand)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(2)
.truncationMode(.middle)
}
.padding(6)
.background(Color.secondary.opacity(0.05))
.cornerRadius(4)
}
}
.padding(8)
.background(Color.secondary.opacity(0.08))
.cornerRadius(6)
}
}
}
}
}
private var pathsSection: some View {
GroupBox("Paths") {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 6) {
Text("OpenClaw project root")
.font(.caption.weight(.semibold))
HStack(spacing: 8) {
TextField("Path to openclaw repo", text: self.$gatewayRootInput)
.textFieldStyle(.roundedBorder)
.font(.caption.monospaced())
.onSubmit { self.saveRelayRoot() }
Button("Save") { self.saveRelayRoot() }
.buttonStyle(.borderedProminent)
Button("Reset") {
let def = FileManager().homeDirectoryForCurrentUser
.appendingPathComponent("Projects/openclaw").path
self.gatewayRootInput = def
self.saveRelayRoot()
}
.buttonStyle(.bordered)
}
Text("Used for pnpm/node fallback and PATH population when launching the gateway.")
.font(.caption2)
.foregroundStyle(.secondary)
}
Divider()
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Session store")
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
TextField("Path", text: self.$sessionStorePath)
.textFieldStyle(.roundedBorder)
.font(.caption.monospaced())
.frame(width: 360)
Button("Save") { self.saveSessionStorePath() }
.buttonStyle(.borderedProminent)
}
if let sessionStoreSaveError {
Text(sessionStoreSaveError)
.font(.footnote)
.foregroundStyle(.secondary)
} else {
Text("Used by the CLI session loader; stored in ~/.openclaw/openclaw.json.")
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
}
}
}
}
private var quickActionsSection: some View {
GroupBox("Quick actions") {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 8) {
Button("Send Test Notification") {
Task { await DebugActions.sendTestNotification() }
}
.buttonStyle(.bordered)
Button("Open Agent Events") {
DebugActions.openAgentEventsWindow()
}
.buttonStyle(.borderedProminent)
Spacer(minLength: 0)
}
VStack(alignment: .leading, spacing: 6) {
Button {
Task { await self.sendVoiceDebug() }
} label: {
Label(
self.debugSendInFlight ? "Sending debug voice…" : "Send debug voice",
systemImage: self.debugSendInFlight ? "bolt.horizontal.circle" : "waveform")
}
.buttonStyle(.borderedProminent)
.disabled(self.debugSendInFlight)
if !self.debugSendInFlight {
if let debugSendStatus {
Text(debugSendStatus)
.font(.caption)
.foregroundStyle(.secondary)
} else if let debugSendError {
Text(debugSendError)
.font(.caption)
.foregroundStyle(.red)
} else {
Text(
"""
Uses the Voice Wake path: forwards over SSH when configured,
otherwise runs locally via rpc.
""")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
VStack(alignment: .leading, spacing: 6) {
Text(
"Note: macOS may require restarting OpenClaw after enabling Accessibility or Screen Recording.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Button {
LaunchdManager.startOpenClaw()
} label: {
Label("Restart OpenClaw", systemImage: "arrow.counterclockwise")
}
.buttonStyle(.bordered)
.controlSize(.small)
}
HStack(spacing: 8) {
Button("Restart app") { DebugActions.restartApp() }
Button("Restart onboarding") { DebugActions.restartOnboarding() }
Button("Reveal app in Finder") { self.revealApp() }
Spacer(minLength: 0)
}
.buttonStyle(.bordered)
}
}
}
private var canvasSection: some View {
GroupBox("Canvas") {
VStack(alignment: .leading, spacing: 10) {
Text("Enable/disable Canvas in General settings.")
.font(.caption)
.foregroundStyle(.secondary)
HStack(spacing: 8) {
TextField("Session", text: self.$canvasSessionKey)
.textFieldStyle(.roundedBorder)
.font(.caption.monospaced())
.frame(width: 160)
Button("Show panel") {
Task { await self.canvasPresent() }
}
.buttonStyle(.borderedProminent)
Button("Hide panel") {
CanvasManager.shared.hideAll()
self.canvasStatus = "hidden"
self.canvasError = nil
}
.buttonStyle(.bordered)
Button("Write sample page") {
Task { await self.canvasWriteSamplePage() }
}
.buttonStyle(.bordered)
Spacer(minLength: 0)
}
HStack(spacing: 8) {
TextField("Eval JS", text: self.$canvasEvalJS)
.textFieldStyle(.roundedBorder)
.font(.caption.monospaced())
.frame(maxWidth: 520)
Button("Eval") {
Task { await self.canvasEval() }
}
.buttonStyle(.bordered)
Button("Snapshot") {
Task { await self.canvasSnapshot() }
}
.buttonStyle(.bordered)
Spacer(minLength: 0)
}
if let canvasStatus {
Text(canvasStatus)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
if let canvasEvalResult {
Text("eval → \(canvasEvalResult)")
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(2)
.truncationMode(.middle)
.textSelection(.enabled)
}
if let canvasSnapshotPath {
HStack(spacing: 8) {
Text("snapshot → \(canvasSnapshotPath)")
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
.textSelection(.enabled)
Button("Reveal") {
NSWorkspace.shared
.activateFileViewerSelecting([URL(fileURLWithPath: canvasSnapshotPath)])
}
.buttonStyle(.bordered)
Spacer(minLength: 0)
}
}
if let canvasError {
Text(canvasError)
.font(.caption2)
.foregroundStyle(.red)
} else {
Text("Tip: the session directory is returned by “Show panel”.")
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
}
}
private var experimentsSection: some View {
GroupBox("Experiments") {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Icon override")
Picker("", selection: self.bindingOverride) {
ForEach(IconOverrideSelection.allCases) { option in
Text(option.label).tag(option.rawValue)
}
}
.labelsHidden()
.frame(maxWidth: 280, alignment: .leading)
}
GridRow {
self.gridLabel("Chat")
Text("Native SwiftUI")
.font(.callout)
.foregroundStyle(.secondary)
}
}
}
}
@MainActor
private func runPortCheck() async {
self.portCheckInFlight = true
self.portKillStatus = nil
let reports = await DebugActions.checkGatewayPorts()
self.portReports = reports
self.portCheckInFlight = false
}
@MainActor
private func resetGatewayTunnel() async {
self.tunnelResetInFlight = true
self.tunnelResetStatus = nil
let result = await DebugActions.resetGatewayTunnel()
switch result {
case let .success(message):
self.tunnelResetStatus = message
case let .failure(err):
self.tunnelResetStatus = err.localizedDescription
}
await self.runPortCheck()
self.tunnelResetInFlight = false
}
@MainActor
private func requestKill(_ listener: DebugActions.PortListener) {
if listener.expected {
self.pendingKill = listener
} else {
Task { await self.killConfirmed(listener.pid) }
}
}
@MainActor
private func killConfirmed(_ pid: Int32) async {
let result = await DebugActions.killProcess(Int(pid))
switch result {
case .success:
self.portKillStatus = "Sent kill to \(pid)."
await self.runPortCheck()
case let .failure(err):
self.portKillStatus = "Kill \(pid) failed: \(err.localizedDescription)"
}
}
private func sendVoiceDebug() async {
await MainActor.run {
self.debugSendInFlight = true
self.debugSendError = nil
self.debugSendStatus = nil
}
let result = await DebugActions.sendDebugVoice()
await MainActor.run {
self.debugSendInFlight = false
switch result {
case let .success(message):
self.debugSendStatus = message
self.debugSendError = nil
case let .failure(error):
self.debugSendStatus = nil
self.debugSendError = error.localizedDescription
}
}
}
private func revealApp() {
let url = Bundle.main.bundleURL
NSWorkspace.shared.activateFileViewerSelecting([url])
}
private func saveRelayRoot() {
GatewayProcessManager.shared.setProjectRoot(path: self.gatewayRootInput)
}
private func loadSessionStorePath() {
let parsed = OpenClawConfigFile.loadDict()
guard
let session = parsed["session"] as? [String: Any],
let path = session["store"] as? String
else {
self.sessionStorePath = SessionLoader.defaultStorePath
return
}
self.sessionStorePath = path
}
private func saveSessionStorePath() {
let trimmed = self.sessionStorePath.trimmingCharacters(in: .whitespacesAndNewlines)
var root = OpenClawConfigFile.loadDict()
var session = root["session"] as? [String: Any] ?? [:]
session["store"] = trimmed.isEmpty ? SessionLoader.defaultStorePath : trimmed
root["session"] = session
guard OpenClawConfigFile.saveDict(root) else {
self.sessionStoreSaveError = "Config write rejected to protect gateway auth/mode."
return
}
self.sessionStoreSaveError = nil
}
private var bindingOverride: Binding<String> {
Binding {
self.iconOverrideRaw
} set: { newValue in
self.iconOverrideRaw = newValue
if let selection = IconOverrideSelection(rawValue: newValue) {
Task { @MainActor in
AppStateStore.shared.iconOverride = selection
WorkActivityStore.shared.resolveIconState(override: selection)
}
}
}
}
private var isRemoteMode: Bool {
CommandResolver.connectionSettings().mode == .remote
}
private var canRestartGateway: Bool {
self.state.connectionMode == .local
}
}
extension DebugSettings {
// MARK: - Canvas debug actions
@MainActor
private func canvasPresent() async {
self.canvasError = nil
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
do {
let dir = try CanvasManager.shared.show(sessionKey: session.isEmpty ? "main" : session, path: "/")
self.canvasStatus = "dir: \(dir)"
} catch {
self.canvasError = error.localizedDescription
}
}
@MainActor
private func canvasWriteSamplePage() async {
self.canvasError = nil
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
do {
let dir = try CanvasManager.shared.show(sessionKey: session.isEmpty ? "main" : session, path: "/")
let url = URL(fileURLWithPath: dir).appendingPathComponent("index.html", isDirectory: false)
let now = ISO8601DateFormatter().string(from: Date())
let html = """
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Canvas Debug</title>
<style>
:root { color-scheme: dark; }
html,body { height:100%; margin:0; background:#0b1020; color:#e5e7eb; }
body { font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
.wrap { padding:16px; }
.row { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
.pill { padding:6px 10px; border-radius:999px; background:rgba(255,255,255,.08);
border:1px solid rgba(255,255,255,.12); }
button { background:#22c55e; color:#04110a; border:0; border-radius:10px;
padding:8px 10px; font-weight:700; cursor:pointer; }
button:active { transform: translateY(1px); }
.panel { margin-top:14px; padding:14px; border-radius:14px; background:rgba(255,255,255,.06);
border:1px solid rgba(255,255,255,.1); }
.grid { display:grid; grid-template-columns: repeat(12, 1fr); gap:10px; margin-top:12px; }
.box { grid-column: span 4; height:80px; border-radius:14px;
background: linear-gradient(135deg, rgba(59,130,246,.35), rgba(168,85,247,.25));
border:1px solid rgba(255,255,255,.12); }
.muted { color: rgba(229,231,235,.7); }
</style>
</head>
<body>
<div class="wrap">
<div class="row">
<div class="pill">Canvas Debug</div>
<div class="pill muted">generated: \(now)</div>
<div class="pill muted">userAgent: <span id="ua"></span></div>
<button id="btn">Click me</button>
<div class="pill">count: <span id="count">0</span></div>
</div>
<div class="panel">
<div class="muted">This is a local file served by the WKURLSchemeHandler.</div>
<div class="grid">
<div class="box"></div><div class="box"></div><div class="box"></div>
<div class="box"></div><div class="box"></div><div class="box"></div>
</div>
</div>
</div>
<script>
document.getElementById('ua').textContent = navigator.userAgent;
let n = 0;
document.getElementById('btn').addEventListener('click', () => {
n++;
document.getElementById('count').textContent = String(n);
document.title = 'Canvas Debug (' + n + ')';
});
</script>
</body>
</html>
"""
try html.write(to: url, atomically: true, encoding: .utf8)
self.canvasStatus = "wrote: \(url.path)"
_ = try CanvasManager.shared.show(sessionKey: session.isEmpty ? "main" : session, path: "/")
} catch {
self.canvasError = error.localizedDescription
}
}
@MainActor
private func canvasEval() async {
self.canvasError = nil
self.canvasEvalResult = nil
do {
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
let result = try await CanvasManager.shared.eval(
sessionKey: session.isEmpty ? "main" : session,
javaScript: self.canvasEvalJS)
self.canvasEvalResult = result
} catch {
self.canvasError = error.localizedDescription
}
}
@MainActor
private func canvasSnapshot() async {
self.canvasError = nil
self.canvasSnapshotPath = nil
do {
let session = self.canvasSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
let path = try await CanvasManager.shared.snapshot(
sessionKey: session.isEmpty ? "main" : session,
outPath: nil)
self.canvasSnapshotPath = path
} catch {
self.canvasError = error.localizedDescription
}
}
}
struct PlainSettingsGroupBoxStyle: GroupBoxStyle {
func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .leading, spacing: 12) {
configuration.label
.font(.subheadline.weight(.semibold))
.foregroundStyle(.secondary)
configuration.content
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.quaternary.opacity(0.34), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.white.opacity(0.055))
}
}
}
private struct DebugMetricCard: View {
let title: String
let value: String
let icon: String
let tint: Color
let subtitle: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: self.icon)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(self.tint)
.frame(width: 34, height: 34)
.background(self.tint.opacity(0.18), in: Circle())
VStack(alignment: .leading, spacing: 3) {
Text(self.title)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Text(self.value)
.font(.callout.weight(.semibold))
.lineLimit(1)
.truncationMode(.tail)
Text(self.subtitle)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.quaternary.opacity(0.28), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.white.opacity(0.055))
}
}
}
extension HealthState {
fileprivate var debugTitle: String {
switch self {
case .unknown: "Unknown"
case .ok: "Healthy"
case .linkingNeeded: "Needs Link"
case .degraded: "Degraded"
}
}
}
extension GatewayProcessManager.Status {
fileprivate var debugTint: Color {
switch self {
case .running, .attachedExisting: .green
case .starting: .orange
case .failed: .red
case .stopped: .secondary
}
}
}
#if DEBUG
struct DebugSettings_Previews: PreviewProvider {
static var previews: some View {
DebugSettings(state: .preview)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
@MainActor
extension DebugSettings {
static func exerciseForTesting() async {
let view = DebugSettings(state: .preview)
view.gatewayRootInput = "/tmp/openclaw"
view.sessionStorePath = "/tmp/sessions.json"
view.sessionStoreSaveError = "Save failed"
view.debugSendInFlight = true
view.debugSendStatus = "Sent"
view.debugSendError = "Failed"
view.portCheckInFlight = true
view.portReports = [
DebugActions.PortReport(
port: GatewayEnvironment.gatewayPort(),
expected: "Gateway websocket (node/tsx)",
status: .missing("Missing"),
listeners: []),
]
view.portKillStatus = "Killed"
view.pendingKill = DebugActions.PortListener(
pid: 1,
command: "node",
fullCommand: "node",
user: nil,
expected: true)
view.canvasSessionKey = "main"
view.canvasStatus = "Canvas ok"
view.canvasError = "Canvas error"
view.canvasEvalJS = "document.title"
view.canvasEvalResult = "Canvas"
view.canvasSnapshotPath = "/tmp/snapshot.png"
_ = view.body
_ = view.header
_ = view.overviewSection
_ = view.appInfoSection
_ = view.gatewaySection
_ = view.logsSection
_ = view.portsSection
_ = view.pathsSection
_ = view.quickActionsSection
_ = view.canvasSection
_ = view.experimentsSection
_ = view.gridLabel("Test")
view.loadSessionStorePath()
}
}
#endif