diff --git a/apps/ios/Sources/Design/AgentProDetailComponents.swift b/apps/ios/Sources/Design/AgentProDetailComponents.swift new file mode 100644 index 000000000000..970609752e11 --- /dev/null +++ b/apps/ios/Sources/Design/AgentProDetailComponents.swift @@ -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) + } +} diff --git a/apps/ios/Sources/Design/AgentProDreamingDestination.swift b/apps/ios/Sources/Design/AgentProDreamingDestination.swift index 4e6213c1355c..39105e15e190 100644 --- a/apps/ios/Sources/Design/AgentProDreamingDestination.swift +++ b/apps/ios/Sources/Design/AgentProDreamingDestination.swift @@ -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)" diff --git a/apps/ios/Sources/Design/AgentProNodesDestination.swift b/apps/ios/Sources/Design/AgentProNodesDestination.swift index ea0367fbbc83..5cc39320b7c7 100644 --- a/apps/ios/Sources/Design/AgentProNodesDestination.swift +++ b/apps/ios/Sources/Design/AgentProNodesDestination.swift @@ -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) diff --git a/apps/ios/Sources/Design/AgentProTab+Cron.swift b/apps/ios/Sources/Design/AgentProTab+Cron.swift index f9ebd56f4da0..2c0b27af1d5d 100644 --- a/apps/ios/Sources/Design/AgentProTab+Cron.swift +++ b/apps/ios/Sources/Design/AgentProTab+Cron.swift @@ -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) diff --git a/apps/ios/Sources/Design/AgentProTab+DetailComponents.swift b/apps/ios/Sources/Design/AgentProTab+DetailComponents.swift deleted file mode 100644 index a6fe7c09ac0f..000000000000 --- a/apps/ios/Sources/Design/AgentProTab+DetailComponents.swift +++ /dev/null @@ -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) - } - } -} diff --git a/apps/ios/Sources/Design/AgentProTab+Skills.swift b/apps/ios/Sources/Design/AgentProTab+Skills.swift index 18d1e19e25b2..bb5aa61bbd30 100644 --- a/apps/ios/Sources/Design/AgentProTab+Skills.swift +++ b/apps/ios/Sources/Design/AgentProTab+Skills.swift @@ -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) diff --git a/apps/ios/Sources/Design/AgentProTab+Usage.swift b/apps/ios/Sources/Design/AgentProTab+Usage.swift index c176733cb5c9..17fb2c77cbfb 100644 --- a/apps/ios/Sources/Design/AgentProTab+Usage.swift +++ b/apps/ios/Sources/Design/AgentProTab+Usage.swift @@ -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.") diff --git a/scripts/apple-app-i18n.ts b/scripts/apple-app-i18n.ts index 23c11f13dcfc..e6b81a5b86de 100644 --- a/scripts/apple-app-i18n.ts +++ b/scripts/apple-app-i18n.ts @@ -216,18 +216,12 @@ const LOCALIZED_WRAPPER_CONTRACTS: Record = { "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", ], diff --git a/test/scripts/apple-app-i18n.test.ts b/test/scripts/apple-app-i18n.test.ts index 19278e8a8ebd..a5420266cf67 100644 --- a/test/scripts/apple-app-i18n.test.ts +++ b/test/scripts/apple-app-i18n.test.ts @@ -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", );