Files
openclaw/apps/ios/Sources/Chat/SessionDashboardScreen.swift
T
Peter Steinberger e5fca42952 fix(apps): per-session desktop button opens that session's machine (#123412)
* fix(apps): open session desktop on its machine

* fix(ui): scope the desktop session lookup to the key's own agent

`sessions.list` has no exact-key filter, so the viewer resolves a `session=`
parameter by searching for the key and matching it exactly in the response. A
key that prefixes longer ones — `agent:main:main` alongside user-named sessions
that start the same way — could push the exact row outside a five-row page and
silently fall back to the picker.

Session keys encode their agent and the list API accepts `agentId`, so scope the
search to that agent and widen the page.

* refactor(ui): stop parking a session key in the desktop environment id

Document-mode inventory failures stashed the requested session key in
`environmentId` purely so the Retry button's non-null guard would pass, even
though document-mode retry refreshes the inventory and never reads the value.
A session key only names a machine once the inventory loads, so it now stays
out of `environmentId`, and the retry branch that ignores it runs before the
guard. Adds E2E coverage for recovering a session-preselected desktop.

* fix(ui): resolve the desktop session with an exact-key lookup

The session-preselect path searched `sessions.list` and scoped the search to
the key's own agent, but a bounded search cannot rule a key out: 25 newer
same-agent sessions sharing the requested key's prefix would push the exact row
off the page, and the viewer would report the source as unavailable for a
session that exists. `sessions.describe` is the exact-key operation and already
projects placement, so the panel calls it directly and the app-root resolver
plumbing goes away with it.

* refactor(ui): move the desktop document keyboard bridge into its own controller

`desktop-panel.ts` crossed the 700-line cap. The mobile keyboard bridge — the
padded sentinel, the value diffing that turns composed input into backspaces
and text, and the field focus/reset helpers — is a self-contained concern, so
it moves into a `DesktopMobileKeyboard` controller alongside the existing
fullscreen controller instead of taking a `max-lines` suppression.

* test(ios): drain text fields instead of assuming a delete burst lands

`testReleaseChatScreenshot` typed a 5-character probe, sent 5 deletes in one
`typeText`, then asserted the field was empty. CI dropped one synthetic
keystroke under simulator load and the assertion failed with a leftover "f".
XCUITest makes no lossless-burst guarantee, so clearing now re-sends against
whatever the field actually still holds, bounded. The two gateway-setup fields
that overtyped through the same burst use the helper for the same reason.

* fix(ui): keep the chat placement owner out of the startup chunk

`desktop-document-mode.ts` is imported by bootstrap, so importing the chat
placement owner from it pulled the chat page's dependency tree into the startup
bundle and pushed startup JS past its gzip budget (331075 B against a 330507 B
allowance). The route module now only parses the URL; resolving a session to its
machine moves next to the lazily loaded desktop panel, which is the only caller.
Startup JS is back to 329710 B.
2026-08-13 23:33:19 -07:00

86 lines
3.3 KiB
Swift

import OpenClawKit
import SwiftUI
/// Session-scoped dashboard rendered by the gateway Control UI.
struct SessionDashboardScreen: View {
@Environment(NodeAppModel.self) private var appModel
@Environment(\.dismiss) private var dismiss
@State private var showsDesktop = false
let sessionKey: String
var body: some View {
let config = self.appModel.activeGatewayConnectConfig
let storedOperatorToken = AuthenticatedControlUI.storedOperatorToken(config: config)
ZStack {
OpenClawProBackground()
if let url = Self.dashboardURL(config: config, sessionKey: self.sessionKey) {
AuthenticatedControlUIWebView(
url: url,
authScript: AuthenticatedControlUI.authUserScript(
config: config,
pageURL: url,
storedOperatorToken: storedOperatorToken),
tls: config?.tls)
.id(AuthenticatedControlUI.webContentIdentity(
config: config,
storedOperatorToken: storedOperatorToken))
.ignoresSafeArea(.container, edges: .bottom)
} else {
self.unavailableCard
}
}
.navigationTitle("Dashboard")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if self.appModel.isDesktopObserveAvailable {
ToolbarItem(placement: .topBarTrailing) {
Button {
self.showsDesktop = true
} label: {
Image(systemName: "display")
.font(OpenClawType.subheadSemiBold)
}
.accessibilityLabel("Open Desktop")
.accessibilityIdentifier("SessionDashboard.Desktop")
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
self.dismiss()
} label: {
Text("Done")
.font(OpenClawType.subheadSemiBold)
}
}
}
.navigationDestination(isPresented: self.$showsDesktop) {
DesktopHubScreen(session: self.sessionKey, usesNativeNavigationChrome: true)
}
}
private var unavailableCard: some View {
VStack(spacing: 12) {
ProIconBadge(systemName: "rectangle.grid.2x2", color: OpenClawBrand.accent)
Text("Dashboard needs a connected gateway")
.font(OpenClawType.subheadSemiBold)
Text("Connect to your gateway to open this session dashboard.")
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.padding(24)
}
/// Converts the active gateway WebSocket endpoint into the one-shot
/// authenticated Control UI route. Credentials stay in the startup script.
static func dashboardURL(config: GatewayConnectConfig?, sessionKey: String) -> URL? {
AuthenticatedControlUI.pageURL(
config: config,
path: "/chat",
queryItems: [
URLQueryItem(name: "session", value: sessionKey),
URLQueryItem(name: "face", value: "dashboard"),
])
}
}