refactor(ios): share Agent Pro detail components (#119868)

* refactor(ios): share Agent Pro detail components

* test(ios): update Agent Pro i18n source contract
This commit is contained in:
Vincent Koc
2026-08-26 08:41:06 +08:00
committed by GitHub
parent 1172d68d31
commit f457c2cea1
9 changed files with 72 additions and 151 deletions
@@ -0,0 +1,38 @@
import OpenClawKit
import SwiftUI
func agentProDetailMetric(label: OpenClawTextValue, value: String) -> some View {
VStack(alignment: .leading, spacing: 3) {
label.text
.font(OpenClawType.caption2Medium)
.foregroundStyle(.secondary)
Text(verbatim: 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))
}
func agentProEmptyDetailRow(
icon: String,
title: OpenClawTextValue,
detail: OpenClawTextValue) -> some View
{
HStack(spacing: 12) {
ProIconBadge(systemName: icon, color: .secondary)
VStack(alignment: .leading, spacing: 3) {
title.text
.font(OpenClawType.subheadSemiBold)
detail.text
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer(minLength: 8)
}
}
@@ -145,13 +145,13 @@ struct AgentProDreamingDestination: View {
ProValuePill(value: self.dreamingValue, color: self.dreamingColor)
}
HStack(spacing: 10) {
self.detailMetric(
agentProDetailMetric(
label: "Short-term",
value: Self.compactNumber(self.overview?.dreaming?.shortTermCount ?? 0))
self.detailMetric(
agentProDetailMetric(
label: "Signals",
value: Self.compactNumber(self.overview?.dreaming?.totalSignalCount ?? 0))
self.detailMetric(
agentProDetailMetric(
label: "Promoted",
value: Self.compactNumber(self.overview?.dreaming?.promotedToday ?? 0))
}
@@ -245,7 +245,7 @@ struct AgentProDreamingDestination: View {
if let selectedDay {
self.dreamDiaryDayView(selectedDay)
} else {
self.emptyDetailRow(
agentProEmptyDetailRow(
icon: "calendar.badge.exclamationmark",
title: "No day entries",
detail: "The diary is present, but it does not contain dated Dream Diary blocks.")
@@ -253,7 +253,7 @@ struct AgentProDreamingDestination: View {
}
.padding(14)
} else {
self.emptyDetailRow(
agentProEmptyDetailRow(
icon: "book.closed",
title: diary.found ? "Dream diary is empty" : "No dream diary yet",
detail: diary.found
@@ -265,7 +265,7 @@ struct AgentProDreamingDestination: View {
.padding(14)
}
} else {
self.emptyDetailRow(
agentProEmptyDetailRow(
icon: "book.closed",
title: self.gatewayConnected ? "Diary unavailable" : "Dreaming unavailable",
detail: self.gatewayConnected
@@ -354,7 +354,7 @@ struct AgentProDreamingDestination: View {
ProSectionHeader(title: title)
ProCard(padding: 0) {
if entries.isEmpty {
self.emptyDetailRow(
agentProEmptyDetailRow(
icon: "doc.text.magnifyingglass",
title: emptyTitle,
detail: self.gatewayConnected ? emptyDetail : "Connect a gateway to load dreaming entries.")
@@ -407,7 +407,7 @@ struct AgentProDreamingDestination: View {
ProCard(padding: 0) {
let phases = self.dreamingPhases
if phases.isEmpty {
self.emptyDetailRow(
agentProEmptyDetailRow(
icon: "moon.zzz",
title: self.gatewayConnected ? "No phase status" : "Dreaming unavailable",
detail: self.gatewayConnected
@@ -467,42 +467,6 @@ struct AgentProDreamingDestination: View {
.padding(.horizontal, 14)
}
private func emptyDetailRow(
icon: String,
title: OpenClawTextValue,
detail: OpenClawTextValue) -> some View
{
HStack(spacing: 12) {
ProIconBadge(systemName: icon, color: .secondary)
VStack(alignment: .leading, spacing: 3) {
title.text
.font(OpenClawType.subheadSemiBold)
detail.text
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer(minLength: 8)
}
}
private func detailMetric(label: OpenClawTextValue, value: String) -> some View {
VStack(alignment: .leading, spacing: 3) {
label.text
.font(OpenClawType.caption2Medium)
.foregroundStyle(.secondary)
Text(verbatim: 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 dreamingEntryTitle(_ entry: DreamingEntryLite) -> String {
let path = entry.path.split(separator: "/").last.map(String.init) ?? entry.path
return "\(path):\(entry.startLine)"
@@ -79,11 +79,11 @@ struct AgentProNodesDestination: View {
ProValuePill(value: self.instancesValue, color: self.instancesColor)
}
HStack(spacing: 10) {
self.detailMetric(
agentProDetailMetric(
label: "Connected",
value: (self.overview?.presence.count ?? 0).formatted())
self.detailMetric(label: "Agents", value: self.agentCount.formatted())
self.detailMetric(
agentProDetailMetric(label: "Agents", value: self.agentCount.formatted())
agentProDetailMetric(
label: "Gateway",
value: self.gatewayConnected
? String(localized: "online")
@@ -100,7 +100,7 @@ struct AgentProNodesDestination: View {
ProCard(padding: 0) {
let nodes = self.sortedPresenceEntries
if nodes.isEmpty {
self.emptyRow(
agentProEmptyDetailRow(
icon: "display",
title: self.gatewayConnected ? "No instances connected" : "Instances unavailable",
detail: self.gatewayConnected
@@ -275,42 +275,6 @@ struct AgentProNodesDestination: View {
}
}
private func detailMetric(label: OpenClawTextValue, value: String) -> some View {
VStack(alignment: .leading, spacing: 3) {
label.text
.font(OpenClawType.caption2Medium)
.foregroundStyle(.secondary)
Text(verbatim: 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: OpenClawTextValue,
detail: OpenClawTextValue) -> some View
{
HStack(spacing: 12) {
ProIconBadge(systemName: icon, color: .secondary)
VStack(alignment: .leading, spacing: 3) {
title.text
.font(OpenClawType.subheadSemiBold)
detail.text
.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)
@@ -18,8 +18,8 @@ extension AgentProTab {
let jobCount = self.overview?.cronStatus?.jobs
?? self.overview?.cronJobs.count
?? 0
self.detailMetric(label: "Automations", value: "\(jobCount)")
self.detailMetric(label: "Next", value: self.cronNextRunLabel)
agentProDetailMetric(label: "Automations", value: "\(jobCount)")
agentProDetailMetric(label: "Next", value: self.cronNextRunLabel)
}
if let cronActionStatusText {
Text(cronActionStatusText)
@@ -1,40 +0,0 @@
import OpenClawKit
import SwiftUI
extension AgentProTab {
func detailMetric(label: OpenClawTextValue, value: String) -> some View {
VStack(alignment: .leading, spacing: 3) {
label.text
.font(OpenClawType.caption2Medium)
.foregroundStyle(.secondary)
Text(verbatim: 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))
}
func emptyDetailRow(
icon: String,
title: OpenClawTextValue,
detail: OpenClawTextValue) -> some View
{
HStack(spacing: 12) {
ProIconBadge(systemName: icon, color: .secondary)
VStack(alignment: .leading, spacing: 3) {
title.text
.font(OpenClawType.subheadSemiBold)
detail.text
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer(minLength: 8)
}
}
}
@@ -203,7 +203,7 @@ extension AgentProTab {
ProCard(padding: 0, radius: AgentLayout.cardRadius) {
let skills = self.filteredSkills
if skills.isEmpty {
self.emptyDetailRow(
agentProEmptyDetailRow(
icon: "sparkles",
title: self.gatewayConnected ? "No skills found" : "Skills unavailable",
detail: self.gatewayConnected
@@ -617,8 +617,8 @@ extension AgentProTab {
func skillEditorMetadata(_ skill: SkillStatusEntryLite) -> some View {
ProCard(radius: AgentLayout.cardRadius) {
VStack(alignment: .leading, spacing: 8) {
self.detailMetric(label: "Key", value: skill.effectiveSkillKey)
self.detailMetric(label: "Source", value: self.normalized(skill.source) ?? "unknown")
agentProDetailMetric(label: "Key", value: skill.effectiveSkillKey)
agentProDetailMetric(label: "Source", value: self.normalized(skill.source) ?? "unknown")
if let filePath = self.normalized(skill.filePath) {
Text(filePath)
.font(OpenClawType.monoCaption2)
@@ -15,9 +15,9 @@ extension AgentProTab {
color: OpenClawBrand.accentForeground)
}
HStack(spacing: 10) {
self.detailMetric(label: "Cost", value: self.usageValue)
self.detailMetric(label: "Tokens", value: self.usageTokenValue)
self.detailMetric(label: "Cache", value: self.usageCacheValue)
agentProDetailMetric(label: "Cost", value: self.usageValue)
agentProDetailMetric(label: "Tokens", value: self.usageTokenValue)
agentProDetailMetric(label: "Cache", value: self.usageCacheValue)
}
}
}
@@ -42,7 +42,7 @@ extension AgentProTab {
ProCard(padding: 0, radius: AgentLayout.cardRadius) {
let days = Self.displayedUsageDays(self.overview?.usage?.daily ?? [])
if days.isEmpty {
self.emptyDetailRow(
agentProEmptyDetailRow(
icon: "chart.bar",
title: "No daily usage yet",
detail: "The gateway returned totals without daily session cost rows.")
+3 -9
View File
@@ -216,18 +216,12 @@ const LOCALIZED_WRAPPER_CONTRACTS: Record<string, readonly string[]> = {
"apps/ios/Sources/Design/AgentProNodesDestination.swift": [
"private func nodeDetailRow(\n _ title: OpenClawTextValue,\n copyLabel: LocalizedStringKey",
"private func nodeListCard(title: OpenClawTextValue, values: [String])",
"private func detailMetric(label: OpenClawTextValue, value: String)",
"title: OpenClawTextValue,\n detail: OpenClawTextValue",
],
"apps/ios/Sources/Design/AgentProDreamingDestination.swift": [
"private func detailMetric(label: OpenClawTextValue, value: String)",
"apps/ios/Sources/Design/AgentProDetailComponents.swift": [
"func agentProDetailMetric(label: OpenClawTextValue, value: String)",
"label.text",
"Text(verbatim: value)",
],
"apps/ios/Sources/Design/AgentProTab+DetailComponents.swift": [
"func detailMetric(label: OpenClawTextValue, value: String)",
"Text(verbatim: value)",
"func emptyDetailRow(\n icon: String,\n title: OpenClawTextValue,\n detail: OpenClawTextValue)",
"func agentProEmptyDetailRow(\n icon: String,\n title: OpenClawTextValue,\n detail: OpenClawTextValue)",
"title.text",
"detail.text",
],
+10 -9
View File
@@ -341,7 +341,7 @@ describe("Apple app i18n catalogs", () => {
"utf8",
);
const agentDetailComponents = await readFile(
"apps/ios/Sources/Design/AgentProTab+DetailComponents.swift",
"apps/ios/Sources/Design/AgentProDetailComponents.swift",
"utf8",
);
const agentDreaming = await readFile(
@@ -391,22 +391,23 @@ describe("Apple app i18n catalogs", () => {
"func metricTile(\n icon: String,\n title: OpenClawTextValue,\n value: String,\n detail: OpenClawTextValue",
);
expect(agentDetailComponents).toContain(
"func detailMetric(label: OpenClawTextValue, value: String)",
"func agentProDetailMetric(label: OpenClawTextValue, value: String)",
);
expect(agentDetailComponents).toContain("Text(verbatim: value)");
expect(agentDetailComponents).toContain(
"func emptyDetailRow(\n icon: String,\n title: OpenClawTextValue,\n detail: OpenClawTextValue)",
"func agentProEmptyDetailRow(\n icon: String,\n title: OpenClawTextValue,\n detail: OpenClawTextValue)",
);
expect(agentDetailComponents).toContain("title.text");
expect(agentDetailComponents).toContain("detail.text");
expect(agentDetailComponents).not.toContain("func detailMetric(label: String");
expect(agentDetailComponents).not.toContain("func emptyDetailRow(icon: String, title: String");
expect(agentDreaming).toContain(
"private func detailMetric(label: OpenClawTextValue, value: String)",
expect(agentDetailComponents).not.toContain("func agentProDetailMetric(label: String");
expect(agentDetailComponents).not.toContain(
"func agentProEmptyDetailRow(icon: String, title: String",
);
expect(agentDreaming).toContain("label.text");
expect(agentDreaming).toContain("Text(verbatim: value)");
expect(agentDreaming).toContain("agentProDetailMetric(");
expect(agentDreaming).toContain("agentProEmptyDetailRow(");
expect(agentDreaming).not.toContain("private func detailMetric(label: String");
expect(agentDreaming).not.toContain("private func detailMetric(");
expect(agentDreaming).not.toContain("private func emptyDetailRow(");
expect(settingsActions).toContain(
"func diagnosticCheckRow(\n icon: String,\n title: OpenClawTextValue,\n detail: OpenClawTextValue,\n value: OpenClawTextValue",
);