Files
openclaw/apps/macos/Sources/OpenClaw/DashboardWindow.swift
Peter Steinberger bc4ed5cf61 feat(macos): native-feel dashboard hosting — instant reopen, preload, frame autosave, ⌘N/⌘K, route memory (#106997)
* feat(macos): native-feel dashboard hosting: instant reopen, preload, frame autosave, ⌘N/⌘K, route memory

The hosted Control UI dashboard now skips SPA reloads when reopening an
unchanged endpoint (auth-equality gated), preloads after launch when a
credentialed local/direct config exists, autosaves its window frame, maps
⌘N/⌘K menu items onto the shipped native web events, filters browser-tell
context-menu items, restores the last committed route via web-side
localStorage memory, and themes the pre-paint background.

* refactor(ui): satisfy TS LOC ratchet: move optional lazy-element helpers out of app-host

app-host.ts is over the 500-line ratchet ceiling and may not grow; move the
self-contained optional custom-element block into lazy-custom-element.ts (its
natural owner) and fold the route-restore decision into native-route-memory's
considerRouteRestore so the shell only replaces-or-persists.

* fix(macos): recover stuck dashboard command queue via deliverability gate

Replace the failure-page-only fast-path check with canDeliverNativeCommands
(live document or in-flight load); a terminally cancelled reload now falls
through to the reload path instead of queueing ⌘N/⌘K forever.

* fix(ui): let in-flight navigation win over native route restore

The one-shot startup restore now checks the rendered/pending match against
the committed bootstrap route; an explicit navigation already in flight
(replayed native new-session, fast click) is no longer clobbered.

* docs(ui): note idempotent collapse of pending native new-session replays

* fix(macos): drop queued dashboard commands on terminal load failure

Queued ⌘N/⌘K are moment-bound; surviving a failure page meant a later
recovery reload replayed stale commands (double palette toggle nets closed,
surprise navigation). Both failure entry points now clear the queue.

* fix(ui): persist native route memory only for settled router states

Mid-navigation emissions still carry the stale committed bootstrap route; an
interrupted restore could overwrite the remembered destination with it.

* fix(macos): coalesce dashboard opens for queued native commands

One in-flight open drains a manager-level queue in press order; a Task per
key press could race window creation (duplicate windows) and reorder ⌘N/⌘K
delivery on the non-immediate remote path.

* fix(macos): correct dashboard placement after frame-autosave restore

setFrameAutosaveName re-applies the stored frame; run ensureOnScreen after
both autosave-name assignments so a frame saved on a disconnected monitor
cannot restore the window off-screen.

* fix(macos): clear dashboard failure state when history restores a real document

Swipe-back/⌘[ off the failure page commits an http(s) document without
passing through load(); the flag previously stayed set and forced a reload
on the next native command.

* fix(macos): keep in-flight dashboard loads instead of restarting them

An open/update during the launch preload previously cancelled and restarted
the same-URL load; in-flight non-failure documents now count as usable in
the reload decision.

* fix(macos,ui): ⌘K legacy fallback + transient search param filtering

Command palette dispatch now sends a cancelable toggle event and falls back
to the shipped open-search event when no handler acknowledges it, so ⌘K
keeps working against older gateway-served bundles. Route memory strips
one-shot action params (?draft=) before persisting; navigation state like
?session= still restores.

* chore(i18n): register dashboard menu strings + drop unused route-memory export

native:i18n:sync inventory + per-locale artifacts for New Session / Command
Palette…; StoredNativeRoute is module-internal after the considerRouteRestore
refactor (deadcode exports gate).

* chore(i18n): rebaseline raw-copy for relocated lazy-element labels

The lazy-custom-element extraction moved three internal load-failure labels
out of app-host.ts; refresh the keyless raw-copy baseline to match (main's
new ui:i18n:verify gate).
2026-07-14 03:33:25 -07:00

78 lines
2.5 KiB
Swift

import AppKit
import Foundation
import OSLog
let dashboardWindowLogger = Logger(subsystem: "ai.openclaw", category: "DashboardWindow")
enum DashboardWindowLayout {
static let windowSize = NSSize(width: 1240, height: 860)
static let windowMinSize = NSSize(width: 900, height: 620)
static let mainBrowserMinWidth: CGFloat = 520
static let linkBrowserMinWidth: CGFloat = 320
static let linkBrowserMaxWidth: CGFloat = 760
static let linkBrowserPreferredFraction: CGFloat = 0.4
static let linkBrowserTabBarHeight: CGFloat = 30
static let linkBrowserSplitAutosaveName = "OpenClawDashboardLinkBrowserSplit"
static let windowFrameAutosaveName = "OpenClawDashboardWindow"
}
/// Raw values are window event names the Control UI handles. `newSession`
/// reuses the shipped pre-web-chrome event; `commandPalette` gets a dedicated
/// toggle event because the legacy `native-open-search` contract is open-only.
enum DashboardNativeCommand: String {
case newSession = "openclaw:native-new-session"
case commandPalette = "openclaw:native-toggle-search"
/// Older gateway bundles lack the toggle listener; dispatch degrades to the
/// open-only legacy event when the primary event goes unhandled.
var legacyFallbackEventName: String? {
switch self {
case .newSession: nil
case .commandPalette: "openclaw:native-open-search"
}
}
}
enum DashboardLinkTarget: String, Equatable {
case inline
case external
}
enum DashboardTargetlessNavigationAction: Equatable {
case allow
case openExternal
case cancel
}
enum DashboardNewWindowAction: Equatable {
case openTab(URL)
case openExternal(URL)
case ignore
}
struct DashboardLinkRequest: Equatable {
let url: URL
let target: DashboardLinkTarget
}
struct DashboardWindowAuth: Equatable {
var gatewayUrl: String?
var token: String?
var password: String?
var hasCredential: Bool {
self.token?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ||
self.password?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
}
}
/// Dashboard URLs carry the auth token in the `#token=...` fragment; strip the
/// fragment before logging so credentials never land in unified logs.
func dashboardLogString(for url: URL) -> String {
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return "<unparseable-url>"
}
components.fragment = nil
return components.url?.absoluteString ?? "<unparseable-url>"
}