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()) }
Text(DebugActions.pinoLogPath()) GridRow {
self.gridLabel("Binary path")
Text(Bundle.main.bundlePath)
.font(.caption2.monospaced()) .font(.caption2.monospaced())
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.textSelection(.enabled) .textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
} }
LabeledContent("Diagnostics log") { }
}
}
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())
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
.lineLimit(1)
.truncationMode(.middle)
}
}
}
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,119 +245,85 @@ 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)") private var portsSection: some View {
.font(.caption2) GroupBox("Ports") {
.foregroundStyle(.secondary) VStack(alignment: .leading, spacing: 10) {
} HStack(spacing: 8) {
} Text("Port diagnostics")
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)
}
Button("Copy sample agent 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)
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Gateway stdout/stderr")
.font(.caption.weight(.semibold)) .font(.caption.weight(.semibold))
ScrollView { if self.portCheckInFlight { ProgressView().controlSize(.small) }
Text(self.gatewayManager.log.isEmpty ? "" : self.gatewayManager.log) Spacer()
.font(.caption.monospaced()) Button("Check gateway ports") {
.frame(maxWidth: .infinity, alignment: .leading) Task { await self.runPortCheck() }
.textSelection(.enabled)
} }
.frame(height: 180) .buttonStyle(.borderedProminent)
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2))) .disabled(self.portCheckInFlight)
} }
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) { if let portKillStatus {
Text("Port diagnostics") Text(portKillStatus)
.font(.caption.weight(.semibold)) .font(.caption2)
if self.portCheckInFlight { ProgressView().controlSize(.small) } .foregroundStyle(.secondary)
Spacer() .fixedSize(horizontal: false, vertical: true)
Button("Check gateway ports") { }
Task { await self.runPortCheck() }
} if self.portReports.isEmpty, !self.portCheckInFlight {
.buttonStyle(.borderedProminent) Text("Check which process owns 18788/18789 and suggest fixes.")
.disabled(self.portCheckInFlight) .font(.caption2)
} .foregroundStyle(.secondary)
if let portKillStatus { } else {
Text(portKillStatus) ForEach(self.portReports) { report in
.font(.caption2) VStack(alignment: .leading, spacing: 4) {
.foregroundStyle(.secondary) Text("Port \(report.port)")
} .font(.footnote.weight(.semibold))
if self.portReports.isEmpty, !self.portCheckInFlight { Text(report.summary)
Text("Check which process owns 18788/18789 and suggest fixes.") .font(.caption)
.font(.caption2) .foregroundStyle(.secondary)
.foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true)
} else { ForEach(report.listeners) { listener in
ForEach(self.portReports) { report in VStack(alignment: .leading, spacing: 2) {
VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) {
Text("Port \(report.port)") Text("\(listener.command) (\(listener.pid))")
.font(.footnote.weight(.semibold)) .font(.caption.monospaced())
Text(report.summary) .foregroundStyle(listener.expected ? .secondary : Color.red)
.font(.caption) .lineLimit(1)
.foregroundStyle(.secondary) Spacer()
ForEach(report.listeners) { listener in Button("Kill") {
VStack(alignment: .leading, spacing: 2) { self.requestKill(listener)
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) .buttonStyle(.bordered)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(2)
.truncationMode(.middle)
} }
.padding(6) Text(listener.fullCommand)
.background(Color.secondary.opacity(0.05)) .font(.caption2.monospaced())
.cornerRadius(4) .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)
} }
.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) { VStack(alignment: .leading, spacing: 6) {
Text("Clawdis project root") Text("Clawdis project root")
.font(.caption.weight(.semibold)) .font(.caption.weight(.semibold))
@@ -214,73 +346,93 @@ struct DebugSettings: View {
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
LabeledContent("Session store") {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
TextField("Path", text: self.$sessionStorePath)
.textFieldStyle(.roundedBorder)
.font(.caption.monospaced())
.frame(width: 340)
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 ~/.clawdis/clawdis.json.")
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
LabeledContent("Model catalog") {
VStack(alignment: .leading, spacing: 6) {
Text(self.modelCatalogPath)
.font(.caption.monospaced())
.foregroundStyle(.secondary)
.lineLimit(2)
HStack(spacing: 8) {
Button {
self.chooseCatalogFile()
} label: {
Label("Choose models.generated.ts…", systemImage: "folder")
}
.buttonStyle(.bordered)
Button { Divider()
Task { await self.reloadModels() }
} label: { Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
Label( GridRow {
self.modelsLoading ? "Reloading…" : "Reload models", self.gridLabel("Session store")
systemImage: "arrow.clockwise") 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 ~/.clawdis/clawdis.json.")
.font(.footnote)
.foregroundStyle(.secondary)
} }
.buttonStyle(.bordered)
.disabled(self.modelsLoading)
} }
if let modelsError { }
Text(modelsError) GridRow {
.font(.footnote) self.gridLabel("Model catalog")
VStack(alignment: .leading, spacing: 6) {
Text(self.modelCatalogPath)
.font(.caption.monospaced())
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else if let modelsCount { .lineLimit(2)
Text("Loaded \(modelsCount) models") HStack(spacing: 8) {
Button {
self.chooseCatalogFile()
} label: {
Label("Choose models.generated.ts…", systemImage: "folder")
}
.buttonStyle(.bordered)
Button {
Task { await self.reloadModels() }
} label: {
Label(
self.modelsLoading ? "Reloading…" : "Reload models",
systemImage: "arrow.clockwise")
}
.buttonStyle(.bordered)
.disabled(self.modelsLoading)
}
if let modelsError {
Text(modelsError)
.font(.footnote)
.foregroundStyle(.secondary)
} else if let modelsCount {
Text("Loaded \(modelsCount) models")
.font(.footnote)
.foregroundStyle(.secondary)
}
Text("Used by the Config tab model picker; point at a different build when debugging.")
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.tertiary)
} }
Text("Used by the Config tab model picker; point at a different build when debugging.")
.font(.footnote)
.foregroundStyle(.tertiary)
} }
} }
Button("Send Test Notification") { }
Task { await DebugActions.sendTestNotification() } }
}
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)
} }
.buttonStyle(.bordered)
Button("Open Agent Events") {
DebugActions.openAgentEventsWindow()
}
.buttonStyle(.borderedProminent)
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Button { Button {
Task { await self.sendVoiceDebug() } Task { await self.sendVoiceDebug() }
@@ -312,126 +464,127 @@ 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))
Toggle("Allow Canvas (agent)", isOn: self.$canvasEnabled) private var canvasSection: some View {
.toggleStyle(.switch) GroupBox("Canvas") {
.help( VStack(alignment: .leading, spacing: 10) {
"When off, agent Canvas requests return “Canvas disabled by user”. Manual debug actions still work.") Toggle("Allow Canvas (agent)", isOn: self.$canvasEnabled)
HStack(spacing: 8) { .toggleStyle(.switch)
TextField("Session", text: self.$canvasSessionKey) .help(
.textFieldStyle(.roundedBorder) "When off, agent Canvas requests return “Canvas disabled by user”. Manual debug actions still work.")
.font(.caption.monospaced())
.frame(width: 160) HStack(spacing: 8) {
Button("Show panel") { TextField("Session", text: self.$canvasSessionKey)
Task { await self.canvasShow() } .textFieldStyle(.roundedBorder)
} .font(.caption.monospaced())
.buttonStyle(.borderedProminent) .frame(width: 160)
Button("Hide panel") { Button("Show panel") {
CanvasManager.shared.hideAll() Task { await self.canvasShow() }
self.canvasStatus = "hidden"
self.canvasError = nil
}
.buttonStyle(.bordered)
Button("Write sample page") {
Task { await self.canvasWriteSamplePage() }
}
.buttonStyle(.bordered)
} }
HStack(spacing: 8) { .buttonStyle(.borderedProminent)
TextField("Eval JS", text: self.$canvasEvalJS) Button("Hide panel") {
.textFieldStyle(.roundedBorder) CanvasManager.shared.hideAll()
.font(.caption.monospaced()) self.canvasStatus = "hidden"
.frame(maxWidth: 420) self.canvasError = nil
Button("Eval") {
Task { await self.canvasEval() }
}
.buttonStyle(.bordered)
Button("Snapshot") {
Task { await self.canvasSnapshot() }
}
.buttonStyle(.bordered)
} }
if let canvasStatus { .buttonStyle(.bordered)
Text(canvasStatus) 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()) .font(.caption2.monospaced())
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.textSelection(.enabled) .lineLimit(1)
}
if let canvasEvalResult {
Text("eval → \(canvasEvalResult)")
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(2)
.truncationMode(.middle) .truncationMode(.middle)
.textSelection(.enabled) .textSelection(.enabled)
} Button("Reveal") {
if let canvasSnapshotPath { NSWorkspace.shared
HStack(spacing: 8) { .activateFileViewerSelecting([URL(fileURLWithPath: canvasSnapshotPath)])
Text("snapshot → \(canvasSnapshotPath)")
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
.textSelection(.enabled)
Button("Reveal") {
NSWorkspace.shared
.activateFileViewerSelecting([URL(fileURLWithPath: canvasSnapshotPath)])
}
.buttonStyle(.bordered)
} }
} .buttonStyle(.bordered)
if let canvasError { Spacer(minLength: 0)
Text(canvasError)
.font(.caption2)
.foregroundStyle(.red)
} else {
Text("Tip: the session directory is returned by “Show panel”.")
.font(.caption2)
.foregroundStyle(.tertiary)
} }
} }
LabeledContent("Icon override") { if let canvasError {
Picker("Icon override", selection: self.bindingOverride) { 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 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)
}
GridRow {
self.gridLabel("Web chat")
Toggle("Use SwiftUI web chat (glass)", isOn: self.$webChatSwiftUIEnabled)
.toggleStyle(.switch)
.help(
"When enabled, the menu bar chat window/panel uses the native SwiftUI UI instead of the bundled WKWebView.")
} }
Toggle("Use SwiftUI web chat (glass, gateway WS)", isOn: self.$webChatSwiftUIEnabled)
.toggleStyle(.switch)
.help(
"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())
} }
} }