Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
374 lines
15 KiB
Swift
374 lines
15 KiB
Swift
import OpenClawProtocol
|
|
import SwiftUI
|
|
import UIKit
|
|
|
|
struct AgentProNodesDestination: View {
|
|
let headerLeadingAction: OpenClawSidebarHeaderAction?
|
|
let overview: AgentOverviewSnapshot?
|
|
let gatewayConnected: Bool
|
|
let agentCount: Int
|
|
let instancesValue: String
|
|
let instancesDetail: String
|
|
let instancesColor: Color
|
|
let refresh: () async -> Void
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
OpenClawProBackground()
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
self.header
|
|
self.summaryCard
|
|
self.totalsCard
|
|
self.nodesList
|
|
}
|
|
.padding(.vertical, 18)
|
|
.font(OpenClawType.body)
|
|
}
|
|
.refreshable {
|
|
await self.refresh()
|
|
}
|
|
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
|
|
}
|
|
.navigationTitle("Instances")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var header: some View {
|
|
if let headerLeadingAction {
|
|
OpenClawAdaptiveHeaderRow(
|
|
title: "Instances",
|
|
subtitle: self.instancesDetail,
|
|
titleFont: OpenClawType.title3SemiBold,
|
|
subtitleFont: OpenClawType.subheadMedium)
|
|
{
|
|
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
|
|
} accessory: {
|
|
EmptyView()
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
}
|
|
|
|
private var summaryCard: some View {
|
|
ProCard {
|
|
HStack(spacing: 12) {
|
|
ProIconBadge(systemName: "display", color: self.instancesColor)
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text("Instances")
|
|
.font(OpenClawType.headline)
|
|
Text(self.instancesDetail)
|
|
.font(OpenClawType.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer(minLength: 8)
|
|
ProValuePill(value: self.instancesValue, color: self.instancesColor)
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
|
|
private var totalsCard: some View {
|
|
ProCard {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("Presence")
|
|
.font(OpenClawType.headline)
|
|
Spacer()
|
|
ProValuePill(value: self.instancesValue, color: self.instancesColor)
|
|
}
|
|
HStack(spacing: 10) {
|
|
self.detailMetric(label: "Connected", value: "\(self.overview?.presence.count ?? 0)")
|
|
self.detailMetric(label: "Agents", value: "\(self.agentCount)")
|
|
self.detailMetric(label: "Gateway", value: self.gatewayConnected ? "online" : "offline")
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
|
|
private var nodesList: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
ProSectionHeader(title: "Connected Instances")
|
|
ProCard(padding: 0) {
|
|
let nodes = self.sortedPresenceEntries
|
|
if nodes.isEmpty {
|
|
self.emptyRow(
|
|
icon: "display",
|
|
title: self.gatewayConnected ? "No instances connected" : "Instances unavailable",
|
|
detail: self.gatewayConnected
|
|
? "The gateway did not report any system presence entries."
|
|
: "Connect a gateway to inspect connected instances.")
|
|
.padding(14)
|
|
} else {
|
|
VStack(spacing: 0) {
|
|
ForEach(Array(nodes.enumerated()), id: \.element.presenceKey) { index, entry in
|
|
NavigationLink {
|
|
self.nodeDetail(entry)
|
|
} label: {
|
|
self.nodePresenceRow(entry, showsChevron: true)
|
|
}
|
|
.buttonStyle(.plain)
|
|
if index < nodes.count - 1 {
|
|
Divider().padding(.leading, 60)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
}
|
|
|
|
private var sortedPresenceEntries: [PresenceEntry] {
|
|
(self.overview?.presence ?? [])
|
|
.sorted { lhs, rhs in
|
|
if lhs.ts != rhs.ts { return lhs.ts > rhs.ts }
|
|
return (Self.presenceLabel(lhs) ?? lhs.presenceKey)
|
|
.localizedCaseInsensitiveCompare(Self.presenceLabel(rhs) ?? rhs.presenceKey) == .orderedAscending
|
|
}
|
|
}
|
|
|
|
private func nodePresenceRow(_ entry: PresenceEntry, showsChevron: Bool = false) -> some View {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
ProIconBadge(systemName: Self.presenceIcon(entry), color: Self.presenceColor(entry))
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(Self.presenceLabel(entry) ?? "Instance")
|
|
.font(OpenClawType.subheadSemiBold)
|
|
.lineLimit(1)
|
|
Text(Self.presenceDetail(entry))
|
|
.font(OpenClawType.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(2)
|
|
if let meta = Self.presenceMeta(entry) {
|
|
Text(meta)
|
|
.font(OpenClawType.caption2)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
Spacer(minLength: 8)
|
|
Text(Self.presenceState(entry))
|
|
.font(OpenClawType.caption2SemiBold)
|
|
.foregroundStyle(Self.presenceColor(entry))
|
|
.lineLimit(1)
|
|
if showsChevron {
|
|
Image(systemName: "chevron.right")
|
|
.font(OpenClawType.caption2Bold)
|
|
.foregroundStyle(.secondary)
|
|
.padding(.top, 2)
|
|
}
|
|
}
|
|
.padding(.vertical, 10)
|
|
.padding(.horizontal, 14)
|
|
}
|
|
|
|
private func nodeDetail(_ entry: PresenceEntry) -> some View {
|
|
ZStack {
|
|
OpenClawProBackground()
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
ProCard {
|
|
HStack(spacing: 12) {
|
|
ProIconBadge(systemName: Self.presenceIcon(entry), color: Self.presenceColor(entry))
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text(Self.presenceLabel(entry) ?? "Instance")
|
|
.font(OpenClawType.headline)
|
|
Text(Self.presenceDetail(entry))
|
|
.font(OpenClawType.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer(minLength: 8)
|
|
ProValuePill(value: Self.presenceState(entry), color: Self.presenceColor(entry))
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
|
|
ProCard {
|
|
VStack(spacing: 0) {
|
|
self.nodeDetailRow("Instance", value: entry.instanceid)
|
|
Divider()
|
|
self.nodeDetailRow("Device", value: entry.deviceid)
|
|
Divider()
|
|
self.nodeDetailRow("Host", value: entry.host)
|
|
Divider()
|
|
self.nodeDetailRow("IP", value: entry.ip)
|
|
Divider()
|
|
self.nodeDetailRow("Platform", value: entry.platform)
|
|
Divider()
|
|
self.nodeDetailRow("Version", value: entry.version)
|
|
Divider()
|
|
self.nodeDetailRow("Mode", value: entry.mode)
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
|
|
self.nodeListCard(title: "Scopes", values: entry.scopes ?? [])
|
|
self.nodeListCard(title: "Roles", values: entry.roles ?? [])
|
|
self.nodeListCard(title: "Tags", values: entry.tags ?? [])
|
|
}
|
|
.padding(.vertical, 18)
|
|
.font(OpenClawType.body)
|
|
}
|
|
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
|
|
}
|
|
.navigationTitle(Self.presenceLabel(entry) ?? "Instance")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
|
|
private func nodeDetailRow(_ title: String, value: String?) -> some View {
|
|
let normalized = Self.normalized(value) ?? "n/a"
|
|
return HStack(spacing: 10) {
|
|
Text(title)
|
|
.font(OpenClawType.subhead)
|
|
.foregroundStyle(.secondary)
|
|
Spacer(minLength: 8)
|
|
Text(normalized)
|
|
.font(OpenClawType.subhead)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
Button {
|
|
UIPasteboard.general.string = normalized
|
|
} label: {
|
|
Image(systemName: "doc.on.doc")
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(normalized == "n/a")
|
|
.accessibilityLabel("Copy \(title)")
|
|
}
|
|
.font(OpenClawType.subhead)
|
|
.padding(.vertical, 10)
|
|
}
|
|
|
|
private func nodeListCard(title: String, values: [String]) -> some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
ProSectionHeader(title: title)
|
|
ProCard {
|
|
if values.isEmpty {
|
|
Text("None reported.")
|
|
.font(OpenClawType.subhead)
|
|
.foregroundStyle(.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
ForEach(values, id: \.self) { value in
|
|
Text(value)
|
|
.font(OpenClawType.monoSmall)
|
|
.textSelection(.enabled)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
}
|
|
|
|
private func detailMetric(label: String, value: String) -> some View {
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text(label)
|
|
.font(OpenClawType.caption2Medium)
|
|
.foregroundStyle(.secondary)
|
|
Text(value)
|
|
.font(OpenClawType.subheadSemiBold)
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.8)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(10)
|
|
.background(
|
|
Color.primary.opacity(0.055),
|
|
in: RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous))
|
|
}
|
|
|
|
private func emptyRow(icon: String, title: String, detail: String) -> some View {
|
|
HStack(spacing: 12) {
|
|
ProIconBadge(systemName: icon, color: .secondary)
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text(title)
|
|
.font(OpenClawType.subheadSemiBold)
|
|
Text(detail)
|
|
.font(OpenClawType.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(2)
|
|
}
|
|
Spacer(minLength: 8)
|
|
}
|
|
}
|
|
|
|
private static func presenceLabel(_ entry: PresenceEntry) -> String? {
|
|
self.normalized(entry.host)
|
|
?? self.normalized(entry.devicefamily)
|
|
?? self.normalized(entry.platform)
|
|
?? self.normalized(entry.mode)
|
|
}
|
|
|
|
private static func presenceDetail(_ entry: PresenceEntry) -> String {
|
|
let parts = [
|
|
Self.normalized(entry.ip),
|
|
Self.normalized(entry.platform),
|
|
Self.normalized(entry.version),
|
|
].compactMap(\.self)
|
|
if !parts.isEmpty {
|
|
return parts.joined(separator: " • ")
|
|
}
|
|
return Self.normalized(entry.text) ?? "Presence beacon received."
|
|
}
|
|
|
|
private static func presenceMeta(_ entry: PresenceEntry) -> String? {
|
|
let tags = (entry.tags ?? []).prefix(2).joined(separator: ", ")
|
|
let scopesCount = entry.scopes?.count ?? 0
|
|
let rolesCount = entry.roles?.count ?? 0
|
|
let labels = [
|
|
Self.normalized(entry.instanceid).map { "instance \($0)" },
|
|
tags.isEmpty ? nil : tags,
|
|
scopesCount > 0 ? "\(scopesCount) scopes" : nil,
|
|
rolesCount > 0 ? "\(rolesCount) roles" : nil,
|
|
].compactMap(\.self)
|
|
return labels.isEmpty ? nil : labels.joined(separator: " • ")
|
|
}
|
|
|
|
private static func presenceState(_ entry: PresenceEntry) -> String {
|
|
if let reason = normalized(entry.reason) {
|
|
return reason
|
|
}
|
|
if let mode = Self.normalized(entry.mode) {
|
|
return mode
|
|
}
|
|
return Self.relativeTime(fromMilliseconds: entry.ts)
|
|
}
|
|
|
|
private static func presenceIcon(_ entry: PresenceEntry) -> String {
|
|
let family = Self.normalized(entry.devicefamily)?.lowercased()
|
|
if family?.contains("phone") == true { return "iphone" }
|
|
if family?.contains("tablet") == true || family?.contains("pad") == true { return "ipad" }
|
|
if family?.contains("desktop") == true || family?.contains("mac") == true { return "desktopcomputer" }
|
|
return "display"
|
|
}
|
|
|
|
private static func presenceColor(_ entry: PresenceEntry) -> Color {
|
|
self.normalized(entry.reason) == nil ? OpenClawBrand.accent : OpenClawBrand.warn
|
|
}
|
|
|
|
private static func normalized(_ value: String?) -> String? {
|
|
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
return trimmed.isEmpty ? nil : trimmed
|
|
}
|
|
|
|
private static func relativeTime(fromMilliseconds milliseconds: Int) -> String {
|
|
let date = Date(timeIntervalSince1970: Double(milliseconds) / 1000)
|
|
return date.formatted(.relative(presentation: .named, unitsStyle: .abbreviated))
|
|
}
|
|
}
|
|
|
|
extension PresenceEntry {
|
|
fileprivate var presenceKey: String {
|
|
self.instanceid
|
|
?? self.deviceid
|
|
?? self.host
|
|
?? self.ip
|
|
?? "\(self.ts)"
|
|
}
|
|
}
|