refactor(mac): reorganize debug settings

This commit is contained in:
Peter Steinberger
2025-12-13 17:35:52 +00:00
parent 050c47d3a7
commit fa1110e4d3

View File

@@ -4,6 +4,7 @@ import UniformTypeIdentifiers
struct DebugSettings: View { struct DebugSettings: View {
private let isPreview = ProcessInfo.processInfo.isPreview private let isPreview = ProcessInfo.processInfo.isPreview
private let labelColumnWidth: CGFloat = 140
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath @AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
@AppStorage(modelCatalogReloadKey) private var modelCatalogReloadBump: Int = 0 @AppStorage(modelCatalogReloadKey) private var modelCatalogReloadBump: Int = 0
@AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue @AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue
@@ -37,29 +38,194 @@ struct DebugSettings: View {
var body: some View { var body: some View {
ScrollView(.vertical) { ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 14) {
LabeledContent("Health") { self.header
self.appInfoSection
self.gatewaySection
self.logsSection
self.portsSection
self.pathsSection
self.quickActionsSection
self.canvasSection
self.experimentsSection
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 24)
.padding(.vertical, 18)
}
.task {
guard !self.isPreview else { return }
await self.reloadModels()
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 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 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) { HStack(spacing: 8) {
Circle().fill(self.healthStore.state.tint).frame(width: 10, height: 10) Circle().fill(self.healthStore.state.tint).frame(width: 10, height: 10)
Text(self.healthStore.summaryLine) Text(self.healthStore.summaryLine)
} }
.frame(maxWidth: .infinity, alignment: .leading)
} }
LabeledContent("CLI helper") { GridRow {
self.gridLabel("CLI helper")
let loc = CLIInstaller.installedLocation() let loc = CLIInstaller.installedLocation()
Text(loc ?? "missing") Text(loc ?? "missing")
.font(.caption.monospaced()) .font(.caption.monospaced())
.foregroundStyle(loc == nil ? Color.red : Color.secondary) .foregroundStyle(loc == nil ? Color.red : Color.secondary)
.textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
} }
LabeledContent("PID") { Text("\(ProcessInfo.processInfo.processIdentifier)") } GridRow {
LabeledContent("Log file") { self.gridLabel("PID")
Button("Open pino log") { DebugActions.openLog() } Text("\(ProcessInfo.processInfo.processIdentifier)")
.help(DebugActions.pinoLogPath()) }
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)
Text("Restarts: \(self.gatewayManager.restartCount)")
.font(.caption2)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
GridRow {
self.gridLabel("Attach only")
Toggle("Only attach (dont spawn locally)", isOn: self.$attachExistingGatewayOnly)
.toggleStyle(.switch)
.help(
"When enabled in local mode, the mac app will only connect to an already-running gateway and will not start one itself.")
}
GridRow {
self.gridLabel("Deep links")
Toggle("Allow URL scheme (agent)", isOn: self.$deepLinkAgentEnabled)
.toggleStyle(.switch)
.help("Enables handling of clawdis://agent?... deep links to trigger an agent run.")
}
}
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 = "clawdis://agent?message=\(encoded)&key=\(key)"
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(url, forType: .string)
}
.buttonStyle(.bordered)
Spacer(minLength: 0)
}
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: 180)
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2)))
HStack(spacing: 8) {
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()) Text(DebugActions.pinoLogPath())
.font(.caption2.monospaced()) .font(.caption2.monospaced())
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.textSelection(.enabled) .textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
} }
LabeledContent("Diagnostics log") { }
}
GridRow {
self.gridLabel("Diagnostics")
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Toggle("Write rolling diagnostics log (JSONL)", isOn: self.$diagnosticsFileLogEnabled) Toggle("Write rolling diagnostics log (JSONL)", isOn: self.$diagnosticsFileLogEnabled)
.toggleStyle(.switch) .toggleStyle(.switch)
@@ -79,61 +245,17 @@ struct DebugSettings: View {
.font(.caption2.monospaced()) .font(.caption2.monospaced())
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.textSelection(.enabled) .textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
} }
} }
LabeledContent("Binary path") { Text(Bundle.main.bundlePath).font(.footnote) }
LabeledContent("Gateway status") {
HStack(spacing: 6) {
Text(self.gatewayManager.status.label)
Text("Restarts: \(self.gatewayManager.restartCount)")
.font(.caption2)
.foregroundStyle(.secondary)
} }
} }
Toggle("Only attach to existing gateway (dont spawn locally)", isOn: self.$attachExistingGatewayOnly)
.toggleStyle(.switch)
.help(
"When enabled in local mode, the mac app will only connect to an already-running gateway and will not start one itself.")
LabeledContent("URL scheme") {
VStack(alignment: .leading, spacing: 8) {
Toggle("Allow URL scheme (agent)", isOn: self.$deepLinkAgentEnabled)
.toggleStyle(.switch)
.help("Enables handling of clawdis://agent?... deep links to trigger an agent run.")
let key = DeepLinkHandler.currentKey()
HStack(spacing: 8) {
Text(key)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
Button("Copy key") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(key, forType: .string)
} }
.buttonStyle(.bordered)
} private var portsSection: some View {
Button("Copy sample agent URL") { GroupBox("Ports") {
let msg = "Hello from deep link" VStack(alignment: .leading, spacing: 10) {
let encoded = msg.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? msg
let url = "clawdis://agent?message=\(encoded)&key=\(key)"
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(url, forType: .string)
}
.buttonStyle(.bordered)
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Gateway 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: 180)
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2)))
}
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) { HStack(spacing: 8) {
Text("Port diagnostics") Text("Port diagnostics")
.font(.caption.weight(.semibold)) .font(.caption.weight(.semibold))
@@ -145,11 +267,14 @@ struct DebugSettings: View {
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.disabled(self.portCheckInFlight) .disabled(self.portCheckInFlight)
} }
if let portKillStatus { if let portKillStatus {
Text(portKillStatus) Text(portKillStatus)
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
} }
if self.portReports.isEmpty, !self.portCheckInFlight { if self.portReports.isEmpty, !self.portCheckInFlight {
Text("Check which process owns 18788/18789 and suggest fixes.") Text("Check which process owns 18788/18789 and suggest fixes.")
.font(.caption2) .font(.caption2)
@@ -162,6 +287,7 @@ struct DebugSettings: View {
Text(report.summary) Text(report.summary)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
ForEach(report.listeners) { listener in ForEach(report.listeners) { listener in
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 8) { HStack(spacing: 8) {
@@ -192,6 +318,12 @@ struct DebugSettings: View {
} }
} }
} }
}
}
private var pathsSection: some View {
GroupBox("Paths") {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text("Clawdis project root") Text("Clawdis project root")
.font(.caption.weight(.semibold)) .font(.caption.weight(.semibold))
@@ -214,13 +346,18 @@ struct DebugSettings: View {
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
LabeledContent("Session store") {
Divider()
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Session store")
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) { HStack(spacing: 8) {
TextField("Path", text: self.$sessionStorePath) TextField("Path", text: self.$sessionStorePath)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.font(.caption.monospaced()) .font(.caption.monospaced())
.frame(width: 340) .frame(width: 360)
Button("Save") { self.saveSessionStorePath() } Button("Save") { self.saveSessionStorePath() }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
} }
@@ -235,7 +372,8 @@ struct DebugSettings: View {
} }
} }
} }
LabeledContent("Model catalog") { GridRow {
self.gridLabel("Model catalog")
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(self.modelCatalogPath) Text(self.modelCatalogPath)
.font(.caption.monospaced()) .font(.caption.monospaced())
@@ -273,14 +411,28 @@ struct DebugSettings: View {
.foregroundStyle(.tertiary) .foregroundStyle(.tertiary)
} }
} }
}
}
}
}
private var quickActionsSection: some View {
GroupBox("Quick actions") {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 8) {
Button("Send Test Notification") { Button("Send Test Notification") {
Task { await DebugActions.sendTestNotification() } Task { await DebugActions.sendTestNotification() }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
Button("Open Agent Events") { Button("Open Agent Events") {
DebugActions.openAgentEventsWindow() DebugActions.openAgentEventsWindow()
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
Spacer(minLength: 0)
}
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Button { Button {
Task { await self.sendVoiceDebug() } Task { await self.sendVoiceDebug() }
@@ -312,21 +464,25 @@ struct DebugSettings: View {
} }
} }
} }
HStack {
HStack(spacing: 8) {
Button("Restart app") { DebugActions.restartApp() } Button("Restart app") { DebugActions.restartApp() }
Button("Reveal app in Finder") { self.revealApp() } Button("Reveal app in Finder") { self.revealApp() }
Button("Restart Gateway") { DebugActions.restartGateway() } Spacer(minLength: 0)
Button("Clear log") { GatewayProcessManager.shared.clearLog() }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
Divider() }
VStack(alignment: .leading, spacing: 8) { }
Text("Canvas") }
.font(.caption.weight(.semibold))
private var canvasSection: some View {
GroupBox("Canvas") {
VStack(alignment: .leading, spacing: 10) {
Toggle("Allow Canvas (agent)", isOn: self.$canvasEnabled) Toggle("Allow Canvas (agent)", isOn: self.$canvasEnabled)
.toggleStyle(.switch) .toggleStyle(.switch)
.help( .help(
"When off, agent Canvas requests return “Canvas disabled by user”. Manual debug actions still work.") "When off, agent Canvas requests return “Canvas disabled by user”. Manual debug actions still work.")
HStack(spacing: 8) { HStack(spacing: 8) {
TextField("Session", text: self.$canvasSessionKey) TextField("Session", text: self.$canvasSessionKey)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
@@ -346,12 +502,14 @@ struct DebugSettings: View {
Task { await self.canvasWriteSamplePage() } Task { await self.canvasWriteSamplePage() }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
Spacer(minLength: 0)
} }
HStack(spacing: 8) { HStack(spacing: 8) {
TextField("Eval JS", text: self.$canvasEvalJS) TextField("Eval JS", text: self.$canvasEvalJS)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.font(.caption.monospaced()) .font(.caption.monospaced())
.frame(maxWidth: 420) .frame(maxWidth: 520)
Button("Eval") { Button("Eval") {
Task { await self.canvasEval() } Task { await self.canvasEval() }
} }
@@ -360,7 +518,9 @@ struct DebugSettings: View {
Task { await self.canvasSnapshot() } Task { await self.canvasSnapshot() }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
Spacer(minLength: 0)
} }
if let canvasStatus { if let canvasStatus {
Text(canvasStatus) Text(canvasStatus)
.font(.caption2.monospaced()) .font(.caption2.monospaced())
@@ -388,6 +548,7 @@ struct DebugSettings: View {
.activateFileViewerSelecting([URL(fileURLWithPath: canvasSnapshotPath)]) .activateFileViewerSelecting([URL(fileURLWithPath: canvasSnapshotPath)])
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
Spacer(minLength: 0)
} }
} }
if let canvasError { if let canvasError {
@@ -400,38 +561,30 @@ struct DebugSettings: View {
.foregroundStyle(.tertiary) .foregroundStyle(.tertiary)
} }
} }
LabeledContent("Icon override") { }
Picker("Icon override", selection: self.bindingOverride) { }
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 ForEach(IconOverrideSelection.allCases) { option in
Text(option.label).tag(option.rawValue) Text(option.label).tag(option.rawValue)
} }
} }
.labelsHidden() .labelsHidden()
.frame(maxWidth: 280) .frame(maxWidth: 280, alignment: .leading)
} }
Toggle("Use SwiftUI web chat (glass, gateway WS)", isOn: self.$webChatSwiftUIEnabled) GridRow {
self.gridLabel("Web chat")
Toggle("Use SwiftUI web chat (glass)", isOn: self.$webChatSwiftUIEnabled)
.toggleStyle(.switch) .toggleStyle(.switch)
.help( .help(
"When enabled, the menu bar chat window/panel uses the native SwiftUI UI instead of the bundled WKWebView.") "When enabled, the menu bar chat window/panel uses the native SwiftUI UI instead of the bundled WKWebView.")
Spacer(minLength: 8)
} }
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
.padding(.vertical, 8)
} }
.task {
guard !self.isPreview else { return }
await self.reloadModels()
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())
} }
} }