mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-26 07:25:14 +00:00
* refactor: remove stale file-backed shims * fix: harden sqlite state ci boundaries * refactor: store matrix idb snapshots in sqlite * fix: satisfy rebased CI guardrails * refactor: store current conversation bindings in sqlite table * refactor: store tui last sessions in sqlite table * refactor: reset sqlite schema history * refactor: drop unshipped sqlite table migration * refactor: remove plugin index file rollback * refactor: drop unshipped sqlite sidecar migrations * refactor: remove runtime commitments kv migration * refactor: preserve kysely sync result types * refactor: drop unshipped sqlite schema migration table * test: keep session usage coverage sqlite-backed * refactor: keep sqlite migration doctor-only * refactor: isolate device legacy imports * refactor: isolate push voicewake legacy imports * refactor: isolate remaining runtime legacy imports * refactor: tighten sqlite migration guardrails * test: cover sqlite persisted enum parsing * refactor: isolate legacy update and tui imports * refactor: tighten sqlite state ownership * refactor: move legacy imports behind doctor * refactor: remove legacy session row lookup * refactor: canonicalize memory transcript locators * refactor: drop transcript path scope fallbacks * refactor: drop runtime legacy session delivery pruning * refactor: store tts prefs only in sqlite * refactor: remove cron store path runtime * refactor: use cron sqlite store keys * refactor: rename telegram message cache scope * refactor: read memory dreaming status from sqlite * refactor: rename cron status store key * refactor: stop remembering transcript file paths * test: use sqlite locators in agent fixtures * refactor: remove file-shaped commitments and cron store surfaces * refactor: keep compaction transcript handles out of session rows * refactor: derive transcript handles from session identity * refactor: derive runtime transcript handles * refactor: remove gateway session locator reads * refactor: remove transcript locator from session rows * refactor: store raw stream diagnostics in sqlite * refactor: remove file-shaped transcript rotation * refactor: hide legacy trajectory paths from runtime * refactor: remove runtime transcript file bridges * refactor: repair database-first rebase fallout * refactor: align tests with database-first state * refactor: remove transcript file handoffs * refactor: sync post-compaction memory by transcript scope * refactor: run codex app-server sessions by id * refactor: bind codex runtime state by session id * refactor: pass memory transcripts by sqlite scope * refactor: remove transcript locator cleanup leftovers * test: remove stale transcript file fixtures * refactor: remove transcript locator test helper * test: make cron sqlite keys explicit * test: remove cron runtime store paths * test: remove stale session file fixtures * test: use sqlite cron keys in diagnostics * refactor: remove runtime delivery queue backfill * test: drop fake export session file mocks * refactor: rename acp session read failure flag * refactor: rename acp row session key * refactor: remove session store test seams * refactor: move legacy session parser tests to doctor * refactor: reindex managed memory in place * refactor: drop stale session store wording * refactor: rename session row helpers * refactor: rename sqlite session entry modules * refactor: remove transcript locator leftovers * refactor: trim file-era audit wording * refactor: clean managed media through sqlite * fix: prefer explicit agent for exports * fix: use prepared agent for session resets * fix: canonicalize legacy codex binding import * test: rename state cleanup helper * docs: align backup docs with sqlite state * refactor: drop legacy Pi usage auth fallback * refactor: move legacy auth profile imports to doctor * refactor: keep Pi model discovery auth in memory * refactor: remove MSTeams legacy learning key fallback * refactor: store model catalog config in sqlite * refactor: use sqlite model catalog at runtime * refactor: remove model json compatibility aliases * refactor: store auth profiles in sqlite * refactor: seed copied auth profiles in sqlite * refactor: make auth profile runtime sqlite-addressed * refactor: migrate hermes secrets into sqlite auth store * refactor: move plugin install config migration to doctor * refactor: rename plugin index audit checks * test: drop auth file assumptions * test: remove legacy transcript file assertions * refactor: drop legacy cli session aliases * refactor: store skill uploads in sqlite * refactor: keep subagent attachments in sqlite vfs * refactor: drop subagent attachment cleanup state * refactor: move legacy session aliases to doctor * refactor: require node 24 for sqlite state runtime * refactor: move provider caches into sqlite state * fix: harden virtual agent filesystem * refactor: enforce database-first runtime state * refactor: rename compaction transcript rotation setting * test: clean sqlite refactor test types * refactor: consolidate sqlite runtime state * refactor: model session conversations in sqlite * refactor: stop deriving cron delivery from session keys * refactor: stop classifying sessions from key shape * refactor: hydrate announce targets from typed delivery * refactor: route heartbeat delivery from typed sqlite context * refactor: tighten typed sqlite session routing * refactor: remove session origin routing shadow * refactor: drop session origin shadow fixtures * perf: query sqlite vfs paths by prefix * refactor: use typed conversation metadata for sessions * refactor: prefer typed session routing metadata * refactor: require typed session routing metadata * refactor: resolve group tool policy from typed sessions * refactor: delete dead session thread info bridge * Show Codex subscription reset times in channel errors (#80456) * feat(plugin-sdk): consolidate session workflow APIs * fix(agents): allow read-only agent mount reads * [codex] refresh plugin regression fixtures * fix(agents): restore compaction gateway logs * test: tighten gateway startup assertions * Redact persisted secret-shaped payloads [AI] (#79006) * test: tighten device pair notify assertions * test: tighten hermes secret assertions * test: assert matrix client error shapes * test: assert config compat warnings * fix(heartbeat): remap cron-run exec events to session keys (#80214) * fix(codex): route btw through native side threads * fix(auth): accept friendly OpenAI order for Codex profiles * fix(codex): rotate auth profiles inside harness * fix: keep browser status page probe within timeout * test: assert agents add outputs * test: pin cron read status * fix(agents): avoid Pi resource discovery stalls Co-authored-by: dataCenter430 <titan032000@gmail.com> * fix: retire timed-out codex app-server clients * test: tighten qa lab runtime assertions * test: check security fix outputs * test: verify extension runtime messages * feat(wake): expose typed sessionKey on wake protocol + system event CLI * fix(gateway): await session_end during shutdown drain and track channel + compaction lifecycle paths (#57790) * test: guard talk consult call helper * fix(codex): scale context engine projection (#80761) * fix(codex): scale context engine projection * fix: document Codex context projection scaling * fix: document Codex context projection scaling * fix: document Codex context projection scaling * fix: document Codex context projection scaling * chore: align Codex projection changelog * chore: realign Codex projection changelog * fix: isolate Codex projection patch --------- Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org> Co-authored-by: Josh Lehman <josh@martian.engineering> * refactor: move agent runtime state toward piless * refactor: remove cron session reaper * refactor: move session management to sqlite * refactor: finish database-first state migration * chore: refresh generated sqlite db types * refactor: remove stale file-backed shims * test: harden kysely type coverage # Conflicts: # .agents/skills/kysely-database-access/SKILL.md # src/infra/kysely-sync.types.test.ts # src/proxy-capture/store.sqlite.test.ts # src/state/openclaw-agent-db.test.ts # src/state/openclaw-state-db.test.ts * refactor: remove cron store path runtime * refactor: keep compaction transcript handles out of session rows * refactor: derive embedded transcripts from sqlite identity * refactor: remove embedded transcript locator handoff * refactor: remove runtime transcript file bridges * refactor: remove transcript file handoffs * refactor: remove MSTeams legacy learning key fallback * refactor: store model catalog config in sqlite * refactor: use sqlite model catalog at runtime # Conflicts: # docs/cli/secrets.md # docs/gateway/authentication.md # docs/gateway/secrets.md * fix: keep oauth sibling sync sqlite-local # Conflicts: # src/commands/onboard-auth.test.ts * refactor: remove task session store maintenance # Conflicts: # src/commands/tasks.ts * refactor: keep diagnostics in state sqlite * refactor: enforce database-first runtime state * refactor: consolidate sqlite runtime state * Show Codex subscription reset times in channel errors (#80456) * fix(codex): refresh subscription limit resets * fix(codex): format reset times for channels * Update CHANGELOG with latest changes and fixes Updated CHANGELOG with recent fixes and improvements. * fix(codex): keep command load failures on codex surface * fix(codex): format account rate limits as rows * fix(codex): summarize account limits as usage status * fix(codex): simplify account limit status * test: tighten subagent announce queue assertion * test: tighten session delete lifecycle assertions * test: tighten cron ops assertions * fix: track cron execution milestones * test: tighten hermes secret assertions * test: assert matrix sync store payloads * test: assert config compat warnings * fix(codex): align btw side thread semantics * fix(codex): honor codex fallback blocking * fix(agents): avoid Pi resource discovery stalls * test: tighten codex event assertions * test: tighten cron assertions * Fix Codex app-server OAuth harness auth * refactor: move agent runtime state toward piless * refactor: move device and push state to sqlite * refactor: move runtime json state imports to doctor * refactor: finish database-first state migration * chore: refresh generated sqlite db types * refactor: clarify cron sqlite store keys * refactor: remove stale file-backed shims * refactor: bind codex runtime state by session id * test: expect sqlite trajectory branch export * refactor: rename session row helpers * fix: keep legacy device identity import in doctor * refactor: enforce database-first runtime state * refactor: consolidate sqlite runtime state * build: align pi contract wrappers * chore: repair database-first rebase * refactor: remove session file test contracts * test: update gateway session expectations * refactor: stop routing from session compatibility shadows * refactor: stop persisting session route shadows * refactor: use typed delivery context in clients * refactor: stop echoing session route shadows * refactor: repair embedded runner rebase imports # Conflicts: # src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts * refactor: align pi contract imports * refactor: satisfy kysely sync helper guard * refactor: remove file transcript bridge remnants * refactor: remove session locator compatibility * refactor: remove session file test contracts * refactor: keep rebase database-first clean * refactor: remove session file assumptions from e2e * docs: clarify database-first goal state * test: remove legacy store markers from sqlite runtime tests * refactor: remove legacy store assumptions from runtime seams * refactor: align sqlite runtime helper seams * test: update memory recall sqlite audit mock * refactor: align database-first runtime type seams * test: clarify doctor cron legacy store names * fix: preserve sqlite session route projections * test: fix copilot token cache test syntax * docs: update database-first proof status * test: align database-first test fixtures * docs: update database-first proof status * refactor: clean extension database-first drift * test: align agent session route proof * test: clarify doctor legacy path fixtures * chore: clean database-first changed checks * chore: repair database-first rebase markers * build: allow baileys git subdependency * chore: repair exp-vfs rebase drift * chore: finish exp-vfs rebase cleanup * chore: satisfy rebase lint drift * chore: fix qqbot rebase type seam * chore: fix rebase drift leftovers * fix: keep auth profile oauth secrets out of sqlite * fix: repair rebase drift tests * test: stabilize pairing request ordering * test: use source manifests in plugin contract checks * fix: restore gateway session metadata after rebase * fix: repair database-first rebase drift * fix: clean up database-first rebase fallout * test: stabilize line quick reply receipt time * fix: repair extension rebase drift * test: keep transcript redaction tests sqlite-backed * fix: carry injected transcript redaction through sqlite * chore: clean database branch rebase residue * fix: repair database branch CI drift * fix: repair database branch CI guard drift * fix: stabilize oauth tls preflight test * test: align database branch fast guards * test: repair build artifact boundary guards * chore: clean changelog rebase markers --------- Co-authored-by: pashpashpash <nik@vault77.ai> Co-authored-by: Eva <eva@100yen.org> Co-authored-by: stainlu <stainlu@newtype-ai.org> Co-authored-by: Jason Zhou <jason.zhou.design@gmail.com> Co-authored-by: Ruben Cuevas <hi@rubencu.com> Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com> Co-authored-by: Shakker <shakkerdroid@gmail.com> Co-authored-by: Kaspre <36520309+Kaspre@users.noreply.github.com> Co-authored-by: dataCenter430 <titan032000@gmail.com> Co-authored-by: Kaspre <kaspre@gmail.com> Co-authored-by: pandadev66 <nova.full.stack@outlook.com> Co-authored-by: Eva <admin@100yen.org> Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org> Co-authored-by: Josh Lehman <josh@martian.engineering> Co-authored-by: jeffjhunter <support@aipersonamethod.com>
950 lines
39 KiB
Swift
950 lines
39 KiB
Swift
import AppKit
|
|
import Observation
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
|
|
struct DebugSettings: View {
|
|
@Bindable var state: AppState
|
|
private let isPreview = ProcessInfo.processInfo.isPreview
|
|
private let labelColumnWidth: CGFloat = 140
|
|
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
|
|
@AppStorage(modelCatalogReloadKey) private var modelCatalogReloadBump: Int = 0
|
|
@AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue
|
|
@AppStorage(canvasEnabledKey) private var canvasEnabled: Bool = true
|
|
@State private var modelsCount: Int?
|
|
@State private var modelsLoading = false
|
|
@State private var modelsError: String?
|
|
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 sessionDatabasePath: String = SessionLoader.defaultDatabasePath
|
|
@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(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.launchdSection
|
|
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)
|
|
.groupBoxStyle(PlainSettingsGroupBoxStyle())
|
|
}
|
|
.task {
|
|
guard !self.isPreview else { return }
|
|
await self.reloadModels()
|
|
self.refreshSessionDatabasePath()
|
|
}
|
|
.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 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: 180)
|
|
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.secondary.opacity(0.2)))
|
|
|
|
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.")
|
|
|
|
Text("Use Console.app or `log stream` for macOS app logs.")
|
|
.font(.caption2.monospaced())
|
|
.foregroundStyle(.secondary)
|
|
.textSelection(.enabled)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 database")
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(self.sessionDatabasePath)
|
|
.font(.caption.monospaced())
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(2)
|
|
.truncationMode(.middle)
|
|
.textSelection(.enabled)
|
|
Text("Runtime session state is stored in the per-agent SQLite database.")
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
GridRow {
|
|
self.gridLabel("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 {
|
|
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("Local fallback for model picker when gateway models.list is unavailable.")
|
|
.font(.footnote)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 chooseCatalogFile() {
|
|
let panel = NSOpenPanel()
|
|
panel.title = "Select models.generated.ts"
|
|
let tsType = UTType(filenameExtension: "ts")
|
|
?? UTType(tag: "ts", tagClass: .filenameExtension, conformingTo: .sourceCode)
|
|
?? .item
|
|
panel.allowedContentTypes = [tsType]
|
|
panel.allowsMultipleSelection = false
|
|
panel.directoryURL = URL(fileURLWithPath: self.modelCatalogPath).deletingLastPathComponent()
|
|
if panel.runModal() == .OK, let url = panel.url {
|
|
self.modelCatalogPath = url.path
|
|
self.modelCatalogReloadBump += 1
|
|
Task { await self.reloadModels() }
|
|
}
|
|
}
|
|
|
|
private func reloadModels() async {
|
|
guard !self.modelsLoading else { return }
|
|
self.modelsLoading = true
|
|
self.modelsError = nil
|
|
self.modelCatalogReloadBump += 1
|
|
defer { self.modelsLoading = false }
|
|
do {
|
|
let loaded = try await ModelCatalogLoader.load(from: self.modelCatalogPath)
|
|
self.modelsCount = loaded.count
|
|
} catch {
|
|
self.modelsCount = nil
|
|
self.modelsError = error.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 refreshSessionDatabasePath() {
|
|
self.sessionDatabasePath = SessionLoader.defaultDatabasePath
|
|
}
|
|
|
|
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: 10) {
|
|
configuration.label
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
configuration.content
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
#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.modelsCount = 3
|
|
view.modelsLoading = false
|
|
view.modelsError = "Failed to load models"
|
|
view.gatewayRootInput = "/tmp/openclaw"
|
|
view.sessionDatabasePath = "/tmp/openclaw-agent.sqlite"
|
|
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.appInfoSection
|
|
_ = view.gatewaySection
|
|
_ = view.logsSection
|
|
_ = view.portsSection
|
|
_ = view.pathsSection
|
|
_ = view.quickActionsSection
|
|
_ = view.canvasSection
|
|
_ = view.experimentsSection
|
|
_ = view.gridLabel("Test")
|
|
|
|
view.refreshSessionDatabasePath()
|
|
await view.reloadModels()
|
|
}
|
|
}
|
|
#endif
|