From fbf8460da3504e2876357433e174180547bad675 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 24 Aug 2026 23:51:27 -0700 Subject: [PATCH] refactor(ui): consolidate presence payload parsing (#129029) * refactor(ui): consolidate presence payload parsing * chore(ui): tighten presence assertion safety baselines --- config/assertion-safety-baseline.txt | 5 ++--- ui/src/components/viewer-facepile.ts | 12 ++---------- ui/src/lib/presence-users.ts | 13 ++----------- ui/src/pages/devices/devices-page.ts | 15 +++++---------- ui/src/pages/new-session/new-session-page.ts | 2 +- ui/src/pages/new-session/new-session-runtime.ts | 6 ------ 6 files changed, 12 insertions(+), 41 deletions(-) diff --git a/config/assertion-safety-baseline.txt b/config/assertion-safety-baseline.txt index 28e149715c38..cb83fa4ec755 100644 --- a/config/assertion-safety-baseline.txt +++ b/config/assertion-safety-baseline.txt @@ -4062,7 +4062,6 @@ ui/src/components/terminal/terminal-panel-upload.ts 2 ui/src/components/terminal/terminal-panel.ts 2 ui/src/components/terminal/terminal-pending-actions.ts 1 ui/src/components/theme-mode-toggle.ts 1 -ui/src/components/viewer-facepile.ts 2 ui/src/components/web-awesome.ts 2 ui/src/components/wizard-step-controls.ts 4 ui/src/i18n/lib/registry.ts 2 @@ -4228,7 +4227,7 @@ ui/src/pages/custodian/custodian-session-store.ts 1 ui/src/pages/custodian/custodian-surface.ts 2 ui/src/pages/custodian/session-lifecycle.ts 1 ui/src/pages/debug/view.ts 3 -ui/src/pages/devices/devices-page.ts 3 +ui/src/pages/devices/devices-page.ts 1 ui/src/pages/devices/view-exec-approvals.ts 7 ui/src/pages/devices/view.ts 5 ui/src/pages/labs/labs-registry.ts 6 @@ -4243,7 +4242,7 @@ ui/src/pages/new-session/composer.ts 1 ui/src/pages/new-session/detail-chip.ts 2 ui/src/pages/new-session/discovery.ts 3 ui/src/pages/new-session/draft-place-browser.ts 4 -ui/src/pages/new-session/new-session-runtime.ts 3 +ui/src/pages/new-session/new-session-runtime.ts 1 ui/src/pages/new-session/place-browser.ts 1 ui/src/pages/new-session/place-facts.ts 1 ui/src/pages/new-session/preferences.ts 1 diff --git a/ui/src/components/viewer-facepile.ts b/ui/src/components/viewer-facepile.ts index c34d920623c7..47a179bc77db 100644 --- a/ui/src/components/viewer-facepile.ts +++ b/ui/src/components/viewer-facepile.ts @@ -1,6 +1,6 @@ import { html, nothing } from "lit"; import { property } from "lit/decorators.js"; -import type { PresenceEntry } from "../api/types.ts"; +import { readPresenceEntries } from "../app/user-profile.ts"; import { presenceViewerLabel, projectPresenceEntries, @@ -20,14 +20,6 @@ import { } from "./person-activity-link.ts"; import "./tooltip.ts"; -function readPresenceEntries(value: unknown): PresenceEntry[] { - if (!value || typeof value !== "object") { - return []; - } - const presence = (value as { presence?: unknown }).presence; - return Array.isArray(presence) ? (presence as PresenceEntry[]) : []; -} - function normalized(value: string | null | undefined): string | undefined { const trimmed = value?.trim(); return trimmed ? trimmed : undefined; @@ -92,7 +84,7 @@ class ViewerFacepile extends OpenClawLightDomContentsElement { override render() { const projection = projectPresenceEntries( - readPresenceEntries(this.presencePayload), + readPresenceEntries(this.presencePayload) ?? [], this.selfUserId, this.selfInstanceId, ); diff --git a/ui/src/lib/presence-users.ts b/ui/src/lib/presence-users.ts index 7c9d0c875cbb..8a6447c2102f 100644 --- a/ui/src/lib/presence-users.ts +++ b/ui/src/lib/presence-users.ts @@ -1,4 +1,5 @@ import type { PresenceEntry } from "../api/types.ts"; +import { readPresenceEntries } from "../app/user-profile.ts"; export type PresenceViewer = { id: string; @@ -24,16 +25,6 @@ function firstSorted(values: Iterable): string | unde .toSorted()[0]; } -function readPresenceEntries(value: unknown): PresenceEntry[] { - if (!value || typeof value !== "object") { - return []; - } - // SAFETY: Gateway snapshots are protocol-validated before reaching UI projections. - const presence = (value as { presence?: unknown }).presence; - // SAFETY: The validated presence array carries PresenceEntry protocol records. - return Array.isArray(presence) ? (presence as PresenceEntry[]) : []; -} - function presenceEntrySortKey(entry: PresenceEntry): string { return [ normalized(entry.host) ?? "", @@ -119,7 +110,7 @@ export function projectPresencePayload( cachedAuthenticatedSelfUserId = authenticatedSelfUserId; cachedSelfInstanceId = selfInstanceId; cachedPresenceProjection = projectPresenceViewers( - readPresenceEntries(value), + readPresenceEntries(value) ?? [], authenticatedSelfUserId, selfInstanceId, ); diff --git a/ui/src/pages/devices/devices-page.ts b/ui/src/pages/devices/devices-page.ts index 04dd98dd93d7..6a4a174d8255 100644 --- a/ui/src/pages/devices/devices-page.ts +++ b/ui/src/pages/devices/devices-page.ts @@ -11,6 +11,7 @@ import { type ApplicationGatewaySnapshot, } from "../../app/context.ts"; import { hasOperatorAdminAccess, hasOperatorPairingAccess } from "../../app/operator-access.ts"; +import { readPresenceEntries } from "../../app/user-profile.ts"; import { showConfirmDialog, type ConfirmDialogOptions } from "../../components/confirm-dialog.ts"; import { showSecretRevealDialog } from "../../components/secret-reveal-dialog.ts"; import { renderDocsLink } from "../../components/settings-ui.ts"; @@ -62,12 +63,6 @@ type InventoryRemovalPrompt = | { kind: "entry"; entry: InventoryRemovalRequest } | { kind: "stale"; entries: InventoryRemovalRequest[] }; -function readPresence(value: unknown): PresenceEntry[] | null { - const presence = - value && typeof value === "object" ? (value as { presence?: unknown }).presence : null; - return Array.isArray(presence) ? (presence as PresenceEntry[]) : null; -} - function presenceConnectivitySignature(entries: PresenceEntry[]): string { const states = new Map(); for (const entry of entries) { @@ -154,7 +149,7 @@ class DevicesPage extends OpenClawLightDomElement { if (this.gateway.gateway !== gateway || this.context.gateway !== gateway) { return; } - const presence = event.event === "presence" ? readPresence(event.payload) : null; + const presence = event.event === "presence" ? readPresenceEntries(event.payload) : null; if (presence) { const connectivityChanged = presenceConnectivitySignature(presence) !== @@ -226,7 +221,7 @@ class DevicesPage extends OpenClawLightDomElement { snapshot.client && (change.identityChanged || change.connectionChanged) ) { - const initialPresence = readPresence(snapshot.hello?.snapshot); + const initialPresence = readPresenceEntries(snapshot.hello?.snapshot); this.presence = initialPresence ?? []; void this.loadPresence(); } @@ -250,7 +245,7 @@ class DevicesPage extends OpenClawLightDomElement { const snapshot = this.context.gateway.snapshot; if (!this.gateway.isRouteDataCurrent(data)) { this.resetServerState(snapshot); - this.presence = readPresence(snapshot.hello?.snapshot) ?? []; + this.presence = readPresenceEntries(snapshot.hello?.snapshot) ?? []; void this.loadPresence(); this.ensureInitialData(); return; @@ -261,7 +256,7 @@ class DevicesPage extends OpenClawLightDomElement { connected: snapshot.phase === "connected", requestGeneration: this.gateway.epoch, }; - const initialPresence = readPresence(snapshot.hello?.snapshot); + const initialPresence = readPresenceEntries(snapshot.hello?.snapshot); if (initialPresence) { this.presence = initialPresence; } diff --git a/ui/src/pages/new-session/new-session-page.ts b/ui/src/pages/new-session/new-session-page.ts index 53d60af17a4f..6306b8ca7d66 100644 --- a/ui/src/pages/new-session/new-session-page.ts +++ b/ui/src/pages/new-session/new-session-page.ts @@ -5,6 +5,7 @@ import { selectApplicationSession } from "../../app/agent-selection.ts"; import { applicationContext, type ApplicationContext } from "../../app/context.ts"; import { beginNativeWindowDragFromTopInset } from "../../app/native-window-drag.ts"; import { loadSettings } from "../../app/settings.ts"; +import { readPresenceEntries } from "../../app/user-profile.ts"; import type { ImageLightboxItem } from "../../components/image-lightbox.ts"; import { t } from "../../i18n/index.ts"; import "../../components/web-awesome-popover.ts"; @@ -39,7 +40,6 @@ import { handleSessionPickerEvent, isPlaceTopologyEvent, presenceStateSignature, - readPresenceEntries, } from "./new-session-runtime.ts"; import { renderProjectChip, resolveProjectChip } from "./project-chip.ts"; import type { SubmissionOutcomeReason } from "./session-placement-recovery-state.ts"; diff --git a/ui/src/pages/new-session/new-session-runtime.ts b/ui/src/pages/new-session/new-session-runtime.ts index de609b544b66..2c841b60248d 100644 --- a/ui/src/pages/new-session/new-session-runtime.ts +++ b/ui/src/pages/new-session/new-session-runtime.ts @@ -16,12 +16,6 @@ export function isPlaceTopologyEvent(event: string): boolean { return PLACE_TOPOLOGY_EVENTS.has(event); } -export function readPresenceEntries(value: unknown): PresenceEntry[] | null { - const presence = - value && typeof value === "object" ? (value as { presence?: unknown }).presence : null; - return Array.isArray(presence) ? (presence as PresenceEntry[]) : null; -} - export function presenceStateSignature(entries: PresenceEntry[]): string { const states = new Map(); for (const entry of entries) {