mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
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:
@@ -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.")
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user