From 864a26b771cf3f8a7bd9e01d9d6910c2fe31cf9d Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 01:36:38 +0800 Subject: [PATCH 01/47] feat(ui): surface package bundles in matrix --- .../Sources/Popskill/App/PopskillStore.swift | 41 +- .../Popskill/Models/MatrixCapability.swift | 108 ++++- .../Sources/Popskill/Models/SkillModels.swift | 71 +++ .../Resources/en.lproj/Localizable.strings | 21 +- .../zh-Hans.lproj/Localizable.strings | 21 +- .../Popskill/Views/InspectorPane.swift | 258 +++++++++- .../Popskill/Views/MatrixPackageRow.swift | 441 ++++++++++++++++++ .../Sources/Popskill/Views/MatrixView.swift | 20 +- .../PopskillTests/PopskillStoreTests.swift | 73 +++ .../PopskillTests/SkillGroupingTests.swift | 7 +- .../PopskillTests/SkillModelsTests.swift | 88 ++++ 11 files changed, 1124 insertions(+), 25 deletions(-) create mode 100644 swift-app/Sources/Popskill/Views/MatrixPackageRow.swift diff --git a/swift-app/Sources/Popskill/App/PopskillStore.swift b/swift-app/Sources/Popskill/App/PopskillStore.swift index a8e575c..9cae355 100644 --- a/swift-app/Sources/Popskill/App/PopskillStore.swift +++ b/swift-app/Sources/Popskill/App/PopskillStore.swift @@ -15,6 +15,7 @@ import Observation final class PopskillStore { // ===== Data slices ===== var skills: [Skill] = [] + var packages: [CapabilityPackage] = [] var unmanagedSkills: [UnmanagedSkill] = [] var localAgents: [LocalAgent] = [] var agentTargets: [AgentTarget] = [] @@ -70,6 +71,10 @@ final class PopskillStore { /// Repo groups the user has explicitly collapsed. Set is keyed by /// `MatrixGroup.id` (== "owner/name" or "ungrouped"). var collapsedGroups: Set = [] + /// Composite packages are expanded by default so the matrix immediately + /// shows the component tree from the reference design. Users can collapse + /// noisy bundles without hiding the whole source group. + var collapsedPackageIDs: Set = [] // ===== System state ===== var lastBootstrapAt: Date? @@ -102,12 +107,14 @@ final class PopskillStore { async let skillsTask = client.list() async let sourcesTask = client.listRepositories() async let agentsTask = client.listAgents() + async let packagesTask = loadPackagesBestEffort() do { let now = Date() self.skills = try await skillsTask self.sources = try await sourcesTask self.localAgents = try await agentsTask + self.packages = await packagesTask self.lastBootstrapAt = now // Bootstrap counts as a fresh sources fetch — secondary views // that .task into refreshSources won't double-pull immediately. @@ -117,6 +124,14 @@ final class PopskillStore { } } + private func loadPackagesBestEffort() async -> [CapabilityPackage] { + do { + return try await client.listPackages() + } catch { + return [] + } + } + // MARK: Cached refresh helpers // // Each secondary view (Sources / Updates / Backups) hits sidecar on its @@ -176,9 +191,16 @@ final class PopskillStore { /// rather than stored so toggle / install / uninstall actions only need /// to mutate `skills` / `localAgents` and the matrix follows. var capabilities: [MatrixCapability] { - skills.map(MatrixCapability.fromSkill) + localAgents.map(MatrixCapability.fromAgent) + compositePackages.map { MatrixCapability.fromPackage($0, skills: skills) } + + skills.map(MatrixCapability.fromSkill) + + localAgents.map(MatrixCapability.fromAgent) + } + + var compositePackages: [CapabilityPackage] { + packages.filter { $0.type == .composite } } + var bundleCount: Int { compositePackages.count } var pendingUpdateCount: Int { updates.count } var brokenLinkCount: Int { linkHealth?.summary.broken ?? 0 } var okLinkCount: Int { linkHealth?.summary.ok ?? 0 } @@ -193,6 +215,11 @@ final class PopskillStore { /// may be scoped ("owner/name:skill") or path-like, so both the full id /// and its useful suffixes are indexed once when `updates` changes. func hasPendingUpdate(for capability: MatrixCapability) -> Bool { + if let package = capability.package { + return updates.contains { update in + package.matchingSkillComponent(for: update) != nil + } + } guard let skillID = capability.underlyingSkillID else { return false } return updateIdentifierCandidates(for: skillID).contains { updateSkillIDs.contains($0) } } @@ -220,6 +247,18 @@ final class PopskillStore { inspectorOpen = true } + func togglePackageExpansion(_ packageID: String) { + if collapsedPackageIDs.contains(packageID) { + collapsedPackageIDs.remove(packageID) + } else { + collapsedPackageIDs.insert(packageID) + } + } + + func skill(for component: PackageComponent) -> Skill? { + skills.first { component.matchesSkill($0) } + } + func closeInspector() { inspectorOpen = false selectedSkillID = nil diff --git a/swift-app/Sources/Popskill/Models/MatrixCapability.swift b/swift-app/Sources/Popskill/Models/MatrixCapability.swift index b4561dc..bbea759 100644 --- a/swift-app/Sources/Popskill/Models/MatrixCapability.swift +++ b/swift-app/Sources/Popskill/Models/MatrixCapability.swift @@ -1,11 +1,11 @@ import Foundation -/// Five capability modalities Popskill surfaces in the matrix. Order in -/// `CaseIterable` mirrors the type-filter chip row in `MatrixView` (skill → -/// agent → cli → mcp → config). v0.4 ships skill + agent fully; cli / mcp / -/// config are placeholders that the matrix renders an "empty for now" hint -/// for until later sprints wire their sidecar discovery. +/// Capability modalities Popskill surfaces in the matrix. Order in +/// `CaseIterable` mirrors the type-filter chip row in `MatrixView`. +/// Bundles are first-class package rows backed by `package-list`; skill and +/// agent rows keep the existing direct management flows. enum CapabilityKind: String, Codable, CaseIterable, Identifiable { + case bundle case skill case agent case cli @@ -16,6 +16,7 @@ enum CapabilityKind: String, Codable, CaseIterable, Identifiable { var titleKey: String { switch self { + case .bundle: return "matrix.type.bundle" case .skill: return "matrix.type.skill" case .agent: return "matrix.type.agent" case .cli: return "matrix.type.cli" @@ -26,6 +27,7 @@ enum CapabilityKind: String, Codable, CaseIterable, Identifiable { var symbol: String { switch self { + case .bundle: return "shippingbox.fill" case .skill: return "square.grid.3x3.fill" case .agent: return "person.crop.square" case .cli: return "terminal" @@ -35,6 +37,14 @@ enum CapabilityKind: String, Codable, CaseIterable, Identifiable { } } +struct CapabilityAppCoverage: Equatable { + let enabled: Int + let total: Int + + var isEnabled: Bool { enabled > 0 } + var label: String { "\(enabled)/\(total)" } +} + /// Unified row model the matrix renders against. Wraps `Skill` / `LocalAgent` /// / future `CLITool` / `MCPServer` / `ConfigEntry` without exposing their /// individual shapes to view code. Toggle actions look up the underlying @@ -59,10 +69,60 @@ struct MatrixCapability: Identifiable, Equatable { let triggerScenarios: [String]? let underlyingSkillID: String? let underlyingAgentID: String? + let underlyingPackageID: String? + let package: CapabilityPackage? + let appCoverage: [TargetApp: CapabilityAppCoverage] + + init( + id: String, + kind: CapabilityKind, + name: String, + summary: String?, + sourceLabel: String, + sourceType: String?, + repoOwner: String?, + repoName: String?, + apps: SkillApps, + deployment: SkillDeployment?, + directory: String, + installedAt: Int?, + updatedAt: Int?, + sizeBytes: UInt64?, + triggerScenarios: [String]?, + underlyingSkillID: String?, + underlyingAgentID: String?, + underlyingPackageID: String? = nil, + package: CapabilityPackage? = nil, + appCoverage: [TargetApp: CapabilityAppCoverage] = [:] + ) { + self.id = id + self.kind = kind + self.name = name + self.summary = summary + self.sourceLabel = sourceLabel + self.sourceType = sourceType + self.repoOwner = repoOwner + self.repoName = repoName + self.apps = apps + self.deployment = deployment + self.directory = directory + self.installedAt = installedAt + self.updatedAt = updatedAt + self.sizeBytes = sizeBytes + self.triggerScenarios = triggerScenarios + self.underlyingSkillID = underlyingSkillID + self.underlyingAgentID = underlyingAgentID + self.underlyingPackageID = underlyingPackageID + self.package = package + self.appCoverage = appCoverage + } /// Source URL is reusable across kinds — the matrix uses it for the /// "Open Source" menu item. var sourceURL: URL? { + if let packageURL = package?.sourceURL { + return packageURL + } if let owner = repoOwner, let name = repoName, !owner.isEmpty, !name.isEmpty { return URL(string: "https://github.com/\(owner)/\(name)") } @@ -89,6 +149,10 @@ struct MatrixCapability: Identifiable, Equatable { capabilityID(kind: .agent, rawID: agentID) } + static func packageCapabilityID(for packageID: String) -> String { + capabilityID(kind: .bundle, rawID: packageID) + } + static func toggleKey(capabilityID: String, app: TargetApp) -> String { "\(capabilityID)|\(app.rawValue)" } @@ -99,6 +163,40 @@ struct MatrixCapability: Identifiable, Equatable { } extension MatrixCapability { + static func fromPackage(_ package: CapabilityPackage, skills: [Skill]) -> MatrixCapability { + let coverage = package.appCoverage(using: skills) + let appState = SkillApps( + claude: coverage[.claude]?.isEnabled == true, + codex: coverage[.codex]?.isEnabled == true, + gemini: coverage[.gemini]?.isEnabled == true, + opencode: coverage[.opencode]?.isEnabled == true, + hermes: coverage[.hermes]?.isEnabled == true + ) + + return MatrixCapability( + id: packageCapabilityID(for: package.id), + kind: .bundle, + name: package.name, + summary: package.summary, + sourceLabel: package.sourceLabel, + sourceType: package.source.kind, + repoOwner: package.source.repoOwner, + repoName: package.source.repoName, + apps: appState, + deployment: nil, + directory: package.source.location, + installedAt: package.lifecycle?.installedAt, + updatedAt: package.lifecycle?.updatedAt, + sizeBytes: nil, + triggerScenarios: nil, + underlyingSkillID: nil, + underlyingAgentID: nil, + underlyingPackageID: package.id, + package: package, + appCoverage: coverage + ) + } + static func fromSkill(_ skill: Skill) -> MatrixCapability { MatrixCapability( id: skillCapabilityID(for: skill.id), diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index 59b32f8..0554d48 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -1160,6 +1160,32 @@ extension SkillUpdateInfo { } extension PackageComponent { + func matchesSkill(_ skill: Skill) -> Bool { + guard kind.caseInsensitiveCompare("skill") == .orderedSame else { + return false + } + + let candidates = [ + id, + name, + location ?? "", + location?.split(separator: "/").last.map(String.init) ?? "" + ] + .map(Self.normalizedIdentifier) + .filter { !$0.isEmpty } + + let skillCandidates = [ + skill.id, + skill.name, + skill.directory, + skill.id.split(separator: ":", maxSplits: 1).last.map(String.init) ?? skill.id + ] + .map(Self.normalizedIdentifier) + .filter { !$0.isEmpty } + + return !Set(candidates).isDisjoint(with: Set(skillCandidates)) + } + func matchesSkillUpdate(_ update: SkillUpdateInfo) -> Bool { guard kind.caseInsensitiveCompare("skill") == .orderedSame else { return false @@ -1179,14 +1205,59 @@ extension PackageComponent { return name.caseInsensitiveCompare(update.name) == .orderedSame } + + private static func normalizedIdentifier(_ value: String) -> String { + value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + } } extension CapabilityPackage { + func matchingInstalledSkill(for component: PackageComponent, in skills: [Skill]) -> Skill? { + skills.first { component.matchesSkill($0) } + } + + func appCoverage(using skills: [Skill]) -> [TargetApp: CapabilityAppCoverage] { + let components = components.all + let total = components.count + guard total > 0 else { + return Dictionary( + uniqueKeysWithValues: TargetApp.supported.map { app in + (app, CapabilityAppCoverage(enabled: 0, total: 0)) + } + ) + } + + return Dictionary(uniqueKeysWithValues: TargetApp.supported.map { app in + let enabled = components.filter { component in + component.isEnabled(for: app, matching: matchingInstalledSkill(for: component, in: skills)) + }.count + return (app, CapabilityAppCoverage(enabled: enabled, total: total)) + }) + } + func matchingSkillComponent(for update: SkillUpdateInfo) -> PackageComponent? { components.skills.first { $0.matchesSkillUpdate(update) } } } +private extension PackageComponent { + func isEnabled(for app: TargetApp, matching skill: Skill?) -> Bool { + switch kind.lowercased() { + case "skill": + if let skill { + return skill.apps.isEnabled(app) + } + return installed && (app == .claude || app == .codex) + case "agent": + return installed && app == .claude + case "cli", "mcp": + return installed && (app == .claude || app == .codex) + default: + return false + } + } +} + struct CLIResponse: Decodable { let ok: Bool let data: T? diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 0f81732..28eadbd 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -306,7 +306,7 @@ "sidebar.section.maintenance" = "MAINTENANCE"; // S3 新增 — Matrix + Inspector -"matrix.subtitle" = "%d capabilities · %d active toggles"; +"matrix.subtitle" = "%d bundles · %d capabilities · %d active toggles"; "matrix.search.placeholder" = "Filter by name, trigger, or directory"; "matrix.col.capability" = "Capability"; "matrix.col.source" = "Source"; @@ -318,6 +318,7 @@ "matrix.filter.inactive" = "Inactive"; "matrix.type.all" = "All types"; +"matrix.type.bundle" = "Bundle"; "matrix.type.skill" = "Skill"; "matrix.type.agent" = "Agent"; "matrix.type.cli" = "CLI"; @@ -336,6 +337,20 @@ "matrix.row.menu.revealInFinder" = "Reveal in Finder"; "matrix.row.menu.help" = "More actions"; "matrix.group.coverageHelp" = "%@: %d of %d enabled"; +"matrix.package.coverageHelp" = "%@ coverage: %d of %d components"; +"matrix.package.expand" = "Expand components"; +"matrix.package.collapse" = "Collapse components"; +"matrix.package.health.active" = "Complete"; +"matrix.package.health.partial" = "Partial"; +"matrix.package.health.inactive" = "Not installed"; +"matrix.package.health.blocked" = "Missing required"; +"matrix.package.component.required" = "Required"; +"matrix.package.component.optional" = "Optional"; +"matrix.package.component.enabledHelp" = "%@ enabled"; +"matrix.package.component.partialHelp" = "%@ is stubbed or recoverable"; +"matrix.package.component.offHelp" = "%@ not enabled"; +"matrix.package.missingRequired" = "%d required missing"; +"matrix.package.config.secret" = "Keychain secret"; "matrix.empty.title" = "No capabilities yet"; "matrix.empty.body" = "Install your first skill from a source, or wait for bootstrap to finish."; @@ -348,6 +363,9 @@ "matrix.inspector.section.summary" = "SUMMARY"; "matrix.inspector.section.triggers" = "TRIGGERS"; "matrix.inspector.section.apps" = "APPS"; +"matrix.inspector.section.coverage" = "COVERAGE"; +"matrix.inspector.section.components" = "COMPONENTS"; +"matrix.inspector.section.config" = "CONFIG"; "matrix.inspector.section.deployment" = "DEPLOYMENT"; "matrix.inspector.section.meta" = "METADATA"; @@ -362,6 +380,7 @@ "matrix.inspector.meta.directory" = "Directory"; "matrix.inspector.meta.sourceType" = "Source type"; +"matrix.inspector.meta.packageType" = "Package type"; "matrix.inspector.meta.installedAt" = "Installed"; "matrix.inspector.meta.updatedAt" = "Updated"; "matrix.inspector.meta.size" = "Size"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 855ee07..aea379b 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -306,7 +306,7 @@ "sidebar.section.maintenance" = "维护"; // S3 新增 — 矩阵主视图 + Inspector -"matrix.subtitle" = "%d 项能力 · %d 个已启用开关"; +"matrix.subtitle" = "%d 个套装 · %d 项能力 · %d 个已启用开关"; "matrix.search.placeholder" = "按名称、触发词或目录过滤"; "matrix.col.capability" = "能力"; "matrix.col.source" = "来源"; @@ -318,6 +318,7 @@ "matrix.filter.inactive" = "未启用"; "matrix.type.all" = "所有类型"; +"matrix.type.bundle" = "Bundle"; "matrix.type.skill" = "Skill"; "matrix.type.agent" = "Agent"; "matrix.type.cli" = "CLI"; @@ -336,6 +337,20 @@ "matrix.row.menu.revealInFinder" = "在 Finder 中显示"; "matrix.row.menu.help" = "更多操作"; "matrix.group.coverageHelp" = "%@:已启用 %d / %d"; +"matrix.package.coverageHelp" = "%@ 覆盖:%d / %d 个组件"; +"matrix.package.expand" = "展开组件"; +"matrix.package.collapse" = "收起组件"; +"matrix.package.health.active" = "完整"; +"matrix.package.health.partial" = "部分"; +"matrix.package.health.inactive" = "未安装"; +"matrix.package.health.blocked" = "缺必需"; +"matrix.package.component.required" = "必需"; +"matrix.package.component.optional" = "可选"; +"matrix.package.component.enabledHelp" = "%@ 已启用"; +"matrix.package.component.partialHelp" = "%@ 是占位或待恢复"; +"matrix.package.component.offHelp" = "%@ 未启用"; +"matrix.package.missingRequired" = "缺 %d 个必需"; +"matrix.package.config.secret" = "Keychain 密钥"; "matrix.empty.title" = "还没有能力"; "matrix.empty.body" = "从数据源安装第一个 skill,或等 sidecar 初始化完成。"; @@ -348,6 +363,9 @@ "matrix.inspector.section.summary" = "简介"; "matrix.inspector.section.triggers" = "触发场景"; "matrix.inspector.section.apps" = "应用启用"; +"matrix.inspector.section.coverage" = "覆盖"; +"matrix.inspector.section.components" = "组件"; +"matrix.inspector.section.config" = "配置"; "matrix.inspector.section.deployment" = "位置与链接"; "matrix.inspector.section.meta" = "元信息"; @@ -362,6 +380,7 @@ "matrix.inspector.meta.directory" = "目录"; "matrix.inspector.meta.sourceType" = "来源类型"; +"matrix.inspector.meta.packageType" = "套装类型"; "matrix.inspector.meta.installedAt" = "安装于"; "matrix.inspector.meta.updatedAt" = "更新于"; "matrix.inspector.meta.size" = "大小"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 31b5586..ae83caa 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -14,17 +14,27 @@ struct InspectorPane: View { ScrollView { VStack(alignment: .leading, spacing: 18) { header - if !primaryDescription.isEmpty { - summarySection - } - if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { - triggerSection(scenarios: scenarios) - } - appsSection - if capability.kind == .skill { - deploymentSection + if let package = capability.package { + packageSummarySection(package) + packageCoverageSection + packageComponentsSection(package) + if !package.configSchema.isEmpty { + packageConfigSection(package) + } + packageMetadataSection(package) + } else { + if !primaryDescription.isEmpty { + summarySection + } + if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { + triggerSection(scenarios: scenarios) + } + appsSection + if capability.kind == .skill { + deploymentSection + } + metadataSection } - metadataSection } .padding(.horizontal, 18) .padding(.vertical, 18) @@ -96,6 +106,190 @@ struct InspectorPane: View { } } + private func packageSummarySection(_ package: CapabilityPackage) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.summary", accent: .popSectionPurple) + Text(package.summary) + .font(.callout) + .foregroundStyle(Color.popLabel) + .textSelection(.enabled) + + HStack(spacing: 8) { + packagePill( + localization.string(package.health.inspectorTitleKey), + color: package.health.inspectorColor + ) + packagePill( + localization.string( + "package.componentSummary", + package.componentCount, + package.installedComponentCount, + package.requiredComponentCount + ), + color: Color.popSecondaryLabel + ) + } + } + } + + private var packageCoverageSection: some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.coverage", accent: .popSectionPurple) + HStack(spacing: 10) { + packageCoverageCard(for: .claude) + packageCoverageCard(for: .codex) + } + } + } + + private func packageCoverageCard(for app: TargetApp) -> some View { + let coverage = capability.appCoverage[app] ?? CapabilityAppCoverage(enabled: 0, total: 0) + return VStack(alignment: .leading, spacing: 5) { + HStack(spacing: 5) { + Image(systemName: app.symbolName) + .font(.system(size: 11, weight: .semibold)) + Text(app.title) + .font(.caption.weight(.medium)) + } + .foregroundStyle(Color.popSecondaryLabel) + Text(coverage.label) + .font(.system(size: 24, weight: .bold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(coverage.enabled > 0 ? app.inspectorAccentColor : Color.popTertiaryLabel) + ProgressView(value: Double(coverage.enabled), total: Double(max(coverage.total, 1))) + .tint(app.inspectorAccentColor) + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + + private func packageComponentsSection(_ package: CapabilityPackage) -> some View { + VStack(alignment: .leading, spacing: 10) { + SectionHeading(title: "matrix.inspector.section.components", accent: .popSectionPurple) + ForEach(package.componentGroupSummaries) { group in + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(group.title) + .font(.caption.weight(.semibold)) + .foregroundStyle(Color.popLabel) + Text("\(group.installed)/\(group.total)") + .font(.caption2.monospacedDigit().weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + if group.missingRequired > 0 { + Text(localization.string("matrix.package.missingRequired", group.missingRequired)) + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popStatusError) + } + Spacer() + } + ForEach(components(in: group.kind, package: package), id: \.displayKey) { component in + packageComponentDetailRow(component) + } + } + .padding(8) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + } + } + + private func components(in kind: String, package: CapabilityPackage) -> [PackageComponent] { + switch kind { + case "skill": return package.components.skills + case "cli": return package.components.cli + case "mcp": return package.components.mcp + case "agent": return package.components.agents + default: return [] + } + } + + private func packageComponentDetailRow(_ component: PackageComponent) -> some View { + HStack(alignment: .top, spacing: 8) { + Image(systemName: component.inspectorKindSymbol) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(component.installed ? Color.popStatusOK : Color.popTertiaryLabel) + .frame(width: 14) + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 5) { + Text(component.name) + .font(.caption.weight(.medium)) + .foregroundStyle(Color.popLabel) + Text(component.kind.uppercased()) + .font(.system(size: 8, weight: .bold)) + .foregroundStyle(Color.popSecondaryLabel) + } + Text(component.location ?? component.id) + .font(.system(size: 10.5, design: .monospaced)) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } + Spacer(minLength: 8) + Text(component.status) + .font(.caption2.weight(.semibold)) + .foregroundStyle(component.installed ? Color.popStatusOK : Color.popStatusWarning) + } + .padding(.vertical, 2) + } + + private func packageConfigSection(_ package: CapabilityPackage) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.config", accent: .popSectionPurple) + ForEach(package.configSchema) { field in + HStack(spacing: 8) { + Text(field.label) + .font(.caption.weight(.medium)) + .foregroundStyle(Color.popLabel) + if field.required { + Text(localization.string("matrix.package.component.required")) + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popStatusWarning) + } + Spacer() + Text(field.secret ? localization.string("matrix.package.config.secret") : field.storage) + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + } + .padding(8) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + } + } + + private func packageMetadataSection(_ package: CapabilityPackage) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.meta") + VStack(alignment: .leading, spacing: 6) { + metaRow(label: localization.string("matrix.inspector.meta.directory"), value: package.source.location) + metaRow(label: localization.string("matrix.inspector.meta.sourceType"), value: package.source.kind) + metaRow(label: localization.string("matrix.inspector.meta.packageType"), value: package.typeLabel) + if let installedAt = package.lifecycle?.installedAt, installedAt > 0 { + metaRow(label: localization.string("matrix.inspector.meta.installedAt"), value: Self.formatTimestamp(installedAt)) + } + if let updatedAt = package.lifecycle?.updatedAt, updatedAt > 0 { + metaRow(label: localization.string("matrix.inspector.meta.updatedAt"), value: Self.formatTimestamp(updatedAt)) + } + } + if let url = package.sourceURL { + Link(destination: url) { + Label(localization.string("matrix.inspector.meta.openSource"), systemImage: "arrow.up.right.square") + .font(.caption) + } + .buttonStyle(.plain) + } + } + } + + private func packagePill(_ title: String, color: Color) -> some View { + Text(title) + .font(.caption2.weight(.semibold)) + .foregroundStyle(color) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background(color.opacity(0.10), in: Capsule()) + } + private func triggerSection(scenarios: [String]) -> some View { VStack(alignment: .leading, spacing: 6) { SectionHeading(title: "matrix.inspector.section.triggers", accent: .accentColor) @@ -336,3 +530,47 @@ struct InspectorPane: View { } } } + +private extension CapabilityPackageHealth { + var inspectorTitleKey: String { + switch self { + case .active: "matrix.package.health.active" + case .partial: "matrix.package.health.partial" + case .inactive: "matrix.package.health.inactive" + case .blocked: "matrix.package.health.blocked" + } + } + + var inspectorColor: Color { + switch self { + case .active: Color.popStatusOK + case .partial: Color.popStatusWarning + case .inactive: Color.popTertiaryLabel + case .blocked: Color.popStatusError + } + } +} + +private extension PackageComponent { + var inspectorKindSymbol: String { + switch kind.lowercased() { + case "skill": "square.grid.3x3.fill" + case "agent": "person.crop.square" + case "cli": "terminal" + case "mcp": "rectangle.connected.to.line.below" + default: "circle.grid.2x2" + } + } +} + +private extension TargetApp { + var inspectorAccentColor: Color { + switch self { + case .claude: .orange + case .codex: .green + case .gemini: .blue + case .opencode: .indigo + case .hermes: .purple + } + } +} diff --git a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift new file mode 100644 index 0000000..2da9286 --- /dev/null +++ b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift @@ -0,0 +1,441 @@ +import SwiftUI + +/// Composite package row in the capability matrix. The row is read-only at the +/// package level for v1.1, but it exposes the component tree inline so Bundle +/// coverage is visible without opening a detail page. +struct MatrixPackageRow: View { + let capability: MatrixCapability + @Bindable var store: PopskillStore + @Environment(\.popskillLocalization) private var localization + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var isHovering = false + + private var package: CapabilityPackage? { capability.package } + + private var isSelected: Bool { + store.selectedSkillID == capability.id + } + + private var isCollapsed: Bool { + guard let package else { return true } + return store.collapsedPackageIDs.contains(package.id) + } + + var body: some View { + VStack(spacing: 0) { + packageHeader + if let package, !isCollapsed { + ForEach(package.components.all, id: \.displayKey) { component in + MatrixPackageComponentRow(component: component, store: store) + Divider().opacity(0.28) + } + } + } + } + + private var packageHeader: some View { + HStack(spacing: 0) { + capabilityCell + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 14) + .padding(.vertical, 9) + + coverageCell(for: .claude) + .frame(width: 100) + coverageCell(for: .codex) + .frame(width: 100) + + sourceCell + .frame(width: 220, alignment: .leading) + + actionCell + .frame(width: 56) + } + .padding(.trailing, 4) + .contentShape(Rectangle()) + .background(rowBackground) + .overlay(alignment: .leading) { + if isSelected { + RoundedRectangle(cornerRadius: 1.5, style: .continuous) + .fill(Color.accentColor) + .frame(width: 3) + .padding(.vertical, 6) + } + } + .onTapGesture { + store.selectCapability(capability.id) + } + .onHover { isHovering = $0 } + .animation(reduceMotion ? nil : .easeInOut(duration: 0.14), value: isHovering) + .animation(reduceMotion ? nil : .easeInOut(duration: 0.16), value: isSelected) + .accessibilityElement(children: .combine) + .accessibilityLabel(Text(capability.name)) + .accessibilityHint(Text(capability.summary ?? localization.string("matrix.row.noSummary"))) + .accessibilityAddTraits(isSelected ? .isSelected : []) + } + + private var capabilityCell: some View { + HStack(alignment: .center, spacing: 10) { + Button { + if let package { + store.togglePackageExpansion(package.id) + } + } label: { + Image(systemName: isCollapsed ? "chevron.right" : "chevron.down") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(Color.popSecondaryLabel) + .frame(width: 14, height: 22) + } + .buttonStyle(.plain) + .help(localization.string(isCollapsed ? "matrix.package.expand" : "matrix.package.collapse")) + + PackageAvatar(name: capability.name, identifier: capability.id, size: 30) + + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 6) { + Text(capability.name) + .font(.system(size: 13.5, weight: .semibold)) + .foregroundStyle(Color.popLabel) + .lineLimit(1) + kindBadge + if let package { + healthBadge(package.health) + } + if store.hasPendingUpdate(for: capability) { + Text(localization.string("matrix.row.updateBadge")) + .font(.system(size: 9.5, weight: .semibold)) + .padding(.horizontal, 4) + .padding(.vertical, 1) + .background(Color.accentColor.opacity(0.16), in: Capsule()) + .foregroundStyle(Color.accentColor) + } + } + Text(packageSubtitle) + .font(.system(size: 11.5)) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + } + } + } + + private var kindBadge: some View { + HStack(spacing: 2) { + Image(systemName: capability.kind.symbol) + .font(.system(size: 8, weight: .semibold)) + Text(localization.string(capability.kind.titleKey).uppercased()) + .font(.system(size: 9, weight: .bold)) + } + .foregroundStyle(Color.popSectionPurple) + .padding(.horizontal, 4) + .padding(.vertical, 1) + .background(Color.popSectionPurple.opacity(0.12), in: Capsule()) + } + + private func healthBadge(_ health: CapabilityPackageHealth) -> some View { + let color = health.badgeColor + return Text(localization.string(health.titleKey)) + .font(.system(size: 9.5, weight: .semibold)) + .foregroundStyle(color) + .padding(.horizontal, 5) + .padding(.vertical, 1.5) + .background(color.opacity(0.12), in: Capsule()) + } + + private var packageSubtitle: String { + guard let package else { return capability.summary ?? localization.string("matrix.row.noSummary") } + return localization.string( + "package.componentSummary", + package.componentCount, + package.installedComponentCount, + package.requiredComponentCount + ) + } + + private func coverageCell(for app: TargetApp) -> some View { + let coverage = capability.appCoverage[app] ?? CapabilityAppCoverage(enabled: 0, total: 0) + return HStack { + Spacer(minLength: 0) + HStack(spacing: 4) { + Image(systemName: app.symbolName) + .font(.system(size: 10, weight: .semibold)) + Text(coverage.label) + .font(.system(size: 11, weight: .semibold).monospacedDigit()) + } + .foregroundStyle(coverage.enabled > 0 ? app.bundleAccentColor : Color.popTertiaryLabel) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background( + coverage.enabled > 0 ? app.bundleAccentColor.opacity(0.10) : Color.popControlFill, + in: Capsule() + ) + .overlay( + Capsule().strokeBorder( + coverage.enabled > 0 ? app.bundleAccentColor.opacity(0.26) : Color.popControlStroke, + lineWidth: 0.7 + ) + ) + .help(localization.string("matrix.package.coverageHelp", app.title, coverage.enabled, coverage.total)) + Spacer(minLength: 0) + } + } + + private var sourceCell: some View { + HStack(spacing: 6) { + Image(systemName: "shippingbox") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(Color.popSecondaryLabel) + Text(capability.sourceLabel) + .font(.system(size: 11.5)) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + .truncationMode(.middle) + } + } + + private var actionCell: some View { + Menu { + Button { + store.selectCapability(capability.id) + } label: { + Label(localization.string("matrix.row.menu.inspect"), systemImage: "sidebar.right") + } + if let url = capability.sourceURL { + Link(destination: url) { + Label(localization.string("matrix.row.menu.openSource"), systemImage: "arrow.up.right.square") + } + } + if let package { + Button { + store.togglePackageExpansion(package.id) + } label: { + Label( + localization.string(isCollapsed ? "matrix.package.expand" : "matrix.package.collapse"), + systemImage: isCollapsed ? "chevron.down" : "chevron.up" + ) + } + } + } label: { + Image(systemName: "ellipsis") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(Color.popSecondaryLabel) + .frame(width: 28, height: 22) + } + .menuStyle(.borderlessButton) + .menuIndicator(.hidden) + .fixedSize() + .help(localization.string("matrix.row.menu.help")) + } + + private var rowBackground: some View { + Group { + if isSelected { + Color.popSelectedRowFill + } else if isHovering { + Color.popSurfaceHover + } else { + Color.popSectionPurple.opacity(0.035) + } + } + } +} + +private struct MatrixPackageComponentRow: View { + let component: PackageComponent + @Bindable var store: PopskillStore + @Environment(\.popskillLocalization) private var localization + + private var matchingSkill: Skill? { + store.skill(for: component) + } + + var body: some View { + HStack(spacing: 0) { + componentCell + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 56) + .padding(.vertical, 6) + + appStateCell(for: .claude) + .frame(width: 100) + appStateCell(for: .codex) + .frame(width: 100) + + sourceCell + .frame(width: 220, alignment: .leading) + + Spacer().frame(width: 56) + } + .padding(.trailing, 4) + .contentShape(Rectangle()) + .onTapGesture { + if let skill = matchingSkill { + store.selectSkill(skill.id) + } + } + .background(Color.popCardBackground.opacity(0.20)) + } + + private var componentCell: some View { + HStack(spacing: 9) { + Text(componentTreePrefix) + .font(.system(size: 12, weight: .medium, design: .monospaced)) + .foregroundStyle(Color.popTertiaryLabel) + .frame(width: 18, alignment: .leading) + + Image(systemName: component.kindSymbol) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(component.installed ? Color.popSecondaryLabel : Color.popTertiaryLabel) + .frame(width: 14) + + VStack(alignment: .leading, spacing: 1) { + HStack(spacing: 6) { + Text(component.name) + .font(.system(size: 12.5, weight: .medium)) + .foregroundStyle(Color.popLabel) + .lineLimit(1) + Text(component.kind.uppercased()) + .font(.system(size: 8.5, weight: .bold)) + .foregroundStyle(Color.popSecondaryLabel) + .padding(.horizontal, 4) + .padding(.vertical, 1) + .background(Color.popControlFill, in: Capsule()) + if let requirement = requirementBadge { + Text(localization.string(requirement.key)) + .font(.system(size: 8.5, weight: .semibold)) + .foregroundStyle(requirement.color) + } + } + Text(component.status) + .font(.system(size: 10.5)) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + } + } + } + + private var componentTreePrefix: String { + "├─" + } + + private var requirementBadge: (key: String, color: Color)? { + if component.required && !component.installed { + return ("matrix.package.component.required", Color.popStatusWarning) + } + if !component.required { + return ("matrix.package.component.optional", Color.popTertiaryLabel) + } + return nil + } + + private func appStateCell(for app: TargetApp) -> some View { + let indicator = component.indicator(for: app, matching: matchingSkill) + return HStack { + Spacer(minLength: 0) + Text(indicator.symbol) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(indicator.color) + .frame(width: 26, height: 22) + .help(localization.string(indicator.helpKey, app.title)) + Spacer(minLength: 0) + } + } + + private var sourceCell: some View { + Text(component.location ?? component.id) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + .truncationMode(.middle) + } +} + +private enum ComponentAppIndicator { + case enabled + case partial + case off + + var symbol: String { + switch self { + case .enabled: "●" + case .partial: "◐" + case .off: "—" + } + } + + var color: Color { + switch self { + case .enabled: Color.popStatusOK + case .partial: Color.popStatusWarning + case .off: Color.popTertiaryLabel + } + } + + var helpKey: String { + switch self { + case .enabled: "matrix.package.component.enabledHelp" + case .partial: "matrix.package.component.partialHelp" + case .off: "matrix.package.component.offHelp" + } + } +} + +private extension PackageComponent { + var kindSymbol: String { + switch kind.lowercased() { + case "skill": "square.grid.3x3.fill" + case "agent": "person.crop.square" + case "cli": "terminal" + case "mcp": "rectangle.connected.to.line.below" + default: "circle.grid.2x2" + } + } + + func indicator(for app: TargetApp, matching skill: Skill?) -> ComponentAppIndicator { + switch kind.lowercased() { + case "skill": + if let skill { + return skill.apps.isEnabled(app) ? .enabled : .off + } + return installed ? .enabled : (status.lowercased() == "stub" ? .partial : .off) + case "agent": + if installed && app == .claude { return .enabled } + return status.lowercased() == "stub" && app == .claude ? .partial : .off + case "cli", "mcp": + return installed && (app == .claude || app == .codex) ? .enabled : .off + default: + return .off + } + } +} + +private extension CapabilityPackageHealth { + var titleKey: String { + switch self { + case .active: "matrix.package.health.active" + case .partial: "matrix.package.health.partial" + case .inactive: "matrix.package.health.inactive" + case .blocked: "matrix.package.health.blocked" + } + } + + var badgeColor: Color { + switch self { + case .active: Color.popStatusOK + case .partial: Color.popStatusWarning + case .inactive: Color.popTertiaryLabel + case .blocked: Color.popStatusError + } + } +} + +private extension TargetApp { + var bundleAccentColor: Color { + switch self { + case .claude: .orange + case .codex: .green + case .gemini: .blue + case .opencode: .indigo + case .hermes: .purple + } + } +} diff --git a/swift-app/Sources/Popskill/Views/MatrixView.swift b/swift-app/Sources/Popskill/Views/MatrixView.swift index 268a826..3181069 100644 --- a/swift-app/Sources/Popskill/Views/MatrixView.swift +++ b/swift-app/Sources/Popskill/Views/MatrixView.swift @@ -69,7 +69,7 @@ struct MatrixView: View { private func subtitle(capabilities: [MatrixCapability]) -> String { let count = capabilities.count let active = store.enabledSkillCount - return localization.string("matrix.subtitle", count, active) + return localization.string("matrix.subtitle", store.bundleCount, count, active) } private var searchField: some View { @@ -182,7 +182,11 @@ struct MatrixView: View { MatrixGroupHeader(group: group, store: store) if !store.collapsedGroups.contains(group.id) { ForEach(group.capabilities, id: \.id) { capability in - MatrixRow(capability: capability, store: store) + if capability.kind == .bundle { + MatrixPackageRow(capability: capability, store: store) + } else { + MatrixRow(capability: capability, store: store) + } Divider().opacity(0.4) } } @@ -275,12 +279,17 @@ struct MatrixView: View { private func filteredSections(in capabilities: [MatrixCapability]) -> [CapabilitySection] { let q = store.trimmedSearch.lowercased() let visible = capabilities.filter { capability in - store.matrixFilter.includes(capability: capability, store: store) + store.matrixFilter.includes(capability: capability, store: store) && store.matrixTypeFilter.includes(capability: capability) && (q.isEmpty || capability.name.lowercased().contains(q) || (capability.summary ?? "").lowercased().contains(q) - || capability.directory.lowercased().contains(q)) + || capability.directory.lowercased().contains(q) + || capability.package?.components.all.contains { component in + component.name.lowercased().contains(q) + || component.id.lowercased().contains(q) + || (component.location ?? "").lowercased().contains(q) + } == true) } return SkillGrouping.sections(visible) } @@ -382,6 +391,7 @@ enum MatrixFilter: String, CaseIterable, Identifiable { enum MatrixTypeFilter: String, CaseIterable, Identifiable { case allTypes + case bundle case skill case agent case cli @@ -392,6 +402,7 @@ enum MatrixTypeFilter: String, CaseIterable, Identifiable { var titleKey: String { switch self { case .allTypes: return "matrix.type.all" + case .bundle: return "matrix.type.bundle" case .skill: return "matrix.type.skill" case .agent: return "matrix.type.agent" case .cli: return "matrix.type.cli" @@ -402,6 +413,7 @@ enum MatrixTypeFilter: String, CaseIterable, Identifiable { func includes(capability: MatrixCapability) -> Bool { switch self { case .allTypes: return true + case .bundle: return capability.kind == .bundle case .skill: return capability.kind == .skill case .agent: return capability.kind == .agent case .cli: return capability.kind == .cli diff --git a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift index 697846d..1137351 100644 --- a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift +++ b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift @@ -33,4 +33,77 @@ struct PopskillStoreTests { #expect(store.selectedSkillID == nil) #expect(store.inspectorOpen == false) } + + @Test + func capabilitiesExposeCompositePackagesBeforeAtomicRows() { + let store = PopskillStore() + store.packages = [ + CapabilityPackage( + id: "pkg:demo", + type: .composite, + name: "Demo Bundle", + vendor: nil, + summary: "Demo bundle", + source: PackageSource( + kind: "builtin", + location: "demo", + updateStrategy: "manual", + repoOwner: nil, + repoName: nil, + repoBranch: nil, + readmeUrl: nil + ), + components: PackageComponents( + cli: [], + skills: [ + PackageComponent( + id: "demo-skill", + name: "Demo Skill", + kind: "skill", + required: true, + installed: true, + status: "installed", + location: "demo-skill" + ) + ], + mcp: [], + agents: [] + ), + configSchema: [], + installed: true, + lifecycle: nil + ), + CapabilityPackage( + id: "pkg:standalone", + type: .standalone, + name: "Standalone", + vendor: nil, + summary: "Standalone wrapper", + source: PackageSource(kind: "builtin", location: "standalone", updateStrategy: "manual", repoOwner: nil, repoName: nil, repoBranch: nil, readmeUrl: nil), + components: PackageComponents(cli: [], skills: [], mcp: [], agents: []), + configSchema: [], + installed: false, + lifecycle: nil + ) + ] + store.skills = [ + Skill( + id: "demo-skill", + name: "Demo Skill", + description: "Skill", + directory: "demo-skill", + repoOwner: nil, + repoName: nil, + readmeUrl: nil, + apps: SkillApps(claude: true, codex: false, gemini: false, opencode: false, hermes: false), + installedAt: nil, + updatedAt: nil, + contentHash: nil + ) + ] + + #expect(store.bundleCount == 1) + #expect(store.capabilities.map(\.kind) == [.bundle, .skill]) + #expect(store.capabilities.first?.appCoverage[.claude]?.label == "1/1") + } } diff --git a/swift-app/Tests/PopskillTests/SkillGroupingTests.swift b/swift-app/Tests/PopskillTests/SkillGroupingTests.swift index f2169b8..7cba243 100644 --- a/swift-app/Tests/PopskillTests/SkillGroupingTests.swift +++ b/swift-app/Tests/PopskillTests/SkillGroupingTests.swift @@ -85,13 +85,14 @@ struct SkillGroupingTests { @Test func sectionsRespectCanonicalKindOrder() { // CapabilityKind.allCases is the canonical order — even with inputs - // interleaved, sections come out in skill/agent/cli/mcp/config order. + // interleaved, sections come out in bundle/skill/agent/cli/mcp/config order. + let bundleCap = capability(name: "package", kind: .bundle, owner: "pkg", name2: "source") let agentCap = capability(name: "z", kind: .agent, owner: nil, name2: nil) let skillCap = capability(name: "y", kind: .skill, owner: "a", name2: "lib") - let sections = SkillGrouping.sections([agentCap, skillCap]) + let sections = SkillGrouping.sections([agentCap, skillCap, bundleCap]) - #expect(sections.map(\.kind) == [.skill, .agent]) + #expect(sections.map(\.kind) == [.bundle, .skill, .agent]) } @Test diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 34b6693..634a537 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -639,6 +639,94 @@ struct SkillModelsTests { #expect(package.components.all.map(\.displayKey) == ["cli:lark-cli", "skill:lark-doc"]) } + @Test + func packageComponentMatchesInstalledSkillByScopedIdentifierAndLocation() { + let component = PackageComponent( + id: "lark-doc", + name: "Lark Doc", + kind: "skill", + required: true, + installed: true, + status: "installed", + location: "skills/lark-doc" + ) + let skill = Skill( + id: "larksuite/cli:lark-doc", + name: "Lark Doc", + description: "Docs skill", + directory: "lark-doc", + repoOwner: "larksuite", + repoName: "cli", + readmeUrl: nil, + apps: SkillApps(claude: true, codex: false, gemini: false, opencode: false, hermes: false), + installedAt: nil, + updatedAt: nil, + contentHash: nil + ) + + #expect(component.matchesSkill(skill)) + } + + @Test + func capabilityPackageCoverageUsesSkillTogglesAndComponentFallbacks() { + let package = CapabilityPackage( + id: "pkg:lark", + type: .composite, + name: "Feishu / Lark", + vendor: "ByteDance", + summary: "Composite office package.", + source: PackageSource( + kind: "builtin", + location: "popskill/builtin/lark", + updateStrategy: "manual", + repoOwner: nil, + repoName: nil, + repoBranch: nil, + readmeUrl: nil + ), + components: PackageComponents( + cli: [ + PackageComponent(id: "lark-cli", name: "lark-cli", kind: "cli", required: true, installed: true, status: "detected", location: nil) + ], + skills: [ + PackageComponent(id: "lark-doc", name: "Lark Doc", kind: "skill", required: true, installed: true, status: "installed", location: "lark-doc") + ], + mcp: [ + PackageComponent(id: "lark-mcp", name: "Lark MCP", kind: "mcp", required: false, installed: false, status: "registry-reference", location: nil) + ], + agents: [ + PackageComponent(id: "lark-agent", name: "Lark Agent", kind: "agent", required: false, installed: true, status: "installed", location: "~/.claude/agents/lark-agent.md") + ] + ), + configSchema: [], + installed: true, + lifecycle: nil + ) + let skill = Skill( + id: "larksuite/cli:lark-doc", + name: "Lark Doc", + description: "Docs skill", + directory: "lark-doc", + repoOwner: "larksuite", + repoName: "cli", + readmeUrl: nil, + apps: SkillApps(claude: true, codex: false, gemini: false, opencode: false, hermes: false), + installedAt: nil, + updatedAt: nil, + contentHash: nil + ) + + let coverage = package.appCoverage(using: [skill]) + let capability = MatrixCapability.fromPackage(package, skills: [skill]) + + #expect(coverage[.claude]?.label == "3/4") + #expect(coverage[.codex]?.label == "1/4") + #expect(capability.id == "bundle:pkg:lark") + #expect(capability.kind == .bundle) + #expect(capability.apps.claude == true) + #expect(capability.apps.codex == true) + } + @Test func capabilityPackageHealthSeparatesActivePartialBlockedAndInactive() { #expect(package(components: []).health == .inactive) From f8c1e3adcaa22f7ff959a0d8fa6e2e12401b6cee Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 01:42:56 +0800 Subject: [PATCH 02/47] feat(ui): enrich package inspector details --- .../Sources/Popskill/Models/SkillModels.swift | 68 ++++++++ .../Sources/Popskill/Models/UsageModels.swift | 34 ++++ .../Resources/en.lproj/Localizable.strings | 10 ++ .../zh-Hans.lproj/Localizable.strings | 10 ++ .../Popskill/Views/InspectorPane.swift | 151 ++++++++++++++++++ .../PopskillTests/SkillModelsTests.swift | 97 +++++++++++ 6 files changed, 370 insertions(+) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index 0554d48..3c44693 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -1186,6 +1186,33 @@ extension PackageComponent { return !Set(candidates).isDisjoint(with: Set(skillCandidates)) } + func matchesAttributionSkill(_ identifier: String) -> Bool { + guard kind.caseInsensitiveCompare("skill") == .orderedSame else { + return false + } + + let normalizedIdentifier = Self.normalizedIdentifier(identifier) + guard !normalizedIdentifier.isEmpty else { + return false + } + + let identifierSuffix = normalizedIdentifier + .split(separator: ":", maxSplits: 1) + .last + .map(String.init) ?? normalizedIdentifier + + let candidates = [ + id, + name, + location ?? "", + location?.split(separator: "/").last.map(String.init) ?? "" + ] + .map(Self.normalizedIdentifier) + .filter { !$0.isEmpty } + + return candidates.contains(normalizedIdentifier) || candidates.contains(identifierSuffix) + } + func matchesSkillUpdate(_ update: SkillUpdateInfo) -> Bool { guard kind.caseInsensitiveCompare("skill") == .orderedSame else { return false @@ -1212,6 +1239,22 @@ extension PackageComponent { } extension CapabilityPackage { + func matchingInstalledSkills(in skills: [Skill]) -> [Skill] { + var seen: Set = [] + var matches: [Skill] = [] + + for component in components.skills { + guard let skill = matchingInstalledSkill(for: component, in: skills), + !seen.contains(skill.id) else { + continue + } + seen.insert(skill.id) + matches.append(skill) + } + + return matches + } + func matchingInstalledSkill(for component: PackageComponent, in skills: [Skill]) -> Skill? { skills.first { component.matchesSkill($0) } } @@ -1238,6 +1281,31 @@ extension CapabilityPackage { func matchingSkillComponent(for update: SkillUpdateInfo) -> PackageComponent? { components.skills.first { $0.matchesSkillUpdate(update) } } + + func usageSnapshot(using summary: UsageSummary?, skills: [Skill]) -> PackageUsageSnapshot? { + guard let summary else { + return nil + } + + let matchedSkills = matchingInstalledSkills(in: skills) + var snapshot = PackageUsageSnapshot() + + for stat in summary.skillStats where matchesUsageStat(stat, matchedSkills: matchedSkills) { + snapshot.add(stat) + } + + return snapshot + } + + private func matchesUsageStat(_ stat: SkillUsageStat, matchedSkills: [Skill]) -> Bool { + if matchedSkills.contains(where: { $0.matchesAttributionSkill(stat.skillID) }) { + return true + } + + return components.skills.contains { component in + component.matchesAttributionSkill(stat.skillID) + } + } } private extension PackageComponent { diff --git a/swift-app/Sources/Popskill/Models/UsageModels.swift b/swift-app/Sources/Popskill/Models/UsageModels.swift index c650a9b..89dcdfd 100644 --- a/swift-app/Sources/Popskill/Models/UsageModels.swift +++ b/swift-app/Sources/Popskill/Models/UsageModels.swift @@ -118,3 +118,37 @@ struct SkillUsageStat: Identifiable, Equatable { } } } + +struct PackageUsageSnapshot: Equatable { + var matchedSkillCount = 0 + var usageEvents = 0 + var inputTokens: Int64 = 0 + var outputTokens: Int64 = 0 + var cacheCreationTokens: Int64 = 0 + var cacheReadTokens: Int64 = 0 + var lastUsedAt: Date? + + var totalTokens: Int64 { + inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens + } + + var hasUsage: Bool { + usageEvents > 0 || totalTokens > 0 + } + + mutating func add(_ stat: SkillUsageStat) { + matchedSkillCount += 1 + usageEvents += stat.usageEvents + inputTokens += stat.inputTokens + outputTokens += stat.outputTokens + cacheCreationTokens += stat.cacheCreationTokens + cacheReadTokens += stat.cacheReadTokens + + guard let date = stat.lastUsedAt else { + return + } + if lastUsedAt.map({ date > $0 }) ?? true { + lastUsedAt = date + } + } +} diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 28eadbd..bfa05dc 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -351,6 +351,14 @@ "matrix.package.component.offHelp" = "%@ not enabled"; "matrix.package.missingRequired" = "%d required missing"; "matrix.package.config.secret" = "Keychain secret"; +"matrix.package.usage.tokens" = "Tokens"; +"matrix.package.usage.calls" = "Calls"; +"matrix.package.usage.lastUsed" = "Last used %@"; +"matrix.package.usage.empty" = "Usage was scanned, but the %d local skills matched to this bundle have no attribution records yet."; +"matrix.package.usage.notScanned" = "Transcript usage has not been scanned yet."; +"matrix.package.usage.scanning" = "Scanning local transcripts…"; +"matrix.package.paths.empty" = "No local skill files matched this bundle."; +"matrix.package.paths.more" = "%d more matched skills hidden."; "matrix.empty.title" = "No capabilities yet"; "matrix.empty.body" = "Install your first skill from a source, or wait for bootstrap to finish."; @@ -364,8 +372,10 @@ "matrix.inspector.section.triggers" = "TRIGGERS"; "matrix.inspector.section.apps" = "APPS"; "matrix.inspector.section.coverage" = "COVERAGE"; +"matrix.inspector.section.usage" = "USAGE"; "matrix.inspector.section.components" = "COMPONENTS"; "matrix.inspector.section.config" = "CONFIG"; +"matrix.inspector.section.paths" = "LOCAL PATHS"; "matrix.inspector.section.deployment" = "DEPLOYMENT"; "matrix.inspector.section.meta" = "METADATA"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index aea379b..b7e534f 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -351,6 +351,14 @@ "matrix.package.component.offHelp" = "%@ 未启用"; "matrix.package.missingRequired" = "缺 %d 个必需"; "matrix.package.config.secret" = "Keychain 密钥"; +"matrix.package.usage.tokens" = "Tokens"; +"matrix.package.usage.calls" = "调用"; +"matrix.package.usage.lastUsed" = "最近使用 %@"; +"matrix.package.usage.empty" = "已扫描用量,但这个套装匹配到的 %d 个本地 skill 暂无归因记录。"; +"matrix.package.usage.notScanned" = "还没有扫描 transcript,用量聚合暂不可用。"; +"matrix.package.usage.scanning" = "正在扫描本机 transcript…"; +"matrix.package.paths.empty" = "没有匹配到本地 skill 真身。"; +"matrix.package.paths.more" = "还有 %d 个匹配 skill 未展开。"; "matrix.empty.title" = "还没有能力"; "matrix.empty.body" = "从数据源安装第一个 skill,或等 sidecar 初始化完成。"; @@ -364,8 +372,10 @@ "matrix.inspector.section.triggers" = "触发场景"; "matrix.inspector.section.apps" = "应用启用"; "matrix.inspector.section.coverage" = "覆盖"; +"matrix.inspector.section.usage" = "用量"; "matrix.inspector.section.components" = "组件"; "matrix.inspector.section.config" = "配置"; +"matrix.inspector.section.paths" = "本地路径"; "matrix.inspector.section.deployment" = "位置与链接"; "matrix.inspector.section.meta" = "元信息"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index ae83caa..573d89e 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -17,10 +17,12 @@ struct InspectorPane: View { if let package = capability.package { packageSummarySection(package) packageCoverageSection + packageUsageSection(package) packageComponentsSection(package) if !package.configSchema.isEmpty { packageConfigSection(package) } + packageLocalPathsSection(package) packageMetadataSection(package) } else { if !primaryDescription.isEmpty { @@ -193,6 +195,74 @@ struct InspectorPane: View { } } + private func packageUsageSection(_ package: CapabilityPackage) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.usage", accent: .accentColor) + + if store.usageScanInFlight { + HStack(spacing: 8) { + ProgressView().controlSize(.mini) + LocalizedText("matrix.package.usage.scanning") + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + } + } else if let snapshot = package.usageSnapshot(using: store.usageSummary, skills: store.skills) { + if snapshot.hasUsage { + HStack(spacing: 10) { + packageUsageMetric( + titleKey: "matrix.package.usage.tokens", + value: Self.formatTokens(snapshot.totalTokens), + tint: .accentColor + ) + packageUsageMetric( + titleKey: "matrix.package.usage.calls", + value: "\(snapshot.usageEvents)", + tint: .popSectionGreen + ) + } + if let lastUsedAt = snapshot.lastUsedAt { + Text(localization.string("matrix.package.usage.lastUsed", Self.relativeFormatter.localizedString(for: lastUsedAt, relativeTo: Date()))) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } else { + Text(localization.string("matrix.package.usage.empty", snapshot.matchedSkillCount)) + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + } + } else { + HStack(alignment: .center, spacing: 8) { + Text(localization.string("matrix.package.usage.notScanned")) + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + Spacer(minLength: 8) + Button { + Task { await store.refreshUsageScan() } + } label: { + Label(localization.string("insights.refresh"), systemImage: "chart.bar.doc.horizontal") + } + .buttonStyle(.bordered) + .controlSize(.mini) + } + } + } + } + + private func packageUsageMetric(titleKey: String, value: String, tint: Color) -> some View { + VStack(alignment: .leading, spacing: 4) { + LocalizedText(titleKey) + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + Text(value) + .font(.system(size: 18, weight: .bold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(tint) + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(tint.opacity(0.07), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + private func components(in kind: String, package: CapabilityPackage) -> [PackageComponent] { switch kind { case "skill": return package.components.skills @@ -257,6 +327,55 @@ struct InspectorPane: View { } } + private func packageLocalPathsSection(_ package: CapabilityPackage) -> some View { + let matchedSkills = package.matchingInstalledSkills(in: store.skills) + return VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.paths") + if matchedSkills.isEmpty { + Text(localization.string("matrix.package.paths.empty")) + .font(.caption) + .foregroundStyle(Color.popTertiaryLabel) + } else { + ForEach(matchedSkills.prefix(8), id: \.id) { skill in + packagePathRow(skill) + } + if matchedSkills.count > 8 { + Text(localization.string("matrix.package.paths.more", matchedSkills.count - 8)) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } + } + } + + private func packagePathRow(_ skill: Skill) -> some View { + HStack(alignment: .top, spacing: 8) { + Image(systemName: "doc.text") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(skill.markdownURL == nil ? Color.popTertiaryLabel : Color.popSecondaryLabel) + .frame(width: 14) + VStack(alignment: .leading, spacing: 2) { + Text(skill.name) + .font(.caption.weight(.medium)) + .foregroundStyle(Color.popLabel) + Text(skill.localStoreURL.path.abbreviatingWithTilde) + .font(.system(size: 10.5, design: .monospaced)) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } + Spacer(minLength: 8) + if skill.markdownURL != nil { + Text("SKILL.md") + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popStatusOK) + } + } + .padding(8) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + private func packageMetadataSection(_ package: CapabilityPackage) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.meta") @@ -506,6 +625,32 @@ struct InspectorPane: View { ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .file) } + private static func formatTokens(_ value: Int64) -> String { + if value < 1_000 { + return decimalFormatter.string(from: NSNumber(value: value)) ?? "0" + } + if value < 1_000_000 { + return String(format: "%.1fK", Double(value) / 1_000.0) + } + if value < 1_000_000_000 { + return String(format: "%.1fM", Double(value) / 1_000_000.0) + } + return String(format: "%.2fB", Double(value) / 1_000_000_000.0) + } + + private static let decimalFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.groupingSeparator = "," + return formatter + }() + + private static let relativeFormatter: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .short + return formatter + }() + // MARK: Toggle helpers private func toggleKey(_ app: TargetApp) -> String { @@ -531,6 +676,12 @@ struct InspectorPane: View { } } +private extension String { + var abbreviatingWithTilde: String { + (self as NSString).abbreviatingWithTildeInPath + } +} + private extension CapabilityPackageHealth { var inspectorTitleKey: String { switch self { diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 634a537..43069e7 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -727,6 +727,103 @@ struct SkillModelsTests { #expect(capability.apps.codex == true) } + @Test + func capabilityPackageUsageSnapshotAggregatesMatchedSkillStats() { + let package = CapabilityPackage( + id: "pkg:baoyu", + type: .composite, + name: "Baoyu Skills", + vendor: "@dotey", + summary: "Prompt package", + source: PackageSource( + kind: "github", + location: "jimliu/baoyu-skills", + updateStrategy: "git", + repoOwner: "jimliu", + repoName: "baoyu-skills", + repoBranch: nil, + readmeUrl: nil + ), + components: PackageComponents( + cli: [], + skills: [ + PackageComponent(id: "baoyu-comic", name: "baoyu-comic", kind: "skill", required: true, installed: true, status: "installed", location: "skills/baoyu-comic"), + PackageComponent(id: "baoyu-translate", name: "baoyu-translate", kind: "skill", required: true, installed: true, status: "installed", location: "skills/baoyu-translate") + ], + mcp: [], + agents: [] + ), + configSchema: [], + installed: true, + lifecycle: nil + ) + let comic = Skill( + id: "jimliu/baoyu-skills:baoyu-comic", + name: "baoyu-comic", + description: "Comic", + directory: "baoyu-comic", + repoOwner: "jimliu", + repoName: "baoyu-skills", + readmeUrl: nil, + apps: SkillApps(claude: true, codex: true, gemini: false, opencode: false, hermes: false), + installedAt: nil, + updatedAt: nil, + contentHash: nil + ) + let lastUsed = Date(timeIntervalSince1970: 1_800_000_000) + let summary = UsageSummary( + filesScanned: 3, + sessions: 2, + usageEvents: 4, + inputTokens: 100, + outputTokens: 80, + cacheCreationTokens: 0, + cacheReadTokens: 0, + attributedSkillUsageEvents: 3, + modelStats: [], + skillStats: [ + SkillUsageStat( + skillID: "jimliu/baoyu-skills:baoyu-comic", + sourcePlugin: nil, + usageEvents: 2, + inputTokens: 20, + outputTokens: 30, + cacheCreationTokens: 5, + cacheReadTokens: 7, + lastUsedAt: lastUsed + ), + SkillUsageStat( + skillID: "other:baoyu-translate", + sourcePlugin: nil, + usageEvents: 1, + inputTokens: 10, + outputTokens: 12, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: Date(timeIntervalSince1970: 1_700_000_000) + ), + SkillUsageStat( + skillID: "unrelated", + sourcePlugin: nil, + usageEvents: 9, + inputTokens: 999, + outputTokens: 999, + cacheCreationTokens: 999, + cacheReadTokens: 999, + lastUsedAt: nil + ) + ], + recentSessions: [] + ) + + let snapshot = package.usageSnapshot(using: summary, skills: [comic]) + + #expect(snapshot?.matchedSkillCount == 2) + #expect(snapshot?.usageEvents == 3) + #expect(snapshot?.totalTokens == 84) + #expect(snapshot?.lastUsedAt == lastUsed) + } + @Test func capabilityPackageHealthSeparatesActivePartialBlockedAndInactive() { #expect(package(components: []).health == .inactive) From 57aff52b16b0553b2e61ef8ca3ee2c124b0e3864 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 01:49:50 +0800 Subject: [PATCH 03/47] feat(ui): add package inspector actions --- .../Sources/Popskill/App/PopskillStore.swift | 5 + .../Resources/en.lproj/Localizable.strings | 5 + .../zh-Hans.lproj/Localizable.strings | 5 + .../Popskill/Views/InspectorPane.swift | 117 ++++++++++++++++++ .../Popskill/Views/MatrixPackageRow.swift | 14 +++ .../Sources/Popskill/Views/UpdatesView.swift | 6 +- 6 files changed, 149 insertions(+), 3 deletions(-) diff --git a/swift-app/Sources/Popskill/App/PopskillStore.swift b/swift-app/Sources/Popskill/App/PopskillStore.swift index 9cae355..4cfa28c 100644 --- a/swift-app/Sources/Popskill/App/PopskillStore.swift +++ b/swift-app/Sources/Popskill/App/PopskillStore.swift @@ -33,6 +33,7 @@ final class PopskillStore { var usageSummary: UsageSummary? var usageScanError: String? var usageScanInFlight: Bool = false + var updatesRefreshInFlight: Bool = false // Per-slice refresh timestamps. Views call refresh*(force: false) from // .task on first appearance; if a recent refresh exists we skip the @@ -153,7 +154,11 @@ final class PopskillStore { } func refreshUpdates(force: Bool = false) async { + guard !updatesRefreshInFlight else { return } guard !shouldSkipRefresh(lastUpdatesRefreshAt, force: force) else { return } + updatesRefreshInFlight = true + defer { updatesRefreshInFlight = false } + do { updates = try await client.checkUpdates() lastUpdatesRefreshAt = Date() diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index bfa05dc..116fcdf 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -369,6 +369,7 @@ "matrix.inspector.close" = "Close inspector"; "matrix.inspector.section.summary" = "SUMMARY"; +"matrix.inspector.section.actions" = "ACTIONS"; "matrix.inspector.section.triggers" = "TRIGGERS"; "matrix.inspector.section.apps" = "APPS"; "matrix.inspector.section.coverage" = "COVERAGE"; @@ -378,6 +379,10 @@ "matrix.inspector.section.paths" = "LOCAL PATHS"; "matrix.inspector.section.deployment" = "DEPLOYMENT"; "matrix.inspector.section.meta" = "METADATA"; +"matrix.package.action.rescanUsage" = "Re-scan usage"; +"matrix.package.action.checkUpdates" = "Check updates"; +"matrix.package.action.openSource" = "Open source"; +"matrix.package.action.revealInFinder" = "Reveal local skill"; "matrix.inspector.deployment.ssot" = "SSOT (real file)"; "matrix.inspector.deployment.strategy" = "Strategy · %@"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index b7e534f..9e282ec 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -369,6 +369,7 @@ "matrix.inspector.close" = "关闭面板"; "matrix.inspector.section.summary" = "简介"; +"matrix.inspector.section.actions" = "操作"; "matrix.inspector.section.triggers" = "触发场景"; "matrix.inspector.section.apps" = "应用启用"; "matrix.inspector.section.coverage" = "覆盖"; @@ -378,6 +379,10 @@ "matrix.inspector.section.paths" = "本地路径"; "matrix.inspector.section.deployment" = "位置与链接"; "matrix.inspector.section.meta" = "元信息"; +"matrix.package.action.rescanUsage" = "重新扫描用量"; +"matrix.package.action.checkUpdates" = "检查更新"; +"matrix.package.action.openSource" = "打开来源"; +"matrix.package.action.revealInFinder" = "定位本地 Skill"; "matrix.inspector.deployment.ssot" = "真身 (SSOT)"; "matrix.inspector.deployment.strategy" = "部署策略 · %@"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 573d89e..091beed 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -1,3 +1,4 @@ +import AppKit import SwiftUI /// Right-pane inspector for the matrix. Renders a single @@ -16,6 +17,7 @@ struct InspectorPane: View { header if let package = capability.package { packageSummarySection(package) + packageActionsSection(package) packageCoverageSection packageUsageSection(package) packageComponentsSection(package) @@ -79,6 +81,99 @@ struct InspectorPane: View { } } + private func packageActionsSection(_ package: CapabilityPackage) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.actions", accent: .accentColor) + LazyVGrid(columns: Self.actionGridColumns, alignment: .leading, spacing: 8) { + packageActionButton( + titleKey: "matrix.package.action.rescanUsage", + systemImage: "chart.bar.doc.horizontal", + inFlight: store.usageScanInFlight, + disabled: store.usageScanInFlight + ) { + Task { await store.refreshUsageScan() } + } + + packageActionButton( + titleKey: "matrix.package.action.checkUpdates", + systemImage: "arrow.clockwise", + inFlight: store.updatesRefreshInFlight, + disabled: store.updatesRefreshInFlight + ) { + Task { await store.refreshUpdates(force: true) } + } + + sourceAction(for: package) + + packageActionButton( + titleKey: "matrix.package.action.revealInFinder", + systemImage: "folder", + disabled: firstRevealableSkillURL(for: package) == nil + ) { + if let url = firstRevealableSkillURL(for: package) { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } + } + } + } + } + + @ViewBuilder + private func sourceAction(for package: CapabilityPackage) -> some View { + if let url = package.sourceURL { + Link(destination: url) { + packageActionLabel( + titleKey: "matrix.package.action.openSource", + systemImage: "arrow.up.right.square" + ) + } + .buttonStyle(.plain) + } else { + packageActionButton( + titleKey: "matrix.package.action.openSource", + systemImage: "arrow.up.right.square", + disabled: true + ) {} + } + } + + private func packageActionButton( + titleKey: String, + systemImage: String, + inFlight: Bool = false, + disabled: Bool = false, + action: @escaping () -> Void + ) -> some View { + Button(action: action) { + packageActionLabel(titleKey: titleKey, systemImage: systemImage, inFlight: inFlight) + } + .buttonStyle(.plain) + .disabled(disabled) + .opacity(disabled ? 0.52 : 1) + } + + private func packageActionLabel(titleKey: String, systemImage: String, inFlight: Bool = false) -> some View { + HStack(spacing: 7) { + if inFlight { + ProgressView() + .controlSize(.mini) + .frame(width: 14, height: 14) + } else { + Image(systemName: systemImage) + .font(.system(size: 12, weight: .semibold)) + .frame(width: 14) + } + Text(localization.string(titleKey)) + .font(.caption.weight(.semibold)) + .lineLimit(1) + } + .foregroundStyle(Color.popLabel) + .padding(.horizontal, 10) + .padding(.vertical, 7) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + private var kindChip: some View { HStack(spacing: 3) { Image(systemName: capability.kind.symbol) @@ -371,6 +466,18 @@ struct InspectorPane: View { .font(.caption2.weight(.semibold)) .foregroundStyle(Color.popStatusOK) } + if FileManager.default.fileExists(atPath: skill.localStoreURL.path) { + Button { + NSWorkspace.shared.activateFileViewerSelecting([skill.localStoreURL]) + } label: { + Image(systemName: "folder") + .font(.system(size: 11, weight: .semibold)) + .frame(width: 18, height: 18) + } + .buttonStyle(.plain) + .foregroundStyle(Color.popSecondaryLabel) + .help(localization.string("matrix.row.menu.revealInFinder")) + } } .padding(8) .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) @@ -651,6 +758,16 @@ struct InspectorPane: View { return formatter }() + private static let actionGridColumns: [GridItem] = [ + GridItem(.adaptive(minimum: 132), spacing: 8, alignment: .leading) + ] + + private func firstRevealableSkillURL(for package: CapabilityPackage) -> URL? { + package.matchingInstalledSkills(in: store.skills) + .first { FileManager.default.fileExists(atPath: $0.localStoreURL.path) }? + .localStoreURL + } + // MARK: Toggle helpers private func toggleKey(_ app: TargetApp) -> String { diff --git a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift index 2da9286..aebc3a6 100644 --- a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift @@ -1,3 +1,4 @@ +import AppKit import SwiftUI /// Composite package row in the capability matrix. The row is read-only at the @@ -213,6 +214,13 @@ struct MatrixPackageRow: View { systemImage: isCollapsed ? "chevron.down" : "chevron.up" ) } + if let url = revealableSkillURL(for: package) { + Button { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } label: { + Label(localization.string("matrix.row.menu.revealInFinder"), systemImage: "folder") + } + } } } label: { Image(systemName: "ellipsis") @@ -226,6 +234,12 @@ struct MatrixPackageRow: View { .help(localization.string("matrix.row.menu.help")) } + private func revealableSkillURL(for package: CapabilityPackage) -> URL? { + package.matchingInstalledSkills(in: store.skills) + .first { FileManager.default.fileExists(atPath: $0.localStoreURL.path) }? + .localStoreURL + } + private var rowBackground: some View { Group { if isSelected { diff --git a/swift-app/Sources/Popskill/Views/UpdatesView.swift b/swift-app/Sources/Popskill/Views/UpdatesView.swift index 3799538..c8a3138 100644 --- a/swift-app/Sources/Popskill/Views/UpdatesView.swift +++ b/swift-app/Sources/Popskill/Views/UpdatesView.swift @@ -24,7 +24,7 @@ struct UpdatesView: View { } .buttonStyle(.bordered) .controlSize(.small) - .disabled(loading) + .disabled(loading || store.updatesRefreshInFlight) Button { Task { await updateAll() } } label: { @@ -35,7 +35,7 @@ struct UpdatesView: View { // Also disable while a scan or per-row update is in // flight — clicking "全部更新" mid-scan stacks two // concurrent rescans and confuses pendingUpdate state. - .disabled(store.updates.isEmpty || loading || !pendingUpdate.isEmpty) + .disabled(store.updates.isEmpty || loading || store.updatesRefreshInFlight || !pendingUpdate.isEmpty) } } @@ -59,7 +59,7 @@ struct UpdatesView: View { } private var subtitle: String { - if loading { + if loading || store.updatesRefreshInFlight { return localization.string("updates.subtitleScanning", store.updates.count) } if let lastScanAt = store.lastUpdatesRefreshAt { From cf801376dc9d8b3031f118fc35503fe0536c59ba Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 01:52:38 +0800 Subject: [PATCH 04/47] feat(ui): show package sync status --- .../Resources/en.lproj/Localizable.strings | 13 +++ .../zh-Hans.lproj/Localizable.strings | 13 +++ .../Popskill/Views/InspectorPane.swift | 110 ++++++++++++++++++ 3 files changed, 136 insertions(+) diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 116fcdf..e2bcf89 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -377,12 +377,25 @@ "matrix.inspector.section.components" = "COMPONENTS"; "matrix.inspector.section.config" = "CONFIG"; "matrix.inspector.section.paths" = "LOCAL PATHS"; +"matrix.inspector.section.version" = "VERSION"; +"matrix.inspector.section.sync" = "SYNC"; "matrix.inspector.section.deployment" = "DEPLOYMENT"; "matrix.inspector.section.meta" = "METADATA"; "matrix.package.action.rescanUsage" = "Re-scan usage"; "matrix.package.action.checkUpdates" = "Check updates"; "matrix.package.action.openSource" = "Open source"; "matrix.package.action.revealInFinder" = "Reveal local skill"; +"matrix.package.version.strategy" = "Strategy"; +"matrix.package.version.branch" = "Branch"; +"matrix.package.version.hash" = "Hash"; +"matrix.package.version.untracked" = "Untracked"; +"matrix.package.sync.upToDate" = "Up to date"; +"matrix.package.sync.pending" = "%d pending"; +"matrix.package.sync.checking" = "Checking upstream updates..."; +"matrix.package.sync.checked" = "Checked %@"; +"matrix.package.sync.notChecked" = "Not checked yet"; +"matrix.package.sync.clean" = "No package component updates detected."; +"matrix.package.sync.more" = "%d more updates hidden."; "matrix.inspector.deployment.ssot" = "SSOT (real file)"; "matrix.inspector.deployment.strategy" = "Strategy · %@"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 9e282ec..3b87901 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -377,12 +377,25 @@ "matrix.inspector.section.components" = "组件"; "matrix.inspector.section.config" = "配置"; "matrix.inspector.section.paths" = "本地路径"; +"matrix.inspector.section.version" = "版本"; +"matrix.inspector.section.sync" = "同步"; "matrix.inspector.section.deployment" = "位置与链接"; "matrix.inspector.section.meta" = "元信息"; "matrix.package.action.rescanUsage" = "重新扫描用量"; "matrix.package.action.checkUpdates" = "检查更新"; "matrix.package.action.openSource" = "打开来源"; "matrix.package.action.revealInFinder" = "定位本地 Skill"; +"matrix.package.version.strategy" = "策略"; +"matrix.package.version.branch" = "分支"; +"matrix.package.version.hash" = "Hash"; +"matrix.package.version.untracked" = "未跟踪"; +"matrix.package.sync.upToDate" = "已是最新"; +"matrix.package.sync.pending" = "%d 个待更新"; +"matrix.package.sync.checking" = "正在检查上游更新..."; +"matrix.package.sync.checked" = "已检查 %@"; +"matrix.package.sync.notChecked" = "尚未检查"; +"matrix.package.sync.clean" = "没有检测到套装组件更新。"; +"matrix.package.sync.more" = "还有 %d 个更新未展开。"; "matrix.inspector.deployment.ssot" = "真身 (SSOT)"; "matrix.inspector.deployment.strategy" = "部署策略 · %@"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 091beed..a9410f5 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -25,6 +25,8 @@ struct InspectorPane: View { packageConfigSection(package) } packageLocalPathsSection(package) + packageVersionSection(package) + packageSyncSection(package) packageMetadataSection(package) } else { if !primaryDescription.isEmpty { @@ -483,6 +485,106 @@ struct InspectorPane: View { .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) } + private func packageVersionSection(_ package: CapabilityPackage) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.version") + VStack(alignment: .leading, spacing: 6) { + metaRow(label: localization.string("matrix.package.version.strategy"), value: package.source.updateStrategy) + if let branch = package.source.repoBranch, !branch.isEmpty { + metaRow(label: localization.string("matrix.package.version.branch"), value: branch) + } + if let installedAt = package.lifecycle?.installedAt, installedAt > 0 { + metaRow(label: localization.string("matrix.inspector.meta.installedAt"), value: Self.formatTimestamp(installedAt)) + } + if let updatedAt = package.lifecycle?.updatedAt, updatedAt > 0 { + metaRow(label: localization.string("matrix.inspector.meta.updatedAt"), value: Self.formatTimestamp(updatedAt)) + } + metaRow( + label: localization.string("matrix.package.version.hash"), + value: package.trackedContentHash.map(Self.shortHash) ?? localization.string("matrix.package.version.untracked") + ) + } + } + } + + private func packageSyncSection(_ package: CapabilityPackage) -> some View { + let pendingUpdates = packagePendingUpdates(package) + return VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.sync") + HStack(spacing: 8) { + syncStatusPill(for: pendingUpdates) + if store.updatesRefreshInFlight { + HStack(spacing: 6) { + ProgressView().controlSize(.mini) + Text(localization.string("matrix.package.sync.checking")) + } + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + } else if let lastCheckedAt = store.lastUpdatesRefreshAt { + Text(localization.string("matrix.package.sync.checked", Self.relativeFormatter.localizedString(for: lastCheckedAt, relativeTo: Date()))) + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + } else { + Text(localization.string("matrix.package.sync.notChecked")) + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + } + } + + if pendingUpdates.isEmpty { + Text(localization.string("matrix.package.sync.clean")) + .font(.caption) + .foregroundStyle(Color.popTertiaryLabel) + } else { + VStack(alignment: .leading, spacing: 6) { + ForEach(pendingUpdates.prefix(4)) { update in + packageSyncUpdateRow(update) + } + if pendingUpdates.count > 4 { + Text(localization.string("matrix.package.sync.more", pendingUpdates.count - 4)) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } + } + } + } + + private func syncStatusPill(for pendingUpdates: [SkillUpdateInfo]) -> some View { + let hasPending = !pendingUpdates.isEmpty + let color = hasPending ? Color.accentColor : Color.popStatusOK + let title = hasPending + ? localization.string("matrix.package.sync.pending", pendingUpdates.count) + : localization.string("matrix.package.sync.upToDate") + + return Text(title) + .font(.caption2.weight(.semibold)) + .foregroundStyle(color) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background(color.opacity(0.10), in: Capsule()) + } + + private func packageSyncUpdateRow(_ update: SkillUpdateInfo) -> some View { + HStack(alignment: .top, spacing: 8) { + Image(systemName: "arrow.down.circle") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(Color.accentColor) + .frame(width: 14) + VStack(alignment: .leading, spacing: 2) { + Text(update.name) + .font(.caption.weight(.medium)) + .foregroundStyle(Color.popLabel) + Text(update.currentHash.map { "\(Self.shortHash($0)) -> \(Self.shortHash(update.remoteHash))" } ?? localization.string("updates.row.firstSync")) + .font(.system(size: 10.5, design: .monospaced)) + .foregroundStyle(Color.popSecondaryLabel) + } + Spacer(minLength: 8) + } + .padding(8) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + private func packageMetadataSection(_ package: CapabilityPackage) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.meta") @@ -745,6 +847,10 @@ struct InspectorPane: View { return String(format: "%.2fB", Double(value) / 1_000_000_000.0) } + private static func shortHash(_ hash: String) -> String { + String(hash.trimmingCharacters(in: .whitespacesAndNewlines).prefix(8)) + } + private static let decimalFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -768,6 +874,10 @@ struct InspectorPane: View { .localStoreURL } + private func packagePendingUpdates(_ package: CapabilityPackage) -> [SkillUpdateInfo] { + store.updates.filter { package.matchingSkillComponent(for: $0) != nil } + } + // MARK: Toggle helpers private func toggleKey(_ app: TargetApp) -> String { From e39639c7a30e6e8e4c5fbcb940bb979095ebe086 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 02:00:40 +0800 Subject: [PATCH 05/47] feat(ui): preview skill readmes in inspector --- .../Sources/Popskill/App/PopskillStore.swift | 40 +++++ .../Popskill/Models/ReadmePreview.swift | 145 ++++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 7 + .../zh-Hans.lproj/Localizable.strings | 7 + .../Popskill/Views/InspectorPane.swift | 100 ++++++++++++ .../PopskillTests/ReadmePreviewTests.swift | 57 +++++++ 6 files changed, 356 insertions(+) create mode 100644 swift-app/Sources/Popskill/Models/ReadmePreview.swift create mode 100644 swift-app/Tests/PopskillTests/ReadmePreviewTests.swift diff --git a/swift-app/Sources/Popskill/App/PopskillStore.swift b/swift-app/Sources/Popskill/App/PopskillStore.swift index 4cfa28c..44a1292 100644 --- a/swift-app/Sources/Popskill/App/PopskillStore.swift +++ b/swift-app/Sources/Popskill/App/PopskillStore.swift @@ -34,6 +34,7 @@ final class PopskillStore { var usageScanError: String? var usageScanInFlight: Bool = false var updatesRefreshInFlight: Bool = false + var readmePreviewStates: [String: ReadmePreviewLoadState] = [:] // Per-slice refresh timestamps. Views call refresh*(force: false) from // .task on first appearance; if a recent refresh exists we skip the @@ -269,6 +270,45 @@ final class PopskillStore { selectedSkillID = nil } + // MARK: README previews + + func readmePreviewState(for skill: Skill) -> ReadmePreviewLoadState? { + readmePreviewStates[skill.id] + } + + func loadReadmePreview(for skill: Skill, force: Bool = false) async { + if !force { + switch readmePreviewStates[skill.id] { + case .loading, .loaded: + return + case .failed, .none: + break + } + } + + guard let readmeURL = skill.markdownURL else { + readmePreviewStates[skill.id] = .failed(ReadmePreviewError.missing.localizedDescription) + return + } + + let skillID = skill.id + let skillName = skill.name + readmePreviewStates[skillID] = .loading + + do { + let preview = try await Task.detached(priority: .utility) { + try ReadmePreview.load( + skillID: skillID, + skillName: skillName, + readmeURL: readmeURL + ) + }.value + readmePreviewStates[skillID] = .loaded(preview) + } catch { + readmePreviewStates[skillID] = .failed(error.localizedDescription) + } + } + // MARK: Usage scanner /// Walk `~/.claude/projects/**/*.jsonl` off the main thread and post the diff --git a/swift-app/Sources/Popskill/Models/ReadmePreview.swift b/swift-app/Sources/Popskill/Models/ReadmePreview.swift new file mode 100644 index 0000000..74f4e8e --- /dev/null +++ b/swift-app/Sources/Popskill/Models/ReadmePreview.swift @@ -0,0 +1,145 @@ +import Foundation + +struct ReadmePreview: Equatable, Sendable { + struct Excerpt: Equatable, Sendable { + let text: String + let truncated: Bool + } + + let skillID: String + let skillName: String + let url: URL + let excerpt: String + let truncated: Bool + + static let maxBytes = 24 * 1024 + static let maxCharacters = 1_600 + static let maxLines = 26 + + static func load(skillID: String, skillName: String, readmeURL: URL) throws -> ReadmePreview { + guard FileManager.default.fileExists(atPath: readmeURL.path) else { + throw ReadmePreviewError.missing + } + + let handle = try FileHandle(forReadingFrom: readmeURL) + defer { try? handle.close() } + + let data = try handle.read(upToCount: maxBytes) ?? Data() + let content = String(decoding: data, as: UTF8.self) + let excerpt = makeExcerpt(from: content) + + guard !excerpt.text.isEmpty else { + throw ReadmePreviewError.empty + } + + return ReadmePreview( + skillID: skillID, + skillName: skillName, + url: readmeURL, + excerpt: excerpt.text, + truncated: excerpt.truncated || data.count >= maxBytes + ) + } + + static func makeExcerpt( + from content: String, + maxCharacters: Int = Self.maxCharacters, + maxLines: Int = Self.maxLines + ) -> Excerpt { + let normalized = content.replacingOccurrences(of: "\r\n", with: "\n") + let rawLines = normalized + .replacingOccurrences(of: "\r", with: "\n") + .split(separator: "\n", omittingEmptySubsequences: false) + .map(String.init) + let contentLines = dropYAMLFrontmatter(from: rawLines) + let compactedLines = compactBlankRuns(in: contentLines) + .drop { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + var selected: [String] = [] + var currentCharacters = 0 + var truncated = false + + for line in compactedLines { + let cleanLine = line.trimmingCharacters(in: .whitespaces) + let additionalCharacters = cleanLine.count + (selected.isEmpty ? 0 : 1) + + guard selected.count < maxLines else { + truncated = true + break + } + guard currentCharacters + additionalCharacters <= maxCharacters else { + truncated = true + break + } + + selected.append(cleanLine) + currentCharacters += additionalCharacters + } + + let excerpt = selected + .joined(separator: "\n") + .trimmingCharacters(in: .whitespacesAndNewlines) + + return Excerpt(text: excerpt, truncated: truncated) + } + + private static func dropYAMLFrontmatter(from lines: [String]) -> ArraySlice { + guard lines.first?.trimmingCharacters(in: .whitespacesAndNewlines) == "---" else { + return ArraySlice(lines) + } + + let searchRange = lines.dropFirst().prefix(40) + guard let closingIndex = searchRange.firstIndex(where: { line in + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed == "---" || trimmed == "..." + }) else { + return ArraySlice(lines) + } + + let nextIndex = lines.index(after: closingIndex) + guard nextIndex < lines.endIndex else { + return [] + } + return lines[nextIndex...] + } + + private static func compactBlankRuns(in lines: ArraySlice) -> [String] { + var compacted: [String] = [] + var previousWasBlank = false + + for line in lines { + let isBlank = line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + if isBlank { + guard !previousWasBlank else { continue } + compacted.append("") + } else { + compacted.append(line) + } + previousWasBlank = isBlank + } + + while compacted.last?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true { + compacted.removeLast() + } + return compacted + } +} + +enum ReadmePreviewLoadState: Equatable { + case loading + case loaded(ReadmePreview) + case failed(String) +} + +enum ReadmePreviewError: LocalizedError { + case missing + case empty + + var errorDescription: String? { + switch self { + case .missing: + "SKILL.md was not found." + case .empty: + "SKILL.md did not contain readable text." + } + } +} diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index e2bcf89..e0057a5 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -370,6 +370,7 @@ "matrix.inspector.section.summary" = "SUMMARY"; "matrix.inspector.section.actions" = "ACTIONS"; +"matrix.inspector.section.readme" = "README"; "matrix.inspector.section.triggers" = "TRIGGERS"; "matrix.inspector.section.apps" = "APPS"; "matrix.inspector.section.coverage" = "COVERAGE"; @@ -396,6 +397,12 @@ "matrix.package.sync.notChecked" = "Not checked yet"; "matrix.package.sync.clean" = "No package component updates detected."; "matrix.package.sync.more" = "%d more updates hidden."; +"matrix.readme.loading" = "Loading README preview..."; +"matrix.readme.showing" = "Showing %@"; +"matrix.readme.openFile" = "Open SKILL.md"; +"matrix.readme.reload" = "Reload README"; +"matrix.readme.error" = "README unavailable: %@"; +"matrix.readme.truncated" = "Preview truncated."; "matrix.inspector.deployment.ssot" = "SSOT (real file)"; "matrix.inspector.deployment.strategy" = "Strategy · %@"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 3b87901..5b4a9b5 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -370,6 +370,7 @@ "matrix.inspector.section.summary" = "简介"; "matrix.inspector.section.actions" = "操作"; +"matrix.inspector.section.readme" = "README"; "matrix.inspector.section.triggers" = "触发场景"; "matrix.inspector.section.apps" = "应用启用"; "matrix.inspector.section.coverage" = "覆盖"; @@ -396,6 +397,12 @@ "matrix.package.sync.notChecked" = "尚未检查"; "matrix.package.sync.clean" = "没有检测到套装组件更新。"; "matrix.package.sync.more" = "还有 %d 个更新未展开。"; +"matrix.readme.loading" = "正在加载 README 预览..."; +"matrix.readme.showing" = "当前显示 %@"; +"matrix.readme.openFile" = "打开 SKILL.md"; +"matrix.readme.reload" = "重新加载 README"; +"matrix.readme.error" = "README 不可用:%@"; +"matrix.readme.truncated" = "预览已截断。"; "matrix.inspector.deployment.ssot" = "真身 (SSOT)"; "matrix.inspector.deployment.strategy" = "部署策略 · %@"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index a9410f5..8f00e5c 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -17,6 +17,12 @@ struct InspectorPane: View { header if let package = capability.package { packageSummarySection(package) + if let skill = readmeSkill(for: package) { + readmePreviewSection( + skill: skill, + context: localization.string("matrix.readme.showing", skill.name) + ) + } packageActionsSection(package) packageCoverageSection packageUsageSection(package) @@ -32,6 +38,9 @@ struct InspectorPane: View { if !primaryDescription.isEmpty { summarySection } + if let skill = selectedSkill { + readmePreviewSection(skill: skill) + } if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { triggerSection(scenarios: scenarios) } @@ -195,6 +204,13 @@ struct InspectorPane: View { capability.summary?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } + private var selectedSkill: Skill? { + guard let skillID = capability.underlyingSkillID else { + return nil + } + return store.skills.first { $0.id == skillID } + } + private var summarySection: some View { VStack(alignment: .leading, spacing: 6) { SectionHeading(title: "matrix.inspector.section.summary") @@ -205,6 +221,85 @@ struct InspectorPane: View { } } + private func readmePreviewSection(skill: Skill, context: String? = nil) -> some View { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.readme", accent: .popSectionPurple) + Spacer() + Button { + Task { await store.loadReadmePreview(for: skill, force: true) } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 11, weight: .semibold)) + } + .buttonStyle(.plain) + .foregroundStyle(Color.popSecondaryLabel) + .help(localization.string("matrix.readme.reload")) + } + + if let context { + Text(context) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + + readmePreviewBody(for: skill) + } + .task(id: skill.id) { + await store.loadReadmePreview(for: skill) + } + } + + @ViewBuilder + private func readmePreviewBody(for skill: Skill) -> some View { + switch store.readmePreviewState(for: skill) { + case .loaded(let preview): + VStack(alignment: .leading, spacing: 8) { + Text(preview.excerpt) + .font(.system(size: 11.5, design: .monospaced)) + .foregroundStyle(Color.popLabel) + .lineSpacing(2) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + + HStack(spacing: 10) { + Button { + NSWorkspace.shared.open(preview.url) + } label: { + Label(localization.string("matrix.readme.openFile"), systemImage: "doc.text") + .font(.caption.weight(.medium)) + } + .buttonStyle(.plain) + + if preview.truncated { + Text(localization.string("matrix.readme.truncated")) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } + } + .padding(10) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + case .failed(let message): + Text(localization.string("matrix.readme.error", message)) + .font(.caption) + .foregroundStyle(Color.popTertiaryLabel) + case .loading: + readmeLoadingRow + case .none: + readmeLoadingRow + } + } + + private var readmeLoadingRow: some View { + HStack(spacing: 8) { + ProgressView().controlSize(.mini) + Text(localization.string("matrix.readme.loading")) + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + } + } + private func packageSummarySection(_ package: CapabilityPackage) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.summary", accent: .popSectionPurple) @@ -874,6 +969,11 @@ struct InspectorPane: View { .localStoreURL } + private func readmeSkill(for package: CapabilityPackage) -> Skill? { + package.matchingInstalledSkills(in: store.skills) + .first { $0.markdownURL != nil } + } + private func packagePendingUpdates(_ package: CapabilityPackage) -> [SkillUpdateInfo] { store.updates.filter { package.matchingSkillComponent(for: $0) != nil } } diff --git a/swift-app/Tests/PopskillTests/ReadmePreviewTests.swift b/swift-app/Tests/PopskillTests/ReadmePreviewTests.swift new file mode 100644 index 0000000..78801be --- /dev/null +++ b/swift-app/Tests/PopskillTests/ReadmePreviewTests.swift @@ -0,0 +1,57 @@ +@testable import Popskill +import Testing + +struct ReadmePreviewTests { + @Test + func excerptDropsYAMLFrontmatter() { + let content = """ + --- + name: demo-skill + description: Internal metadata + --- + + # Demo Skill + + Use this when the user asks for a demo. + """ + + let excerpt = ReadmePreview.makeExcerpt(from: content) + + #expect(excerpt.text.hasPrefix("# Demo Skill")) + #expect(!excerpt.text.contains("description: Internal metadata")) + #expect(excerpt.text.contains("Use this when")) + #expect(excerpt.truncated == false) + } + + @Test + func excerptCompactsBlankRunsAndTruncatesByLineCount() { + let content = """ + # Demo + + + One + + + Two + Three + Four + """ + + let excerpt = ReadmePreview.makeExcerpt(from: content, maxCharacters: 200, maxLines: 5) + + #expect(excerpt.text == "# Demo\n\nOne\n\nTwo") + #expect(excerpt.truncated) + } + + @Test + func excerptTruncatesByCharacterBudget() { + let excerpt = ReadmePreview.makeExcerpt( + from: "# Demo Skill\nThis line is longer than the budget.", + maxCharacters: 12, + maxLines: 10 + ) + + #expect(excerpt.text == "# Demo Skill") + #expect(excerpt.truncated) + } +} From a5e81ec811757350074b21a3d678713e38e1c7dc Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 02:04:31 +0800 Subject: [PATCH 06/47] feat(ui): search package bundles in spotlight --- .../Popskill/Models/PackageSearchScorer.swift | 87 ++++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 1 + .../zh-Hans.lproj/Localizable.strings | 1 + .../Popskill/Views/SpotlightView.swift | 75 +++++++++++++-- .../PackageSearchScorerTests.swift | 91 +++++++++++++++++++ 5 files changed, 246 insertions(+), 9 deletions(-) create mode 100644 swift-app/Sources/Popskill/Models/PackageSearchScorer.swift create mode 100644 swift-app/Tests/PopskillTests/PackageSearchScorerTests.swift diff --git a/swift-app/Sources/Popskill/Models/PackageSearchScorer.swift b/swift-app/Sources/Popskill/Models/PackageSearchScorer.swift new file mode 100644 index 0000000..05eea78 --- /dev/null +++ b/swift-app/Sources/Popskill/Models/PackageSearchScorer.swift @@ -0,0 +1,87 @@ +import Foundation + +struct PackageSearchHit: Equatable { + let score: Int + let matchedComponents: [String] + let matchedOnName: Bool + + static let recent = PackageSearchHit(score: 0, matchedComponents: [], matchedOnName: false) +} + +enum PackageSearchScorer { + static func score(package: CapabilityPackage, query: String) -> PackageSearchHit? { + let q = normalized(query) + guard !q.isEmpty else { + return nil + } + + var score = 0 + var matchedOnName = false + var matchedComponents: [String] = [] + + let nameScore = scoreText(package.name, query: q, exact: 1_000, prefix: 760, contains: 430) + if nameScore > 0 { + score += nameScore + matchedOnName = true + } + + score += scoreText(package.id, query: q, exact: 420, prefix: 260, contains: 160) + score += scoreText(package.vendor ?? "", query: q, exact: 340, prefix: 220, contains: 120) + score += scoreText(package.source.location, query: q, exact: 360, prefix: 240, contains: 140) + score += scoreText(package.summary, query: q, exact: 180, prefix: 120, contains: 70) + + if ["bundle", "package", "suite", "套装"].contains(q) { + score += package.type == .composite ? 220 : 80 + } + + var seenComponents: Set = [] + for component in package.components.all { + let componentScore = scoreComponent(component, query: q) + guard componentScore > 0 else { continue } + score += component.installed ? componentScore : max(20, componentScore - 35) + + let label = component.name.trimmingCharacters(in: .whitespacesAndNewlines) + if !label.isEmpty, !seenComponents.contains(label.lowercased()) { + seenComponents.insert(label.lowercased()) + matchedComponents.append(label) + } + } + + guard score > 0 else { + return nil + } + + return PackageSearchHit( + score: score, + matchedComponents: Array(matchedComponents.prefix(3)), + matchedOnName: matchedOnName + ) + } + + private static func scoreComponent(_ component: PackageComponent, query: String) -> Int { + scoreText(component.name, query: query, exact: 260, prefix: 190, contains: 120) + + scoreText(component.id, query: query, exact: 220, prefix: 150, contains: 90) + + scoreText(component.kind, query: query, exact: 140, prefix: 90, contains: 40) + + scoreText(component.location ?? "", query: query, exact: 180, prefix: 120, contains: 80) + + scoreText(component.status, query: query, exact: 80, prefix: 50, contains: 25) + } + + private static func scoreText( + _ text: String, + query: String, + exact: Int, + prefix: Int, + contains: Int + ) -> Int { + let value = normalized(text) + guard !value.isEmpty else { return 0 } + if value == query { return exact } + if value.hasPrefix(query) { return prefix } + if value.contains(query) { return contains } + return 0 + } + + private static func normalized(_ value: String) -> String { + value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + } +} diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index e0057a5..ed9a2a1 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -428,6 +428,7 @@ "spotlight.section.capabilities" = "CAPABILITIES"; "spotlight.section.actions" = "ACTIONS"; +"spotlight.bundle.matchedComponents" = "Matched components: %@"; "spotlight.empty.title" = "No matches"; "spotlight.empty.body" = "Try a name, trigger word, or use ↑↓ to pick a recent capability."; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 5b4a9b5..078fb24 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -428,6 +428,7 @@ "spotlight.section.capabilities" = "能力"; "spotlight.section.actions" = "操作"; +"spotlight.bundle.matchedComponents" = "匹配组件:%@"; "spotlight.empty.title" = "没有匹配"; "spotlight.empty.body" = "试试名字、触发词,或按 ↑↓ 挑一条最近更新的能力。"; diff --git a/swift-app/Sources/Popskill/Views/SpotlightView.swift b/swift-app/Sources/Popskill/Views/SpotlightView.swift index 925a524..f3a18c0 100644 --- a/swift-app/Sources/Popskill/Views/SpotlightView.swift +++ b/swift-app/Sources/Popskill/Views/SpotlightView.swift @@ -2,7 +2,7 @@ import SwiftUI /// ⌘K command palette. Opens over the entire app, search box auto-focused. /// Two sections shown together, deduped by source: -/// 1. **能力** — top N (8) `Skill` hits ranked by `SkillSearchScorer`. +/// 1. **能力** — top package / skill hits ranked by local scorers. /// 2. **操作** — fixed quick actions (refresh / link-health / updates / /// settings) filtered by query substring match. /// @@ -17,7 +17,8 @@ struct SpotlightView: View { @State private var highlighted: Int = 0 @FocusState private var queryFocused: Bool - private let maxSkillHits = 8 + private let maxCapabilityHits = 8 + private let maxPackageHits = 3 var body: some View { ZStack { @@ -121,11 +122,11 @@ struct SpotlightView: View { ScrollViewReader { proxy in ScrollView { LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { - if !skillHits.isEmpty { + if !capabilityItems.isEmpty { Section { - ForEach(Array(skillHits.enumerated()), id: \.offset) { offset, hit in + ForEach(Array(capabilityItems.enumerated()), id: \.offset) { offset, item in row( - item: .skill(hit.skill, hit.hit), + item: item, index: offset, proxy: proxy ) @@ -139,7 +140,7 @@ struct SpotlightView: View { ForEach(Array(actionHits.enumerated()), id: \.offset) { offset, action in row( item: .action(action), - index: skillHits.count + offset, + index: capabilityItems.count + offset, proxy: proxy ) } @@ -210,6 +211,8 @@ struct SpotlightView: View { private func icon(for item: SpotlightItem) -> some View { Group { switch item { + case let .package(package, _): + PackageAvatar(name: package.name, identifier: package.id, size: 24) case let .skill(skill, _): InitialAvatarView(name: skill.name, identifier: skill.id) .frame(width: 24, height: 24) @@ -225,6 +228,7 @@ struct SpotlightView: View { private func primaryLabel(for item: SpotlightItem) -> String { switch item { + case let .package(package, _): return package.name case let .skill(skill, _): return skill.name case let .action(action): return localization.string(action.titleKey) } @@ -232,6 +236,19 @@ struct SpotlightView: View { private func secondaryLabel(for item: SpotlightItem) -> String? { switch item { + case let .package(package, hit): + if !hit.matchedComponents.isEmpty { + return localization.string( + "spotlight.bundle.matchedComponents", + hit.matchedComponents.joined(separator: " · ") + ) + } + return localization.string( + "package.componentSummary", + package.componentCount, + package.installedComponentCount, + package.requiredComponentCount + ) case let .skill(skill, hit): if !hit.matchedTriggers.isEmpty { return hit.matchedTriggers.prefix(2).joined(separator: " · ") @@ -246,6 +263,13 @@ struct SpotlightView: View { @ViewBuilder private func trailing(for item: SpotlightItem) -> some View { switch item { + case .package: + Text(localization.string("matrix.type.bundle").uppercased()) + .font(.system(size: 9, weight: .bold)) + .foregroundStyle(Color.popSectionPurple) + .padding(.horizontal, 5) + .padding(.vertical, 2.5) + .background(Color.popSectionPurple.opacity(0.12), in: Capsule()) case let .skill(skill, _): HStack(spacing: 6) { quickToggle(skill: skill, app: .claude, shortcut: "1") @@ -316,13 +340,36 @@ struct SpotlightView: View { // MARK: Derived + private var packageHits: [(package: CapabilityPackage, hit: PackageSearchHit)] { + let q = localQuery.trimmingCharacters(in: .whitespacesAndNewlines) + guard !q.isEmpty else { + return store.compositePackages + .sorted { ($0.lastLifecycleTimestamp ?? 0) > ($1.lastLifecycleTimestamp ?? 0) } + .prefix(maxPackageHits) + .map { ($0, PackageSearchHit.recent) } + } + + return store.compositePackages + .compactMap { package -> (CapabilityPackage, PackageSearchHit)? in + guard let hit = PackageSearchScorer.score(package: package, query: q) else { return nil } + return (package, hit) + } + .sorted { lhs, rhs in + if lhs.1.score != rhs.1.score { return lhs.1.score > rhs.1.score } + return lhs.0.name.localizedCaseInsensitiveCompare(rhs.0.name) == .orderedAscending + } + .prefix(maxPackageHits) + .map { ($0.0, $0.1) } + } + private var skillHits: [(skill: Skill, hit: SkillSearchHit)] { let q = localQuery.trimmingCharacters(in: .whitespacesAndNewlines) + let remainingSlots = max(0, maxCapabilityHits - packageHits.count) guard !q.isEmpty else { // Empty query: show recently installed / updated skills (max N). return store.skills .sorted { ($0.lastLifecycleTimestamp ?? 0) > ($1.lastLifecycleTimestamp ?? 0) } - .prefix(maxSkillHits) + .prefix(remainingSlots) .map { ($0, SkillSearchHit(score: 0, matchedTriggers: [], matchedOnName: false)) } } return store.skills @@ -331,7 +378,7 @@ struct SpotlightView: View { return (skill, hit) } .sorted { $0.1.score > $1.1.score } - .prefix(maxSkillHits) + .prefix(remainingSlots) .map { ($0.0, $0.1) } } @@ -346,7 +393,12 @@ struct SpotlightView: View { } private var combined: [SpotlightItem] { - skillHits.map { .skill($0.skill, $0.hit) } + actionHits.map { .action($0) } + capabilityItems + actionHits.map { .action($0) } + } + + private var capabilityItems: [SpotlightItem] { + packageHits.map { .package($0.package, $0.hit) } + + skillHits.map { .skill($0.skill, $0.hit) } } // MARK: Activation @@ -360,6 +412,10 @@ struct SpotlightView: View { private func activate(index: Int) { guard combined.indices.contains(index) else { return } switch combined[index] { + case let .package(package, _): + store.currentSelection = .matrix + store.selectCapability(MatrixCapability.packageCapabilityID(for: package.id)) + close() case let .skill(skill, _): store.currentSelection = .matrix store.selectSkill(skill.id) @@ -396,6 +452,7 @@ struct SpotlightView: View { // MARK: - Items private enum SpotlightItem { + case package(CapabilityPackage, PackageSearchHit) case skill(Skill, SkillSearchHit) case action(SpotlightAction) } diff --git a/swift-app/Tests/PopskillTests/PackageSearchScorerTests.swift b/swift-app/Tests/PopskillTests/PackageSearchScorerTests.swift new file mode 100644 index 0000000..9a86a04 --- /dev/null +++ b/swift-app/Tests/PopskillTests/PackageSearchScorerTests.swift @@ -0,0 +1,91 @@ +@testable import Popskill +import Testing + +struct PackageSearchScorerTests { + @Test + func queryMatchesPackageNameAndSource() { + let package = demoPackage() + + let hit = PackageSearchScorer.score(package: package, query: "feishu") + + #expect(hit != nil) + #expect(hit?.matchedOnName == true) + #expect((hit?.score ?? 0) > 700) + } + + @Test + func queryMatchesComponentNamesAndKinds() { + let package = demoPackage() + + let cliHit = PackageSearchScorer.score(package: package, query: "lark-cli") + let mcpHit = PackageSearchScorer.score(package: package, query: "mcp") + + #expect(cliHit?.matchedComponents == ["lark-cli"]) + #expect(mcpHit?.matchedComponents.contains("Lark OpenAPI MCP") == true) + } + + @Test + func unrelatedQueryReturnsNil() { + let package = demoPackage() + + #expect(PackageSearchScorer.score(package: package, query: "pdf") == nil) + } + + private func demoPackage() -> CapabilityPackage { + CapabilityPackage( + id: "pkg:lark", + type: .composite, + name: "Feishu / Lark", + vendor: "ByteDance", + summary: "Composite office package", + source: PackageSource( + kind: "builtin", + location: "popskill/builtin/lark", + updateStrategy: "manual", + repoOwner: "larksuite", + repoName: "cli", + repoBranch: "main", + readmeUrl: nil + ), + components: PackageComponents( + cli: [ + PackageComponent( + id: "lark-cli", + name: "lark-cli", + kind: "cli", + required: true, + installed: true, + status: "detected", + location: nil + ) + ], + skills: [ + PackageComponent( + id: "lark-doc", + name: "Lark Doc", + kind: "skill", + required: true, + installed: true, + status: "installed", + location: "lark-doc" + ) + ], + mcp: [ + PackageComponent( + id: "lark-openapi-mcp", + name: "Lark OpenAPI MCP", + kind: "mcp", + required: false, + installed: false, + status: "registry-reference", + location: "anthropic-mcp-registry/bytedance/lark-openapi-mcp" + ) + ], + agents: [] + ), + configSchema: [], + installed: true, + lifecycle: .untracked + ) + } +} From f07121824bb52ee6c82d02996ec7aff00c24ca68 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 02:09:24 +0800 Subject: [PATCH 07/47] feat(ui): show package component usage --- .../Sources/Popskill/Models/SkillModels.swift | 16 +++-- .../Sources/Popskill/Models/UsageModels.swift | 65 ++++++++++++++++++- .../Resources/en.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../Popskill/Views/InspectorPane.swift | 57 ++++++++++++++++ .../PopskillTests/SkillModelsTests.swift | 4 ++ 6 files changed, 139 insertions(+), 7 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index 3c44693..4227d07 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -1290,19 +1290,23 @@ extension CapabilityPackage { let matchedSkills = matchingInstalledSkills(in: skills) var snapshot = PackageUsageSnapshot() - for stat in summary.skillStats where matchesUsageStat(stat, matchedSkills: matchedSkills) { - snapshot.add(stat) + for stat in summary.skillStats { + guard let component = usageComponent(for: stat, matchedSkills: matchedSkills) else { + continue + } + snapshot.add(stat, component: component) } return snapshot } - private func matchesUsageStat(_ stat: SkillUsageStat, matchedSkills: [Skill]) -> Bool { - if matchedSkills.contains(where: { $0.matchesAttributionSkill(stat.skillID) }) { - return true + private func usageComponent(for stat: SkillUsageStat, matchedSkills: [Skill]) -> PackageComponent? { + if let matchedSkill = matchedSkills.first(where: { $0.matchesAttributionSkill(stat.skillID) }), + let component = components.skills.first(where: { $0.matchesSkill(matchedSkill) }) { + return component } - return components.skills.contains { component in + return components.skills.first { component in component.matchesAttributionSkill(stat.skillID) } } diff --git a/swift-app/Sources/Popskill/Models/UsageModels.swift b/swift-app/Sources/Popskill/Models/UsageModels.swift index 89dcdfd..7b0b0b8 100644 --- a/swift-app/Sources/Popskill/Models/UsageModels.swift +++ b/swift-app/Sources/Popskill/Models/UsageModels.swift @@ -127,6 +127,7 @@ struct PackageUsageSnapshot: Equatable { var cacheCreationTokens: Int64 = 0 var cacheReadTokens: Int64 = 0 var lastUsedAt: Date? + var componentStats: [PackageComponentUsageStat] = [] var totalTokens: Int64 { inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens @@ -136,13 +137,75 @@ struct PackageUsageSnapshot: Equatable { usageEvents > 0 || totalTokens > 0 } - mutating func add(_ stat: SkillUsageStat) { + mutating func add(_ stat: SkillUsageStat, component: PackageComponent) { matchedSkillCount += 1 usageEvents += stat.usageEvents inputTokens += stat.inputTokens outputTokens += stat.outputTokens cacheCreationTokens += stat.cacheCreationTokens cacheReadTokens += stat.cacheReadTokens + upsertComponentStat(stat, component: component) + + guard let date = stat.lastUsedAt else { + return + } + if lastUsedAt.map({ date > $0 }) ?? true { + lastUsedAt = date + } + } + + private mutating func upsertComponentStat(_ stat: SkillUsageStat, component: PackageComponent) { + if let index = componentStats.firstIndex(where: { $0.componentID == component.id }) { + componentStats[index].add(stat) + } else { + componentStats.append(PackageComponentUsageStat(component: component, stat: stat)) + } + + componentStats.sort { lhs, rhs in + if lhs.usageEvents != rhs.usageEvents { return lhs.usageEvents > rhs.usageEvents } + if lhs.totalTokens != rhs.totalTokens { return lhs.totalTokens > rhs.totalTokens } + return lhs.componentName.localizedCaseInsensitiveCompare(rhs.componentName) == .orderedAscending + } + } +} + +struct PackageComponentUsageStat: Identifiable, Equatable { + var id: String { componentID } + + let componentID: String + let componentName: String + let componentKind: String + let installed: Bool + var usageEvents: Int + var inputTokens: Int64 + var outputTokens: Int64 + var cacheCreationTokens: Int64 + var cacheReadTokens: Int64 + var lastUsedAt: Date? + + var totalTokens: Int64 { + inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens + } + + init(component: PackageComponent, stat: SkillUsageStat) { + componentID = component.id + componentName = component.name + componentKind = component.kind + installed = component.installed + usageEvents = stat.usageEvents + inputTokens = stat.inputTokens + outputTokens = stat.outputTokens + cacheCreationTokens = stat.cacheCreationTokens + cacheReadTokens = stat.cacheReadTokens + lastUsedAt = stat.lastUsedAt + } + + mutating func add(_ stat: SkillUsageStat) { + usageEvents += stat.usageEvents + inputTokens += stat.inputTokens + outputTokens += stat.outputTokens + cacheCreationTokens += stat.cacheCreationTokens + cacheReadTokens += stat.cacheReadTokens guard let date = stat.lastUsedAt else { return diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index ed9a2a1..ccfb704 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -357,6 +357,8 @@ "matrix.package.usage.empty" = "Usage was scanned, but the %d local skills matched to this bundle have no attribution records yet."; "matrix.package.usage.notScanned" = "Transcript usage has not been scanned yet."; "matrix.package.usage.scanning" = "Scanning local transcripts…"; +"matrix.package.usage.topComponents" = "TOP COMPONENTS"; +"matrix.package.usage.componentCalls" = "%d calls"; "matrix.package.paths.empty" = "No local skill files matched this bundle."; "matrix.package.paths.more" = "%d more matched skills hidden."; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 078fb24..b1bd61d 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -357,6 +357,8 @@ "matrix.package.usage.empty" = "已扫描用量,但这个套装匹配到的 %d 个本地 skill 暂无归因记录。"; "matrix.package.usage.notScanned" = "还没有扫描 transcript,用量聚合暂不可用。"; "matrix.package.usage.scanning" = "正在扫描本机 transcript…"; +"matrix.package.usage.topComponents" = "最常用组件"; +"matrix.package.usage.componentCalls" = "%d 次调用"; "matrix.package.paths.empty" = "没有匹配到本地 skill 真身。"; "matrix.package.paths.more" = "还有 %d 个匹配 skill 未展开。"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 8f00e5c..e0e09b4 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -417,6 +417,9 @@ struct InspectorPane: View { .font(.caption2) .foregroundStyle(Color.popTertiaryLabel) } + if !snapshot.componentStats.isEmpty { + packageUsageBreakdown(snapshot.componentStats) + } } else { Text(localization.string("matrix.package.usage.empty", snapshot.matchedSkillCount)) .font(.caption) @@ -440,6 +443,48 @@ struct InspectorPane: View { } } + private func packageUsageBreakdown(_ stats: [PackageComponentUsageStat]) -> some View { + VStack(alignment: .leading, spacing: 6) { + LocalizedText("matrix.package.usage.topComponents") + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + + ForEach(stats.prefix(5)) { stat in + packageUsageComponentRow(stat) + } + } + } + + private func packageUsageComponentRow(_ stat: PackageComponentUsageStat) -> some View { + HStack(alignment: .center, spacing: 8) { + Image(systemName: stat.componentKind.usageKindSymbol) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(stat.installed ? Color.popStatusOK : Color.popTertiaryLabel) + .frame(width: 14) + VStack(alignment: .leading, spacing: 1) { + Text(stat.componentName) + .font(.caption.weight(.medium)) + .foregroundStyle(Color.popLabel) + .lineLimit(1) + Text(stat.componentKind.uppercased()) + .font(.system(size: 9, weight: .semibold)) + .foregroundStyle(Color.popTertiaryLabel) + } + Spacer(minLength: 8) + VStack(alignment: .trailing, spacing: 1) { + Text(localization.string("matrix.package.usage.componentCalls", stat.usageEvents)) + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popLabel) + .monospacedDigit() + Text(Self.formatTokens(stat.totalTokens)) + .font(.caption2.monospacedDigit()) + .foregroundStyle(Color.popSecondaryLabel) + } + } + .padding(8) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + private func packageUsageMetric(titleKey: String, value: String, tint: Color) -> some View { VStack(alignment: .leading, spacing: 4) { LocalizedText(titleKey) @@ -1041,6 +1086,18 @@ private extension PackageComponent { } } +private extension String { + var usageKindSymbol: String { + switch lowercased() { + case "skill": "square.grid.3x3.fill" + case "agent": "person.crop.square" + case "cli": "terminal" + case "mcp": "rectangle.connected.to.line.below" + default: "circle.grid.2x2" + } + } +} + private extension TargetApp { var inspectorAccentColor: Color { switch self { diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 43069e7..2105827 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -822,6 +822,10 @@ struct SkillModelsTests { #expect(snapshot?.usageEvents == 3) #expect(snapshot?.totalTokens == 84) #expect(snapshot?.lastUsedAt == lastUsed) + #expect(snapshot?.componentStats.map(\.componentID) == ["baoyu-comic", "baoyu-translate"]) + #expect(snapshot?.componentStats.first?.componentName == "baoyu-comic") + #expect(snapshot?.componentStats.first?.usageEvents == 2) + #expect(snapshot?.componentStats.first?.totalTokens == 62) } @Test From a716566adf0850ece705b94e917d7dd98b6e9dab Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 02:14:42 +0800 Subject: [PATCH 08/47] feat(ui): show skill usage in inspector --- .../Sources/Popskill/Models/SkillModels.swift | 12 ++++ .../Sources/Popskill/Models/UsageModels.swift | 32 +++++++++++ .../Resources/en.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../Popskill/Views/InspectorPane.swift | 54 ++++++++++++++++++ .../PopskillTests/SkillModelsTests.swift | 56 +++++++++++++++++++ 6 files changed, 158 insertions(+) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index 4227d07..cd71c6e 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -213,6 +213,18 @@ struct Skill: Identifiable, Codable, Equatable { private static func normalizedAttributionIdentifier(_ value: String) -> String { value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } + + func usageSnapshot(using summary: UsageSummary?) -> SkillUsageSnapshot? { + guard let summary else { + return nil + } + + var snapshot = SkillUsageSnapshot() + for stat in summary.skillStats where matchesAttributionSkill(stat.skillID) { + snapshot.add(stat) + } + return snapshot + } } struct LocalAgent: Identifiable, Codable, Equatable { diff --git a/swift-app/Sources/Popskill/Models/UsageModels.swift b/swift-app/Sources/Popskill/Models/UsageModels.swift index 7b0b0b8..1a7bee8 100644 --- a/swift-app/Sources/Popskill/Models/UsageModels.swift +++ b/swift-app/Sources/Popskill/Models/UsageModels.swift @@ -119,6 +119,38 @@ struct SkillUsageStat: Identifiable, Equatable { } } +struct SkillUsageSnapshot: Equatable { + var usageEvents = 0 + var inputTokens: Int64 = 0 + var outputTokens: Int64 = 0 + var cacheCreationTokens: Int64 = 0 + var cacheReadTokens: Int64 = 0 + var lastUsedAt: Date? + + var totalTokens: Int64 { + inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens + } + + var hasUsage: Bool { + usageEvents > 0 || totalTokens > 0 + } + + mutating func add(_ stat: SkillUsageStat) { + usageEvents += stat.usageEvents + inputTokens += stat.inputTokens + outputTokens += stat.outputTokens + cacheCreationTokens += stat.cacheCreationTokens + cacheReadTokens += stat.cacheReadTokens + + guard let date = stat.lastUsedAt else { + return + } + if lastUsedAt.map({ date > $0 }) ?? true { + lastUsedAt = date + } + } +} + struct PackageUsageSnapshot: Equatable { var matchedSkillCount = 0 var usageEvents = 0 diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index ccfb704..6de08a6 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -359,6 +359,8 @@ "matrix.package.usage.scanning" = "Scanning local transcripts…"; "matrix.package.usage.topComponents" = "TOP COMPONENTS"; "matrix.package.usage.componentCalls" = "%d calls"; +"matrix.skill.usage.empty" = "Usage was scanned, but this skill has no attribution records yet."; +"matrix.skill.usage.notScanned" = "Transcript usage has not been scanned yet."; "matrix.package.paths.empty" = "No local skill files matched this bundle."; "matrix.package.paths.more" = "%d more matched skills hidden."; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index b1bd61d..b305893 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -359,6 +359,8 @@ "matrix.package.usage.scanning" = "正在扫描本机 transcript…"; "matrix.package.usage.topComponents" = "最常用组件"; "matrix.package.usage.componentCalls" = "%d 次调用"; +"matrix.skill.usage.empty" = "已扫描用量,但这个 skill 暂无归因记录。"; +"matrix.skill.usage.notScanned" = "还没有扫描 transcript,用量暂不可用。"; "matrix.package.paths.empty" = "没有匹配到本地 skill 真身。"; "matrix.package.paths.more" = "还有 %d 个匹配 skill 未展开。"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index e0e09b4..f2a5e90 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -40,6 +40,7 @@ struct InspectorPane: View { } if let skill = selectedSkill { readmePreviewSection(skill: skill) + skillUsageSection(skill) } if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { triggerSection(scenarios: scenarios) @@ -300,6 +301,59 @@ struct InspectorPane: View { } } + private func skillUsageSection(_ skill: Skill) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.usage", accent: .accentColor) + + if store.usageScanInFlight { + HStack(spacing: 8) { + ProgressView().controlSize(.mini) + LocalizedText("matrix.package.usage.scanning") + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + } + } else if let snapshot = skill.usageSnapshot(using: store.usageSummary) { + if snapshot.hasUsage { + HStack(spacing: 10) { + packageUsageMetric( + titleKey: "matrix.package.usage.tokens", + value: Self.formatTokens(snapshot.totalTokens), + tint: .accentColor + ) + packageUsageMetric( + titleKey: "matrix.package.usage.calls", + value: "\(snapshot.usageEvents)", + tint: .popSectionGreen + ) + } + if let lastUsedAt = snapshot.lastUsedAt { + Text(localization.string("matrix.package.usage.lastUsed", Self.relativeFormatter.localizedString(for: lastUsedAt, relativeTo: Date()))) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } else { + Text(localization.string("matrix.skill.usage.empty")) + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + } + } else { + HStack(alignment: .center, spacing: 8) { + Text(localization.string("matrix.skill.usage.notScanned")) + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + Spacer(minLength: 8) + Button { + Task { await store.refreshUsageScan() } + } label: { + Label(localization.string("insights.refresh"), systemImage: "chart.bar.doc.horizontal") + } + .buttonStyle(.bordered) + .controlSize(.mini) + } + } + } + } + private func packageSummarySection(_ package: CapabilityPackage) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.summary", accent: .popSectionPurple) diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 2105827..efcc9f3 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -225,6 +225,62 @@ struct SkillModelsTests { #expect(!skill.matchesAttributionSkill("baoyu-cover-image")) } + @Test + func installedSkillUsageSnapshotAggregatesAttributionStats() { + let skill = installedSkill(directory: "baoyu-comic") + let lastUsed = Date(timeIntervalSince1970: 1_800_000_000) + let summary = UsageSummary( + filesScanned: 2, + sessions: 2, + usageEvents: 3, + inputTokens: 100, + outputTokens: 80, + cacheCreationTokens: 0, + cacheReadTokens: 0, + attributedSkillUsageEvents: 2, + modelStats: [], + skillStats: [ + SkillUsageStat( + skillID: "baoyu-comic", + sourcePlugin: nil, + usageEvents: 2, + inputTokens: 20, + outputTokens: 30, + cacheCreationTokens: 5, + cacheReadTokens: 7, + lastUsedAt: lastUsed + ), + SkillUsageStat( + skillID: "jimliu/baoyu-skills:baoyu-comic", + sourcePlugin: nil, + usageEvents: 1, + inputTokens: 10, + outputTokens: 12, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: Date(timeIntervalSince1970: 1_700_000_000) + ), + SkillUsageStat( + skillID: "unrelated", + sourcePlugin: nil, + usageEvents: 9, + inputTokens: 999, + outputTokens: 999, + cacheCreationTokens: 999, + cacheReadTokens: 999, + lastUsedAt: nil + ) + ], + recentSessions: [] + ) + + let snapshot = skill.usageSnapshot(using: summary) + + #expect(snapshot?.usageEvents == 3) + #expect(snapshot?.totalTokens == 84) + #expect(snapshot?.lastUsedAt == lastUsed) + } + @Test func installPlanDecodesPreviewPayload() throws { let data = """ From 211c32d2f023b38d3516c067cd5465caefe3cfb2 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 02:26:03 +0800 Subject: [PATCH 09/47] feat(ui): show matrix usage metrics --- .../Sources/Popskill/Models/UsageModels.swift | 81 +++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../Sources/Popskill/Views/BackupsView.swift | 1 + .../Sources/Popskill/Views/IdleView.swift | 1 + .../Sources/Popskill/Views/InsightsView.swift | 1 + .../Popskill/Views/InspectorPane.swift | 1 + .../Popskill/Views/LinkHealthView.swift | 1 + .../Popskill/Views/MatrixPackageRow.swift | 87 +++++++++++++++++-- .../Sources/Popskill/Views/MatrixRow.swift | 57 ++++++++++-- .../Sources/Popskill/Views/MatrixView.swift | 34 ++++++-- .../Popskill/Views/OnboardingWizardView.swift | 1 + .../Sources/Popskill/Views/RootView.swift | 1 + .../Sources/Popskill/Views/SettingsView.swift | 1 + .../Sources/Popskill/Views/SourcesView.swift | 2 + .../Popskill/Views/SpotlightView.swift | 1 + .../Sources/Popskill/Views/UpdatesView.swift | 1 + .../PopskillTests/SkillModelsTests.swift | 61 +++++++++++++ 18 files changed, 313 insertions(+), 23 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/UsageModels.swift b/swift-app/Sources/Popskill/Models/UsageModels.swift index 1a7bee8..d53f0fb 100644 --- a/swift-app/Sources/Popskill/Models/UsageModels.swift +++ b/swift-app/Sources/Popskill/Models/UsageModels.swift @@ -151,6 +151,87 @@ struct SkillUsageSnapshot: Equatable { } } +struct MatrixUsageIndex: Equatable { + let hasSummary: Bool + private var skillSnapshotsByID: [String: SkillUsageSnapshot] = [:] + private var packageSnapshotsByID: [String: PackageUsageSnapshot] = [:] + private var packageComponentStatsByID: [String: [String: PackageComponentUsageStat]] = [:] + + init(summary: UsageSummary?, skills: [Skill], packages: [CapabilityPackage]) { + guard let summary else { + hasSummary = false + return + } + + hasSummary = true + + for skill in skills { + skillSnapshotsByID[skill.id] = skill.usageSnapshot(using: summary) + } + + for package in packages { + guard let snapshot = package.usageSnapshot(using: summary, skills: skills) else { + continue + } + packageSnapshotsByID[package.id] = snapshot + packageComponentStatsByID[package.id] = Dictionary( + uniqueKeysWithValues: snapshot.componentStats.map { ($0.componentID, $0) } + ) + } + } + + func skillSnapshot(for skillID: String?) -> SkillUsageSnapshot? { + guard hasSummary, let skillID else { + return nil + } + return skillSnapshotsByID[skillID] ?? SkillUsageSnapshot() + } + + func packageSnapshot(for packageID: String?) -> PackageUsageSnapshot? { + guard hasSummary, let packageID else { + return nil + } + return packageSnapshotsByID[packageID] ?? PackageUsageSnapshot() + } + + func packageComponentStat(packageID: String?, componentID: String) -> PackageComponentUsageStat? { + guard hasSummary, let packageID else { + return nil + } + return packageComponentStatsByID[packageID]?[componentID] + } +} + +enum UsageDisplayFormatter { + static func compactTokens(_ value: Int64) -> String { + compact(Int(value)) + } + + static func compactCount(_ value: Int) -> String { + compact(value) + } + + private static func compact(_ value: Int) -> String { + if value < 1_000 { + return integerFormatter.string(from: NSNumber(value: value)) ?? "0" + } + if value < 1_000_000 { + return String(format: "%.1fK", Double(value) / 1_000.0) + } + if value < 1_000_000_000 { + return String(format: "%.1fM", Double(value) / 1_000_000.0) + } + return String(format: "%.2fB", Double(value) / 1_000_000_000.0) + } + + private static let integerFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 0 + return formatter + }() +} + struct PackageUsageSnapshot: Equatable { var matchedSkillCount = 0 var usageEvents = 0 diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 6de08a6..5b12aa5 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -310,6 +310,8 @@ "matrix.search.placeholder" = "Filter by name, trigger, or directory"; "matrix.col.capability" = "Capability"; "matrix.col.source" = "Source"; +"matrix.col.tokens" = "Tokens"; +"matrix.col.calls" = "Calls"; "matrix.filter.all" = "All"; "matrix.filter.updates" = "Updates"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index b305893..fe07427 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -310,6 +310,8 @@ "matrix.search.placeholder" = "按名称、触发词或目录过滤"; "matrix.col.capability" = "能力"; "matrix.col.source" = "来源"; +"matrix.col.tokens" = "Tokens"; +"matrix.col.calls" = "调用"; "matrix.filter.all" = "全部"; "matrix.filter.updates" = "可更新"; diff --git a/swift-app/Sources/Popskill/Views/BackupsView.swift b/swift-app/Sources/Popskill/Views/BackupsView.swift index 2831a50..e92e4a3 100644 --- a/swift-app/Sources/Popskill/Views/BackupsView.swift +++ b/swift-app/Sources/Popskill/Views/BackupsView.swift @@ -5,6 +5,7 @@ import SwiftUI /// with date headers and per-row restore / delete. Restore picks Claude by /// default since most users only have Claude wired; v0.4 will add a target /// chooser. +@MainActor struct BackupsView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization diff --git a/swift-app/Sources/Popskill/Views/IdleView.swift b/swift-app/Sources/Popskill/Views/IdleView.swift index 1732381..d94ffd5 100644 --- a/swift-app/Sources/Popskill/Views/IdleView.swift +++ b/swift-app/Sources/Popskill/Views/IdleView.swift @@ -3,6 +3,7 @@ import SwiftUI /// Idle — skills that are toggled off across every app **and** haven't been /// touched (install / update / use) for ≥ 60 days. Surfaces the standard /// majia "卸载也安全" three-strategy decision per row. +@MainActor struct IdleView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization diff --git a/swift-app/Sources/Popskill/Views/InsightsView.swift b/swift-app/Sources/Popskill/Views/InsightsView.swift index 957e0e4..f763ab7 100644 --- a/swift-app/Sources/Popskill/Views/InsightsView.swift +++ b/swift-app/Sources/Popskill/Views/InsightsView.swift @@ -6,6 +6,7 @@ import SwiftUI /// token consumption inferred from `~/.claude/projects/**/*.jsonl`. The scan /// runs off the main thread; the section degrades gracefully when there's no /// `~/.claude/projects/` directory yet. +@MainActor struct InsightsView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index f2a5e90..e9663c8 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -6,6 +6,7 @@ import SwiftUI /// the full set of sections (summary / triggers / apps / deployment / /// metadata); other kinds gracefully omit the irrelevant pieces (an agent /// has no SSOT symlink to chart, a CLI has no per-app toggle). +@MainActor struct InspectorPane: View { @Bindable var store: PopskillStore let capability: MatrixCapability diff --git a/swift-app/Sources/Popskill/Views/LinkHealthView.swift b/swift-app/Sources/Popskill/Views/LinkHealthView.swift index 4259df7..bd65f42 100644 --- a/swift-app/Sources/Popskill/Views/LinkHealthView.swift +++ b/swift-app/Sources/Popskill/Views/LinkHealthView.swift @@ -4,6 +4,7 @@ import SwiftUI /// summary + a flat table of every row. A row is just "skillName · status /// across N apps". Clicking a row jumps to the matrix and opens the inspector /// so the user can read the SSOT paths in context. +@MainActor struct LinkHealthView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization diff --git a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift index aebc3a6..24a67ca 100644 --- a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift @@ -4,9 +4,11 @@ import SwiftUI /// Composite package row in the capability matrix. The row is read-only at the /// package level for v1.1, but it exposes the component tree inline so Bundle /// coverage is visible without opening a detail page. +@MainActor struct MatrixPackageRow: View { let capability: MatrixCapability @Bindable var store: PopskillStore + let usageIndex: MatrixUsageIndex @Environment(\.popskillLocalization) private var localization @Environment(\.accessibilityReduceMotion) private var reduceMotion @State private var isHovering = false @@ -27,7 +29,12 @@ struct MatrixPackageRow: View { packageHeader if let package, !isCollapsed { ForEach(package.components.all, id: \.displayKey) { component in - MatrixPackageComponentRow(component: component, store: store) + MatrixPackageComponentRow( + component: component, + packageID: package.id, + store: store, + usageIndex: usageIndex + ) Divider().opacity(0.28) } } @@ -42,15 +49,20 @@ struct MatrixPackageRow: View { .padding(.vertical, 9) coverageCell(for: .claude) - .frame(width: 100) + .frame(width: MatrixTableLayout.appColumnWidth) coverageCell(for: .codex) - .frame(width: 100) + .frame(width: MatrixTableLayout.appColumnWidth) sourceCell - .frame(width: 220, alignment: .leading) + .frame(width: MatrixTableLayout.sourceColumnWidth, alignment: .leading) + + tokensCell + .frame(width: MatrixTableLayout.tokensColumnWidth, alignment: .trailing) + callsCell + .frame(width: MatrixTableLayout.callsColumnWidth, alignment: .trailing) actionCell - .frame(width: 56) + .frame(width: MatrixTableLayout.actionColumnWidth) } .padding(.trailing, 4) .contentShape(Rectangle()) @@ -193,6 +205,29 @@ struct MatrixPackageRow: View { } } + private var usageSnapshot: PackageUsageSnapshot? { + usageIndex.packageSnapshot(for: package?.id) + } + + private var tokensCell: some View { + MatrixUsageValueCell(value: usageText { snapshot in + UsageDisplayFormatter.compactTokens(snapshot.totalTokens) + }) + } + + private var callsCell: some View { + MatrixUsageValueCell(value: usageText { snapshot in + UsageDisplayFormatter.compactCount(snapshot.usageEvents) + }) + } + + private func usageText(_ format: (PackageUsageSnapshot) -> String) -> String? { + guard let snapshot = usageSnapshot else { + return nil + } + return snapshot.hasUsage ? format(snapshot) : "0" + } + private var actionCell: some View { Menu { Button { @@ -253,9 +288,12 @@ struct MatrixPackageRow: View { } } +@MainActor private struct MatrixPackageComponentRow: View { let component: PackageComponent + let packageID: String @Bindable var store: PopskillStore + let usageIndex: MatrixUsageIndex @Environment(\.popskillLocalization) private var localization private var matchingSkill: Skill? { @@ -270,14 +308,19 @@ private struct MatrixPackageComponentRow: View { .padding(.vertical, 6) appStateCell(for: .claude) - .frame(width: 100) + .frame(width: MatrixTableLayout.appColumnWidth) appStateCell(for: .codex) - .frame(width: 100) + .frame(width: MatrixTableLayout.appColumnWidth) sourceCell - .frame(width: 220, alignment: .leading) + .frame(width: MatrixTableLayout.sourceColumnWidth, alignment: .leading) - Spacer().frame(width: 56) + tokensCell + .frame(width: MatrixTableLayout.tokensColumnWidth, alignment: .trailing) + callsCell + .frame(width: MatrixTableLayout.callsColumnWidth, alignment: .trailing) + + Spacer().frame(width: MatrixTableLayout.actionColumnWidth) } .padding(.trailing, 4) .contentShape(Rectangle()) @@ -361,6 +404,32 @@ private struct MatrixPackageComponentRow: View { .lineLimit(1) .truncationMode(.middle) } + + private var usageStat: PackageComponentUsageStat? { + usageIndex.packageComponentStat(packageID: packageID, componentID: component.id) + } + + private var tokensCell: some View { + MatrixUsageValueCell(value: usageText { stat in + UsageDisplayFormatter.compactTokens(stat.totalTokens) + }, isSubtle: true) + } + + private var callsCell: some View { + MatrixUsageValueCell(value: usageText { stat in + UsageDisplayFormatter.compactCount(stat.usageEvents) + }, isSubtle: true) + } + + private func usageText(_ format: (PackageComponentUsageStat) -> String) -> String? { + guard usageIndex.hasSummary else { + return nil + } + guard let usageStat else { + return "0" + } + return usageStat.usageEvents > 0 || usageStat.totalTokens > 0 ? format(usageStat) : "0" + } } private enum ComponentAppIndicator { diff --git a/swift-app/Sources/Popskill/Views/MatrixRow.swift b/swift-app/Sources/Popskill/Views/MatrixRow.swift index e5aac3b..a552a50 100644 --- a/swift-app/Sources/Popskill/Views/MatrixRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixRow.swift @@ -1,14 +1,16 @@ import SwiftUI /// One capability row inside the matrix. Layout mirrors `matrixColumnHeader` -/// in `MatrixView.swift`: capability column (flexible), Claude toggle -/// (100pt), Codex toggle (100pt), source label (220pt), action menu (56pt). +/// in `MatrixView.swift`: capability column (flexible), tool coverage, +/// source, usage metrics, and action menu. /// Renders Skill / Agent / CLI / MCP / Config via the unified /// `MatrixCapability` model; non-toggleable kinds (anything but skill) show /// a read-only "on" icon instead of the interactive switch. +@MainActor struct MatrixRow: View { let capability: MatrixCapability @Bindable var store: PopskillStore + let usageIndex: MatrixUsageIndex @Environment(\.popskillLocalization) private var localization @Environment(\.accessibilityReduceMotion) private var reduceMotion @State private var isHovering = false @@ -29,15 +31,20 @@ struct MatrixRow: View { .padding(.vertical, 8) appToggleCell(for: .claude) - .frame(width: 100) + .frame(width: MatrixTableLayout.appColumnWidth) appToggleCell(for: .codex) - .frame(width: 100) + .frame(width: MatrixTableLayout.appColumnWidth) sourceCell - .frame(width: 220, alignment: .leading) + .frame(width: MatrixTableLayout.sourceColumnWidth, alignment: .leading) + + tokensCell + .frame(width: MatrixTableLayout.tokensColumnWidth, alignment: .trailing) + callsCell + .frame(width: MatrixTableLayout.callsColumnWidth, alignment: .trailing) actionCell - .frame(width: 56) + .frame(width: MatrixTableLayout.actionColumnWidth) } .padding(.trailing, 4) .contentShape(Rectangle()) @@ -191,6 +198,29 @@ struct MatrixRow: View { } } + private var usageSnapshot: SkillUsageSnapshot? { + usageIndex.skillSnapshot(for: capability.underlyingSkillID) + } + + private var tokensCell: some View { + MatrixUsageValueCell(value: usageText { snapshot in + UsageDisplayFormatter.compactTokens(snapshot.totalTokens) + }) + } + + private var callsCell: some View { + MatrixUsageValueCell(value: usageText { snapshot in + UsageDisplayFormatter.compactCount(snapshot.usageEvents) + }) + } + + private func usageText(_ format: (SkillUsageSnapshot) -> String) -> String? { + guard let snapshot = usageSnapshot else { + return nil + } + return snapshot.hasUsage ? format(snapshot) : "0" + } + private var sourceSymbol: String { switch (capability.sourceType ?? "").lowercased() { case "github": return "chevron.left.forwardslash.chevron.right" @@ -253,11 +283,26 @@ struct MatrixRow: View { } } +struct MatrixUsageValueCell: View { + let value: String? + var isSubtle = false + + var body: some View { + Text(value ?? "—") + .font(.system(size: isSubtle ? 10.8 : 11.5, weight: isSubtle ? .regular : .medium).monospacedDigit()) + .foregroundStyle(value == nil ? Color.popTertiaryLabel : (isSubtle ? Color.popSecondaryLabel : Color.popLabel)) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.trailing, 8) + } +} + /// Sticky header above each repo bucket. Clicking the chevron collapses / /// expands the bucket. The right side shows aggregate "%d enabled on Claude /// / Codex" so users can see coverage without scanning every row. v0.4 /// renders the same header for any capability kind — the kind-level banner /// in MatrixView provides the kind context. +@MainActor struct MatrixGroupHeader: View { let group: MatrixGroup @Bindable var store: PopskillStore diff --git a/swift-app/Sources/Popskill/Views/MatrixView.swift b/swift-app/Sources/Popskill/Views/MatrixView.swift index 3181069..cbb719a 100644 --- a/swift-app/Sources/Popskill/Views/MatrixView.swift +++ b/swift-app/Sources/Popskill/Views/MatrixView.swift @@ -1,9 +1,18 @@ import SwiftUI +enum MatrixTableLayout { + static let appColumnWidth: CGFloat = 92 + static let sourceColumnWidth: CGFloat = 184 + static let tokensColumnWidth: CGFloat = 78 + static let callsColumnWidth: CGFloat = 62 + static let actionColumnWidth: CGFloat = 52 +} + /// Skills × Tools — the matrix is Popskill's灵魂主视图: rows = capabilities /// (skill / cli / mcp / agent), columns = AI tools (Claude Code / Codex), /// the cell is a direct toggle. Selecting a row slides an Inspector pane in /// from the right showing the position-and-link section. +@MainActor struct MatrixView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization @@ -11,6 +20,11 @@ struct MatrixView: View { var body: some View { let capabilities = store.capabilities + let usageIndex = MatrixUsageIndex( + summary: store.usageSummary, + skills: store.skills, + packages: store.compositePackages + ) let sections = filteredSections(in: capabilities) VStack(spacing: 0) { header(capabilities: capabilities) @@ -27,7 +41,7 @@ struct MatrixView: View { } else if sections.isEmpty { noResultsState } else { - matrixTable(sections: sections) + matrixTable(sections: sections, usageIndex: usageIndex) } } .popPageBackground() @@ -167,7 +181,7 @@ struct MatrixView: View { // MARK: Matrix table - private func matrixTable(sections: [CapabilitySection]) -> some View { + private func matrixTable(sections: [CapabilitySection], usageIndex: MatrixUsageIndex) -> some View { ScrollView { LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) { Section { @@ -183,9 +197,9 @@ struct MatrixView: View { if !store.collapsedGroups.contains(group.id) { ForEach(group.capabilities, id: \.id) { capability in if capability.kind == .bundle { - MatrixPackageRow(capability: capability, store: store) + MatrixPackageRow(capability: capability, store: store, usageIndex: usageIndex) } else { - MatrixRow(capability: capability, store: store) + MatrixRow(capability: capability, store: store, usageIndex: usageIndex) } Divider().opacity(0.4) } @@ -237,12 +251,16 @@ struct MatrixView: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 14) Text("Claude Code") - .frame(width: 100, alignment: .center) + .frame(width: MatrixTableLayout.appColumnWidth, alignment: .center) Text("Codex") - .frame(width: 100, alignment: .center) + .frame(width: MatrixTableLayout.appColumnWidth, alignment: .center) Text(localization.string("matrix.col.source")) - .frame(width: 220, alignment: .leading) - Spacer().frame(width: 56) + .frame(width: MatrixTableLayout.sourceColumnWidth, alignment: .leading) + Text(localization.string("matrix.col.tokens")) + .frame(width: MatrixTableLayout.tokensColumnWidth, alignment: .trailing) + Text(localization.string("matrix.col.calls")) + .frame(width: MatrixTableLayout.callsColumnWidth, alignment: .trailing) + Spacer().frame(width: MatrixTableLayout.actionColumnWidth) } .font(.system(size: 11.5, weight: .medium)) .foregroundStyle(Color.popSecondaryLabel) diff --git a/swift-app/Sources/Popskill/Views/OnboardingWizardView.swift b/swift-app/Sources/Popskill/Views/OnboardingWizardView.swift index 966ee0d..027fa72 100644 --- a/swift-app/Sources/Popskill/Views/OnboardingWizardView.swift +++ b/swift-app/Sources/Popskill/Views/OnboardingWizardView.swift @@ -11,6 +11,7 @@ import SwiftUI /// 3. Storage + sync — confirm SSOT + pick provider (iCloud recommended on /// Mac with iCloud Drive available, Git otherwise). /// 4. Done — open matrix. +@MainActor struct OnboardingWizardView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization diff --git a/swift-app/Sources/Popskill/Views/RootView.swift b/swift-app/Sources/Popskill/Views/RootView.swift index e3227f1..fde6b24 100644 --- a/swift-app/Sources/Popskill/Views/RootView.swift +++ b/swift-app/Sources/Popskill/Views/RootView.swift @@ -4,6 +4,7 @@ import SwiftUI /// with 3 sectioned groups (操控台 / 来源 / 维护) + Settings + Spotlight trigger. /// Each detail-area view is its own file and pulls data straight off the /// shared `PopskillStore`. +@MainActor struct RootView: View { @State private var store = PopskillStore() @Environment(\.popskillLocalization) private var localization diff --git a/swift-app/Sources/Popskill/Views/SettingsView.swift b/swift-app/Sources/Popskill/Views/SettingsView.swift index f7c97af..7bf905a 100644 --- a/swift-app/Sources/Popskill/Views/SettingsView.swift +++ b/swift-app/Sources/Popskill/Views/SettingsView.swift @@ -6,6 +6,7 @@ import SwiftUI /// The non-Git providers are placeholder buttons (sidecar gates them). /// 3. 数据源管理 — count + jump to Sources view. /// 4. 重新引导 — re-run the 5-step Onboarding (S6) explicitly. +@MainActor struct SettingsView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization diff --git a/swift-app/Sources/Popskill/Views/SourcesView.swift b/swift-app/Sources/Popskill/Views/SourcesView.swift index e38885b..084a920 100644 --- a/swift-app/Sources/Popskill/Views/SourcesView.swift +++ b/swift-app/Sources/Popskill/Views/SourcesView.swift @@ -4,6 +4,7 @@ import SwiftUI /// enable/disable + remove + a small `addOpen` popover for adding a new /// `owner/name@branch`. Full multi-type wizard (npm / brew / folder / zip) is /// scheduled for v0.4 and surfaces as the disabled buttons here. +@MainActor struct SourcesView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization @@ -288,6 +289,7 @@ struct AddSourceInput: Equatable { } } +@MainActor private struct AddSourcePopover: View { @Bindable var store: PopskillStore @Binding var isPresented: Bool diff --git a/swift-app/Sources/Popskill/Views/SpotlightView.swift b/swift-app/Sources/Popskill/Views/SpotlightView.swift index f3a18c0..1c7c717 100644 --- a/swift-app/Sources/Popskill/Views/SpotlightView.swift +++ b/swift-app/Sources/Popskill/Views/SpotlightView.swift @@ -9,6 +9,7 @@ import SwiftUI /// Keyboard: ↑↓ moves highlight, Enter triggers the highlighted row, Esc /// closes. Clicking the scrim closes too. Pressing ⌘1 / ⌘2 on a skill row /// toggles Claude / Codex without leaving the palette. +@MainActor struct SpotlightView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization diff --git a/swift-app/Sources/Popskill/Views/UpdatesView.swift b/swift-app/Sources/Popskill/Views/UpdatesView.swift index c8a3138..7081bbd 100644 --- a/swift-app/Sources/Popskill/Views/UpdatesView.swift +++ b/swift-app/Sources/Popskill/Views/UpdatesView.swift @@ -3,6 +3,7 @@ import SwiftUI /// Updates — lists every skill whose remote content hash differs from local. /// On `.task` we re-run `client.checkUpdates()` so the badge in the sidebar /// reflects the latest scan; the user can also trigger a manual re-scan. +@MainActor struct UpdatesView: View { @Bindable var store: PopskillStore @Environment(\.popskillLocalization) private var localization diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index efcc9f3..707cf5e 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -884,6 +884,67 @@ struct SkillModelsTests { #expect(snapshot?.componentStats.first?.totalTokens == 62) } + @Test + func matrixUsageIndexCachesSkillPackageAndComponentUsage() { + let skill = installedSkill(directory: "baoyu-comic") + let package = self.package(components: [ + component(id: "baoyu-comic", installed: true) + ]) + let summary = UsageSummary( + filesScanned: 1, + sessions: 1, + usageEvents: 3, + inputTokens: 50, + outputTokens: 30, + cacheCreationTokens: 0, + cacheReadTokens: 0, + attributedSkillUsageEvents: 2, + modelStats: [], + skillStats: [ + SkillUsageStat( + skillID: "baoyu-comic", + sourcePlugin: nil, + usageEvents: 2, + inputTokens: 20, + outputTokens: 10, + cacheCreationTokens: 0, + cacheReadTokens: 5, + lastUsedAt: nil + ), + SkillUsageStat( + skillID: "unrelated", + sourcePlugin: nil, + usageEvents: 1, + inputTokens: 99, + outputTokens: 99, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: nil + ) + ], + recentSessions: [] + ) + + let index = MatrixUsageIndex(summary: summary, skills: [skill], packages: [package]) + let emptyIndex = MatrixUsageIndex(summary: nil, skills: [skill], packages: [package]) + + #expect(index.hasSummary) + #expect(index.skillSnapshot(for: "baoyu-comic")?.usageEvents == 2) + #expect(index.skillSnapshot(for: "baoyu-comic")?.totalTokens == 35) + #expect(index.packageSnapshot(for: "pkg:demo")?.usageEvents == 2) + #expect(index.packageComponentStat(packageID: "pkg:demo", componentID: "baoyu-comic")?.totalTokens == 35) + #expect(index.skillSnapshot(for: "missing")?.hasUsage == false) + #expect(!emptyIndex.hasSummary) + #expect(emptyIndex.skillSnapshot(for: "baoyu-comic") == nil) + } + + @Test + func usageDisplayFormatterCompactsMatrixMetrics() { + #expect(UsageDisplayFormatter.compactTokens(999) == "999") + #expect(UsageDisplayFormatter.compactTokens(1_500) == "1.5K") + #expect(UsageDisplayFormatter.compactCount(2_500_000) == "2.5M") + } + @Test func capabilityPackageHealthSeparatesActivePartialBlockedAndInactive() { #expect(package(components: []).health == .inactive) From f68966046b6f95db09c65ba105484d0b17255918 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 02:31:29 +0800 Subject: [PATCH 10/47] ci: avoid duplicate branch builds --- .github/workflows/ci.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f8faf9..2be672f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,12 +2,19 @@ name: CI on: push: + branches: [main] pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: macos: name: macOS Build runs-on: macos-14 + timeout-minutes: 30 steps: - name: Checkout @@ -16,13 +23,17 @@ jobs: submodules: recursive - name: Install Rust + timeout-minutes: 5 run: rustup toolchain install stable --profile minimal - name: Build sidecar - run: cargo build --manifest-path skill-cli/Cargo.toml + timeout-minutes: 10 + run: cargo build --locked --manifest-path skill-cli/Cargo.toml - name: Build SwiftUI app + timeout-minutes: 10 run: swift build --package-path swift-app - name: Run Swift tests + timeout-minutes: 10 run: swift test --package-path swift-app From aeb4143d43ecfe71cb2a97ada012673774ec9cbb Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 02:38:46 +0800 Subject: [PATCH 11/47] ci: select Xcode 16 for Swift tests --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2be672f..964b9ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,14 @@ jobs: with: submodules: recursive + - name: Select Xcode 16 + timeout-minutes: 2 + run: | + XCODE_PATH="$(ls -d /Applications/Xcode_16*.app 2>/dev/null | sort | tail -n 1)" + test -n "$XCODE_PATH" + sudo xcode-select -s "$XCODE_PATH" + swift --version + - name: Install Rust timeout-minutes: 5 run: rustup toolchain install stable --profile minimal From de1c96ea89a8bd9331950bc126aac96f8d1ce4ed Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 02:44:35 +0800 Subject: [PATCH 12/47] fix(ui): stabilize CJK skill search matching --- .../Popskill/Models/SkillSearchScorer.swift | 56 +++++++++++++++---- .../SkillSearchScorerTests.swift | 5 +- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/SkillSearchScorer.swift b/swift-app/Sources/Popskill/Models/SkillSearchScorer.swift index ebd8fcf..6bb3e2f 100644 --- a/swift-app/Sources/Popskill/Models/SkillSearchScorer.swift +++ b/swift-app/Sources/Popskill/Models/SkillSearchScorer.swift @@ -51,14 +51,14 @@ enum SkillSearchScorer { } else if name.hasPrefix(q) { score += 500 matchedOnName = true - } else if name.contains(q) { + } else if matches(name, query: q) { score += 200 matchedOnName = true } for trigger in triggers { let lowerTrigger = trigger.lowercased() - guard lowerTrigger.contains(q), !seenTriggers.contains(lowerTrigger) else { + guard matches(lowerTrigger, query: q), !seenTriggers.contains(lowerTrigger) else { continue } seenTriggers.insert(lowerTrigger) @@ -66,19 +66,19 @@ enum SkillSearchScorer { matchedTriggers.append(trigger) } - if !summary.isEmpty, summary.contains(q) { + if !summary.isEmpty, matches(summary, query: q) { score += 50 } - if description.contains(q) { + if matches(description, query: q) { score += 20 } - if source.contains(q) { + if matches(source, query: q) { score += 10 } - if directory.contains(q) { + if matches(directory, query: q) { score += 5 } @@ -116,29 +116,29 @@ enum SkillSearchScorer { } else if name.hasPrefix(q) { score += 500 matchedOnName = true - } else if name.contains(q) { + } else if matches(name, query: q) { score += 200 matchedOnName = true } for trigger in triggers { let lower = trigger.lowercased() - guard lower.contains(q), !seenTriggers.contains(lower) else { continue } + guard matches(lower, query: q), !seenTriggers.contains(lower) else { continue } seenTriggers.insert(lower) score += 100 matchedTriggers.append(trigger) } - if !summary.isEmpty, summary.contains(q) { + if !summary.isEmpty, matches(summary, query: q) { score += 50 } - if description.contains(q) { + if matches(description, query: q) { score += 20 } - if category.contains(q) { + if matches(category, query: q) { score += 10 } - if fileName.contains(q) { + if matches(fileName, query: q) { score += 5 } @@ -150,4 +150,36 @@ enum SkillSearchScorer { matchedOnName: matchedOnName ) } + + private static func matches(_ text: String, query: String) -> Bool { + guard !query.isEmpty else { return false } + if text.contains(query) { + return true + } + guard containsCJKScalar(query), containsCJKScalar(text) else { + return false + } + return text.containsCharactersInOrder(query) + } + + private static func containsCJKScalar(_ value: String) -> Bool { + value.unicodeScalars.contains { scalar in + (0x4E00...0x9FFF).contains(scalar.value) + || (0x3400...0x4DBF).contains(scalar.value) + || (0xF900...0xFAFF).contains(scalar.value) + } + } +} + +private extension String { + func containsCharactersInOrder(_ query: String) -> Bool { + var searchStart = startIndex + for character in query { + guard let match = self[searchStart...].firstIndex(of: character) else { + return false + } + searchStart = index(after: match) + } + return true + } } diff --git a/swift-app/Tests/PopskillTests/SkillSearchScorerTests.swift b/swift-app/Tests/PopskillTests/SkillSearchScorerTests.swift index ab37672..aff591d 100644 --- a/swift-app/Tests/PopskillTests/SkillSearchScorerTests.swift +++ b/swift-app/Tests/PopskillTests/SkillSearchScorerTests.swift @@ -89,8 +89,9 @@ struct SkillSearchScorerTests { ), query: "画图" ) - #expect((hit?.matchedTriggers ?? []).contains("画图")) - #expect((hit?.matchedTriggers ?? []).contains("画个图")) + let matchedTriggers = hit?.matchedTriggers ?? [] + #expect(matchedTriggers.contains("画图") == true) + #expect(matchedTriggers.contains("画个图") == true) } @Test From 4317b355960b02c9faa6a067927a9aa2d0ac5a56 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 03:01:48 +0800 Subject: [PATCH 13/47] feat(ui): densify capability matrix --- docs/screenshots/matrix.jpg | Bin 362285 -> 228174 bytes .../Popskill/Components/CommonViews.swift | 5 +- .../Popskill/Design/PopskillColors.swift | 8 +- .../Resources/en.lproj/Localizable.strings | 7 + .../zh-Hans.lproj/Localizable.strings | 7 + .../Popskill/Views/MatrixPackageRow.swift | 32 ++-- .../Sources/Popskill/Views/MatrixRow.swift | 19 +-- .../Sources/Popskill/Views/MatrixView.swift | 154 +++++++++++++++--- .../Popskill/Views/SpotlightView.swift | 3 +- 9 files changed, 175 insertions(+), 60 deletions(-) diff --git a/docs/screenshots/matrix.jpg b/docs/screenshots/matrix.jpg index bf87ae3c21d0ef598660a4ff2e7dfc16bd9ebf0e..7c2db92a1cf1bcd701f4f2346bd03c3c1c474de1 100644 GIT binary patch literal 228174 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 B7iIte literal 362285 zcmeEu1z48Lw(v`LNtd*gbSf>~9n#X>jigFThqNFd9g-r5bVx``hm>@8{4cuq7Cn3K zd+xc<{hxd9b6yy}nR(Z&Sv70ctZyE!K3~lO=u%>mVgLjL07!s8z||ZeDC%Zz0sykI z0384T@Bl=hIRFjTKoEccEC2xc67u&R1pSv#zt>?VuU}qm0Ne^@wobN=X0~>uY>dnR z_X9~;=xYq%^Q}(xt%^QG95xmKyn+7ei|}Io_<{G;JOhoSu&|zjlDwFtjOaI_81Nq1 z*;zp|1Aw)SlY^4@15%BLnxt^601SWuVnGB@9vM2?2`MPZT;u%x^Bev@{v3{cqa7Gz zxW<+ADVjdVHEF({Ge!0;1{u~LV1HggH!^lK1eq5BUz-`)IXHp!La?sj>ST9aKL_hr z4j@sWSkSNQrr+z7*L8#Mb?$F4DoVm23`i#=vcb1Hw8nMa^jjU)=(_G=XbR$lw*vrJ zCPTAFU?1vBaO_=SH!)JM-VFf6QX(P>%3v2B*pFc4Y;6wKzs+TB?DPn%U(Z!zZEXvV zy}J$8X{@cBLHfTv*BV4NwowJg1K08*0v-UyfDNDskOH!R5Wos>0nC6ZKm)cO019B6 z87!<|ixtoV*Z>yrhXMR#1-QYsIQYp9&;pErIbaAF{>41(-{uhpYymsK9dH0)O#w5& z37k;~?6Cu^Knh3!NstOd5Yt_-{(V-?YdwQw!3WeIaCHR$gMvT$d}#oH_a*pJ??<1U zF#wd!0RURck3Jen0Ko7DfMzj6X9t&SY~KwTKmu?9a*&&QfCwNDXaOdGJ@6O^0%CwP zARnj%J^Ff0w)mA5aV*K`}t- zK!rm!LLEZig*JjtgC2vyfl-BthxrUk1Zx1B2fGKy4;KnI4o?s74L^uLi{OVaiO7u@ zk9dUi5UC!S0yz}<5XAuH6RHquIT|%u8agg|GzJ<*_${heYYb(!{5?^GS3_F-g0~o{|eu02Gas�vDVBMLaE~4?G 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
w5(f4b-u-RbzRU85l&3)W67G&5yNo2orrG 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 diff --git a/swift-app/Sources/Popskill/Components/CommonViews.swift b/swift-app/Sources/Popskill/Components/CommonViews.swift index a71404e..164d13e 100644 --- a/swift-app/Sources/Popskill/Components/CommonViews.swift +++ b/swift-app/Sources/Popskill/Components/CommonViews.swift @@ -166,6 +166,7 @@ struct SummaryMetric: View { struct InitialAvatarView: View { let name: String let identifier: String + var size: CGFloat = 44 private var initial: String { name.trimmingCharacters(in: .whitespacesAndNewlines).first.map { String($0).uppercased() } ?? "S" @@ -193,10 +194,10 @@ struct InitialAvatarView: View { RoundedRectangle(cornerRadius: PopskillRadius.smallCard) .fill(color.gradient) Text(initial) - .font(.system(size: 20, weight: .bold)) + .font(.system(size: max(10, size * 0.45), weight: .bold)) .foregroundStyle(.white) } - .frame(width: 44, height: 44) + .frame(width: size, height: size) } } diff --git a/swift-app/Sources/Popskill/Design/PopskillColors.swift b/swift-app/Sources/Popskill/Design/PopskillColors.swift index f0cf7ae..4fd935e 100644 --- a/swift-app/Sources/Popskill/Design/PopskillColors.swift +++ b/swift-app/Sources/Popskill/Design/PopskillColors.swift @@ -59,10 +59,10 @@ enum PopskillSpacing { } enum PopskillRadius { - static let button: CGFloat = 8 - static let smallCard: CGFloat = 12 - static let card: CGFloat = 18 - static let largeCard: CGFloat = 20 + static let button: CGFloat = 6 + static let smallCard: CGFloat = 8 + static let card: CGFloat = 8 + static let largeCard: CGFloat = 12 } enum PopskillShadow { diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 5b12aa5..343f34f 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -308,6 +308,13 @@ // S3 新增 — Matrix + Inspector "matrix.subtitle" = "%d bundles · %d capabilities · %d active toggles"; "matrix.search.placeholder" = "Filter by name, trigger, or directory"; +"matrix.metric.capabilities" = "Capabilities"; +"matrix.metric.claudeActive" = "Claude active"; +"matrix.metric.codexActive" = "Codex active"; +"matrix.metric.stubs" = "Stubs"; +"matrix.metric.brokenLinks" = "Broken links"; +"matrix.metric.tokenUsage" = "Month tokens"; +"matrix.sort.typeDescending" = "Sort: type"; "matrix.col.capability" = "Capability"; "matrix.col.source" = "Source"; "matrix.col.tokens" = "Tokens"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index fe07427..ea056e9 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -308,6 +308,13 @@ // S3 新增 — 矩阵主视图 + Inspector "matrix.subtitle" = "%d 个套装 · %d 项能力 · %d 个已启用开关"; "matrix.search.placeholder" = "按名称、触发词或目录过滤"; +"matrix.metric.capabilities" = "能力总数"; +"matrix.metric.claudeActive" = "Claude 已激活"; +"matrix.metric.codexActive" = "Codex 已激活"; +"matrix.metric.stubs" = "占位 (stub)"; +"matrix.metric.brokenLinks" = "断链"; +"matrix.metric.tokenUsage" = "本月 token"; +"matrix.sort.typeDescending" = "排序:类型"; "matrix.col.capability" = "能力"; "matrix.col.source" = "来源"; "matrix.col.tokens" = "Tokens"; diff --git a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift index 24a67ca..9f9328d 100644 --- a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift @@ -46,7 +46,7 @@ struct MatrixPackageRow: View { capabilityCell .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 14) - .padding(.vertical, 9) + .padding(.vertical, 7) coverageCell(for: .claude) .frame(width: MatrixTableLayout.appColumnWidth) @@ -102,12 +102,12 @@ struct MatrixPackageRow: View { .buttonStyle(.plain) .help(localization.string(isCollapsed ? "matrix.package.expand" : "matrix.package.collapse")) - PackageAvatar(name: capability.name, identifier: capability.id, size: 30) + PackageAvatar(name: capability.name, identifier: capability.id, size: 26) VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Text(capability.name) - .font(.system(size: 13.5, weight: .semibold)) + .font(.system(size: 12.8, weight: .semibold)) .foregroundStyle(Color.popLabel) .lineLimit(1) kindBadge @@ -124,7 +124,7 @@ struct MatrixPackageRow: View { } } Text(packageSubtitle) - .font(.system(size: 11.5)) + .font(.system(size: 11.2)) .foregroundStyle(Color.popSecondaryLabel) .lineLimit(1) } @@ -170,9 +170,9 @@ struct MatrixPackageRow: View { Spacer(minLength: 0) HStack(spacing: 4) { Image(systemName: app.symbolName) - .font(.system(size: 10, weight: .semibold)) + .font(.system(size: 9.5, weight: .semibold)) Text(coverage.label) - .font(.system(size: 11, weight: .semibold).monospacedDigit()) + .font(.system(size: 10.8, weight: .semibold).monospacedDigit()) } .foregroundStyle(coverage.enabled > 0 ? app.bundleAccentColor : Color.popTertiaryLabel) .padding(.horizontal, 7) @@ -195,10 +195,10 @@ struct MatrixPackageRow: View { private var sourceCell: some View { HStack(spacing: 6) { Image(systemName: "shippingbox") - .font(.system(size: 11, weight: .semibold)) + .font(.system(size: 10.5, weight: .semibold)) .foregroundStyle(Color.popSecondaryLabel) Text(capability.sourceLabel) - .font(.system(size: 11.5)) + .font(.system(size: 11)) .foregroundStyle(Color.popSecondaryLabel) .lineLimit(1) .truncationMode(.middle) @@ -304,8 +304,8 @@ private struct MatrixPackageComponentRow: View { HStack(spacing: 0) { componentCell .frame(maxWidth: .infinity, alignment: .leading) - .padding(.leading, 56) - .padding(.vertical, 6) + .padding(.leading, 44) + .padding(.vertical, 5) appStateCell(for: .claude) .frame(width: MatrixTableLayout.appColumnWidth) @@ -329,25 +329,25 @@ private struct MatrixPackageComponentRow: View { store.selectSkill(skill.id) } } - .background(Color.popCardBackground.opacity(0.20)) + .background(Color.popCardBackground.opacity(0.14)) } private var componentCell: some View { HStack(spacing: 9) { Text(componentTreePrefix) - .font(.system(size: 12, weight: .medium, design: .monospaced)) + .font(.system(size: 11.5, weight: .medium, design: .monospaced)) .foregroundStyle(Color.popTertiaryLabel) .frame(width: 18, alignment: .leading) Image(systemName: component.kindSymbol) - .font(.system(size: 11, weight: .semibold)) + .font(.system(size: 10.5, weight: .semibold)) .foregroundStyle(component.installed ? Color.popSecondaryLabel : Color.popTertiaryLabel) .frame(width: 14) VStack(alignment: .leading, spacing: 1) { HStack(spacing: 6) { Text(component.name) - .font(.system(size: 12.5, weight: .medium)) + .font(.system(size: 12, weight: .medium)) .foregroundStyle(Color.popLabel) .lineLimit(1) Text(component.kind.uppercased()) @@ -363,7 +363,7 @@ private struct MatrixPackageComponentRow: View { } } Text(component.status) - .font(.system(size: 10.5)) + .font(.system(size: 10.2)) .foregroundStyle(Color.popSecondaryLabel) .lineLimit(1) } @@ -399,7 +399,7 @@ private struct MatrixPackageComponentRow: View { private var sourceCell: some View { Text(component.location ?? component.id) - .font(.system(size: 11, design: .monospaced)) + .font(.system(size: 10.5, design: .monospaced)) .foregroundStyle(Color.popSecondaryLabel) .lineLimit(1) .truncationMode(.middle) diff --git a/swift-app/Sources/Popskill/Views/MatrixRow.swift b/swift-app/Sources/Popskill/Views/MatrixRow.swift index a552a50..461014f 100644 --- a/swift-app/Sources/Popskill/Views/MatrixRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixRow.swift @@ -28,7 +28,7 @@ struct MatrixRow: View { capabilityCell .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 14) - .padding(.vertical, 8) + .padding(.vertical, 6) appToggleCell(for: .claude) .frame(width: MatrixTableLayout.appColumnWidth) @@ -73,12 +73,11 @@ struct MatrixRow: View { private var capabilityCell: some View { HStack(alignment: .center, spacing: 10) { - InitialAvatarView(name: capability.name, identifier: capability.id) - .frame(width: 28, height: 28) + InitialAvatarView(name: capability.name, identifier: capability.id, size: 24) VStack(alignment: .leading, spacing: 1) { HStack(spacing: 6) { Text(capability.name) - .font(.system(size: 13, weight: .semibold)) + .font(.system(size: 12.5, weight: .semibold)) .foregroundStyle(Color.popLabel) .lineLimit(1) kindBadge @@ -135,7 +134,7 @@ struct MatrixRow: View { onChange: { newValue in Task { await toggle(app: app, enabled: newValue) } }, - size: 26 + size: 22 ) } else { readOnlyAppBadge(app: app, isOn: capability.apps.isEnabled(app)) @@ -188,10 +187,10 @@ struct MatrixRow: View { private var sourceCell: some View { HStack(spacing: 6) { Image(systemName: sourceSymbol) - .font(.system(size: 11, weight: .semibold)) + .font(.system(size: 10.5, weight: .semibold)) .foregroundStyle(Color.popSecondaryLabel) Text(capability.sourceLabel) - .font(.system(size: 11.5)) + .font(.system(size: 11)) .foregroundStyle(Color.popSecondaryLabel) .lineLimit(1) .truncationMode(.middle) @@ -289,7 +288,7 @@ struct MatrixUsageValueCell: View { var body: some View { Text(value ?? "—") - .font(.system(size: isSubtle ? 10.8 : 11.5, weight: isSubtle ? .regular : .medium).monospacedDigit()) + .font(.system(size: isSubtle ? 10.5 : 11, weight: isSubtle ? .regular : .medium).monospacedDigit()) .foregroundStyle(value == nil ? Color.popTertiaryLabel : (isSubtle ? Color.popSecondaryLabel : Color.popLabel)) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .trailing) @@ -346,8 +345,8 @@ struct MatrixGroupHeader: View { coverageChip(symbol: "chevron.left.forwardslash.chevron.right", label: "Codex", enabled: codexOn, total: group.capabilities.count) } .padding(.horizontal, 14) - .padding(.vertical, 7) - .background(Color.popSurface.opacity(0.52)) + .padding(.vertical, 6) + .background(Color.popSurface.opacity(0.36)) .overlay(alignment: .bottom) { Rectangle() .fill(Color.popSeparator) diff --git a/swift-app/Sources/Popskill/Views/MatrixView.swift b/swift-app/Sources/Popskill/Views/MatrixView.swift index cbb719a..b3d437d 100644 --- a/swift-app/Sources/Popskill/Views/MatrixView.swift +++ b/swift-app/Sources/Popskill/Views/MatrixView.swift @@ -60,7 +60,7 @@ struct MatrixView: View { // MARK: Header private func header(capabilities: [MatrixCapability]) -> some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 14) { HStack(alignment: .firstTextBaseline, spacing: 16) { VStack(alignment: .leading, spacing: 4) { LocalizedText("sidebar.matrix") @@ -73,11 +73,19 @@ struct MatrixView: View { Spacer(minLength: 16) searchField } - filterChips + metricStrip(capabilities: capabilities) + HStack(spacing: 10) { + filterChips + Spacer(minLength: 8) + Label(localization.string("matrix.sort.typeDescending"), systemImage: "arrow.down") + .font(.system(size: 11.5, weight: .medium)) + .foregroundStyle(Color.popSecondaryLabel) + .labelStyle(.titleAndIcon) + } } .padding(.horizontal, 28) - .padding(.top, 24) - .padding(.bottom, 14) + .padding(.top, 22) + .padding(.bottom, 12) } private func subtitle(capabilities: [MatrixCapability]) -> String { @@ -119,6 +127,65 @@ struct MatrixView: View { .accessibilityLabel(Text(localization.string("matrix.search.placeholder"))) } + private func metricStrip(capabilities: [MatrixCapability]) -> some View { + let metrics = summaryMetrics(capabilities: capabilities) + return HStack(spacing: 0) { + ForEach(Array(metrics.enumerated()), id: \.element.id) { index, metric in + MatrixSummaryMetricView(metric: metric) + .frame(minWidth: metric.preferredWidth, alignment: .leading) + if index < metrics.count - 1 { + Rectangle() + .fill(Color.popSeparator.opacity(0.65)) + .frame(width: 0.5, height: 32) + .padding(.horizontal, 16) + } + } + Spacer(minLength: 0) + } + } + + private func summaryMetrics(capabilities: [MatrixCapability]) -> [MatrixSummaryMetric] { + [ + MatrixSummaryMetric( + id: "capabilities", + value: "\(capabilities.count)", + title: localization.string("matrix.metric.capabilities"), + tint: .popLabel + ), + MatrixSummaryMetric( + id: "claude", + value: "\(capabilities.filter { $0.apps.claude }.count)", + title: localization.string("matrix.metric.claudeActive"), + tint: .popLabel + ), + MatrixSummaryMetric( + id: "codex", + value: "\(capabilities.filter { $0.apps.codex }.count)", + title: localization.string("matrix.metric.codexActive"), + tint: .popLabel + ), + MatrixSummaryMetric( + id: "stubs", + value: "\(store.stubs.count)", + title: localization.string("matrix.metric.stubs"), + tint: store.stubs.isEmpty ? .popSecondaryLabel : .popStatusWarning + ), + MatrixSummaryMetric( + id: "broken-links", + value: "\(store.brokenLinkCount)", + title: localization.string("matrix.metric.brokenLinks"), + tint: store.brokenLinkCount > 0 ? .popStatusError : .popSecondaryLabel + ), + MatrixSummaryMetric( + id: "tokens", + value: store.usageSummary.map { UsageDisplayFormatter.compactTokens($0.totalTokens) } ?? "—", + title: localization.string("matrix.metric.tokenUsage"), + tint: .popLabel, + preferredWidth: 116 + ) + ] + } + private var filterChips: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 6) { @@ -131,6 +198,7 @@ struct MatrixView: View { } } } + .frame(maxWidth: .infinity, alignment: .leading) } private func chipButton(filter: MatrixFilter) -> some View { @@ -145,15 +213,18 @@ struct MatrixView: View { .font(.system(size: 10.5, weight: .semibold).monospacedDigit()) } } - .font(.system(size: 12, weight: active ? .semibold : .regular)) - .foregroundStyle(active ? Color.accentColor : Color.popSecondaryLabel) - .padding(.horizontal, 10) + .font(.system(size: 11.5, weight: active ? .semibold : .regular)) + .foregroundStyle(active ? Color.popCardBackground : Color.popLabel) + .padding(.horizontal, 9) .padding(.vertical, 4) .background( - active ? Color.popAccentSoft : Color.popControlFill, - in: Capsule() + active ? Color.popLabel : Color.popControlFill, + in: RoundedRectangle(cornerRadius: 5, style: .continuous) + ) + .overlay( + RoundedRectangle(cornerRadius: 5, style: .continuous) + .strokeBorder(active ? Color.popLabel.opacity(0.10) : Color.popControlStroke, lineWidth: 0.7) ) - .overlay(Capsule().strokeBorder(active ? Color.accentColor.opacity(0.30) : Color.popControlStroke, lineWidth: 0.7)) } .buttonStyle(.plain) .accessibilityAddTraits(active ? .isSelected : []) @@ -165,15 +236,18 @@ struct MatrixView: View { store.matrixTypeFilter = filter } label: { Text(localization.string(filter.titleKey)) - .font(.system(size: 12, weight: active ? .semibold : .regular)) - .foregroundStyle(active ? Color.accentColor : Color.popSecondaryLabel) - .padding(.horizontal, 10) + .font(.system(size: 11.5, weight: active ? .semibold : .regular)) + .foregroundStyle(active ? Color.popCardBackground : Color.popLabel) + .padding(.horizontal, 9) .padding(.vertical, 4) .background( - active ? Color.popAccentSoft : Color.popControlFill, - in: Capsule() + active ? Color.popLabel : Color.popControlFill, + in: RoundedRectangle(cornerRadius: 5, style: .continuous) + ) + .overlay( + RoundedRectangle(cornerRadius: 5, style: .continuous) + .strokeBorder(active ? Color.popLabel.opacity(0.10) : Color.popControlStroke, lineWidth: 0.7) ) - .overlay(Capsule().strokeBorder(active ? Color.accentColor.opacity(0.30) : Color.popControlStroke, lineWidth: 0.7)) } .buttonStyle(.plain) .accessibilityAddTraits(active ? .isSelected : []) @@ -210,14 +284,12 @@ struct MatrixView: View { Color.clear.frame(height: 24) } } - .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) - .background(Color.popSurfaceElevated.opacity(0.18), in: RoundedRectangle(cornerRadius: 12, style: .continuous)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .background(Color.popSurfaceElevated.opacity(0.42), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .strokeBorder(Color.popBorder, lineWidth: 0.7) + RoundedRectangle(cornerRadius: 8, style: .continuous) + .strokeBorder(Color.popBorder.opacity(0.82), lineWidth: 0.7) ) - .shadow(color: .black.opacity(0.035), radius: 12, x: 0, y: 3) .padding(.horizontal, 16) .padding(.bottom, 16) } @@ -262,10 +334,11 @@ struct MatrixView: View { .frame(width: MatrixTableLayout.callsColumnWidth, alignment: .trailing) Spacer().frame(width: MatrixTableLayout.actionColumnWidth) } - .font(.system(size: 11.5, weight: .medium)) - .foregroundStyle(Color.popSecondaryLabel) - .padding(.vertical, 8) - .background(Color.popTableHeaderFill) + .font(.system(size: 10.5, weight: .semibold)) + .foregroundStyle(Color.popTertiaryLabel) + .textCase(.uppercase) + .padding(.vertical, 7) + .background(Color.popTableHeaderFill.opacity(0.70)) .overlay(alignment: .bottom) { Rectangle() .fill(Color.popSeparator) @@ -361,6 +434,35 @@ struct MatrixView: View { } } +private struct MatrixSummaryMetric: Identifiable { + let id: String + let value: String + let title: String + let tint: Color + var preferredWidth: CGFloat = 92 +} + +private struct MatrixSummaryMetricView: View { + let metric: MatrixSummaryMetric + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(metric.value) + .font(.system(size: 22, weight: .bold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(metric.tint) + .lineLimit(1) + .minimumScaleFactor(0.82) + Text(metric.title) + .font(.system(size: 10.5, weight: .semibold)) + .foregroundStyle(Color.popSecondaryLabel) + .textCase(.uppercase) + .lineLimit(1) + } + .accessibilityElement(children: .combine) + } +} + // MARK: - Filters enum MatrixFilter: String, CaseIterable, Identifiable { diff --git a/swift-app/Sources/Popskill/Views/SpotlightView.swift b/swift-app/Sources/Popskill/Views/SpotlightView.swift index 1c7c717..fe19b32 100644 --- a/swift-app/Sources/Popskill/Views/SpotlightView.swift +++ b/swift-app/Sources/Popskill/Views/SpotlightView.swift @@ -215,8 +215,7 @@ struct SpotlightView: View { case let .package(package, _): PackageAvatar(name: package.name, identifier: package.id, size: 24) case let .skill(skill, _): - InitialAvatarView(name: skill.name, identifier: skill.id) - .frame(width: 24, height: 24) + InitialAvatarView(name: skill.name, identifier: skill.id, size: 24) case let .action(action): Image(systemName: action.symbol) .font(.system(size: 13, weight: .semibold)) From 9da655b2d7b0af3cf257ae09257999cc34581353 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 03:14:11 +0800 Subject: [PATCH 14/47] fix(ui): load persisted stubs on launch --- .../Sources/Popskill/App/PopskillStore.swift | 30 +++++++- .../Sources/Popskill/Views/IdleView.swift | 2 +- .../PopskillTests/PopskillStoreTests.swift | 76 +++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/swift-app/Sources/Popskill/App/PopskillStore.swift b/swift-app/Sources/Popskill/App/PopskillStore.swift index 44a1292..42d9a42 100644 --- a/swift-app/Sources/Popskill/App/PopskillStore.swift +++ b/swift-app/Sources/Popskill/App/PopskillStore.swift @@ -86,14 +86,16 @@ final class PopskillStore { var errorMessage: String? // ===== Services ===== - let client = SkillCLIClient() + let client: SkillCLIClient // Per-skill toggle / uninstall in-flight tracking so the matrix can dim // controls during pending IO. var pendingToggles: Set = [] var pendingUninstalls: Set = [] - init() {} + init(client: SkillCLIClient = SkillCLIClient()) { + self.client = client + } // ===== Bootstrap ===== @@ -110,6 +112,7 @@ final class PopskillStore { async let sourcesTask = client.listRepositories() async let agentsTask = client.listAgents() async let packagesTask = loadPackagesBestEffort() + async let stubsTask = loadStubsBestEffort() do { let now = Date() @@ -117,6 +120,7 @@ final class PopskillStore { self.sources = try await sourcesTask self.localAgents = try await agentsTask self.packages = await packagesTask + self.stubs = await stubsTask self.lastBootstrapAt = now // Bootstrap counts as a fresh sources fetch — secondary views // that .task into refreshSources won't double-pull immediately. @@ -134,6 +138,14 @@ final class PopskillStore { } } + private func loadStubsBestEffort() async -> [StubbedSkill] { + do { + return try await client.listStubs() + } catch { + return [] + } + } + // MARK: Cached refresh helpers // // Each secondary view (Sources / Updates / Backups) hits sidecar on its @@ -178,6 +190,20 @@ final class PopskillStore { } } + func refreshStubs() async { + do { + stubs = try await client.listStubs() + } catch { + errorMessage = error.localizedDescription + } + } + + func upsertStub(_ stub: StubbedSkill) { + stubs.removeAll { $0.skill.id == stub.skill.id } + stubs.append(stub) + stubs.sort { $0.stubbedAt > $1.stubbedAt } + } + private func shouldSkipRefresh(_ lastRefresh: Date?, force: Bool) -> Bool { guard !force, let lastRefresh else { return false } return Date().timeIntervalSince(lastRefresh) < Self.refreshTTL diff --git a/swift-app/Sources/Popskill/Views/IdleView.swift b/swift-app/Sources/Popskill/Views/IdleView.swift index d94ffd5..984cdfe 100644 --- a/swift-app/Sources/Popskill/Views/IdleView.swift +++ b/swift-app/Sources/Popskill/Views/IdleView.swift @@ -144,7 +144,7 @@ struct IdleView: View { do { let stub = try await store.client.stub(skillID: skill.id) - store.stubs.append(stub) + store.upsertStub(stub) store.skills.removeAll { $0.id == skill.id } } catch { store.errorMessage = error.localizedDescription diff --git a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift index 1137351..5ce4dfc 100644 --- a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift +++ b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift @@ -1,4 +1,5 @@ @testable import Popskill +import Foundation import Testing @MainActor @@ -106,4 +107,79 @@ struct PopskillStoreTests { #expect(store.capabilities.map(\.kind) == [.bundle, .skill]) #expect(store.capabilities.first?.appCoverage[.claude]?.label == "1/1") } + + @Test + func bootstrapLoadsPersistedStubsFromSidecar() async throws { + let client = try fakeClient(stubbedAt: 123) + let store = PopskillStore(client: client) + + await store.bootstrap() + + #expect(store.stubs.map(\.id) == ["demo-stub"]) + #expect(store.stubs.first?.backupId == "backup-demo-stub") + #expect(store.errorMessage == nil) + } + + @Test + func upsertStubReplacesExistingSkillAndSortsNewestFirst() { + let store = PopskillStore() + + store.upsertStub(stubFixture(id: "older", stubbedAt: 10)) + store.upsertStub(stubFixture(id: "newer", stubbedAt: 30)) + store.upsertStub(stubFixture(id: "older", stubbedAt: 40)) + + #expect(store.stubs.map(\.id) == ["older", "newer"]) + #expect(store.stubs.map(\.stubbedAt) == [40, 30]) + } + + private func fakeClient(stubbedAt: Int) throws -> SkillCLIClient { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("PopskillStoreTests-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + let executable = root.appendingPathComponent("fake-skill-cli") + let script = """ + #!/bin/sh + case "$1" in + list|repo-list|agent-list|package-list) + printf '{"ok":true,"data":[]}' + ;; + stub-list) + cat <<'JSON' + {"ok":true,"data":[{"skill":{"id":"demo-stub","name":"Demo Stub","description":"Persisted stub","directory":"demo-stub","repo_owner":null,"repo_name":null,"readme_url":null,"apps":{"claude":false,"codex":false,"gemini":false,"opencode":false,"hermes":false},"installed_at":null,"updated_at":null,"content_hash":null},"backup_id":"backup-demo-stub","backup_path":"/tmp/backup-demo-stub","stubbed_at":\(stubbedAt)}]} + JSON + ;; + *) + printf '{"ok":false,"error":{"code":"UNKNOWN","message":"unexpected command"}}' + exit 1 + ;; + esac + """ + try script.write(to: executable, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes( + [.posixPermissions: NSNumber(value: Int16(0o755))], + ofItemAtPath: executable.path + ) + return SkillCLIClient(executableURL: executable) + } + + private func stubFixture(id: String, stubbedAt: Int) -> StubbedSkill { + StubbedSkill( + skill: Skill( + id: id, + name: id, + description: "Stub fixture", + directory: id, + repoOwner: nil, + repoName: nil, + readmeUrl: nil, + apps: SkillApps(claude: false, codex: false, gemini: false, opencode: false, hermes: false), + installedAt: nil, + updatedAt: nil, + contentHash: nil + ), + backupId: "backup-\(id)", + backupPath: "/tmp/backup-\(id)", + stubbedAt: stubbedAt + ) + } } From 4098c07b00080e4382a0aa884cbb004c7bf4546c Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 03:24:40 +0800 Subject: [PATCH 15/47] fix(ui): normalize capability search delimiters --- .../Popskill/Models/PackageSearchScorer.swift | 21 +++-- .../Models/SearchTextNormalizer.swift | 67 ++++++++++++++++ .../Popskill/Models/SkillSearchScorer.swift | 79 +++++++++++-------- .../Sources/Popskill/Views/MatrixView.swift | 14 ++-- .../Popskill/Views/SpotlightView.swift | 7 +- .../PackageSearchScorerTests.swift | 35 +++++++- .../SkillSearchScorerTests.swift | 27 +++++++ 7 files changed, 189 insertions(+), 61 deletions(-) create mode 100644 swift-app/Sources/Popskill/Models/SearchTextNormalizer.swift diff --git a/swift-app/Sources/Popskill/Models/PackageSearchScorer.swift b/swift-app/Sources/Popskill/Models/PackageSearchScorer.swift index 05eea78..77c96b1 100644 --- a/swift-app/Sources/Popskill/Models/PackageSearchScorer.swift +++ b/swift-app/Sources/Popskill/Models/PackageSearchScorer.swift @@ -10,7 +10,7 @@ struct PackageSearchHit: Equatable { enum PackageSearchScorer { static func score(package: CapabilityPackage, query: String) -> PackageSearchHit? { - let q = normalized(query) + let q = SearchTextNormalizer.key(query) guard !q.isEmpty else { return nil } @@ -30,7 +30,7 @@ enum PackageSearchScorer { score += scoreText(package.source.location, query: q, exact: 360, prefix: 240, contains: 140) score += scoreText(package.summary, query: q, exact: 180, prefix: 120, contains: 70) - if ["bundle", "package", "suite", "套装"].contains(q) { + if ["bundle", "package", "suite", "套装"].contains(q.separated) { score += package.type == .composite ? 220 : 80 } @@ -41,8 +41,9 @@ enum PackageSearchScorer { score += component.installed ? componentScore : max(20, componentScore - 35) let label = component.name.trimmingCharacters(in: .whitespacesAndNewlines) - if !label.isEmpty, !seenComponents.contains(label.lowercased()) { - seenComponents.insert(label.lowercased()) + let normalizedLabel = SearchTextNormalizer.key(label).compact + if !label.isEmpty, !seenComponents.contains(normalizedLabel) { + seenComponents.insert(normalizedLabel) matchedComponents.append(label) } } @@ -58,7 +59,7 @@ enum PackageSearchScorer { ) } - private static func scoreComponent(_ component: PackageComponent, query: String) -> Int { + private static func scoreComponent(_ component: PackageComponent, query: SearchTextKey) -> Int { scoreText(component.name, query: query, exact: 260, prefix: 190, contains: 120) + scoreText(component.id, query: query, exact: 220, prefix: 150, contains: 90) + scoreText(component.kind, query: query, exact: 140, prefix: 90, contains: 40) @@ -68,20 +69,16 @@ enum PackageSearchScorer { private static func scoreText( _ text: String, - query: String, + query: SearchTextKey, exact: Int, prefix: Int, contains: Int ) -> Int { - let value = normalized(text) + let value = SearchTextNormalizer.key(text) guard !value.isEmpty else { return 0 } - if value == query { return exact } + if value.equals(query) { return exact } if value.hasPrefix(query) { return prefix } if value.contains(query) { return contains } return 0 } - - private static func normalized(_ value: String) -> String { - value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - } } diff --git a/swift-app/Sources/Popskill/Models/SearchTextNormalizer.swift b/swift-app/Sources/Popskill/Models/SearchTextNormalizer.swift new file mode 100644 index 0000000..7899375 --- /dev/null +++ b/swift-app/Sources/Popskill/Models/SearchTextNormalizer.swift @@ -0,0 +1,67 @@ +import Foundation + +struct SearchTextKey: Equatable { + let separated: String + let compact: String + + var isEmpty: Bool { + separated.isEmpty && compact.isEmpty + } + + func equals(_ query: SearchTextKey) -> Bool { + separated == query.separated || (!compact.isEmpty && compact == query.compact) + } + + func hasPrefix(_ query: SearchTextKey) -> Bool { + (!query.separated.isEmpty && separated.hasPrefix(query.separated)) + || (!query.compact.isEmpty && compact.hasPrefix(query.compact)) + } + + func contains(_ query: SearchTextKey) -> Bool { + (!query.separated.isEmpty && separated.contains(query.separated)) + || (!query.compact.isEmpty && compact.contains(query.compact)) + } +} + +enum SearchTextNormalizer { + static func key(_ value: String) -> SearchTextKey { + let folded = value + .folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current) + .lowercased() + + var scalars: [UnicodeScalar] = [] + scalars.reserveCapacity(folded.unicodeScalars.count) + + var previousWasSeparator = true + for scalar in folded.unicodeScalars { + if isSearchScalar(scalar) { + scalars.append(scalar) + previousWasSeparator = false + } else if !previousWasSeparator { + scalars.append(" ") + previousWasSeparator = true + } + } + + if scalars.last == " " { + scalars.removeLast() + } + + let separated = String(String.UnicodeScalarView(scalars)) + return SearchTextKey( + separated: separated, + compact: separated.replacingOccurrences(of: " ", with: "") + ) + } + + static func matches(_ text: String, query: SearchTextKey) -> Bool { + key(text).contains(query) + } + + private static func isSearchScalar(_ scalar: UnicodeScalar) -> Bool { + CharacterSet.alphanumerics.contains(scalar) + || (0x4E00...0x9FFF).contains(scalar.value) + || (0x3400...0x4DBF).contains(scalar.value) + || (0xF900...0xFAFF).contains(scalar.value) + } +} diff --git a/swift-app/Sources/Popskill/Models/SkillSearchScorer.swift b/swift-app/Sources/Popskill/Models/SkillSearchScorer.swift index 6bb3e2f..02a15ce 100644 --- a/swift-app/Sources/Popskill/Models/SkillSearchScorer.swift +++ b/swift-app/Sources/Popskill/Models/SkillSearchScorer.swift @@ -30,55 +30,57 @@ enum SkillSearchScorer { /// sourceLabel contains 10 /// directory contains 5 static func score(skill: Skill, query: String) -> SkillSearchHit? { - let q = query.lowercased() + let q = SearchTextNormalizer.key(query) guard !q.isEmpty else { return nil } - let name = skill.name.lowercased() - let summary = (skill.capabilitySummary ?? "").lowercased() - let description = skill.description.lowercased() + let name = SearchTextNormalizer.key(skill.name) + let summary = SearchTextNormalizer.key(skill.capabilitySummary ?? "") + let description = SearchTextNormalizer.key(skill.description) let triggers = skill.triggerScenarios ?? [] - let source = skill.sourceLabel.lowercased() - let directory = skill.directory.lowercased() + let source = SearchTextNormalizer.key(skill.sourceLabel) + let directory = SearchTextNormalizer.key(skill.directory) var score = 0 var matchedOnName = false var matchedTriggers: [String] = [] var seenTriggers: Set = [] - if name == q { + if name.equals(q) { score += 1000 matchedOnName = true } else if name.hasPrefix(q) { score += 500 matchedOnName = true - } else if matches(name, query: q) { + } else if matches(skill.name, key: name, query: q, rawQuery: query) { score += 200 matchedOnName = true } for trigger in triggers { - let lowerTrigger = trigger.lowercased() - guard matches(lowerTrigger, query: q), !seenTriggers.contains(lowerTrigger) else { + let triggerKey = SearchTextNormalizer.key(trigger) + guard matches(trigger, key: triggerKey, query: q, rawQuery: query), + !seenTriggers.contains(triggerKey.compact) + else { continue } - seenTriggers.insert(lowerTrigger) + seenTriggers.insert(triggerKey.compact) score += 100 matchedTriggers.append(trigger) } - if !summary.isEmpty, matches(summary, query: q) { + if !summary.isEmpty, summary.contains(q) { score += 50 } - if matches(description, query: q) { + if description.contains(q) { score += 20 } - if matches(source, query: q) { + if source.contains(q) { score += 10 } - if matches(directory, query: q) { + if directory.contains(q) { score += 5 } @@ -95,50 +97,52 @@ enum SkillSearchScorer { /// uses `categoryLabel` and `fileName` as auxiliary fields instead of /// `sourceLabel`/`directory`. Returns nil when no field matches. static func score(agent: LocalAgent, query: String) -> SkillSearchHit? { - let q = query.lowercased() + let q = SearchTextNormalizer.key(query) guard !q.isEmpty else { return nil } - let name = agent.name.lowercased() - let summary = (agent.capabilitySummary ?? "").lowercased() - let description = agent.description.lowercased() + let name = SearchTextNormalizer.key(agent.name) + let summary = SearchTextNormalizer.key(agent.capabilitySummary ?? "") + let description = SearchTextNormalizer.key(agent.description) let triggers = agent.triggerScenarios ?? [] - let category = agent.categoryLabel.lowercased() - let fileName = agent.fileName.lowercased() + let category = SearchTextNormalizer.key(agent.categoryLabel) + let fileName = SearchTextNormalizer.key(agent.fileName) var score = 0 var matchedOnName = false var matchedTriggers: [String] = [] var seenTriggers: Set = [] - if name == q { + if name.equals(q) { score += 1000 matchedOnName = true } else if name.hasPrefix(q) { score += 500 matchedOnName = true - } else if matches(name, query: q) { + } else if matches(agent.name, key: name, query: q, rawQuery: query) { score += 200 matchedOnName = true } for trigger in triggers { - let lower = trigger.lowercased() - guard matches(lower, query: q), !seenTriggers.contains(lower) else { continue } - seenTriggers.insert(lower) + let triggerKey = SearchTextNormalizer.key(trigger) + guard matches(trigger, key: triggerKey, query: q, rawQuery: query), + !seenTriggers.contains(triggerKey.compact) + else { continue } + seenTriggers.insert(triggerKey.compact) score += 100 matchedTriggers.append(trigger) } - if !summary.isEmpty, matches(summary, query: q) { + if !summary.isEmpty, summary.contains(q) { score += 50 } - if matches(description, query: q) { + if description.contains(q) { score += 20 } - if matches(category, query: q) { + if category.contains(q) { score += 10 } - if matches(fileName, query: q) { + if fileName.contains(q) { score += 5 } @@ -151,15 +155,22 @@ enum SkillSearchScorer { ) } - private static func matches(_ text: String, query: String) -> Bool { + private static func matches( + _ text: String, + key: SearchTextKey, + query: SearchTextKey, + rawQuery: String + ) -> Bool { guard !query.isEmpty else { return false } - if text.contains(query) { + if key.contains(query) { return true } - guard containsCJKScalar(query), containsCJKScalar(text) else { + let normalizedQuery = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let normalizedText = text.lowercased() + guard containsCJKScalar(normalizedQuery), containsCJKScalar(normalizedText) else { return false } - return text.containsCharactersInOrder(query) + return normalizedText.containsCharactersInOrder(normalizedQuery) } private static func containsCJKScalar(_ value: String) -> Bool { diff --git a/swift-app/Sources/Popskill/Views/MatrixView.swift b/swift-app/Sources/Popskill/Views/MatrixView.swift index b3d437d..0dfd7a0 100644 --- a/swift-app/Sources/Popskill/Views/MatrixView.swift +++ b/swift-app/Sources/Popskill/Views/MatrixView.swift @@ -368,18 +368,18 @@ struct MatrixView: View { // MARK: Filtering & grouping private func filteredSections(in capabilities: [MatrixCapability]) -> [CapabilitySection] { - let q = store.trimmedSearch.lowercased() + let q = SearchTextNormalizer.key(store.trimmedSearch) let visible = capabilities.filter { capability in store.matrixFilter.includes(capability: capability, store: store) && store.matrixTypeFilter.includes(capability: capability) && (q.isEmpty - || capability.name.lowercased().contains(q) - || (capability.summary ?? "").lowercased().contains(q) - || capability.directory.lowercased().contains(q) + || SearchTextNormalizer.matches(capability.name, query: q) + || SearchTextNormalizer.matches(capability.summary ?? "", query: q) + || SearchTextNormalizer.matches(capability.directory, query: q) || capability.package?.components.all.contains { component in - component.name.lowercased().contains(q) - || component.id.lowercased().contains(q) - || (component.location ?? "").lowercased().contains(q) + SearchTextNormalizer.matches(component.name, query: q) + || SearchTextNormalizer.matches(component.id, query: q) + || SearchTextNormalizer.matches(component.location ?? "", query: q) } == true) } return SkillGrouping.sections(visible) diff --git a/swift-app/Sources/Popskill/Views/SpotlightView.swift b/swift-app/Sources/Popskill/Views/SpotlightView.swift index fe19b32..0d4bc8e 100644 --- a/swift-app/Sources/Popskill/Views/SpotlightView.swift +++ b/swift-app/Sources/Popskill/Views/SpotlightView.swift @@ -383,12 +383,11 @@ struct SpotlightView: View { } private var actionHits: [SpotlightAction] { - let q = localQuery.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let q = SearchTextNormalizer.key(localQuery) return SpotlightAction.all.filter { action in guard !q.isEmpty else { return true } - let title = localization.string(action.titleKey).lowercased() - let subtitle = localization.string(action.subtitleKey).lowercased() - return title.contains(q) || subtitle.contains(q) + return SearchTextNormalizer.matches(localization.string(action.titleKey), query: q) + || SearchTextNormalizer.matches(localization.string(action.subtitleKey), query: q) } } diff --git a/swift-app/Tests/PopskillTests/PackageSearchScorerTests.swift b/swift-app/Tests/PopskillTests/PackageSearchScorerTests.swift index 9a86a04..cdcd778 100644 --- a/swift-app/Tests/PopskillTests/PackageSearchScorerTests.swift +++ b/swift-app/Tests/PopskillTests/PackageSearchScorerTests.swift @@ -24,6 +24,29 @@ struct PackageSearchScorerTests { #expect(mcpHit?.matchedComponents.contains("Lark OpenAPI MCP") == true) } + @Test + func spaceSeparatedQueryMatchesDashedComponentName() { + let package = demoPackage() + + let hit = PackageSearchScorer.score(package: package, query: "lark cli") + + #expect(hit?.matchedComponents == ["lark-cli"]) + #expect((hit?.score ?? 0) > 0) + } + + @Test + func separatedQueryMatchesCompactRepositoryName() { + let package = demoPackage( + sourceLocation: "github.com/larksuite/cli", + repoOwner: "larksuite", + repoName: "cli" + ) + + let hit = PackageSearchScorer.score(package: package, query: "lark suite") + + #expect(hit != nil) + } + @Test func unrelatedQueryReturnsNil() { let package = demoPackage() @@ -31,7 +54,11 @@ struct PackageSearchScorerTests { #expect(PackageSearchScorer.score(package: package, query: "pdf") == nil) } - private func demoPackage() -> CapabilityPackage { + private func demoPackage( + sourceLocation: String = "popskill/builtin/lark", + repoOwner: String = "larksuite", + repoName: String = "cli" + ) -> CapabilityPackage { CapabilityPackage( id: "pkg:lark", type: .composite, @@ -40,10 +67,10 @@ struct PackageSearchScorerTests { summary: "Composite office package", source: PackageSource( kind: "builtin", - location: "popskill/builtin/lark", + location: sourceLocation, updateStrategy: "manual", - repoOwner: "larksuite", - repoName: "cli", + repoOwner: repoOwner, + repoName: repoName, repoBranch: "main", readmeUrl: nil ), diff --git a/swift-app/Tests/PopskillTests/SkillSearchScorerTests.swift b/swift-app/Tests/PopskillTests/SkillSearchScorerTests.swift index aff591d..b1c07ad 100644 --- a/swift-app/Tests/PopskillTests/SkillSearchScorerTests.swift +++ b/swift-app/Tests/PopskillTests/SkillSearchScorerTests.swift @@ -30,6 +30,22 @@ struct SkillSearchScorerTests { #expect((prefix?.score ?? 0) > (contains?.score ?? 0)) } + @Test + func spaceSeparatedQueryMatchesDashedSkillName() { + let hit = SkillSearchScorer.score(skill: skill(name: "baoyu-comic"), query: "baoyu comic") + + #expect(hit?.matchedOnName == true) + #expect((hit?.score ?? 0) >= 1000) + } + + @Test + func compactQueryMatchesSeparatedSkillName() { + let hit = SkillSearchScorer.score(skill: skill(name: "baoyu comic"), query: "baoyucomic") + + #expect(hit?.matchedOnName == true) + #expect((hit?.score ?? 0) >= 1000) + } + @Test func triggerMatchesAddOneHundredEach() { let hit = SkillSearchScorer.score( @@ -110,6 +126,17 @@ struct SkillSearchScorerTests { #expect(hit?.matchedOnName == false) } + @Test + func agentQueryMatchesDelimitedFileName() { + let hit = SkillSearchScorer.score( + agent: localAgent(name: "lark-office-assistant", description: "Drafts documents"), + query: "office assistant" + ) + + #expect(hit?.matchedOnName == true) + #expect((hit?.score ?? 0) >= 200) + } + @Test func agentScoringCombinesNameTriggerAndSummary() { let hit = SkillSearchScorer.score( From ae9b0fa551cd36aa1ef6987c3d6c714df346d56b Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 03:32:24 +0800 Subject: [PATCH 16/47] feat(ui): segment matrix inspector details --- .../Resources/en.lproj/Localizable.strings | 6 + .../zh-Hans.lproj/Localizable.strings | 6 + .../Popskill/Views/InspectorPane.swift | 163 ++++++++++++++---- .../PopskillTests/LocalizationTests.swift | 9 + 4 files changed, 151 insertions(+), 33 deletions(-) diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 343f34f..ae65642 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -380,6 +380,12 @@ "matrix.inspector.empty.title" = "Select a capability"; "matrix.inspector.empty.body" = "Pick a row to see its description, deployment links, and source."; "matrix.inspector.close" = "Close inspector"; +"matrix.inspector.tab.overview" = "Overview"; +"matrix.inspector.tab.readme" = "README"; +"matrix.inspector.tab.usage" = "Usage"; +"matrix.inspector.tab.version" = "Version"; +"matrix.inspector.tab.sync" = "Sync"; +"matrix.inspector.tab.metadata" = "Metadata"; "matrix.inspector.section.summary" = "SUMMARY"; "matrix.inspector.section.actions" = "ACTIONS"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index ea056e9..4b2480c 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -380,6 +380,12 @@ "matrix.inspector.empty.title" = "选个能力看详情"; "matrix.inspector.empty.body" = "点左边的能力,这里会显示描述、部署链接和来源。"; "matrix.inspector.close" = "关闭面板"; +"matrix.inspector.tab.overview" = "概览"; +"matrix.inspector.tab.readme" = "README"; +"matrix.inspector.tab.usage" = "用量"; +"matrix.inspector.tab.version" = "版本"; +"matrix.inspector.tab.sync" = "同步"; +"matrix.inspector.tab.metadata" = "元信息"; "matrix.inspector.section.summary" = "简介"; "matrix.inspector.section.actions" = "操作"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index e9663c8..e6a5d89 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -11,52 +11,30 @@ struct InspectorPane: View { @Bindable var store: PopskillStore let capability: MatrixCapability @Environment(\.popskillLocalization) private var localization + @State private var selectedTab: InspectorTab = .overview var body: some View { ScrollView { VStack(alignment: .leading, spacing: 18) { header + tabPicker if let package = capability.package { - packageSummarySection(package) - if let skill = readmeSkill(for: package) { - readmePreviewSection( - skill: skill, - context: localization.string("matrix.readme.showing", skill.name) - ) - } - packageActionsSection(package) - packageCoverageSection - packageUsageSection(package) - packageComponentsSection(package) - if !package.configSchema.isEmpty { - packageConfigSection(package) - } - packageLocalPathsSection(package) - packageVersionSection(package) - packageSyncSection(package) - packageMetadataSection(package) + packageContent(package) } else { - if !primaryDescription.isEmpty { - summarySection - } - if let skill = selectedSkill { - readmePreviewSection(skill: skill) - skillUsageSection(skill) - } - if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { - triggerSection(scenarios: scenarios) - } - appsSection - if capability.kind == .skill { - deploymentSection - } - metadataSection + capabilityContent } } .padding(.horizontal, 18) .padding(.vertical, 18) } .background(Color.popCardBackground.opacity(0.72)) + .onAppear { + normalizeSelectedTab() + } + .onChange(of: capability.id) { _, _ in + selectedTab = .overview + normalizeSelectedTab() + } } // MARK: Header @@ -94,6 +72,103 @@ struct InspectorPane: View { } } + private var tabPicker: some View { + Picker("", selection: $selectedTab) { + ForEach(availableTabs) { tab in + Text(localization.string(tab.titleKey)).tag(tab) + } + } + .pickerStyle(.segmented) + .labelsHidden() + .controlSize(.small) + .onChange(of: selectedTab) { _, _ in + normalizeSelectedTab() + } + } + + @ViewBuilder + private func packageContent(_ package: CapabilityPackage) -> some View { + switch selectedTab { + case .overview: + packageSummarySection(package) + packageCoverageSection + packageComponentsSection(package) + case .readme: + if let skill = readmeSkill(for: package) { + readmePreviewSection( + skill: skill, + context: localization.string("matrix.readme.showing", skill.name) + ) + } + case .usage: + packageUsageSection(package) + case .version: + if !package.configSchema.isEmpty { + packageConfigSection(package) + } + packageLocalPathsSection(package) + packageVersionSection(package) + packageMetadataSection(package) + case .sync: + packageActionsSection(package) + packageSyncSection(package) + case .metadata: + packageMetadataSection(package) + } + } + + @ViewBuilder + private var capabilityContent: some View { + switch selectedTab { + case .overview: + if !primaryDescription.isEmpty { + summarySection + } + if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { + triggerSection(scenarios: scenarios) + } + appsSection + if capability.kind == .skill { + deploymentSection + } + case .readme: + if let skill = selectedSkill { + readmePreviewSection(skill: skill) + } + case .usage: + if let skill = selectedSkill { + skillUsageSection(skill) + } + case .metadata: + metadataSection + case .version, .sync: + metadataSection + } + } + + private var availableTabs: [InspectorTab] { + if let package = capability.package { + var tabs: [InspectorTab] = [.overview] + if readmeSkill(for: package) != nil { + tabs.append(.readme) + } + tabs.append(contentsOf: [.usage, .version, .sync]) + return tabs + } + + var tabs: [InspectorTab] = [.overview] + if selectedSkill != nil { + tabs.append(contentsOf: [.readme, .usage]) + } + tabs.append(.metadata) + return tabs + } + + private func normalizeSelectedTab() { + guard !availableTabs.contains(selectedTab) else { return } + selectedTab = .overview + } + private func packageActionsSection(_ package: CapabilityPackage) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.actions", accent: .accentColor) @@ -1109,6 +1184,28 @@ private extension String { } } +private enum InspectorTab: String, CaseIterable, Identifiable { + case overview + case readme + case usage + case version + case sync + case metadata + + var id: String { rawValue } + + var titleKey: String { + switch self { + case .overview: "matrix.inspector.tab.overview" + case .readme: "matrix.inspector.tab.readme" + case .usage: "matrix.inspector.tab.usage" + case .version: "matrix.inspector.tab.version" + case .sync: "matrix.inspector.tab.sync" + case .metadata: "matrix.inspector.tab.metadata" + } + } +} + private extension CapabilityPackageHealth { var inspectorTitleKey: String { switch self { diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 467a828..25079d8 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -25,4 +25,13 @@ struct LocalizationTests { #expect(localization.string("library.summary", 2, 61, 24) == "2 个能力包 · 61 个 Skill · 24 个已启用") } + + @Test + func inspectorTabLabelsAreLocalized() { + let localization = PopskillLocalization(language: .simplifiedChinese) + + #expect(localization.string("matrix.inspector.tab.overview") == "概览") + #expect(localization.string("matrix.inspector.tab.usage") == "用量") + #expect(localization.string("matrix.inspector.tab.sync") == "同步") + } } From dbf3664415919a923e6c082002e27e0713ef54f0 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 03:44:54 +0800 Subject: [PATCH 17/47] fix(ui): surface sync diagnostics --- .../Sources/Popskill/Models/SkillModels.swift | 109 ++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 10 ++ .../zh-Hans.lproj/Localizable.strings | 10 ++ .../Sources/Popskill/Views/SettingsView.swift | 121 ++++++++++++++++-- .../PopskillTests/SettingsViewTests.swift | 57 +++++++++ 5 files changed, 298 insertions(+), 9 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index cd71c6e..db75e8c 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -1134,6 +1134,115 @@ struct SyncResult: Codable, Equatable { let stderr: String? let message: String? let implemented: Bool? + let localPath: String? + let remotePath: String? + let localCount: Int? + let remoteCount: Int? + + init( + provider: String, + action: String, + ok: Bool? = nil, + exitCode: Int? = nil, + stdout: String? = nil, + stderr: String? = nil, + message: String? = nil, + implemented: Bool? = nil, + localPath: String? = nil, + remotePath: String? = nil, + localCount: Int? = nil, + remoteCount: Int? = nil + ) { + self.provider = provider + self.action = action + self.ok = ok + self.exitCode = exitCode + self.stdout = stdout + self.stderr = stderr + self.message = message + self.implemented = implemented + self.localPath = localPath + self.remotePath = remotePath + self.localCount = localCount + self.remoteCount = remoteCount + } +} + +struct SyncResultSummary: Equatable { + enum State: Equatable { + case success + case failure + case unavailable + case unknown + } + + enum Detail: Equatable { + case exitCode(Int) + case localEndpoint(path: String?, count: Int?) + case remoteEndpoint(path: String?, count: Int?) + } + + let state: State + let message: String + let details: [Detail] +} + +extension SyncResult { + func summary(successMessage: String, emptyMessage: String) -> SyncResultSummary { + let state = summaryState + let message = preferredMessage(for: state) ?? fallbackMessage(for: state, successMessage: successMessage, emptyMessage: emptyMessage) + var details: [SyncResultSummary.Detail] = [] + + if let exitCode, state != .success || exitCode != 0 { + details.append(.exitCode(exitCode)) + } + if localPath != nil || localCount != nil { + details.append(.localEndpoint(path: localPath, count: localCount)) + } + if remotePath != nil || remoteCount != nil { + details.append(.remoteEndpoint(path: remotePath, count: remoteCount)) + } + + return SyncResultSummary(state: state, message: message, details: details) + } + + private var summaryState: SyncResultSummary.State { + if implemented == false { + return .unavailable + } + if ok == true { + return .success + } + if ok == false { + return .failure + } + return .unknown + } + + private func preferredMessage(for state: SyncResultSummary.State) -> String? { + let candidates: [String?] + switch state { + case .failure, .unavailable: + candidates = [message, stderr, stdout] + case .success, .unknown: + candidates = [message, stdout, stderr] + } + return candidates.compactMap(Self.cleanedOutput).first + } + + private func fallbackMessage(for state: SyncResultSummary.State, successMessage: String, emptyMessage: String) -> String { + state == .success ? successMessage : emptyMessage + } + + private static func cleanedOutput(_ value: String?) -> String? { + guard let value else { return nil } + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + if trimmed.count <= 280 { + return trimmed + } + return String(trimmed.prefix(280)).trimmingCharacters(in: .whitespacesAndNewlines) + "..." + } } struct UnmanagedSkill: Identifiable, Codable, Equatable { diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index ae65642..baa755b 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -581,7 +581,17 @@ "settings.sync.soon" = "SOON"; "settings.sync.push" = "Push"; "settings.sync.pull" = "Pull"; +"settings.sync.status" = "Status"; "settings.sync.done" = "%@ via %@ done"; +"settings.sync.noDetails" = "No sync details returned."; +"settings.sync.result.success" = "Sync complete"; +"settings.sync.result.failure" = "Sync failed"; +"settings.sync.result.unavailable" = "Provider unavailable"; +"settings.sync.result.unknown" = "Sync response"; +"settings.sync.detail.exitCode" = "Exit code %d"; +"settings.sync.detail.local" = "Local: %@"; +"settings.sync.detail.remote" = "Remote: %@"; +"settings.sync.detail.count" = "%d items"; "settings.sources.title" = "SOURCES"; "settings.sources.summary" = "%d of %d sources enabled"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 4b2480c..1cddd7a 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -581,7 +581,17 @@ "settings.sync.soon" = "稍后"; "settings.sync.push" = "推送"; "settings.sync.pull" = "拉取"; +"settings.sync.status" = "状态"; "settings.sync.done" = "已通过 %2$@ %1$@"; +"settings.sync.noDetails" = "同步没有返回详细信息。"; +"settings.sync.result.success" = "同步完成"; +"settings.sync.result.failure" = "同步失败"; +"settings.sync.result.unavailable" = "同步方式不可用"; +"settings.sync.result.unknown" = "同步响应"; +"settings.sync.detail.exitCode" = "退出码 %d"; +"settings.sync.detail.local" = "本地:%@"; +"settings.sync.detail.remote" = "远端:%@"; +"settings.sync.detail.count" = "%d 项"; "settings.sources.title" = "数据源"; "settings.sources.summary" = "%d / %d 个数据源已启用"; diff --git a/swift-app/Sources/Popskill/Views/SettingsView.swift b/swift-app/Sources/Popskill/Views/SettingsView.swift index 7bf905a..463c8ad 100644 --- a/swift-app/Sources/Popskill/Views/SettingsView.swift +++ b/swift-app/Sources/Popskill/Views/SettingsView.swift @@ -13,7 +13,7 @@ struct SettingsView: View { @State private var syncProvider: SyncProvider = .git @State private var pendingSync: Bool = false - @State private var syncMessage: String? + @State private var syncSummary: SyncResultSummary? var body: some View { ScrollView { @@ -137,15 +137,22 @@ struct SettingsView: View { .controlSize(.small) .disabled(pendingSync || !syncProvider.actionable) + Button { + Task { await runSync(.status) } + } label: { + Label(localization.string("settings.sync.status"), systemImage: "list.bullet.rectangle") + } + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(pendingSync || !syncProvider.actionable) + if pendingSync { ProgressView().controlSize(.small) } Spacer() } - if let syncMessage { - Text(syncMessage) - .font(.caption) - .foregroundStyle(Color.popSecondaryLabel) + if let syncSummary { + syncStatusPanel(syncSummary) } } .padding(16) @@ -157,6 +164,7 @@ struct SettingsView: View { Button { syncProvider = provider store.lastSyncProvider = provider.rawValue + syncSummary = nil } label: { HStack(spacing: 10) { Image(systemName: syncProvider == provider ? "largecircle.fill.circle" : "circle") @@ -186,20 +194,115 @@ struct SettingsView: View { .buttonStyle(.plain) } + private func syncStatusPanel(_ summary: SyncResultSummary) -> some View { + let tint = syncStatusColor(summary.state) + let detailLines = summary.details.map(syncDetailText) + + return VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 6) { + Image(systemName: syncStatusSymbol(summary.state)) + .foregroundStyle(tint) + Text(localization.string(syncStatusTitleKey(summary.state))) + .font(.caption.weight(.semibold)) + .foregroundStyle(Color.popLabel) + Spacer() + } + Text(summary.message) + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + .textSelection(.enabled) + .lineLimit(4) + if !detailLines.isEmpty { + VStack(alignment: .leading, spacing: 3) { + ForEach(detailLines.indices, id: \.self) { index in + Text(detailLines[index]) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + .textSelection(.enabled) + .lineLimit(2) + .truncationMode(.middle) + } + } + } + } + .padding(10) + .background(tint.opacity(0.09), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .strokeBorder(tint.opacity(0.24), lineWidth: 0.7) + } + } + + private func syncStatusTitleKey(_ state: SyncResultSummary.State) -> String { + switch state { + case .success: return "settings.sync.result.success" + case .failure: return "settings.sync.result.failure" + case .unavailable: return "settings.sync.result.unavailable" + case .unknown: return "settings.sync.result.unknown" + } + } + + private func syncStatusSymbol(_ state: SyncResultSummary.State) -> String { + switch state { + case .success: return "checkmark.circle.fill" + case .failure: return "exclamationmark.circle.fill" + case .unavailable: return "clock.fill" + case .unknown: return "info.circle.fill" + } + } + + private func syncStatusColor(_ state: SyncResultSummary.State) -> Color { + switch state { + case .success: return Color.popStatusOK + case .failure: return Color.popStatusError + case .unavailable: return Color.popStatusWarning + case .unknown: return Color.popStatusNeutral + } + } + + private func syncDetailText(_ detail: SyncResultSummary.Detail) -> String { + switch detail { + case .exitCode(let code): + return localization.string("settings.sync.detail.exitCode", code) + case .localEndpoint(let path, let count): + return localization.string("settings.sync.detail.local", endpointDetail(path: path, count: count)) + case .remoteEndpoint(let path, let count): + return localization.string("settings.sync.detail.remote", endpointDetail(path: path, count: count)) + } + } + + private func endpointDetail(path: String?, count: Int?) -> String { + var parts: [String] = [] + if let count { + parts.append(localization.string("settings.sync.detail.count", count)) + } + if let path { + parts.append(path) + } + return parts.joined(separator: " - ") + } + @MainActor private func runSync(_ action: SyncAction) async { guard !pendingSync, syncProvider.actionable else { return } pendingSync = true - syncMessage = nil + syncSummary = nil defer { pendingSync = false } do { let result = try await store.client.sync(action: action.rawValue, provider: syncProvider.rawValue) - syncMessage = result.message ?? localization.string("settings.sync.done", action.rawValue, syncProvider.rawValue) - if result.ok == true { + syncSummary = result.summary( + successMessage: localization.string("settings.sync.done", action.rawValue, syncProvider.rawValue), + emptyMessage: localization.string("settings.sync.noDetails") + ) + if result.ok == true && action != .status { store.lastSyncAt = Date() } } catch { - syncMessage = error.localizedDescription + syncSummary = SyncResultSummary( + state: .failure, + message: error.localizedDescription, + details: [] + ) } } diff --git a/swift-app/Tests/PopskillTests/SettingsViewTests.swift b/swift-app/Tests/PopskillTests/SettingsViewTests.swift index ed11c48..45dc018 100644 --- a/swift-app/Tests/PopskillTests/SettingsViewTests.swift +++ b/swift-app/Tests/PopskillTests/SettingsViewTests.swift @@ -9,4 +9,61 @@ struct SettingsViewTests { #expect(!SyncProvider.webdav.actionable) #expect(!SyncProvider.none.actionable) } + + @Test + func syncSummaryUsesStderrAndExitCodeForCommandFailures() { + let result = SyncResult( + provider: "icloud", + action: "push", + ok: false, + exitCode: 23, + stdout: "", + stderr: "rsync: permission denied\n", + message: nil + ) + + let summary = result.summary(successMessage: "Done", emptyMessage: "No details") + + #expect(summary.state == .failure) + #expect(summary.message == "rsync: permission denied") + #expect(summary.details == [.exitCode(23)]) + } + + @Test + func syncSummarySurfacesICloudStatusEndpoints() { + let result = SyncResult( + provider: "icloud", + action: "status", + ok: true, + localPath: "/Users/me/.cc-switch/skills", + remotePath: "/Users/me/Library/Mobile Documents/com~apple~CloudDocs/Popskill/skills", + localCount: 8, + remoteCount: 6 + ) + + let summary = result.summary(successMessage: "Status checked", emptyMessage: "No details") + + #expect(summary.state == .success) + #expect(summary.message == "Status checked") + #expect(summary.details == [ + .localEndpoint(path: "/Users/me/.cc-switch/skills", count: 8), + .remoteEndpoint(path: "/Users/me/Library/Mobile Documents/com~apple~CloudDocs/Popskill/skills", count: 6) + ]) + } + + @Test + func syncSummaryTreatsUnimplementedProviderAsUnavailable() { + let result = SyncResult( + provider: "webdav", + action: "push", + message: "WebDAV sync is not available", + implemented: false + ) + + let summary = result.summary(successMessage: "Done", emptyMessage: "No details") + + #expect(summary.state == .unavailable) + #expect(summary.message == "WebDAV sync is not available") + #expect(summary.details.isEmpty) + } } From 8eb1d68654f632d6d2bdafcf65bea52dc5793107 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 03:56:57 +0800 Subject: [PATCH 18/47] feat(ui): enrich package component statuses --- .../Sources/Popskill/Models/SkillModels.swift | 57 +++++++++- .../Resources/en.lproj/Localizable.strings | 6 ++ .../zh-Hans.lproj/Localizable.strings | 6 ++ .../Popskill/Views/InspectorPane.swift | 101 ++++++++++++++++-- .../PopskillTests/SkillModelsTests.swift | 42 ++++++++ 5 files changed, 200 insertions(+), 12 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index db75e8c..b7ff95b 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -759,6 +759,23 @@ struct PackageComponent: Codable, Equatable { return false } } + + var isStubbed: Bool { + status.trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() + .contains("stub") + } +} + +enum PackageComponentAppState: Equatable { + case active + case stub + case off + case unsupported + + var isEnabled: Bool { + self == .active + } } struct PackageConfigField: Identifiable, Codable, Equatable { @@ -1435,18 +1452,48 @@ extension CapabilityPackage { private extension PackageComponent { func isEnabled(for app: TargetApp, matching skill: Skill?) -> Bool { + appState(for: app, matching: skill).isEnabled + } +} + +extension PackageComponent { + func appState(for app: TargetApp, matching skill: Skill?) -> PackageComponentAppState { switch kind.lowercased() { case "skill": if let skill { - return skill.apps.isEnabled(app) + if skill.apps.isEnabled(app) { + return .active + } + return isStubbed ? .stub : .off + } + guard app == .claude || app == .codex else { + return .unsupported } - return installed && (app == .claude || app == .codex) + if installed { + return .active + } + return isStubbed ? .stub : .off case "agent": - return installed && app == .claude + guard app == .claude || app == .codex else { + return .unsupported + } + guard app == .claude else { + return .off + } + if installed { + return .active + } + return isStubbed ? .stub : .off case "cli", "mcp": - return installed && (app == .claude || app == .codex) + guard app == .claude || app == .codex else { + return .unsupported + } + if installed { + return .active + } + return isStubbed ? .stub : .off default: - return false + return .unsupported } } } diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index baa755b..f673da8 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -358,6 +358,12 @@ "matrix.package.component.enabledHelp" = "%@ enabled"; "matrix.package.component.partialHelp" = "%@ is stubbed or recoverable"; "matrix.package.component.offHelp" = "%@ not enabled"; +"matrix.package.component.unsupportedHelp" = "%@ is not supported for this component"; +"matrix.package.component.state.active" = "Active"; +"matrix.package.component.state.stub" = "Stub"; +"matrix.package.component.state.off" = "Off"; +"matrix.package.component.state.unsupported" = "N/A"; +"matrix.package.component.calls" = "%@ calls"; "matrix.package.missingRequired" = "%d required missing"; "matrix.package.config.secret" = "Keychain secret"; "matrix.package.usage.tokens" = "Tokens"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 1cddd7a..7b38b43 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -358,6 +358,12 @@ "matrix.package.component.enabledHelp" = "%@ 已启用"; "matrix.package.component.partialHelp" = "%@ 是占位或待恢复"; "matrix.package.component.offHelp" = "%@ 未启用"; +"matrix.package.component.unsupportedHelp" = "%@ 暂不支持这个组件"; +"matrix.package.component.state.active" = "已激活"; +"matrix.package.component.state.stub" = "占位"; +"matrix.package.component.state.off" = "未启用"; +"matrix.package.component.state.unsupported" = "不可用"; +"matrix.package.component.calls" = "%@ 次"; "matrix.package.missingRequired" = "缺 %d 个必需"; "matrix.package.config.secret" = "Keychain 密钥"; "matrix.package.usage.tokens" = "Tokens"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index e6a5d89..d1c5515 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -489,7 +489,14 @@ struct InspectorPane: View { } private func packageComponentsSection(_ package: CapabilityPackage) -> some View { - VStack(alignment: .leading, spacing: 10) { + let usageStatsByComponentID = Dictionary( + uniqueKeysWithValues: package + .usageSnapshot(using: store.usageSummary, skills: store.skills)? + .componentStats + .map { ($0.componentID, $0) } ?? [] + ) + + return VStack(alignment: .leading, spacing: 10) { SectionHeading(title: "matrix.inspector.section.components", accent: .popSectionPurple) ForEach(package.componentGroupSummaries) { group in VStack(alignment: .leading, spacing: 6) { @@ -508,7 +515,11 @@ struct InspectorPane: View { Spacer() } ForEach(components(in: group.kind, package: package), id: \.displayKey) { component in - packageComponentDetailRow(component) + packageComponentDetailRow( + component, + package: package, + usageStat: usageStatsByComponentID[component.id] + ) } } .padding(8) @@ -640,8 +651,14 @@ struct InspectorPane: View { } } - private func packageComponentDetailRow(_ component: PackageComponent) -> some View { - HStack(alignment: .top, spacing: 8) { + private func packageComponentDetailRow( + _ component: PackageComponent, + package: CapabilityPackage, + usageStat: PackageComponentUsageStat? + ) -> some View { + let matchedSkill = package.matchingInstalledSkill(for: component, in: store.skills) + + return HStack(alignment: .top, spacing: 8) { Image(systemName: component.inspectorKindSymbol) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(component.installed ? Color.popStatusOK : Color.popTertiaryLabel) @@ -663,13 +680,83 @@ struct InspectorPane: View { .textSelection(.enabled) } Spacer(minLength: 8) - Text(component.status) - .font(.caption2.weight(.semibold)) - .foregroundStyle(component.installed ? Color.popStatusOK : Color.popStatusWarning) + VStack(alignment: .trailing, spacing: 5) { + HStack(spacing: 4) { + packageComponentAppStateBadge( + component.appState(for: .claude, matching: matchedSkill), + app: .claude + ) + packageComponentAppStateBadge( + component.appState(for: .codex, matching: matchedSkill), + app: .codex + ) + } + HStack(spacing: 5) { + Text(component.status) + .font(.caption2.weight(.semibold)) + .foregroundStyle(component.installed ? Color.popStatusOK : Color.popStatusWarning) + Text(localization.string( + "matrix.package.component.calls", + usageStat.map { UsageDisplayFormatter.compactCount($0.usageEvents) } ?? "—" + )) + .font(.caption2.monospacedDigit()) + .foregroundStyle(Color.popSecondaryLabel) + } + } } .padding(.vertical, 2) } + private func packageComponentAppStateBadge(_ state: PackageComponentAppState, app: TargetApp) -> some View { + let color = packageComponentAppStateColor(state) + return HStack(spacing: 3) { + Text(packageComponentAppShortLabel(app)) + .font(.system(size: 8.5, weight: .bold)) + Text(localization.string(packageComponentAppStateTitleKey(state))) + .font(.system(size: 9.5, weight: .semibold)) + } + .foregroundStyle(color) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background(color.opacity(0.10), in: Capsule()) + .help(localization.string(packageComponentAppStateHelpKey(state), app.title)) + } + + private func packageComponentAppShortLabel(_ app: TargetApp) -> String { + switch app { + case .claude: return "CC" + case .codex: return "CDX" + default: return app.title.uppercased() + } + } + + private func packageComponentAppStateTitleKey(_ state: PackageComponentAppState) -> String { + switch state { + case .active: return "matrix.package.component.state.active" + case .stub: return "matrix.package.component.state.stub" + case .off: return "matrix.package.component.state.off" + case .unsupported: return "matrix.package.component.state.unsupported" + } + } + + private func packageComponentAppStateHelpKey(_ state: PackageComponentAppState) -> String { + switch state { + case .active: return "matrix.package.component.enabledHelp" + case .stub: return "matrix.package.component.partialHelp" + case .off: return "matrix.package.component.offHelp" + case .unsupported: return "matrix.package.component.unsupportedHelp" + } + } + + private func packageComponentAppStateColor(_ state: PackageComponentAppState) -> Color { + switch state { + case .active: return Color.popStatusOK + case .stub: return Color.popStatusWarning + case .off: return Color.popTertiaryLabel + case .unsupported: return Color.popSecondaryLabel + } + } + private func packageConfigSection(_ package: CapabilityPackage) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.config", accent: .popSectionPurple) diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 707cf5e..061a8b2 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -783,6 +783,48 @@ struct SkillModelsTests { #expect(capability.apps.codex == true) } + @Test + func packageComponentAppStateUsesInstalledSkillTogglesAndStubStatus() { + let skill = installedSkill( + directory: "baoyu-comic", + apps: SkillApps(claude: true, codex: false, gemini: false, opencode: false, hermes: false) + ) + let skillComponent = PackageComponent( + id: "baoyu-comic", + name: "baoyu-comic", + kind: "skill", + required: true, + installed: true, + status: "installed", + location: "skills/baoyu-comic" + ) + let stubbedAgent = PackageComponent( + id: "base-analyst", + name: "base-analyst", + kind: "agent", + required: false, + installed: false, + status: "stub", + location: "~/.claude/agents/base-analyst.md" + ) + let installedCLI = PackageComponent( + id: "lark-cli", + name: "lark-cli", + kind: "cli", + required: true, + installed: true, + status: "detected", + location: nil + ) + + #expect(skillComponent.appState(for: .claude, matching: skill) == .active) + #expect(skillComponent.appState(for: .codex, matching: skill) == .off) + #expect(stubbedAgent.appState(for: .claude, matching: nil) == .stub) + #expect(stubbedAgent.appState(for: .codex, matching: nil) == .off) + #expect(installedCLI.appState(for: .codex, matching: nil) == .active) + #expect(installedCLI.appState(for: .gemini, matching: nil) == .unsupported) + } + @Test func capabilityPackageUsageSnapshotAggregatesMatchedSkillStats() { let package = CapabilityPackage( From 873bde4cf66a7d42cbb5e316ed48bb073d30c3a3 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 04:05:44 +0800 Subject: [PATCH 19/47] feat(ui): split skill inspector paths and version --- .../Resources/en.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../Popskill/Views/InspectorPane.swift | 108 ++++++++++++++++-- .../PopskillTests/LocalizationTests.swift | 1 + 4 files changed, 104 insertions(+), 9 deletions(-) diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index f673da8..cb545c8 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -390,6 +390,7 @@ "matrix.inspector.tab.readme" = "README"; "matrix.inspector.tab.usage" = "Usage"; "matrix.inspector.tab.version" = "Version"; +"matrix.inspector.tab.paths" = "Paths"; "matrix.inspector.tab.sync" = "Sync"; "matrix.inspector.tab.metadata" = "Metadata"; @@ -411,6 +412,7 @@ "matrix.package.action.checkUpdates" = "Check updates"; "matrix.package.action.openSource" = "Open source"; "matrix.package.action.revealInFinder" = "Reveal local skill"; +"matrix.skill.paths.readme" = "SKILL.md"; "matrix.package.version.strategy" = "Strategy"; "matrix.package.version.branch" = "Branch"; "matrix.package.version.hash" = "Hash"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 7b38b43..0fc4e1d 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -390,6 +390,7 @@ "matrix.inspector.tab.readme" = "README"; "matrix.inspector.tab.usage" = "用量"; "matrix.inspector.tab.version" = "版本"; +"matrix.inspector.tab.paths" = "路径"; "matrix.inspector.tab.sync" = "同步"; "matrix.inspector.tab.metadata" = "元信息"; @@ -411,6 +412,7 @@ "matrix.package.action.checkUpdates" = "检查更新"; "matrix.package.action.openSource" = "打开来源"; "matrix.package.action.revealInFinder" = "定位本地 Skill"; +"matrix.skill.paths.readme" = "SKILL.md"; "matrix.package.version.strategy" = "策略"; "matrix.package.version.branch" = "分支"; "matrix.package.version.hash" = "Hash"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index d1c5515..529fbd2 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -109,6 +109,8 @@ struct InspectorPane: View { packageLocalPathsSection(package) packageVersionSection(package) packageMetadataSection(package) + case .paths: + packageLocalPathsSection(package) case .sync: packageActionsSection(package) packageSyncSection(package) @@ -128,9 +130,6 @@ struct InspectorPane: View { triggerSection(scenarios: scenarios) } appsSection - if capability.kind == .skill { - deploymentSection - } case .readme: if let skill = selectedSkill { readmePreviewSection(skill: skill) @@ -139,9 +138,21 @@ struct InspectorPane: View { if let skill = selectedSkill { skillUsageSection(skill) } + case .version: + if let skill = selectedSkill { + skillVersionSection(skill) + } else { + metadataSection + } + case .paths: + if let skill = selectedSkill { + skillPathsSection(skill) + } else { + metadataSection + } case .metadata: metadataSection - case .version, .sync: + case .sync: metadataSection } } @@ -158,9 +169,11 @@ struct InspectorPane: View { var tabs: [InspectorTab] = [.overview] if selectedSkill != nil { - tabs.append(contentsOf: [.readme, .usage]) + tabs.append(contentsOf: [.readme, .usage, .version, .paths]) + } + if selectedSkill == nil { + tabs.append(.metadata) } - tabs.append(.metadata) return tabs } @@ -864,6 +877,37 @@ struct InspectorPane: View { } } + private func skillVersionSection(_ skill: Skill) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.version") + VStack(alignment: .leading, spacing: 6) { + metaRow( + label: localization.string("matrix.package.version.hash"), + value: skill.contentHash.map(Self.shortHash) ?? localization.string("matrix.package.version.untracked") + ) + if let installedAt = skill.installedAt, installedAt > 0 { + metaRow(label: localization.string("matrix.inspector.meta.installedAt"), value: Self.formatTimestamp(installedAt)) + } + if let updatedAt = skill.updatedAt, updatedAt > 0 { + metaRow(label: localization.string("matrix.inspector.meta.updatedAt"), value: Self.formatTimestamp(updatedAt)) + } + if let size = skill.sizeBytes, size > 0 { + metaRow(label: localization.string("matrix.inspector.meta.size"), value: Self.formatBytes(size)) + } + if let source = skill.sourceType, !source.isEmpty { + metaRow(label: localization.string("matrix.inspector.meta.sourceType"), value: source) + } + } + if let url = skill.sourceURL { + Link(destination: url) { + Label(localization.string("matrix.inspector.meta.openSource"), systemImage: "arrow.up.right.square") + .font(.caption) + } + .buttonStyle(.plain) + } + } + } + private func packageSyncSection(_ package: CapabilityPackage) -> some View { let pendingUpdates = packagePendingUpdates(package) return VStack(alignment: .leading, spacing: 8) { @@ -1038,9 +1082,9 @@ struct InspectorPane: View { .disabled(pending || !capability.isToggleable) } - private var deploymentSection: some View { + private func skillPathsSection(_ skill: Skill) -> some View { VStack(alignment: .leading, spacing: 10) { - SectionHeading(title: "matrix.inspector.section.deployment") + SectionHeading(title: "matrix.inspector.section.paths") if let deployment = capability.deployment { VStack(alignment: .leading, spacing: 6) { deploymentRow( @@ -1056,6 +1100,13 @@ struct InspectorPane: View { ) } } + if let markdownURL = skill.markdownURL { + deploymentRow( + title: localization.string("matrix.skill.paths.readme"), + path: markdownURL.path, + status: "ok" + ) + } HStack(spacing: 4) { Image(systemName: "link") .font(.system(size: 10, weight: .semibold)) @@ -1064,8 +1115,22 @@ struct InspectorPane: View { } .foregroundStyle(Color.popTertiaryLabel) } else { + VStack(alignment: .leading, spacing: 6) { + deploymentRow( + title: localization.string("matrix.inspector.deployment.ssot"), + path: skill.localStoreURL.path, + status: FileManager.default.fileExists(atPath: skill.localStoreURL.path) ? "ok" : nil + ) + if let markdownURL = skill.markdownURL { + deploymentRow( + title: localization.string("matrix.skill.paths.readme"), + path: markdownURL.path, + status: "ok" + ) + } + } Text(localization.string("matrix.inspector.deployment.empty")) - .font(.caption) + .font(.caption2) .foregroundStyle(Color.popTertiaryLabel) } } @@ -1109,11 +1174,34 @@ struct InspectorPane: View { if let status { linkStatusBadge(status) } + if let url = revealableURL(for: path) { + Button { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } label: { + Image(systemName: "folder") + .font(.system(size: 11, weight: .semibold)) + .frame(width: 18, height: 18) + } + .buttonStyle(.plain) + .foregroundStyle(Color.popSecondaryLabel) + .help(localization.string("matrix.row.menu.revealInFinder")) + } } .padding(8) .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 6)) } + private func revealableURL(for path: String) -> URL? { + guard !path.isEmpty, path != "—" else { + return nil + } + let expanded = (path as NSString).expandingTildeInPath + guard FileManager.default.fileExists(atPath: expanded) else { + return nil + } + return URL(fileURLWithPath: expanded) + } + private func linkStatusBadge(_ status: String) -> some View { let (label, color): (String, Color) = { switch status.lowercased() { @@ -1276,6 +1364,7 @@ private enum InspectorTab: String, CaseIterable, Identifiable { case readme case usage case version + case paths case sync case metadata @@ -1287,6 +1376,7 @@ private enum InspectorTab: String, CaseIterable, Identifiable { case .readme: "matrix.inspector.tab.readme" case .usage: "matrix.inspector.tab.usage" case .version: "matrix.inspector.tab.version" + case .paths: "matrix.inspector.tab.paths" case .sync: "matrix.inspector.tab.sync" case .metadata: "matrix.inspector.tab.metadata" } diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 25079d8..4ac7e9b 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -32,6 +32,7 @@ struct LocalizationTests { #expect(localization.string("matrix.inspector.tab.overview") == "概览") #expect(localization.string("matrix.inspector.tab.usage") == "用量") + #expect(localization.string("matrix.inspector.tab.paths") == "路径") #expect(localization.string("matrix.inspector.tab.sync") == "同步") } } From 3d13771192cfe51014b08193ce939240cddedb6f Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 04:14:33 +0800 Subject: [PATCH 20/47] feat(ui): add inspector header status chips --- .../Resources/en.lproj/Localizable.strings | 3 + .../zh-Hans.lproj/Localizable.strings | 3 + .../Popskill/Views/InspectorPane.swift | 90 ++++++++++++++++++- .../PopskillTests/LocalizationTests.swift | 1 + 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index cb545c8..73a0f36 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -394,6 +394,9 @@ "matrix.inspector.tab.sync" = "Sync"; "matrix.inspector.tab.metadata" = "Metadata"; +"matrix.inspector.header.calls" = "%@ calls"; +"matrix.inspector.header.tokens" = "%@ tokens"; + "matrix.inspector.section.summary" = "SUMMARY"; "matrix.inspector.section.actions" = "ACTIONS"; "matrix.inspector.section.readme" = "README"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 0fc4e1d..3099495 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -394,6 +394,9 @@ "matrix.inspector.tab.sync" = "同步"; "matrix.inspector.tab.metadata" = "元信息"; +"matrix.inspector.header.calls" = "%@ 次调用"; +"matrix.inspector.header.tokens" = "%@ tokens"; + "matrix.inspector.section.summary" = "简介"; "matrix.inspector.section.actions" = "操作"; "matrix.inspector.section.readme" = "README"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 529fbd2..cd29175 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -48,14 +48,13 @@ struct InspectorPane: View { .font(.title3.weight(.semibold)) .foregroundStyle(Color.popLabel) .lineLimit(2) - if capability.kind != .skill { - kindChip - } + kindChip } Text(capability.sourceLabel) .font(.caption) .foregroundStyle(Color.popSecondaryLabel) .lineLimit(1) + headerChipStrip } Spacer() Button { @@ -72,6 +71,85 @@ struct InspectorPane: View { } } + @ViewBuilder + private var headerChipStrip: some View { + let chips = inspectorHeaderChips() + if !chips.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 5) { + ForEach(chips) { chip in + Text(chip.title) + .font(.system(size: 10.5, weight: .semibold)) + .foregroundStyle(chip.tint) + .lineLimit(1) + .padding(.horizontal, 6) + .padding(.vertical, 2.5) + .background(chip.tint.opacity(0.10), in: Capsule()) + } + } + } + .padding(.top, 3) + } + } + + private func inspectorHeaderChips() -> [InspectorHeaderChip] { + if let package = capability.package { + return packageHeaderChips(package) + } + if let skill = selectedSkill { + return skillHeaderChips(skill) + } + return [] + } + + private func packageHeaderChips(_ package: CapabilityPackage) -> [InspectorHeaderChip] { + var chips: [InspectorHeaderChip] = [] + for app in [TargetApp.claude, .codex] { + if let coverage = capability.appCoverage[app], coverage.total > 0 { + chips.append(InspectorHeaderChip( + id: "\(app.rawValue)-coverage", + title: "\(app.title) \(coverage.label)", + tint: coverage.enabled > 0 ? app.inspectorAccentColor : Color.popTertiaryLabel + )) + } + } + if let snapshot = package.usageSnapshot(using: store.usageSummary, skills: store.skills), snapshot.hasUsage { + chips.append(contentsOf: usageHeaderChips(calls: snapshot.usageEvents, tokens: snapshot.totalTokens)) + } + return chips + } + + private func skillHeaderChips(_ skill: Skill) -> [InspectorHeaderChip] { + var chips = TargetApp.quickToggleSupported.map { app in + let isOn = skill.apps.isEnabled(app) + let stateKey = isOn ? "matrix.package.component.state.active" : "matrix.package.component.state.off" + return InspectorHeaderChip( + id: "\(app.rawValue)-state", + title: "\(app.title) \(localization.string(stateKey))", + tint: isOn ? app.inspectorAccentColor : Color.popTertiaryLabel + ) + } + if let snapshot = skill.usageSnapshot(using: store.usageSummary), snapshot.hasUsage { + chips.append(contentsOf: usageHeaderChips(calls: snapshot.usageEvents, tokens: snapshot.totalTokens)) + } + return chips + } + + private func usageHeaderChips(calls: Int, tokens: Int64) -> [InspectorHeaderChip] { + [ + InspectorHeaderChip( + id: "calls", + title: localization.string("matrix.inspector.header.calls", UsageDisplayFormatter.compactCount(calls)), + tint: Color.popSectionGreen + ), + InspectorHeaderChip( + id: "tokens", + title: localization.string("matrix.inspector.header.tokens", UsageDisplayFormatter.compactTokens(tokens)), + tint: Color.accentColor + ) + ] + } + private var tabPicker: some View { Picker("", selection: $selectedTab) { ForEach(availableTabs) { tab in @@ -1359,6 +1437,12 @@ private extension String { } } +private struct InspectorHeaderChip: Identifiable { + let id: String + let title: String + let tint: Color +} + private enum InspectorTab: String, CaseIterable, Identifiable { case overview case readme diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 4ac7e9b..8520bb2 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -34,5 +34,6 @@ struct LocalizationTests { #expect(localization.string("matrix.inspector.tab.usage") == "用量") #expect(localization.string("matrix.inspector.tab.paths") == "路径") #expect(localization.string("matrix.inspector.tab.sync") == "同步") + #expect(localization.string("matrix.inspector.header.calls", "412") == "412 次调用") } } From 09e6d2b8b3696689d6927d58bcf895353bd09187 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 04:23:50 +0800 Subject: [PATCH 21/47] feat(ui): filter matrix broken links --- .../Popskill/Models/MatrixCapability.swift | 4 ++ .../Sources/Popskill/Models/SkillModels.swift | 20 ++++++++++ .../Resources/en.lproj/Localizable.strings | 1 + .../zh-Hans.lproj/Localizable.strings | 1 + .../Sources/Popskill/Views/MatrixView.swift | 12 +++++- .../PopskillTests/SkillModelsTests.swift | 37 +++++++++++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/MatrixCapability.swift b/swift-app/Sources/Popskill/Models/MatrixCapability.swift index bbea759..ffd04db 100644 --- a/swift-app/Sources/Popskill/Models/MatrixCapability.swift +++ b/swift-app/Sources/Popskill/Models/MatrixCapability.swift @@ -137,6 +137,10 @@ struct MatrixCapability: Identifiable, Equatable { kind == .skill } + var hasBrokenLink: Bool { + deployment?.hasBrokenLink == true + } + static func capabilityID(kind: CapabilityKind, rawID: String) -> String { "\(kind.rawValue):\(rawID)" } diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index b7ff95b..f06bc16 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -157,6 +157,10 @@ struct Skill: Identifiable, Codable, Equatable { TargetApp.supported.filter { apps.isEnabled($0) }.count } + var hasBrokenLink: Bool { + deployment?.hasBrokenLink == true + } + var localStoreURL: URL { URL(fileURLWithPath: NSHomeDirectory()) .appendingPathComponent(".cc-switch") @@ -1088,6 +1092,18 @@ struct AppLinkStatus: Codable, Equatable, Hashable { let status: String } +extension SkillDeployment { + var hasBrokenLink: Bool { + appLinks.values.contains { $0.isBroken } + } +} + +extension AppLinkStatus { + var isBroken: Bool { + status.caseInsensitiveCompare("broken") == .orderedSame + } +} + /// Top-level envelope returned by `skill-cli link-health --json`. struct LinkHealthReport: Codable, Equatable { let summary: LinkHealthSummary @@ -1393,6 +1409,10 @@ extension CapabilityPackage { return matches } + func hasBrokenLinks(in skills: [Skill]) -> Bool { + matchingInstalledSkills(in: skills).contains { $0.hasBrokenLink } + } + func matchingInstalledSkill(for component: PackageComponent, in skills: [Skill]) -> Skill? { skills.first { component.matchesSkill($0) } } diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 73a0f36..5f28466 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -322,6 +322,7 @@ "matrix.filter.all" = "All"; "matrix.filter.updates" = "Updates"; +"matrix.filter.brokenLinks" = "Broken links"; "matrix.filter.claudeOnly" = "Claude only"; "matrix.filter.codexOnly" = "Codex only"; "matrix.filter.inactive" = "Inactive"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 3099495..a550dbd 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -322,6 +322,7 @@ "matrix.filter.all" = "全部"; "matrix.filter.updates" = "可更新"; +"matrix.filter.brokenLinks" = "断链"; "matrix.filter.claudeOnly" = "仅 Claude"; "matrix.filter.codexOnly" = "仅 Codex"; "matrix.filter.inactive" = "未启用"; diff --git a/swift-app/Sources/Popskill/Views/MatrixView.swift b/swift-app/Sources/Popskill/Views/MatrixView.swift index 0dfd7a0..c812c2b 100644 --- a/swift-app/Sources/Popskill/Views/MatrixView.swift +++ b/swift-app/Sources/Popskill/Views/MatrixView.swift @@ -468,6 +468,7 @@ private struct MatrixSummaryMetricView: View { enum MatrixFilter: String, CaseIterable, Identifiable { case all case updates + case brokenLinks = "broken-links" case claudeOnly = "claude-only" case codexOnly = "codex-only" case inactive @@ -478,6 +479,7 @@ enum MatrixFilter: String, CaseIterable, Identifiable { switch self { case .all: return "matrix.filter.all" case .updates: return "matrix.filter.updates" + case .brokenLinks: return "matrix.filter.brokenLinks" case .claudeOnly: return "matrix.filter.claudeOnly" case .codexOnly: return "matrix.filter.codexOnly" case .inactive: return "matrix.filter.inactive" @@ -487,8 +489,12 @@ enum MatrixFilter: String, CaseIterable, Identifiable { @MainActor func badge(store: PopskillStore) -> Int? { switch self { - case .updates: return store.pendingUpdateCount > 0 ? store.pendingUpdateCount : nil - default: return nil + case .updates: + return store.pendingUpdateCount > 0 ? store.pendingUpdateCount : nil + case .brokenLinks: + return store.brokenLinkCount > 0 ? store.brokenLinkCount : nil + default: + return nil } } @@ -499,6 +505,8 @@ enum MatrixFilter: String, CaseIterable, Identifiable { return true case .updates: return store.hasPendingUpdate(for: capability) + case .brokenLinks: + return capability.hasBrokenLink || capability.package?.hasBrokenLinks(in: store.skills) == true case .claudeOnly: return capability.apps.claude && !capability.apps.codex case .codexOnly: diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 061a8b2..bd9a9e8 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -154,6 +154,43 @@ struct SkillModelsTests { #expect(skill.enabledAppCount == 3) } + @Test + func skillBrokenLinkDetectionUsesDeploymentStatuses() { + var skill = installedSkill(directory: "demo-skill") + + #expect(skill.hasBrokenLink == false) + + skill.deployment = SkillDeployment( + strategy: "symlink", + ssotPath: "/Users/example/.cc-switch/skills/demo-skill", + appLinks: [ + "claude": AppLinkStatus(path: "/Users/example/.claude/skills/demo-skill", status: "ok"), + "codex": AppLinkStatus(path: "/Users/example/.codex/skills/demo-skill", status: "BROKEN") + ] + ) + + #expect(skill.deployment?.hasBrokenLink == true) + #expect(skill.hasBrokenLink == true) + } + + @Test + func capabilityPackageBrokenLinksAggregateMatchedSkills() { + let package = self.package(components: [component(id: "demo-skill", installed: true)]) + let unrelatedPackage = self.package(components: [component(id: "healthy-skill", installed: true)]) + var skill = installedSkill(directory: "demo-skill") + + skill.deployment = SkillDeployment( + strategy: "symlink", + ssotPath: "/Users/example/.cc-switch/skills/demo-skill", + appLinks: [ + "claude": AppLinkStatus(path: "/Users/example/.claude/skills/demo-skill", status: "broken") + ] + ) + + #expect(package.hasBrokenLinks(in: [skill]) == true) + #expect(unrelatedPackage.hasBrokenLinks(in: [skill]) == false) + } + @Test func targetAppRegistryCoversCurrentSkillTargets() { #expect(TargetAppRegistry.all.map(\.app) == TargetApp.supported) From 7fbf241fb13fff103f39ae738a892fc35218ba1b Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 04:33:42 +0800 Subject: [PATCH 22/47] feat(ui): add matrix sidebar shortcuts --- .../Sources/Popskill/App/PopskillStore.swift | 23 ++++ .../Resources/en.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../Sources/Popskill/Views/RootView.swift | 122 ++++++++++++++++++ .../PopskillTests/PopskillStoreTests.swift | 121 +++++++++++++++-- 5 files changed, 260 insertions(+), 10 deletions(-) diff --git a/swift-app/Sources/Popskill/App/PopskillStore.swift b/swift-app/Sources/Popskill/App/PopskillStore.swift index 42d9a42..9563cd9 100644 --- a/swift-app/Sources/Popskill/App/PopskillStore.swift +++ b/swift-app/Sources/Popskill/App/PopskillStore.swift @@ -243,6 +243,29 @@ final class PopskillStore { var isSearchActive: Bool { !trimmedSearch.isEmpty } + func showMatrix( + filter: MatrixFilter = .all, + typeFilter: MatrixTypeFilter = .allTypes, + clearSearch: Bool = true + ) { + currentSelection = .matrix + matrixFilter = filter + matrixTypeFilter = typeFilter + inspectorOpen = false + selectedSkillID = nil + if clearSearch { + searchText = "" + } + } + + func matrixFilterCount(_ filter: MatrixFilter) -> Int { + capabilities.filter { filter.includes(capability: $0, store: self) }.count + } + + func matrixTypeFilterCount(_ typeFilter: MatrixTypeFilter) -> Int { + capabilities.filter { typeFilter.includes(capability: $0) }.count + } + /// O(1) update lookup for matrix rows and filters. `SkillUpdateInfo.id` /// may be scoped ("owner/name:skill") or path-like, so both the full id /// and its useful suffixes are indexed once when `updates` changes. diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 5f28466..8416683 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -304,6 +304,8 @@ "sidebar.section.control" = "CONTROL"; "sidebar.section.sources" = "SOURCES"; "sidebar.section.maintenance" = "MAINTENANCE"; +"sidebar.matrixFilters" = "Filters"; +"sidebar.matrixTypes" = "Types"; // S3 新增 — Matrix + Inspector "matrix.subtitle" = "%d bundles · %d capabilities · %d active toggles"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index a550dbd..7c23e69 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -304,6 +304,8 @@ "sidebar.section.control" = "操控台"; "sidebar.section.sources" = "来源"; "sidebar.section.maintenance" = "维护"; +"sidebar.matrixFilters" = "筛选"; +"sidebar.matrixTypes" = "类型"; // S3 新增 — 矩阵主视图 + Inspector "matrix.subtitle" = "%d 个套装 · %d 项能力 · %d 个已启用开关"; diff --git a/swift-app/Sources/Popskill/Views/RootView.swift b/swift-app/Sources/Popskill/Views/RootView.swift index fde6b24..9a39687 100644 --- a/swift-app/Sources/Popskill/Views/RootView.swift +++ b/swift-app/Sources/Popskill/Views/RootView.swift @@ -69,6 +69,7 @@ struct RootView: View { )) { Section { row(.matrix) + matrixShortcutRows } header: { sectionHeader(.control) } Section { @@ -129,6 +130,127 @@ struct RootView: View { } } + @ViewBuilder + private var matrixShortcutRows: some View { + let statusFilters: [MatrixFilter] = [.claudeOnly, .codexOnly, .brokenLinks] + let typeFilters: [MatrixTypeFilter] = [.bundle, .skill, .agent, .mcp, .cli] + if !store.capabilities.isEmpty { + VStack(alignment: .leading, spacing: 7) { + shortcutHeader("sidebar.matrixFilters") + ForEach(statusFilters) { filter in + let count = store.matrixFilterCount(filter) + if count > 0 { + sidebarShortcutButton( + titleKey: filter.titleKey, + count: count, + symbolName: sidebarSymbol(for: filter), + warning: filter == .brokenLinks, + active: store.currentSelection == .matrix + && store.matrixFilter == filter + && store.matrixTypeFilter == .allTypes + ) { + store.showMatrix(filter: filter) + } + } + } + + shortcutHeader("sidebar.matrixTypes") + .padding(.top, 4) + ForEach(typeFilters) { filter in + let count = store.matrixTypeFilterCount(filter) + if count > 0 { + sidebarShortcutButton( + titleKey: filter.titleKey, + count: count, + symbolName: sidebarSymbol(for: filter), + active: store.currentSelection == .matrix + && store.matrixFilter == .all + && store.matrixTypeFilter == filter + ) { + store.showMatrix(typeFilter: filter) + } + } + } + } + .padding(.top, 2) + .padding(.bottom, 4) + .listRowInsets(EdgeInsets(top: 2, leading: 18, bottom: 6, trailing: 12)) + } + } + + private func shortcutHeader(_ key: String) -> some View { + Text(localization.string(key)) + .font(.system(size: 10.5, weight: .semibold)) + .foregroundStyle(Color.popTertiaryLabel) + .textCase(.uppercase) + .tracking(0.4) + .padding(.leading, 4) + } + + private func sidebarShortcutButton( + titleKey: String, + count: Int, + symbolName: String, + warning: Bool = false, + active: Bool, + action: @escaping () -> Void + ) -> some View { + Button(action: action) { + HStack(spacing: 7) { + Image(systemName: symbolName) + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(active ? Color.accentColor : Color.popSecondaryLabel) + .frame(width: 16) + Text(localization.string(titleKey)) + .font(.system(size: 12)) + .foregroundStyle(Color.popSidebarTitle) + .lineLimit(1) + Spacer(minLength: 8) + Text("\(count)") + .font(.system(size: 10.5, weight: .semibold).monospacedDigit()) + .foregroundStyle(warning && count > 0 ? Color.popStatusWarning : Color.popSecondaryLabel) + .padding(.horizontal, 5) + .padding(.vertical, 1.5) + .background( + Capsule().fill( + (warning && count > 0 ? Color.popStatusWarning : Color.popSecondaryLabel).opacity(0.13) + ) + ) + } + .padding(.horizontal, 6) + .padding(.vertical, 4) + .background( + active ? Color.accentColor.opacity(0.12) : Color.clear, + in: RoundedRectangle(cornerRadius: 6, style: .continuous) + ) + .contentShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) + } + .buttonStyle(.plain) + .accessibilityAddTraits(active ? .isSelected : []) + } + + private func sidebarSymbol(for filter: MatrixFilter) -> String { + switch filter { + case .all: return "square.grid.3x3" + case .updates: return "arrow.triangle.2.circlepath" + case .brokenLinks: return "exclamationmark.triangle" + case .claudeOnly: return TargetApp.claude.symbolName + case .codexOnly: return TargetApp.codex.symbolName + case .inactive: return "moon" + } + } + + private func sidebarSymbol(for filter: MatrixTypeFilter) -> String { + switch filter { + case .allTypes: return "square.stack.3d.up" + case .bundle: return CapabilityKind.bundle.symbol + case .skill: return CapabilityKind.skill.symbol + case .agent: return CapabilityKind.agent.symbol + case .cli: return CapabilityKind.cli.symbol + case .mcp: return CapabilityKind.mcp.symbol + } + } + // MARK: Error toast private func errorToast(message: String) -> some View { diff --git a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift index 5ce4dfc..fcf87eb 100644 --- a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift +++ b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift @@ -35,6 +35,95 @@ struct PopskillStoreTests { #expect(store.inspectorOpen == false) } + @Test + func showMatrixShortcutSetsFiltersAndClearsTransientContext() { + let store = PopskillStore() + store.currentSelection = .settings + store.searchText = "baoyu" + store.matrixFilter = .claudeOnly + store.matrixTypeFilter = .skill + store.selectedSkillID = "skill:demo" + store.inspectorOpen = true + + store.showMatrix(filter: .brokenLinks) + + #expect(store.currentSelection == .matrix) + #expect(store.searchText == "") + #expect(store.matrixFilter == .brokenLinks) + #expect(store.matrixTypeFilter == .allTypes) + #expect(store.selectedSkillID == nil) + #expect(store.inspectorOpen == false) + + store.searchText = "keep" + store.showMatrix(typeFilter: .bundle, clearSearch: false) + + #expect(store.searchText == "keep") + #expect(store.matrixFilter == .all) + #expect(store.matrixTypeFilter == .bundle) + } + + @Test + func matrixShortcutCountsUseCurrentCapabilities() { + let store = PopskillStore() + var claudeOnly = skillFixture( + id: "claude-only", + apps: SkillApps(claude: true, codex: false, gemini: false, opencode: false, hermes: false) + ) + claudeOnly.deployment = SkillDeployment( + strategy: "symlink", + ssotPath: "/Users/example/.cc-switch/skills/claude-only", + appLinks: [ + "claude": AppLinkStatus(path: "/Users/example/.claude/skills/claude-only", status: "broken") + ] + ) + store.skills = [ + claudeOnly, + skillFixture( + id: "codex-only", + apps: SkillApps(claude: false, codex: true, gemini: false, opencode: false, hermes: false) + ), + skillFixture( + id: "both", + apps: SkillApps(claude: true, codex: true, gemini: false, opencode: false, hermes: false) + ) + ] + store.localAgents = [ + LocalAgent( + id: "agent:demo", + name: "Demo Agent", + description: "Agent", + fileName: "demo.md", + path: "/tmp/demo.md", + category: "ops", + tools: [], + model: nil, + lastModifiedAt: nil, + sizeBytes: 100 + ) + ] + store.packages = [ + CapabilityPackage( + id: "pkg:empty", + type: .composite, + name: "Empty Bundle", + vendor: nil, + summary: "Bundle", + source: PackageSource(kind: "builtin", location: "empty", updateStrategy: "manual", repoOwner: nil, repoName: nil, repoBranch: nil, readmeUrl: nil), + components: PackageComponents(cli: [], skills: [], mcp: [], agents: []), + configSchema: [], + installed: false, + lifecycle: nil + ) + ] + + #expect(store.matrixFilterCount(.claudeOnly) == 2) + #expect(store.matrixFilterCount(.codexOnly) == 1) + #expect(store.matrixFilterCount(.brokenLinks) == 1) + #expect(store.matrixTypeFilterCount(.bundle) == 1) + #expect(store.matrixTypeFilterCount(.skill) == 3) + #expect(store.matrixTypeFilterCount(.agent) == 1) + } + @Test func capabilitiesExposeCompositePackagesBeforeAtomicRows() { let store = PopskillStore() @@ -164,22 +253,34 @@ struct PopskillStoreTests { private func stubFixture(id: String, stubbedAt: Int) -> StubbedSkill { StubbedSkill( - skill: Skill( + skill: skillFixture( id: id, - name: id, description: "Stub fixture", - directory: id, - repoOwner: nil, - repoName: nil, - readmeUrl: nil, - apps: SkillApps(claude: false, codex: false, gemini: false, opencode: false, hermes: false), - installedAt: nil, - updatedAt: nil, - contentHash: nil + apps: SkillApps(claude: false, codex: false, gemini: false, opencode: false, hermes: false) ), backupId: "backup-\(id)", backupPath: "/tmp/backup-\(id)", stubbedAt: stubbedAt ) } + + private func skillFixture( + id: String, + description: String = "Skill", + apps: SkillApps + ) -> Skill { + Skill( + id: id, + name: id, + description: description, + directory: id, + repoOwner: nil, + repoName: nil, + readmeUrl: nil, + apps: apps, + installedAt: nil, + updatedAt: nil, + contentHash: nil + ) + } } From c58bf884bfb6bf078fbf0c324324b5b2ec3d31b5 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 04:41:06 +0800 Subject: [PATCH 23/47] feat(ui): surface sidebar sync status --- .../Sources/Popskill/App/PopskillStore.swift | 4 ++ .../Resources/en.lproj/Localizable.strings | 5 ++ .../zh-Hans.lproj/Localizable.strings | 5 ++ .../Sources/Popskill/Views/RootView.swift | 72 +++++++++++++++++++ .../PopskillTests/PopskillStoreTests.swift | 14 ++++ 5 files changed, 100 insertions(+) diff --git a/swift-app/Sources/Popskill/App/PopskillStore.swift b/swift-app/Sources/Popskill/App/PopskillStore.swift index 9563cd9..f365d55 100644 --- a/swift-app/Sources/Popskill/App/PopskillStore.swift +++ b/swift-app/Sources/Popskill/App/PopskillStore.swift @@ -258,6 +258,10 @@ final class PopskillStore { } } + func showSettings() { + currentSelection = .settings + } + func matrixFilterCount(_ filter: MatrixFilter) -> Int { capabilities.filter { filter.includes(capability: $0, store: self) }.count } diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 8416683..83ec416 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -306,6 +306,11 @@ "sidebar.section.maintenance" = "MAINTENANCE"; "sidebar.matrixFilters" = "Filters"; "sidebar.matrixTypes" = "Types"; +"sidebar.sync" = "Sync"; +"sidebar.sync.settings" = "Settings"; +"sidebar.sync.openSettings" = "Open sync settings"; +"sidebar.sync.never" = "Not synced yet"; +"sidebar.sync.last" = "Last %@"; // S3 新增 — Matrix + Inspector "matrix.subtitle" = "%d bundles · %d capabilities · %d active toggles"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 7c23e69..5a4deb1 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -306,6 +306,11 @@ "sidebar.section.maintenance" = "维护"; "sidebar.matrixFilters" = "筛选"; "sidebar.matrixTypes" = "类型"; +"sidebar.sync" = "同步"; +"sidebar.sync.settings" = "设置"; +"sidebar.sync.openSettings" = "打开同步设置"; +"sidebar.sync.never" = "尚未同步"; +"sidebar.sync.last" = "上次 %@"; // S3 新增 — 矩阵主视图 + Inspector "matrix.subtitle" = "%d 个套装 · %d 项能力 · %d 个已启用开关"; diff --git a/swift-app/Sources/Popskill/Views/RootView.swift b/swift-app/Sources/Popskill/Views/RootView.swift index 9a39687..05b887a 100644 --- a/swift-app/Sources/Popskill/Views/RootView.swift +++ b/swift-app/Sources/Popskill/Views/RootView.swift @@ -8,6 +8,11 @@ import SwiftUI struct RootView: View { @State private var store = PopskillStore() @Environment(\.popskillLocalization) private var localization + private static let sidebarRelativeFormatter: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .short + return formatter + }() var body: some View { ZStack(alignment: .top) { @@ -70,6 +75,7 @@ struct RootView: View { Section { row(.matrix) matrixShortcutRows + sidebarSyncStatus } header: { sectionHeader(.control) } Section { @@ -178,6 +184,72 @@ struct RootView: View { } } + private var sidebarSyncStatus: some View { + let provider = SyncProvider(rawValue: store.lastSyncProvider) ?? .git + let statusTint = provider.actionable ? Color.accentColor : Color.popStatusWarning + return Button { + store.showSettings() + } label: { + VStack(alignment: .leading, spacing: 7) { + HStack(spacing: 6) { + Text(localization.string("sidebar.sync")) + .font(.system(size: 10.5, weight: .semibold)) + .foregroundStyle(Color.popTertiaryLabel) + .textCase(.uppercase) + .tracking(0.4) + Spacer(minLength: 8) + Label(localization.string("sidebar.sync.settings"), systemImage: "gearshape") + .font(.system(size: 10.5, weight: .medium)) + .labelStyle(.titleAndIcon) + .foregroundStyle(Color.popSecondaryLabel) + } + HStack(spacing: 8) { + Image(systemName: provider.symbol) + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(statusTint) + .frame(width: 16) + VStack(alignment: .leading, spacing: 1) { + Text(localization.string(provider.titleKey)) + .font(.system(size: 12.2, weight: .medium)) + .foregroundStyle(Color.popSidebarTitle) + .lineLimit(1) + Text(syncStatusSubtitle(provider: provider)) + .font(.system(size: 10.5)) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + } + Spacer(minLength: 8) + Circle() + .fill(statusTint) + .frame(width: 7, height: 7) + .accessibilityHidden(true) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 9) + .background(Color.popControlFill.opacity(0.62), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .strokeBorder(Color.popControlStroke.opacity(0.75), lineWidth: 0.7) + ) + .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + .buttonStyle(.plain) + .help(localization.string("sidebar.sync.openSettings")) + .listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 6, trailing: 12)) + } + + private func syncStatusSubtitle(provider: SyncProvider) -> String { + if !provider.actionable { + return localization.string("settings.sync.soon") + } + guard let lastSyncAt = store.lastSyncAt else { + return localization.string("sidebar.sync.never") + } + let relative = Self.sidebarRelativeFormatter.localizedString(for: lastSyncAt, relativeTo: Date()) + return localization.string("sidebar.sync.last", relative) + } + private func shortcutHeader(_ key: String) -> some View { Text(localization.string(key)) .font(.system(size: 10.5, weight: .semibold)) diff --git a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift index fcf87eb..02060ec 100644 --- a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift +++ b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift @@ -62,6 +62,20 @@ struct PopskillStoreTests { #expect(store.matrixTypeFilter == .bundle) } + @Test + func showSettingsSelectsSettingsWithoutChangingMatrixFilters() { + let store = PopskillStore() + store.currentSelection = .matrix + store.matrixFilter = .brokenLinks + store.matrixTypeFilter = .bundle + + store.showSettings() + + #expect(store.currentSelection == .settings) + #expect(store.matrixFilter == .brokenLinks) + #expect(store.matrixTypeFilter == .bundle) + } + @Test func matrixShortcutCountsUseCurrentCapabilities() { let store = PopskillStore() From 6d338d396d5b065e0d47c0d0d33aeaafdf6a8a72 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 04:49:26 +0800 Subject: [PATCH 24/47] feat(ui): add matrix version column --- .../Models/MatrixVersionFormatter.swift | 29 +++++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 1 + .../zh-Hans.lproj/Localizable.strings | 1 + .../Popskill/Views/MatrixPackageRow.swift | 21 ++++++++++++++ .../Sources/Popskill/Views/MatrixRow.swift | 29 ++++++++++++++++++- .../Sources/Popskill/Views/MatrixView.swift | 3 ++ .../PopskillTests/SkillModelsTests.swift | 7 +++++ 7 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 swift-app/Sources/Popskill/Models/MatrixVersionFormatter.swift diff --git a/swift-app/Sources/Popskill/Models/MatrixVersionFormatter.swift b/swift-app/Sources/Popskill/Models/MatrixVersionFormatter.swift new file mode 100644 index 0000000..39a5b9a --- /dev/null +++ b/swift-app/Sources/Popskill/Models/MatrixVersionFormatter.swift @@ -0,0 +1,29 @@ +import Foundation + +enum MatrixVersionFormatter { + static func value(contentHash: String?, updatedAt: Int?) -> String? { + if let hash = shortHash(contentHash) { + return hash + } + if let updatedAt, updatedAt > 0 { + return dateString(updatedAt) + } + return nil + } + + static func shortHash(_ hash: String?) -> String? { + guard let hash = hash?.trimmingCharacters(in: .whitespacesAndNewlines), + !hash.isEmpty else { + return nil + } + return String(hash.prefix(7)) + } + + private static func dateString(_ timestamp: Int) -> String { + let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withFullDate] + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter.string(from: date) + } +} diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 83ec416..11ebbde 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -324,6 +324,7 @@ "matrix.sort.typeDescending" = "Sort: type"; "matrix.col.capability" = "Capability"; "matrix.col.source" = "Source"; +"matrix.col.version" = "Version"; "matrix.col.tokens" = "Tokens"; "matrix.col.calls" = "Calls"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 5a4deb1..da2d527 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -324,6 +324,7 @@ "matrix.sort.typeDescending" = "排序:类型"; "matrix.col.capability" = "能力"; "matrix.col.source" = "来源"; +"matrix.col.version" = "版本"; "matrix.col.tokens" = "Tokens"; "matrix.col.calls" = "调用"; diff --git a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift index 9f9328d..546dfea 100644 --- a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift @@ -55,6 +55,8 @@ struct MatrixPackageRow: View { sourceCell .frame(width: MatrixTableLayout.sourceColumnWidth, alignment: .leading) + versionCell + .frame(width: MatrixTableLayout.versionColumnWidth, alignment: .leading) tokensCell .frame(width: MatrixTableLayout.tokensColumnWidth, alignment: .trailing) @@ -209,6 +211,13 @@ struct MatrixPackageRow: View { usageIndex.packageSnapshot(for: package?.id) } + private var versionCell: some View { + MatrixVersionValueCell(value: MatrixVersionFormatter.value( + contentHash: package?.trackedContentHash, + updatedAt: package?.lifecycle?.updatedAt + )) + } + private var tokensCell: some View { MatrixUsageValueCell(value: usageText { snapshot in UsageDisplayFormatter.compactTokens(snapshot.totalTokens) @@ -314,6 +323,8 @@ private struct MatrixPackageComponentRow: View { sourceCell .frame(width: MatrixTableLayout.sourceColumnWidth, alignment: .leading) + versionCell + .frame(width: MatrixTableLayout.versionColumnWidth, alignment: .leading) tokensCell .frame(width: MatrixTableLayout.tokensColumnWidth, alignment: .trailing) @@ -409,6 +420,16 @@ private struct MatrixPackageComponentRow: View { usageIndex.packageComponentStat(packageID: packageID, componentID: component.id) } + private var versionCell: some View { + MatrixVersionValueCell( + value: MatrixVersionFormatter.value( + contentHash: matchingSkill?.contentHash, + updatedAt: matchingSkill?.updatedAt + ), + isSubtle: true + ) + } + private var tokensCell: some View { MatrixUsageValueCell(value: usageText { stat in UsageDisplayFormatter.compactTokens(stat.totalTokens) diff --git a/swift-app/Sources/Popskill/Views/MatrixRow.swift b/swift-app/Sources/Popskill/Views/MatrixRow.swift index 461014f..4c80876 100644 --- a/swift-app/Sources/Popskill/Views/MatrixRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixRow.swift @@ -2,7 +2,7 @@ import SwiftUI /// One capability row inside the matrix. Layout mirrors `matrixColumnHeader` /// in `MatrixView.swift`: capability column (flexible), tool coverage, -/// source, usage metrics, and action menu. +/// source, version identity, usage metrics, and action menu. /// Renders Skill / Agent / CLI / MCP / Config via the unified /// `MatrixCapability` model; non-toggleable kinds (anything but skill) show /// a read-only "on" icon instead of the interactive switch. @@ -37,6 +37,8 @@ struct MatrixRow: View { sourceCell .frame(width: MatrixTableLayout.sourceColumnWidth, alignment: .leading) + versionCell + .frame(width: MatrixTableLayout.versionColumnWidth, alignment: .leading) tokensCell .frame(width: MatrixTableLayout.tokensColumnWidth, alignment: .trailing) @@ -201,6 +203,16 @@ struct MatrixRow: View { usageIndex.skillSnapshot(for: capability.underlyingSkillID) } + private var versionCell: some View { + let skill = capability.underlyingSkillID.flatMap { skillID in + store.skills.first { $0.id == skillID } + } + return MatrixVersionValueCell(value: MatrixVersionFormatter.value( + contentHash: skill?.contentHash, + updatedAt: capability.updatedAt + )) + } + private var tokensCell: some View { MatrixUsageValueCell(value: usageText { snapshot in UsageDisplayFormatter.compactTokens(snapshot.totalTokens) @@ -282,6 +294,21 @@ struct MatrixRow: View { } } +struct MatrixVersionValueCell: View { + let value: String? + var isSubtle = false + + var body: some View { + Text(value ?? "—") + .font(.system(size: isSubtle ? 10.3 : 10.8, weight: .medium, design: .monospaced)) + .foregroundStyle(value == nil ? Color.popTertiaryLabel : (isSubtle ? Color.popSecondaryLabel : Color.popLabel)) + .lineLimit(1) + .truncationMode(.middle) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.trailing, 8) + } +} + struct MatrixUsageValueCell: View { let value: String? var isSubtle = false diff --git a/swift-app/Sources/Popskill/Views/MatrixView.swift b/swift-app/Sources/Popskill/Views/MatrixView.swift index c812c2b..9fbc800 100644 --- a/swift-app/Sources/Popskill/Views/MatrixView.swift +++ b/swift-app/Sources/Popskill/Views/MatrixView.swift @@ -3,6 +3,7 @@ import SwiftUI enum MatrixTableLayout { static let appColumnWidth: CGFloat = 92 static let sourceColumnWidth: CGFloat = 184 + static let versionColumnWidth: CGFloat = 86 static let tokensColumnWidth: CGFloat = 78 static let callsColumnWidth: CGFloat = 62 static let actionColumnWidth: CGFloat = 52 @@ -328,6 +329,8 @@ struct MatrixView: View { .frame(width: MatrixTableLayout.appColumnWidth, alignment: .center) Text(localization.string("matrix.col.source")) .frame(width: MatrixTableLayout.sourceColumnWidth, alignment: .leading) + Text(localization.string("matrix.col.version")) + .frame(width: MatrixTableLayout.versionColumnWidth, alignment: .leading) Text(localization.string("matrix.col.tokens")) .frame(width: MatrixTableLayout.tokensColumnWidth, alignment: .trailing) Text(localization.string("matrix.col.calls")) diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index bd9a9e8..b82c657 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -1214,6 +1214,13 @@ struct SkillModelsTests { #expect(package.trackedContentHash == nil) } + @Test + func matrixVersionFormatterPrefersHashThenUpdatedDate() { + #expect(MatrixVersionFormatter.value(contentHash: " abcdef123456 ", updatedAt: 1_700_000_000) == "abcdef1") + #expect(MatrixVersionFormatter.value(contentHash: nil, updatedAt: 1_700_000_000) == "2023-11-14") + #expect(MatrixVersionFormatter.value(contentHash: " ", updatedAt: 0) == nil) + } + @Test func capabilityPackageMatchesScopedSkillUpdateIdentifier() { let package = self.package( From f0b283ae4a581e1221c8622fb79c12094f8deed8 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 04:57:34 +0800 Subject: [PATCH 25/47] perf(ui): cache matrix sidebar counts --- .../Sources/Popskill/App/PopskillStore.swift | 39 +++++++++++++++++++ .../Sources/Popskill/Views/RootView.swift | 7 ++-- .../PopskillTests/PopskillStoreTests.swift | 17 +++++--- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/swift-app/Sources/Popskill/App/PopskillStore.swift b/swift-app/Sources/Popskill/App/PopskillStore.swift index f365d55..40a9cfb 100644 --- a/swift-app/Sources/Popskill/App/PopskillStore.swift +++ b/swift-app/Sources/Popskill/App/PopskillStore.swift @@ -270,6 +270,21 @@ final class PopskillStore { capabilities.filter { typeFilter.includes(capability: $0) }.count } + func matrixShortcutCounts() -> MatrixShortcutCounts { + let currentCapabilities = capabilities + let filterCounts = Dictionary(uniqueKeysWithValues: MatrixFilter.allCases.map { filter in + (filter, currentCapabilities.filter { filter.includes(capability: $0, store: self) }.count) + }) + let typeCounts = Dictionary(uniqueKeysWithValues: MatrixTypeFilter.allCases.map { filter in + (filter, currentCapabilities.filter { filter.includes(capability: $0) }.count) + }) + return MatrixShortcutCounts( + capabilityCount: currentCapabilities.count, + filterCounts: filterCounts, + typeCounts: typeCounts + ) + } + /// O(1) update lookup for matrix rows and filters. `SkillUpdateInfo.id` /// may be scoped ("owner/name:skill") or path-like, so both the full id /// and its useful suffixes are indexed once when `updates` changes. @@ -390,3 +405,27 @@ final class PopskillStore { Set(updates.flatMap(\.normalizedIdentifierCandidates)) } } + +struct MatrixShortcutCounts: Equatable { + let capabilityCount: Int + private let filterCounts: [MatrixFilter: Int] + private let typeCounts: [MatrixTypeFilter: Int] + + init( + capabilityCount: Int, + filterCounts: [MatrixFilter: Int], + typeCounts: [MatrixTypeFilter: Int] + ) { + self.capabilityCount = capabilityCount + self.filterCounts = filterCounts + self.typeCounts = typeCounts + } + + func count(for filter: MatrixFilter) -> Int { + filterCounts[filter] ?? 0 + } + + func count(for filter: MatrixTypeFilter) -> Int { + typeCounts[filter] ?? 0 + } +} diff --git a/swift-app/Sources/Popskill/Views/RootView.swift b/swift-app/Sources/Popskill/Views/RootView.swift index 05b887a..329118e 100644 --- a/swift-app/Sources/Popskill/Views/RootView.swift +++ b/swift-app/Sources/Popskill/Views/RootView.swift @@ -140,11 +140,12 @@ struct RootView: View { private var matrixShortcutRows: some View { let statusFilters: [MatrixFilter] = [.claudeOnly, .codexOnly, .brokenLinks] let typeFilters: [MatrixTypeFilter] = [.bundle, .skill, .agent, .mcp, .cli] - if !store.capabilities.isEmpty { + let counts = store.matrixShortcutCounts() + if counts.capabilityCount > 0 { VStack(alignment: .leading, spacing: 7) { shortcutHeader("sidebar.matrixFilters") ForEach(statusFilters) { filter in - let count = store.matrixFilterCount(filter) + let count = counts.count(for: filter) if count > 0 { sidebarShortcutButton( titleKey: filter.titleKey, @@ -163,7 +164,7 @@ struct RootView: View { shortcutHeader("sidebar.matrixTypes") .padding(.top, 4) ForEach(typeFilters) { filter in - let count = store.matrixTypeFilterCount(filter) + let count = counts.count(for: filter) if count > 0 { sidebarShortcutButton( titleKey: filter.titleKey, diff --git a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift index 02060ec..4d2b70a 100644 --- a/swift-app/Tests/PopskillTests/PopskillStoreTests.swift +++ b/swift-app/Tests/PopskillTests/PopskillStoreTests.swift @@ -130,12 +130,17 @@ struct PopskillStoreTests { ) ] - #expect(store.matrixFilterCount(.claudeOnly) == 2) - #expect(store.matrixFilterCount(.codexOnly) == 1) - #expect(store.matrixFilterCount(.brokenLinks) == 1) - #expect(store.matrixTypeFilterCount(.bundle) == 1) - #expect(store.matrixTypeFilterCount(.skill) == 3) - #expect(store.matrixTypeFilterCount(.agent) == 1) + let counts = store.matrixShortcutCounts() + + #expect(counts.capabilityCount == 5) + #expect(counts.count(for: .claudeOnly) == 2) + #expect(counts.count(for: .codexOnly) == 1) + #expect(counts.count(for: .brokenLinks) == 1) + #expect(counts.count(for: .bundle) == 1) + #expect(counts.count(for: .skill) == 3) + #expect(counts.count(for: .agent) == 1) + #expect(store.matrixFilterCount(.claudeOnly) == counts.count(for: .claudeOnly)) + #expect(store.matrixTypeFilterCount(.bundle) == counts.count(for: .bundle)) } @Test From 4e5216529ecc7467358412b71b06bca95eb29821 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 05:07:09 +0800 Subject: [PATCH 26/47] feat(ui): badge broken matrix rows --- .../Popskill/Models/MatrixCapability.swift | 4 ++++ .../Resources/en.lproj/Localizable.strings | 2 ++ .../zh-Hans.lproj/Localizable.strings | 2 ++ .../Sources/Popskill/Views/MatrixBadges.swift | 17 +++++++++++++++++ .../Popskill/Views/MatrixPackageRow.swift | 6 ++++++ .../Sources/Popskill/Views/MatrixRow.swift | 3 +++ .../PopskillTests/SkillModelsTests.swift | 19 +++++++++++++++++++ 7 files changed, 53 insertions(+) create mode 100644 swift-app/Sources/Popskill/Views/MatrixBadges.swift diff --git a/swift-app/Sources/Popskill/Models/MatrixCapability.swift b/swift-app/Sources/Popskill/Models/MatrixCapability.swift index ffd04db..6156a1f 100644 --- a/swift-app/Sources/Popskill/Models/MatrixCapability.swift +++ b/swift-app/Sources/Popskill/Models/MatrixCapability.swift @@ -141,6 +141,10 @@ struct MatrixCapability: Identifiable, Equatable { deployment?.hasBrokenLink == true } + func hasBrokenLinks(in skills: [Skill]) -> Bool { + hasBrokenLink || package?.hasBrokenLinks(in: skills) == true + } + static func capabilityID(kind: CapabilityKind, rawID: String) -> String { "\(kind.rawValue):\(rawID)" } diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 11ebbde..5509acf 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -348,6 +348,8 @@ "matrix.group.ungrouped" = "Other"; +"matrix.row.brokenLinkBadge" = "Broken"; +"matrix.row.brokenLinkHelp" = "One or more app links are broken"; "matrix.row.updateBadge" = "UPDATE"; "matrix.row.noSummary" = "No summary"; "matrix.row.menu.inspect" = "Open Inspector"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index da2d527..a81930b 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -348,6 +348,8 @@ "matrix.group.ungrouped" = "其他"; +"matrix.row.brokenLinkBadge" = "断链"; +"matrix.row.brokenLinkHelp" = "至少一个应用链接已断开"; "matrix.row.updateBadge" = "可更新"; "matrix.row.noSummary" = "暂无说明"; "matrix.row.menu.inspect" = "打开 Inspector"; diff --git a/swift-app/Sources/Popskill/Views/MatrixBadges.swift b/swift-app/Sources/Popskill/Views/MatrixBadges.swift new file mode 100644 index 0000000..dd57310 --- /dev/null +++ b/swift-app/Sources/Popskill/Views/MatrixBadges.swift @@ -0,0 +1,17 @@ +import SwiftUI + +struct MatrixBrokenLinkBadge: View { + @Environment(\.popskillLocalization) private var localization + + var body: some View { + Label(localization.string("matrix.row.brokenLinkBadge"), systemImage: "exclamationmark.triangle.fill") + .font(.system(size: 9.5, weight: .semibold)) + .labelStyle(.titleAndIcon) + .foregroundStyle(Color.popStatusError) + .padding(.horizontal, 4) + .padding(.vertical, 1) + .background(Color.popStatusError.opacity(0.13), in: Capsule()) + .fixedSize(horizontal: true, vertical: false) + .help(localization.string("matrix.row.brokenLinkHelp")) + } +} diff --git a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift index 546dfea..4a66b28 100644 --- a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift @@ -116,6 +116,9 @@ struct MatrixPackageRow: View { if let package { healthBadge(package.health) } + if capability.hasBrokenLinks(in: store.skills) { + MatrixBrokenLinkBadge() + } if store.hasPendingUpdate(for: capability) { Text(localization.string("matrix.row.updateBadge")) .font(.system(size: 9.5, weight: .semibold)) @@ -372,6 +375,9 @@ private struct MatrixPackageComponentRow: View { .font(.system(size: 8.5, weight: .semibold)) .foregroundStyle(requirement.color) } + if matchingSkill?.hasBrokenLink == true { + MatrixBrokenLinkBadge() + } } Text(component.status) .font(.system(size: 10.2)) diff --git a/swift-app/Sources/Popskill/Views/MatrixRow.swift b/swift-app/Sources/Popskill/Views/MatrixRow.swift index 4c80876..c2059bb 100644 --- a/swift-app/Sources/Popskill/Views/MatrixRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixRow.swift @@ -83,6 +83,9 @@ struct MatrixRow: View { .foregroundStyle(Color.popLabel) .lineLimit(1) kindBadge + if capability.hasBrokenLinks(in: store.skills) { + MatrixBrokenLinkBadge() + } if hasUpdate { Text(localization.string("matrix.row.updateBadge")) .font(.system(size: 9.5, weight: .semibold)) diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index b82c657..33c76ad 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -191,6 +191,25 @@ struct SkillModelsTests { #expect(unrelatedPackage.hasBrokenLinks(in: [skill]) == false) } + @Test + func matrixCapabilityBrokenLinksAggregateDirectAndPackageLinks() { + let package = self.package(components: [component(id: "demo-skill", installed: true)]) + let unrelatedPackage = self.package(components: [component(id: "healthy-skill", installed: true)]) + var skill = installedSkill(directory: "demo-skill") + + skill.deployment = SkillDeployment( + strategy: "symlink", + ssotPath: "/Users/example/.cc-switch/skills/demo-skill", + appLinks: [ + "codex": AppLinkStatus(path: "/Users/example/.codex/skills/demo-skill", status: "broken") + ] + ) + + #expect(MatrixCapability.fromSkill(skill).hasBrokenLinks(in: []) == true) + #expect(MatrixCapability.fromPackage(package, skills: [skill]).hasBrokenLinks(in: [skill]) == true) + #expect(MatrixCapability.fromPackage(unrelatedPackage, skills: [skill]).hasBrokenLinks(in: [skill]) == false) + } + @Test func targetAppRegistryCoversCurrentSkillTargets() { #expect(TargetAppRegistry.all.map(\.app) == TargetApp.supported) From fba0aecc98abe6ccd71d63d53cb32f20f6dbfa75 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 05:18:24 +0800 Subject: [PATCH 27/47] feat(ui): make matrix sort interactive --- .../Sources/Popskill/App/PopskillStore.swift | 1 + .../Popskill/Models/SkillGrouping.swift | 125 ++++++++++++++++-- .../Resources/en.lproj/Localizable.strings | 7 + .../zh-Hans.lproj/Localizable.strings | 9 +- .../Sources/Popskill/Views/MatrixView.swift | 50 ++++++- .../PopskillTests/SkillGroupingTests.swift | 79 +++++++++++ 6 files changed, 253 insertions(+), 18 deletions(-) diff --git a/swift-app/Sources/Popskill/App/PopskillStore.swift b/swift-app/Sources/Popskill/App/PopskillStore.swift index 40a9cfb..d29e7b6 100644 --- a/swift-app/Sources/Popskill/App/PopskillStore.swift +++ b/swift-app/Sources/Popskill/App/PopskillStore.swift @@ -70,6 +70,7 @@ final class PopskillStore { // ===== Matrix state ===== var matrixFilter: MatrixFilter = .all var matrixTypeFilter: MatrixTypeFilter = .allTypes + var matrixSortMode: MatrixSortMode = .typeDescending /// Repo groups the user has explicitly collapsed. Set is keyed by /// `MatrixGroup.id` (== "owner/name" or "ungrouped"). var collapsedGroups: Set = [] diff --git a/swift-app/Sources/Popskill/Models/SkillGrouping.swift b/swift-app/Sources/Popskill/Models/SkillGrouping.swift index a84eb03..09a68c5 100644 --- a/swift-app/Sources/Popskill/Models/SkillGrouping.swift +++ b/swift-app/Sources/Popskill/Models/SkillGrouping.swift @@ -31,25 +31,73 @@ struct CapabilitySection: Identifiable, Equatable { var totalCount: Int { groups.reduce(0) { $0 + $1.capabilities.count } } } +enum MatrixSortMode: String, CaseIterable, Identifiable { + case typeDescending + case typeAscending + case nameAscending + case nameDescending + case callsDescending + case tokensDescending + case recentDescending + + var id: String { rawValue } + + var titleKey: String { + switch self { + case .typeDescending: return "matrix.sort.typeDescending" + case .typeAscending: return "matrix.sort.typeAscending" + case .nameAscending: return "matrix.sort.nameAscending" + case .nameDescending: return "matrix.sort.nameDescending" + case .callsDescending: return "matrix.sort.callsDescending" + case .tokensDescending: return "matrix.sort.tokensDescending" + case .recentDescending: return "matrix.sort.recentDescending" + } + } + + var symbolName: String { + switch self { + case .typeDescending: return "arrow.down" + case .typeAscending: return "arrow.up" + case .nameAscending: return "textformat.abc" + case .nameDescending: return "textformat.abc.dottedunderline" + case .callsDescending: return "phone.arrow.up.right" + case .tokensDescending: return "number" + case .recentDescending: return "clock.arrow.circlepath" + } + } +} + enum SkillGrouping { static let ungroupedID = "ungrouped" /// Group capabilities first by `CapabilityKind`, then by `owner/name` - /// source bucket. Inside each bucket capabilities sort by name - /// (case-insensitive). Ungrouped is always pinned at the bottom of each - /// kind section, and kinds without any capabilities are dropped. - static func sections(_ capabilities: [MatrixCapability]) -> [CapabilitySection] { + /// source bucket. The active matrix sort chooses the row order inside + /// each bucket. Ungrouped is always pinned at the bottom of each kind + /// section, and kinds without any capabilities are dropped. + static func sections( + _ capabilities: [MatrixCapability], + sort: MatrixSortMode = .typeDescending, + usageIndex: MatrixUsageIndex? = nil + ) -> [CapabilitySection] { let byKind: [CapabilityKind: [MatrixCapability]] = Dictionary(grouping: capabilities, by: \.kind) - return CapabilityKind.allCases.compactMap { kind in + let kindOrder: [CapabilityKind] = sort == .typeAscending + ? Array(CapabilityKind.allCases.reversed()) + : CapabilityKind.allCases + + return kindOrder.compactMap { kind in guard let bucket = byKind[kind], !bucket.isEmpty else { return nil } - return CapabilitySection(kind: kind, groups: group(bucket)) + return CapabilitySection(kind: kind, groups: group(bucket, sort: sort, usageIndex: usageIndex)) } } /// Group a homogeneous capability bucket by `owner/name`. Kept as a /// public helper for tests + the special-case "skills only" view in /// SpotlightView. - static func group(_ capabilities: [MatrixCapability]) -> [MatrixGroup] { + static func group( + _ capabilities: [MatrixCapability], + sort: MatrixSortMode = .typeDescending, + usageIndex: MatrixUsageIndex? = nil + ) -> [MatrixGroup] { var buckets: [String: (owner: String?, name: String?, capabilities: [MatrixCapability])] = [:] for capability in capabilities { @@ -65,9 +113,7 @@ enum SkillGrouping { } let groups = buckets.map { key, value -> MatrixGroup in - let sorted = value.capabilities.sorted { lhs, rhs in - lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending - } + let sorted = sortCapabilities(value.capabilities, sort: sort, usageIndex: usageIndex) return MatrixGroup(id: key, owner: value.owner, name: value.name, capabilities: sorted) } @@ -82,6 +128,65 @@ enum SkillGrouping { return ungroupedID } + private static func sortCapabilities( + _ capabilities: [MatrixCapability], + sort: MatrixSortMode, + usageIndex: MatrixUsageIndex? + ) -> [MatrixCapability] { + capabilities.sorted { lhs, rhs in + switch sort { + case .nameDescending: + return nameCompare(lhs, rhs) == .orderedDescending + case .callsDescending: + let lhsUsage = usage(for: lhs, usageIndex: usageIndex) + let rhsUsage = usage(for: rhs, usageIndex: usageIndex) + if lhsUsage.calls != rhsUsage.calls { return lhsUsage.calls > rhsUsage.calls } + if lhsUsage.tokens != rhsUsage.tokens { return lhsUsage.tokens > rhsUsage.tokens } + return nameCompare(lhs, rhs) == .orderedAscending + case .tokensDescending: + let lhsUsage = usage(for: lhs, usageIndex: usageIndex) + let rhsUsage = usage(for: rhs, usageIndex: usageIndex) + if lhsUsage.tokens != rhsUsage.tokens { return lhsUsage.tokens > rhsUsage.tokens } + if lhsUsage.calls != rhsUsage.calls { return lhsUsage.calls > rhsUsage.calls } + return nameCompare(lhs, rhs) == .orderedAscending + case .recentDescending: + let lhsUsage = usage(for: lhs, usageIndex: usageIndex) + let rhsUsage = usage(for: rhs, usageIndex: usageIndex) + if lhsUsage.lastUsedAt != rhsUsage.lastUsedAt { + if let lhsDate = lhsUsage.lastUsedAt, let rhsDate = rhsUsage.lastUsedAt { + return lhsDate > rhsDate + } + return lhsUsage.lastUsedAt != nil + } + if lhsUsage.calls != rhsUsage.calls { return lhsUsage.calls > rhsUsage.calls } + return nameCompare(lhs, rhs) == .orderedAscending + case .typeAscending, .typeDescending, .nameAscending: + return nameCompare(lhs, rhs) == .orderedAscending + } + } + } + + private static func nameCompare(_ lhs: MatrixCapability, _ rhs: MatrixCapability) -> ComparisonResult { + lhs.name.localizedCaseInsensitiveCompare(rhs.name) + } + + private static func usage( + for capability: MatrixCapability, + usageIndex: MatrixUsageIndex? + ) -> (calls: Int, tokens: Int64, lastUsedAt: Date?) { + if let packageID = capability.underlyingPackageID, + let snapshot = usageIndex?.packageSnapshot(for: packageID) { + return (snapshot.usageEvents, snapshot.totalTokens, snapshot.lastUsedAt) + } + + if let skillID = capability.underlyingSkillID, + let snapshot = usageIndex?.skillSnapshot(for: skillID) { + return (snapshot.usageEvents, snapshot.totalTokens, snapshot.lastUsedAt) + } + + return (0, 0, nil) + } + private static func areInOrder(_ lhs: MatrixGroup, _ rhs: MatrixGroup) -> Bool { if lhs.id == ungroupedID { return false } if rhs.id == ungroupedID { return true } diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 5509acf..8598159 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -322,6 +322,13 @@ "matrix.metric.brokenLinks" = "Broken links"; "matrix.metric.tokenUsage" = "Month tokens"; "matrix.sort.typeDescending" = "Sort: type"; +"matrix.sort.typeAscending" = "Sort: type reverse"; +"matrix.sort.nameAscending" = "Sort: name A-Z"; +"matrix.sort.nameDescending" = "Sort: name Z-A"; +"matrix.sort.callsDescending" = "Sort: calls"; +"matrix.sort.tokensDescending" = "Sort: tokens"; +"matrix.sort.recentDescending" = "Sort: recent"; +"matrix.sort.help" = "Choose matrix row sort"; "matrix.col.capability" = "Capability"; "matrix.col.source" = "Source"; "matrix.col.version" = "Version"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index a81930b..29f1ff9 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -321,7 +321,14 @@ "matrix.metric.stubs" = "占位 (stub)"; "matrix.metric.brokenLinks" = "断链"; "matrix.metric.tokenUsage" = "本月 token"; -"matrix.sort.typeDescending" = "排序:类型"; +"matrix.sort.typeDescending" = "排序:类型 ↓"; +"matrix.sort.typeAscending" = "排序:类型 ↑"; +"matrix.sort.nameAscending" = "排序:名称 A-Z"; +"matrix.sort.nameDescending" = "排序:名称 Z-A"; +"matrix.sort.callsDescending" = "排序:调用 ↓"; +"matrix.sort.tokensDescending" = "排序:Tokens ↓"; +"matrix.sort.recentDescending" = "排序:最近使用 ↓"; +"matrix.sort.help" = "选择矩阵行排序方式"; "matrix.col.capability" = "能力"; "matrix.col.source" = "来源"; "matrix.col.version" = "版本"; diff --git a/swift-app/Sources/Popskill/Views/MatrixView.swift b/swift-app/Sources/Popskill/Views/MatrixView.swift index 9fbc800..41570d4 100644 --- a/swift-app/Sources/Popskill/Views/MatrixView.swift +++ b/swift-app/Sources/Popskill/Views/MatrixView.swift @@ -26,7 +26,7 @@ struct MatrixView: View { skills: store.skills, packages: store.compositePackages ) - let sections = filteredSections(in: capabilities) + let sections = filteredSections(in: capabilities, usageIndex: usageIndex) VStack(spacing: 0) { header(capabilities: capabilities) Divider() @@ -78,10 +78,7 @@ struct MatrixView: View { HStack(spacing: 10) { filterChips Spacer(minLength: 8) - Label(localization.string("matrix.sort.typeDescending"), systemImage: "arrow.down") - .font(.system(size: 11.5, weight: .medium)) - .foregroundStyle(Color.popSecondaryLabel) - .labelStyle(.titleAndIcon) + sortMenu } } .padding(.horizontal, 28) @@ -254,6 +251,42 @@ struct MatrixView: View { .accessibilityAddTraits(active ? .isSelected : []) } + private var sortMenu: some View { + Menu { + ForEach(MatrixSortMode.allCases) { mode in + Button { + store.matrixSortMode = mode + } label: { + Label( + localization.string(mode.titleKey), + systemImage: store.matrixSortMode == mode ? "checkmark" : mode.symbolName + ) + } + } + } label: { + HStack(spacing: 5) { + Image(systemName: store.matrixSortMode.symbolName) + .font(.system(size: 10.5, weight: .semibold)) + Text(localization.string(store.matrixSortMode.titleKey)) + .font(.system(size: 11.5, weight: .medium)) + Image(systemName: "chevron.down") + .font(.system(size: 8, weight: .bold)) + .foregroundStyle(Color.popTertiaryLabel) + } + .foregroundStyle(Color.popSecondaryLabel) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.popControlFill, in: RoundedRectangle(cornerRadius: 5, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 5, style: .continuous) + .strokeBorder(Color.popControlStroke, lineWidth: 0.7) + ) + } + .menuStyle(.borderlessButton) + .fixedSize() + .help(localization.string("matrix.sort.help")) + } + // MARK: Matrix table private func matrixTable(sections: [CapabilitySection], usageIndex: MatrixUsageIndex) -> some View { @@ -370,7 +403,10 @@ struct MatrixView: View { // MARK: Filtering & grouping - private func filteredSections(in capabilities: [MatrixCapability]) -> [CapabilitySection] { + private func filteredSections( + in capabilities: [MatrixCapability], + usageIndex: MatrixUsageIndex + ) -> [CapabilitySection] { let q = SearchTextNormalizer.key(store.trimmedSearch) let visible = capabilities.filter { capability in store.matrixFilter.includes(capability: capability, store: store) @@ -385,7 +421,7 @@ struct MatrixView: View { || SearchTextNormalizer.matches(component.location ?? "", query: q) } == true) } - return SkillGrouping.sections(visible) + return SkillGrouping.sections(visible, sort: store.matrixSortMode, usageIndex: usageIndex) } private var noResultsState: some View { diff --git a/swift-app/Tests/PopskillTests/SkillGroupingTests.swift b/swift-app/Tests/PopskillTests/SkillGroupingTests.swift index 7cba243..d92d1a3 100644 --- a/swift-app/Tests/PopskillTests/SkillGroupingTests.swift +++ b/swift-app/Tests/PopskillTests/SkillGroupingTests.swift @@ -95,6 +95,69 @@ struct SkillGroupingTests { #expect(sections.map(\.kind) == [.bundle, .skill, .agent]) } + @Test + func sectionsCanReverseKindOrderForTypeAscendingSort() { + let bundleCap = capability(name: "package", kind: .bundle, owner: "pkg", name2: "source") + let agentCap = capability(name: "z", kind: .agent, owner: nil, name2: nil) + let skillCap = capability(name: "y", kind: .skill, owner: "a", name2: "lib") + + let sections = SkillGrouping.sections( + [agentCap, skillCap, bundleCap], + sort: .typeAscending + ) + + #expect(sections.map(\.kind) == [.agent, .skill, .bundle]) + } + + @Test + func groupSortsCapabilitiesByNameDescending() { + let alpha = capability(name: "Alpha", owner: "anthropics", name2: "skills") + let beta = capability(name: "beta", owner: "anthropics", name2: "skills") + let charlie = capability(name: "Charlie", owner: "anthropics", name2: "skills") + + let group = SkillGrouping.group([alpha, charlie, beta], sort: .nameDescending).first + + #expect(group?.capabilities.map(\.name) == ["Charlie", "beta", "Alpha"]) + } + + @Test + func groupSortsCapabilitiesByCallsDescending() { + let alpha = capability(name: "Alpha", owner: "anthropics", name2: "skills") + let beta = capability(name: "beta", owner: "anthropics", name2: "skills") + let usageIndex = MatrixUsageIndex( + summary: UsageSummary( + skillStats: [ + SkillUsageStat( + skillID: "Alpha", + sourcePlugin: nil, + usageEvents: 2, + inputTokens: 10, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: nil + ), + SkillUsageStat( + skillID: "beta", + sourcePlugin: nil, + usageEvents: 8, + inputTokens: 3, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: nil + ) + ] + ), + skills: [skill(id: "Alpha"), skill(id: "beta")], + packages: [] + ) + + let group = SkillGrouping.group([alpha, beta], sort: .callsDescending, usageIndex: usageIndex).first + + #expect(group?.capabilities.map(\.name) == ["beta", "Alpha"]) + } + @Test func sectionsAreEmptyForEmptyInput() { #expect(SkillGrouping.sections([]).isEmpty) @@ -128,4 +191,20 @@ struct SkillGroupingTests { underlyingAgentID: kind == .agent ? name : nil ) } + + private func skill(id: String) -> Skill { + Skill( + id: id, + name: id, + description: "Test skill", + directory: id, + repoOwner: nil, + repoName: nil, + readmeUrl: nil, + apps: SkillApps(claude: false, codex: false, gemini: false, opencode: false, hermes: false), + installedAt: nil, + updatedAt: nil, + contentHash: nil + ) + } } From 8c69910be2765ea1251126313cf82b04c35a7b38 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 05:27:27 +0800 Subject: [PATCH 28/47] feat(ui): broaden matrix search --- .../Popskill/Models/MatrixCapability.swift | 132 ++++++++++++++++++ .../Sources/Popskill/Views/MatrixView.swift | 10 +- .../PopskillTests/SkillModelsTests.swift | 70 ++++++++++ 3 files changed, 203 insertions(+), 9 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/MatrixCapability.swift b/swift-app/Sources/Popskill/Models/MatrixCapability.swift index 6156a1f..c8b1264 100644 --- a/swift-app/Sources/Popskill/Models/MatrixCapability.swift +++ b/swift-app/Sources/Popskill/Models/MatrixCapability.swift @@ -145,6 +145,26 @@ struct MatrixCapability: Identifiable, Equatable { hasBrokenLink || package?.hasBrokenLinks(in: skills) == true } + func matchesSearch(query: SearchTextKey) -> Bool { + guard !query.isEmpty else { return true } + + if Self.matches(searchFields, query: query) { + return true + } + + guard let package else { + return false + } + + return Self.matches(package.searchFields, query: query) + || package.components.all.contains { component in + Self.matches(component.searchFields, query: query) + } + || package.configSchema.contains { field in + Self.matches(field.searchFields, query: query) + } + } + static func capabilityID(kind: CapabilityKind, rawID: String) -> String { "\(kind.rawValue):\(rawID)" } @@ -168,6 +188,30 @@ struct MatrixCapability: Identifiable, Equatable { static func skillToggleKey(for skillID: String, app: TargetApp) -> String { toggleKey(capabilityID: skillCapabilityID(for: skillID), app: app) } + + private var searchFields: [String] { + var fields = [ + id, + kind.rawValue, + name, + summary ?? "", + sourceLabel, + sourceType ?? "", + repoOwner ?? "", + repoName ?? "", + directory + ] + fields.append(contentsOf: kind.searchAliases) + fields.append(contentsOf: triggerScenarios ?? []) + fields.append(contentsOf: TargetApp.supported.flatMap { app in + apps.isEnabled(app) ? app.searchAliases : [] + }) + return fields + } + + private static func matches(_ fields: [String], query: SearchTextKey) -> Bool { + fields.contains { SearchTextNormalizer.matches($0, query: query) } + } } extension MatrixCapability { @@ -254,3 +298,91 @@ extension MatrixCapability { ) } } + +private extension CapabilityKind { + var searchAliases: [String] { + switch self { + case .bundle: ["bundle", "package", "suite", "套装", "能力包"] + case .skill: ["skill", "技能", "能力"] + case .agent: ["agent", "智能体"] + case .cli: ["cli", "command line", "命令行"] + case .mcp: ["mcp", "server", "服务"] + case .config: ["config", "settings", "配置", "设置"] + } + } +} + +private extension TargetApp { + var searchAliases: [String] { + switch self { + case .claude: ["Claude", "Claude Code", "CC"] + case .codex: ["Codex", "CDX"] + case .gemini: ["Gemini"] + case .opencode: ["OpenCode"] + case .hermes: ["Hermes"] + } + } +} + +private extension CapabilityPackage { + var searchFields: [String] { + [ + id, + type.rawValue, + type.title, + health.rawValue, + health.title, + name, + vendor ?? "", + summary, + source.kind, + source.location, + source.updateStrategy, + source.repoOwner ?? "", + source.repoName ?? "", + source.repoBranch ?? "", + source.readmeUrl ?? "", + installed ? "installed 已装 active" : "missing 未装 inactive", + "components 组件" + ] + } +} + +private extension PackageComponent { + var searchFields: [String] { + [ + id, + name, + kind, + status, + statusAliases, + required ? "required 必需" : "optional 可选", + installed ? "installed 已装" : "missing 未装", + location ?? "" + ] + } + + private var statusAliases: String { + switch status.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { + case "stub": "stub 占位" + case "registry-reference": "registry reference 注册表" + case "detected": "detected 检测到" + case "installed": "installed 已装" + case "declared": "declared 声明" + case "available": "available 可用" + default: "" + } + } +} + +private extension PackageConfigField { + var searchFields: [String] { + [ + id, + label, + storage, + required ? "required 必需" : "optional 可选", + secret ? "secret 密钥" : "" + ] + } +} diff --git a/swift-app/Sources/Popskill/Views/MatrixView.swift b/swift-app/Sources/Popskill/Views/MatrixView.swift index 41570d4..abc842b 100644 --- a/swift-app/Sources/Popskill/Views/MatrixView.swift +++ b/swift-app/Sources/Popskill/Views/MatrixView.swift @@ -411,15 +411,7 @@ struct MatrixView: View { let visible = capabilities.filter { capability in store.matrixFilter.includes(capability: capability, store: store) && store.matrixTypeFilter.includes(capability: capability) - && (q.isEmpty - || SearchTextNormalizer.matches(capability.name, query: q) - || SearchTextNormalizer.matches(capability.summary ?? "", query: q) - || SearchTextNormalizer.matches(capability.directory, query: q) - || capability.package?.components.all.contains { component in - SearchTextNormalizer.matches(component.name, query: q) - || SearchTextNormalizer.matches(component.id, query: q) - || SearchTextNormalizer.matches(component.location ?? "", query: q) - } == true) + && capability.matchesSearch(query: q) } return SkillGrouping.sections(visible, sort: store.matrixSortMode, usageIndex: usageIndex) } diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 33c76ad..657384a 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -210,6 +210,76 @@ struct SkillModelsTests { #expect(MatrixCapability.fromPackage(unrelatedPackage, skills: [skill]).hasBrokenLinks(in: [skill]) == false) } + @Test + func matrixCapabilitySearchMatchesSourceTriggersAndAppTargets() { + var skill = Skill( + id: "dotey/prompt-engineering:baoyu-comic", + name: "baoyu-comic", + description: "Comic generator", + directory: "baoyu-comic", + repoOwner: "dotey", + repoName: "prompt-engineering", + readmeUrl: nil, + apps: SkillApps(claude: false, codex: true, gemini: false, opencode: false, hermes: false), + installedAt: nil, + updatedAt: nil, + contentHash: nil + ) + skill.capabilitySummary = "Turns topics into four-panel comics." + skill.triggerScenarios = ["用 baoyu-comic 把 X 画成四格"] + skill.sourceType = "github" + let capability = MatrixCapability.fromSkill(skill) + + #expect(capability.matchesSearch(query: SearchTextNormalizer.key("dotey"))) + #expect(capability.matchesSearch(query: SearchTextNormalizer.key("prompt engineering"))) + #expect(capability.matchesSearch(query: SearchTextNormalizer.key("四格"))) + #expect(capability.matchesSearch(query: SearchTextNormalizer.key("codex"))) + #expect(!capability.matchesSearch(query: SearchTextNormalizer.key("feishu"))) + } + + @Test + func matrixCapabilitySearchMatchesPackageSourceComponentsAndConfig() { + let package = CapabilityPackage( + id: "pkg:feishu-suite", + type: .composite, + name: "Feishu Suite", + vendor: "ByteDance", + summary: "Office automation suite", + source: PackageSource( + kind: "github", + location: "github.com/feishu/lark-suite", + updateStrategy: "manual", + repoOwner: "feishu", + repoName: "lark-suite", + repoBranch: "main", + readmeUrl: nil + ), + components: PackageComponents( + cli: [ + PackageComponent(id: "lark-cli", name: "lark-cli", kind: "cli", required: true, installed: false, status: "stub", location: "feishu-suite/lark-cli") + ], + skills: [], + mcp: [ + PackageComponent(id: "lark-openapi-mcp", name: "Lark OpenAPI MCP", kind: "mcp", required: false, installed: false, status: "registry-reference", location: nil) + ], + agents: [] + ), + configSchema: [ + PackageConfigField(id: "lark.app_secret", label: "App Secret", required: true, secret: true, storage: "keychain") + ], + installed: false, + lifecycle: nil + ) + let capability = MatrixCapability.fromPackage(package, skills: []) + + #expect(capability.matchesSearch(query: SearchTextNormalizer.key("bytedance"))) + #expect(capability.matchesSearch(query: SearchTextNormalizer.key("lark suite"))) + #expect(capability.matchesSearch(query: SearchTextNormalizer.key("套装"))) + #expect(capability.matchesSearch(query: SearchTextNormalizer.key("占位"))) + #expect(capability.matchesSearch(query: SearchTextNormalizer.key("keychain"))) + #expect(!capability.matchesSearch(query: SearchTextNormalizer.key("baoyu comic"))) + } + @Test func targetAppRegistryCoversCurrentSkillTargets() { #expect(TargetAppRegistry.all.map(\.app) == TargetApp.supported) From cc71641899c617683e05e7d4d7ea1b21abb10641 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 05:39:11 +0800 Subject: [PATCH 29/47] feat(ui): scope matrix usage to recent window --- .../Sources/Popskill/Models/SkillModels.swift | 4 +- .../Sources/Popskill/Models/UsageModels.swift | 31 +++ .../Resources/en.lproj/Localizable.strings | 7 +- .../zh-Hans.lproj/Localizable.strings | 7 +- .../Storage/TranscriptUsageScanner.swift | 195 ++++++++++++++---- .../Popskill/Views/InspectorPane.swift | 11 + .../Sources/Popskill/Views/MatrixView.swift | 2 +- .../PopskillTests/SkillModelsTests.swift | 99 +++++++++ .../TranscriptUsageScannerTests.swift | 36 ++++ 9 files changed, 343 insertions(+), 49 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index f06bc16..88f7d4c 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -224,7 +224,7 @@ struct Skill: Identifiable, Codable, Equatable { } var snapshot = SkillUsageSnapshot() - for stat in summary.skillStats where matchesAttributionSkill(stat.skillID) { + for stat in summary.thirtyDaySkillStats where matchesAttributionSkill(stat.skillID) { snapshot.add(stat) } return snapshot @@ -1448,7 +1448,7 @@ extension CapabilityPackage { let matchedSkills = matchingInstalledSkills(in: skills) var snapshot = PackageUsageSnapshot() - for stat in summary.skillStats { + for stat in summary.thirtyDaySkillStats { guard let component = usageComponent(for: stat, matchedSkills: matchedSkills) else { continue } diff --git a/swift-app/Sources/Popskill/Models/UsageModels.swift b/swift-app/Sources/Popskill/Models/UsageModels.swift index d53f0fb..c6338dc 100644 --- a/swift-app/Sources/Popskill/Models/UsageModels.swift +++ b/swift-app/Sources/Popskill/Models/UsageModels.swift @@ -12,6 +12,37 @@ struct UsageSummary: Equatable { var modelStats: [ModelUsageStat] = [] var skillStats: [SkillUsageStat] = [] var recentSessions: [SessionUsageStat] = [] + var recent30Days: UsageWindowSummary? + + var totalTokens: Int64 { + inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens + } + + var unattributedUsageEvents: Int { + max(0, usageEvents - attributedSkillUsageEvents) + } + + var thirtyDayTotalTokens: Int64 { + recent30Days?.totalTokens ?? totalTokens + } + + var thirtyDaySkillStats: [SkillUsageStat] { + recent30Days?.skillStats ?? skillStats + } +} + +struct UsageWindowSummary: Equatable { + let days: Int + let startedAt: Date + let endedAt: Date + var usageEvents = 0 + var inputTokens: Int64 = 0 + var outputTokens: Int64 = 0 + var cacheCreationTokens: Int64 = 0 + var cacheReadTokens: Int64 = 0 + var attributedSkillUsageEvents = 0 + var modelStats: [ModelUsageStat] = [] + var skillStats: [SkillUsageStat] = [] var totalTokens: Int64 { inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 8598159..00c410a 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -320,7 +320,7 @@ "matrix.metric.codexActive" = "Codex active"; "matrix.metric.stubs" = "Stubs"; "matrix.metric.brokenLinks" = "Broken links"; -"matrix.metric.tokenUsage" = "Month tokens"; +"matrix.metric.tokenUsage" = "30d tokens"; "matrix.sort.typeDescending" = "Sort: type"; "matrix.sort.typeAscending" = "Sort: type reverse"; "matrix.sort.nameAscending" = "Sort: name A-Z"; @@ -384,8 +384,9 @@ "matrix.package.component.calls" = "%@ calls"; "matrix.package.missingRequired" = "%d required missing"; "matrix.package.config.secret" = "Keychain secret"; -"matrix.package.usage.tokens" = "Tokens"; -"matrix.package.usage.calls" = "Calls"; +"matrix.package.usage.tokens" = "30d tokens"; +"matrix.package.usage.calls" = "30d calls"; +"matrix.package.usage.window" = "Last %d days from local transcript attribution."; "matrix.package.usage.lastUsed" = "Last used %@"; "matrix.package.usage.empty" = "Usage was scanned, but the %d local skills matched to this bundle have no attribution records yet."; "matrix.package.usage.notScanned" = "Transcript usage has not been scanned yet."; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 29f1ff9..7a3f9c4 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -320,7 +320,7 @@ "matrix.metric.codexActive" = "Codex 已激活"; "matrix.metric.stubs" = "占位 (stub)"; "matrix.metric.brokenLinks" = "断链"; -"matrix.metric.tokenUsage" = "本月 token"; +"matrix.metric.tokenUsage" = "近30天 token"; "matrix.sort.typeDescending" = "排序:类型 ↓"; "matrix.sort.typeAscending" = "排序:类型 ↑"; "matrix.sort.nameAscending" = "排序:名称 A-Z"; @@ -384,8 +384,9 @@ "matrix.package.component.calls" = "%@ 次"; "matrix.package.missingRequired" = "缺 %d 个必需"; "matrix.package.config.secret" = "Keychain 密钥"; -"matrix.package.usage.tokens" = "Tokens"; -"matrix.package.usage.calls" = "调用"; +"matrix.package.usage.tokens" = "近30天 tokens"; +"matrix.package.usage.calls" = "近30天调用"; +"matrix.package.usage.window" = "最近 %d 天的本机 transcript 归因统计。"; "matrix.package.usage.lastUsed" = "最近使用 %@"; "matrix.package.usage.empty" = "已扫描用量,但这个套装匹配到的 %d 个本地 skill 暂无归因记录。"; "matrix.package.usage.notScanned" = "还没有扫描 transcript,用量聚合暂不可用。"; diff --git a/swift-app/Sources/Popskill/Storage/TranscriptUsageScanner.swift b/swift-app/Sources/Popskill/Storage/TranscriptUsageScanner.swift index 055215f..a196dc8 100644 --- a/swift-app/Sources/Popskill/Storage/TranscriptUsageScanner.swift +++ b/swift-app/Sources/Popskill/Storage/TranscriptUsageScanner.swift @@ -2,12 +2,16 @@ import Foundation struct TranscriptUsageScanner { private let projectsURL: URL + private let referenceDate: Date + private let recentWindowDays: Int - init(projectsURL: URL? = nil) { + init(projectsURL: URL? = nil, referenceDate: Date = Date(), recentWindowDays: Int = 30) { self.projectsURL = projectsURL ?? URL(fileURLWithPath: NSHomeDirectory()) .appendingPathComponent(".claude") .appendingPathComponent("projects") + self.referenceDate = referenceDate + self.recentWindowDays = max(1, recentWindowDays) } func scan() throws -> UsageSummary { @@ -15,12 +19,21 @@ struct TranscriptUsageScanner { var modelStats: [String: ModelUsageStat] = [:] var skillStats: [String: SkillUsageStat] = [:] var sessionStats: [String: SessionUsageStat] = [:] + let recentStart = referenceDate.addingTimeInterval(-Double(recentWindowDays) * 24 * 60 * 60) + var recentSummary = UsageWindowSummary( + days: recentWindowDays, + startedAt: recentStart, + endedAt: referenceDate + ) + var recentModelStats: [String: ModelUsageStat] = [:] + var recentSkillStats: [String: SkillUsageStat] = [:] guard let enumerator = FileManager.default.enumerator( at: projectsURL, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles] ) else { + summary.recent30Days = recentSummary return summary } @@ -31,23 +44,17 @@ struct TranscriptUsageScanner { summary: &summary, modelStats: &modelStats, skillStats: &skillStats, - sessionStats: &sessionStats + sessionStats: &sessionStats, + recentSummary: &recentSummary, + recentModelStats: &recentModelStats, + recentSkillStats: &recentSkillStats, + recentStart: recentStart ) } summary.sessions = sessionStats.count - summary.modelStats = modelStats.values.sorted { - if $0.totalTokens == $1.totalTokens { - return $0.model < $1.model - } - return $0.totalTokens > $1.totalTokens - } - summary.skillStats = skillStats.values.sorted { - if $0.totalTokens == $1.totalTokens { - return $0.skillID < $1.skillID - } - return $0.totalTokens > $1.totalTokens - } + summary.modelStats = sortedModelStats(modelStats) + summary.skillStats = sortedSkillStats(skillStats) summary.recentSessions = sessionStats.values.sorted { switch ($0.lastActivityAt, $1.lastActivityAt) { case let (left?, right?): @@ -63,6 +70,9 @@ struct TranscriptUsageScanner { return $0.sessionID < $1.sessionID } } + recentSummary.modelStats = sortedModelStats(recentModelStats) + recentSummary.skillStats = sortedSkillStats(recentSkillStats) + summary.recent30Days = recentSummary return summary } @@ -71,7 +81,11 @@ struct TranscriptUsageScanner { summary: inout UsageSummary, modelStats: inout [String: ModelUsageStat], skillStats: inout [String: SkillUsageStat], - sessionStats: inout [String: SessionUsageStat] + sessionStats: inout [String: SessionUsageStat], + recentSummary: inout UsageWindowSummary, + recentModelStats: inout [String: ModelUsageStat], + recentSkillStats: inout [String: SkillUsageStat], + recentStart: Date ) throws { let projectName = projectName(for: fileURL) let handle = try FileHandle(forReadingFrom: fileURL) @@ -91,7 +105,11 @@ struct TranscriptUsageScanner { summary: &summary, modelStats: &modelStats, skillStats: &skillStats, - sessionStats: &sessionStats + sessionStats: &sessionStats, + recentSummary: &recentSummary, + recentModelStats: &recentModelStats, + recentSkillStats: &recentSkillStats, + recentStart: recentStart ) buffer.removeSubrange(buffer.startIndex..= recentStart && $0 <= referenceDate }) == true { + recentSummary.usageEvents += 1 + recentSummary.inputTokens += inputTokens + recentSummary.outputTokens += outputTokens + recentSummary.cacheCreationTokens += cacheCreationTokens + recentSummary.cacheReadTokens += cacheReadTokens + recordModelUsage( + model: model, + inputTokens: inputTokens, + outputTokens: outputTokens, + cacheCreationTokens: cacheCreationTokens, + cacheReadTokens: cacheReadTokens, + modelStats: &recentModelStats + ) + if let attributionSkill { + recentSummary.attributedSkillUsageEvents += 1 + recordSkillUsage( + skillID: attributionSkill, + sourcePlugin: attributionPlugin, + timestamp: timestamp, + inputTokens: inputTokens, + outputTokens: outputTokens, + cacheCreationTokens: cacheCreationTokens, + cacheReadTokens: cacheReadTokens, + skillStats: &recentSkillStats + ) + } + } + } + + private func recordModelUsage( + model: String, + inputTokens: Int64, + outputTokens: Int64, + cacheCreationTokens: Int64, + cacheReadTokens: Int64, + modelStats: inout [String: ModelUsageStat] + ) { var stat = modelStats[model] ?? ModelUsageStat( model: model, usageEvents: 0, @@ -207,30 +296,56 @@ struct TranscriptUsageScanner { stat.cacheCreationTokens += cacheCreationTokens stat.cacheReadTokens += cacheReadTokens modelStats[model] = stat + } - if let attributionSkill = attributionIdentifier(object["attributionSkill"]) { - summary.attributedSkillUsageEvents += 1 - var skillStat = skillStats[attributionSkill] ?? SkillUsageStat( - skillID: attributionSkill, - sourcePlugin: stringValue(object["attributionPlugin"]), - usageEvents: 0, - inputTokens: 0, - outputTokens: 0, - cacheCreationTokens: 0, - cacheReadTokens: 0, - lastUsedAt: nil - ) - if skillStat.sourcePlugin == nil { - skillStat.sourcePlugin = stringValue(object["attributionPlugin"]) + private func recordSkillUsage( + skillID: String, + sourcePlugin: String?, + timestamp: Date?, + inputTokens: Int64, + outputTokens: Int64, + cacheCreationTokens: Int64, + cacheReadTokens: Int64, + skillStats: inout [String: SkillUsageStat] + ) { + var skillStat = skillStats[skillID] ?? SkillUsageStat( + skillID: skillID, + sourcePlugin: sourcePlugin, + usageEvents: 0, + inputTokens: 0, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: nil + ) + if skillStat.sourcePlugin == nil { + skillStat.sourcePlugin = sourcePlugin + } + skillStat.addUsage( + inputTokens: inputTokens, + outputTokens: outputTokens, + cacheCreationTokens: cacheCreationTokens, + cacheReadTokens: cacheReadTokens, + timestamp: timestamp + ) + skillStats[skillID] = skillStat + } + + private func sortedModelStats(_ stats: [String: ModelUsageStat]) -> [ModelUsageStat] { + stats.values.sorted { + if $0.totalTokens == $1.totalTokens { + return $0.model < $1.model } - skillStat.addUsage( - inputTokens: inputTokens, - outputTokens: outputTokens, - cacheCreationTokens: cacheCreationTokens, - cacheReadTokens: cacheReadTokens, - timestamp: timestamp - ) - skillStats[attributionSkill] = skillStat + return $0.totalTokens > $1.totalTokens + } + } + + private func sortedSkillStats(_ stats: [String: SkillUsageStat]) -> [SkillUsageStat] { + stats.values.sorted { + if $0.totalTokens == $1.totalTokens { + return $0.skillID < $1.skillID + } + return $0.totalTokens > $1.totalTokens } } diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index cd29175..6b03e30 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -471,6 +471,7 @@ struct InspectorPane: View { private func skillUsageSection(_ skill: Skill) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.usage", accent: .accentColor) + usageWindowCaption if store.usageScanInFlight { HStack(spacing: 8) { @@ -622,6 +623,7 @@ struct InspectorPane: View { private func packageUsageSection(_ package: CapabilityPackage) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.usage", accent: .accentColor) + usageWindowCaption if store.usageScanInFlight { HStack(spacing: 8) { @@ -732,6 +734,15 @@ struct InspectorPane: View { .background(tint.opacity(0.07), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) } + @ViewBuilder + private var usageWindowCaption: some View { + if let window = store.usageSummary?.recent30Days { + Text(localization.string("matrix.package.usage.window", window.days)) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } + private func components(in kind: String, package: CapabilityPackage) -> [PackageComponent] { switch kind { case "skill": return package.components.skills diff --git a/swift-app/Sources/Popskill/Views/MatrixView.swift b/swift-app/Sources/Popskill/Views/MatrixView.swift index abc842b..81c8567 100644 --- a/swift-app/Sources/Popskill/Views/MatrixView.swift +++ b/swift-app/Sources/Popskill/Views/MatrixView.swift @@ -176,7 +176,7 @@ struct MatrixView: View { ), MatrixSummaryMetric( id: "tokens", - value: store.usageSummary.map { UsageDisplayFormatter.compactTokens($0.totalTokens) } ?? "—", + value: store.usageSummary.map { UsageDisplayFormatter.compactTokens($0.thirtyDayTotalTokens) } ?? "—", title: localization.string("matrix.metric.tokenUsage"), tint: .popLabel, preferredWidth: 116 diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 657384a..496810d 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -407,6 +407,55 @@ struct SkillModelsTests { #expect(snapshot?.lastUsedAt == lastUsed) } + @Test + func installedSkillUsageSnapshotPrefersRecentThirtyDayWindow() { + let skill = installedSkill(directory: "baoyu-comic") + let oldStat = SkillUsageStat( + skillID: "baoyu-comic", + sourcePlugin: nil, + usageEvents: 9, + inputTokens: 90, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: Date(timeIntervalSince1970: 1_700_000_000) + ) + let recentStat = SkillUsageStat( + skillID: "baoyu-comic", + sourcePlugin: nil, + usageEvents: 2, + inputTokens: 4, + outputTokens: 6, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: Date(timeIntervalSince1970: 1_800_000_000) + ) + var recentWindow = UsageWindowSummary( + days: 30, + startedAt: Date(timeIntervalSince1970: 1_799_000_000), + endedAt: Date(timeIntervalSince1970: 1_800_000_000) + ) + recentWindow.usageEvents = 2 + recentWindow.inputTokens = 4 + recentWindow.outputTokens = 6 + recentWindow.attributedSkillUsageEvents = 2 + recentWindow.skillStats = [recentStat] + let summary = UsageSummary( + usageEvents: 11, + inputTokens: 94, + outputTokens: 6, + attributedSkillUsageEvents: 11, + skillStats: [oldStat], + recent30Days: recentWindow + ) + + let snapshot = skill.usageSnapshot(using: summary) + + #expect(snapshot?.usageEvents == 2) + #expect(snapshot?.totalTokens == 10) + #expect(snapshot?.lastUsedAt == recentStat.lastUsedAt) + } + @Test func installPlanDecodesPreviewPayload() throws { let data = """ @@ -1052,6 +1101,56 @@ struct SkillModelsTests { #expect(snapshot?.componentStats.first?.totalTokens == 62) } + @Test + func capabilityPackageUsageSnapshotPrefersRecentThirtyDayWindow() { + let package = self.package(components: [component(id: "baoyu-comic", installed: true)]) + let skill = installedSkill(directory: "baoyu-comic") + let oldStat = SkillUsageStat( + skillID: "baoyu-comic", + sourcePlugin: nil, + usageEvents: 6, + inputTokens: 60, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: nil + ) + let recentStat = SkillUsageStat( + skillID: "baoyu-comic", + sourcePlugin: nil, + usageEvents: 1, + inputTokens: 2, + outputTokens: 3, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: Date(timeIntervalSince1970: 1_800_000_000) + ) + var recentWindow = UsageWindowSummary( + days: 30, + startedAt: Date(timeIntervalSince1970: 1_799_000_000), + endedAt: Date(timeIntervalSince1970: 1_800_000_000) + ) + recentWindow.usageEvents = 1 + recentWindow.inputTokens = 2 + recentWindow.outputTokens = 3 + recentWindow.attributedSkillUsageEvents = 1 + recentWindow.skillStats = [recentStat] + let summary = UsageSummary( + usageEvents: 7, + inputTokens: 62, + outputTokens: 3, + attributedSkillUsageEvents: 7, + skillStats: [oldStat], + recent30Days: recentWindow + ) + + let snapshot = package.usageSnapshot(using: summary, skills: [skill]) + + #expect(snapshot?.usageEvents == 1) + #expect(snapshot?.totalTokens == 5) + #expect(snapshot?.componentStats.first?.componentID == "baoyu-comic") + } + @Test func matrixUsageIndexCachesSkillPackageAndComponentUsage() { let skill = installedSkill(directory: "baoyu-comic") diff --git a/swift-app/Tests/PopskillTests/TranscriptUsageScannerTests.swift b/swift-app/Tests/PopskillTests/TranscriptUsageScannerTests.swift index 8fffcd2..46da113 100644 --- a/swift-app/Tests/PopskillTests/TranscriptUsageScannerTests.swift +++ b/swift-app/Tests/PopskillTests/TranscriptUsageScannerTests.swift @@ -113,6 +113,36 @@ struct TranscriptUsageScannerTests { #expect(stat.lastUsedAt != nil) } + @Test + func recentThirtyDayWindowAggregatesOnlyRecentUsage() throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + let project = root.appendingPathComponent("project", isDirectory: true) + try FileManager.default.createDirectory(at: project, withIntermediateDirectories: true) + defer { + try? FileManager.default.removeItem(at: root) + } + + let transcript = project.appendingPathComponent("session.jsonl") + let lines = [ + #"{"type":"assistant","sessionId":"recent","timestamp":"2026-05-12T01:01:00.000Z","attributionSkill":"recent-skill","message":{"role":"assistant","model":"claude-opus","usage":{"input_tokens":10,"output_tokens":5}}}"#, + #"{"type":"assistant","sessionId":"old","timestamp":"2026-03-01T01:01:00.000Z","attributionSkill":"old-skill","message":{"role":"assistant","model":"claude-sonnet","usage":{"input_tokens":90,"output_tokens":10}}}"# + ] + try lines.joined(separator: "\n").write(to: transcript, atomically: true, encoding: .utf8) + + let referenceDate = try #require(Self.iso8601.date(from: "2026-05-20T00:00:00Z")) + let summary = try TranscriptUsageScanner(projectsURL: root, referenceDate: referenceDate).scan() + + #expect(summary.usageEvents == 2) + #expect(summary.totalTokens == 115) + #expect(summary.skillStats.map(\.skillID) == ["old-skill", "recent-skill"]) + #expect(summary.recent30Days?.days == 30) + #expect(summary.recent30Days?.usageEvents == 1) + #expect(summary.recent30Days?.totalTokens == 15) + #expect(summary.recent30Days?.skillStats.map(\.skillID) == ["recent-skill"]) + #expect(summary.recent30Days?.modelStats.map(\.model) == ["claude-opus"]) + } + @Test func streamsCRLFLinesAndSkipsMalformedRecords() throws { let root = FileManager.default.temporaryDirectory @@ -139,4 +169,10 @@ struct TranscriptUsageScannerTests { #expect(summary.inputTokens == 6) #expect(summary.outputTokens == 8) } + + private static let iso8601: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime] + return formatter + }() } From 00a496956a3f88d3e13e75e3e84a78b75f1f729a Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 05:53:02 +0800 Subject: [PATCH 30/47] feat(ui): chart inspector usage trend --- .../Sources/Popskill/Models/UsageModels.swift | 93 ++++++++++++- .../Resources/en.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../Storage/TranscriptUsageScanner.swift | 28 +++- .../Popskill/Views/InspectorPane.swift | 122 ++++++++++++++++++ .../PopskillTests/SkillModelsTests.swift | 30 ++++- .../TranscriptUsageScannerTests.swift | 17 ++- 7 files changed, 281 insertions(+), 13 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/UsageModels.swift b/swift-app/Sources/Popskill/Models/UsageModels.swift index c6338dc..1c9dd11 100644 --- a/swift-app/Sources/Popskill/Models/UsageModels.swift +++ b/swift-app/Sources/Popskill/Models/UsageModels.swift @@ -43,6 +43,7 @@ struct UsageWindowSummary: Equatable { var attributedSkillUsageEvents = 0 var modelStats: [ModelUsageStat] = [] var skillStats: [SkillUsageStat] = [] + var dailyStats: [UsageBucketStat] = [] var totalTokens: Int64 { inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens @@ -51,6 +52,63 @@ struct UsageWindowSummary: Equatable { var unattributedUsageEvents: Int { max(0, usageEvents - attributedSkillUsageEvents) } + + mutating func addUsage( + inputTokens: Int64, + outputTokens: Int64, + cacheCreationTokens: Int64, + cacheReadTokens: Int64, + dayStart: Date + ) { + usageEvents += 1 + self.inputTokens += inputTokens + self.outputTokens += outputTokens + self.cacheCreationTokens += cacheCreationTokens + self.cacheReadTokens += cacheReadTokens + Self.mergeDailyStat( + UsageBucketStat( + dayStart: dayStart, + usageEvents: 1, + inputTokens: inputTokens, + outputTokens: outputTokens, + cacheCreationTokens: cacheCreationTokens, + cacheReadTokens: cacheReadTokens + ), + into: &dailyStats + ) + } + + static func mergeDailyStat(_ stat: UsageBucketStat, into stats: inout [UsageBucketStat]) { + if let index = stats.firstIndex(where: { $0.dayStart == stat.dayStart }) { + stats[index].add(stat) + } else { + stats.append(stat) + } + stats.sort { $0.dayStart < $1.dayStart } + } +} + +struct UsageBucketStat: Identifiable, Equatable { + var id: Date { dayStart } + + let dayStart: Date + var usageEvents: Int + var inputTokens: Int64 + var outputTokens: Int64 + var cacheCreationTokens: Int64 + var cacheReadTokens: Int64 + + var totalTokens: Int64 { + inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens + } + + mutating func add(_ stat: UsageBucketStat) { + usageEvents += stat.usageEvents + inputTokens += stat.inputTokens + outputTokens += stat.outputTokens + cacheCreationTokens += stat.cacheCreationTokens + cacheReadTokens += stat.cacheReadTokens + } } struct ModelUsageStat: Identifiable, Equatable { @@ -123,6 +181,7 @@ struct SkillUsageStat: Identifiable, Equatable { var cacheCreationTokens: Int64 var cacheReadTokens: Int64 var lastUsedAt: Date? + var dailyStats: [UsageBucketStat] = [] var totalTokens: Int64 { inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens @@ -133,7 +192,8 @@ struct SkillUsageStat: Identifiable, Equatable { outputTokens: Int64, cacheCreationTokens: Int64, cacheReadTokens: Int64, - timestamp: Date? + timestamp: Date?, + dayStart: Date? = nil ) { usageEvents += 1 self.inputTokens += inputTokens @@ -141,6 +201,20 @@ struct SkillUsageStat: Identifiable, Equatable { self.cacheCreationTokens += cacheCreationTokens self.cacheReadTokens += cacheReadTokens + if let dayStart { + UsageWindowSummary.mergeDailyStat( + UsageBucketStat( + dayStart: dayStart, + usageEvents: 1, + inputTokens: inputTokens, + outputTokens: outputTokens, + cacheCreationTokens: cacheCreationTokens, + cacheReadTokens: cacheReadTokens + ), + into: &dailyStats + ) + } + guard let timestamp else { return } @@ -157,6 +231,7 @@ struct SkillUsageSnapshot: Equatable { var cacheCreationTokens: Int64 = 0 var cacheReadTokens: Int64 = 0 var lastUsedAt: Date? + var dailyStats: [UsageBucketStat] = [] var totalTokens: Int64 { inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens @@ -172,6 +247,7 @@ struct SkillUsageSnapshot: Equatable { outputTokens += stat.outputTokens cacheCreationTokens += stat.cacheCreationTokens cacheReadTokens += stat.cacheReadTokens + mergeDailyStats(stat.dailyStats) guard let date = stat.lastUsedAt else { return @@ -180,6 +256,12 @@ struct SkillUsageSnapshot: Equatable { lastUsedAt = date } } + + mutating func mergeDailyStats(_ stats: [UsageBucketStat]) { + for stat in stats { + UsageWindowSummary.mergeDailyStat(stat, into: &dailyStats) + } + } } struct MatrixUsageIndex: Equatable { @@ -272,6 +354,7 @@ struct PackageUsageSnapshot: Equatable { var cacheReadTokens: Int64 = 0 var lastUsedAt: Date? var componentStats: [PackageComponentUsageStat] = [] + var dailyStats: [UsageBucketStat] = [] var totalTokens: Int64 { inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens @@ -288,6 +371,9 @@ struct PackageUsageSnapshot: Equatable { outputTokens += stat.outputTokens cacheCreationTokens += stat.cacheCreationTokens cacheReadTokens += stat.cacheReadTokens + for bucket in stat.dailyStats { + UsageWindowSummary.mergeDailyStat(bucket, into: &dailyStats) + } upsertComponentStat(stat, component: component) guard let date = stat.lastUsedAt else { @@ -326,6 +412,7 @@ struct PackageComponentUsageStat: Identifiable, Equatable { var cacheCreationTokens: Int64 var cacheReadTokens: Int64 var lastUsedAt: Date? + var dailyStats: [UsageBucketStat] var totalTokens: Int64 { inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens @@ -342,6 +429,7 @@ struct PackageComponentUsageStat: Identifiable, Equatable { cacheCreationTokens = stat.cacheCreationTokens cacheReadTokens = stat.cacheReadTokens lastUsedAt = stat.lastUsedAt + dailyStats = stat.dailyStats } mutating func add(_ stat: SkillUsageStat) { @@ -350,6 +438,9 @@ struct PackageComponentUsageStat: Identifiable, Equatable { outputTokens += stat.outputTokens cacheCreationTokens += stat.cacheCreationTokens cacheReadTokens += stat.cacheReadTokens + for bucket in stat.dailyStats { + UsageWindowSummary.mergeDailyStat(bucket, into: &dailyStats) + } guard let date = stat.lastUsedAt else { return diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 00c410a..6d1cf7e 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -388,6 +388,8 @@ "matrix.package.usage.calls" = "30d calls"; "matrix.package.usage.window" = "Last %d days from local transcript attribution."; "matrix.package.usage.lastUsed" = "Last used %@"; +"matrix.package.usage.trend" = "30d call trend"; +"matrix.package.usage.peak" = "Peak %@ calls on %@"; "matrix.package.usage.empty" = "Usage was scanned, but the %d local skills matched to this bundle have no attribution records yet."; "matrix.package.usage.notScanned" = "Transcript usage has not been scanned yet."; "matrix.package.usage.scanning" = "Scanning local transcripts…"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 7a3f9c4..d4d45b3 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -388,6 +388,8 @@ "matrix.package.usage.calls" = "近30天调用"; "matrix.package.usage.window" = "最近 %d 天的本机 transcript 归因统计。"; "matrix.package.usage.lastUsed" = "最近使用 %@"; +"matrix.package.usage.trend" = "近30天调用趋势"; +"matrix.package.usage.peak" = "峰值 %@ 次 · %@"; "matrix.package.usage.empty" = "已扫描用量,但这个套装匹配到的 %d 个本地 skill 暂无归因记录。"; "matrix.package.usage.notScanned" = "还没有扫描 transcript,用量聚合暂不可用。"; "matrix.package.usage.scanning" = "正在扫描本机 transcript…"; diff --git a/swift-app/Sources/Popskill/Storage/TranscriptUsageScanner.swift b/swift-app/Sources/Popskill/Storage/TranscriptUsageScanner.swift index a196dc8..779d51f 100644 --- a/swift-app/Sources/Popskill/Storage/TranscriptUsageScanner.swift +++ b/swift-app/Sources/Popskill/Storage/TranscriptUsageScanner.swift @@ -236,6 +236,7 @@ struct TranscriptUsageScanner { skillID: attributionSkill, sourcePlugin: attributionPlugin, timestamp: timestamp, + dayStart: nil, inputTokens: inputTokens, outputTokens: outputTokens, cacheCreationTokens: cacheCreationTokens, @@ -245,11 +246,14 @@ struct TranscriptUsageScanner { } if timestamp.map({ $0 >= recentStart && $0 <= referenceDate }) == true { - recentSummary.usageEvents += 1 - recentSummary.inputTokens += inputTokens - recentSummary.outputTokens += outputTokens - recentSummary.cacheCreationTokens += cacheCreationTokens - recentSummary.cacheReadTokens += cacheReadTokens + let dayStart = timestamp.map(normalizedDayStart) ?? normalizedDayStart(referenceDate) + recentSummary.addUsage( + inputTokens: inputTokens, + outputTokens: outputTokens, + cacheCreationTokens: cacheCreationTokens, + cacheReadTokens: cacheReadTokens, + dayStart: dayStart + ) recordModelUsage( model: model, inputTokens: inputTokens, @@ -264,6 +268,7 @@ struct TranscriptUsageScanner { skillID: attributionSkill, sourcePlugin: attributionPlugin, timestamp: timestamp, + dayStart: dayStart, inputTokens: inputTokens, outputTokens: outputTokens, cacheCreationTokens: cacheCreationTokens, @@ -302,6 +307,7 @@ struct TranscriptUsageScanner { skillID: String, sourcePlugin: String?, timestamp: Date?, + dayStart: Date?, inputTokens: Int64, outputTokens: Int64, cacheCreationTokens: Int64, @@ -326,11 +332,16 @@ struct TranscriptUsageScanner { outputTokens: outputTokens, cacheCreationTokens: cacheCreationTokens, cacheReadTokens: cacheReadTokens, - timestamp: timestamp + timestamp: timestamp, + dayStart: dayStart ) skillStats[skillID] = skillStat } + private func normalizedDayStart(_ timestamp: Date) -> Date { + Self.utcCalendar.startOfDay(for: timestamp) + } + private func sortedModelStats(_ stats: [String: ModelUsageStat]) -> [ModelUsageStat] { stats.values.sorted { if $0.totalTokens == $1.totalTokens { @@ -440,4 +451,9 @@ struct TranscriptUsageScanner { private static let readChunkSize = 64 * 1024 private static let newlineData = Data([0x0A]) private static let carriageReturnByte: UInt8 = 0x0D + private static let utcCalendar: Calendar = { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? TimeZone.current + return calendar + }() } diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 6b03e30..c9a4287 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -499,6 +499,9 @@ struct InspectorPane: View { .font(.caption2) .foregroundStyle(Color.popTertiaryLabel) } + if !snapshot.dailyStats.isEmpty { + usageTrend(snapshot.dailyStats) + } } else { Text(localization.string("matrix.skill.usage.empty")) .font(.caption) @@ -651,6 +654,9 @@ struct InspectorPane: View { .font(.caption2) .foregroundStyle(Color.popTertiaryLabel) } + if !snapshot.dailyStats.isEmpty { + usageTrend(snapshot.dailyStats) + } if !snapshot.componentStats.isEmpty { packageUsageBreakdown(snapshot.componentStats) } @@ -677,6 +683,79 @@ struct InspectorPane: View { } } + private func usageTrend(_ stats: [UsageBucketStat]) -> some View { + let buckets = usageTrendBuckets(stats, window: store.usageSummary?.recent30Days) + let peak = buckets.max { lhs, rhs in + if lhs.usageEvents == rhs.usageEvents { + return lhs.dayStart < rhs.dayStart + } + return lhs.usageEvents < rhs.usageEvents + } + + return VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + LocalizedText("matrix.package.usage.trend") + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + Spacer(minLength: 8) + if let peak, peak.usageEvents > 0 { + Text(localization.string( + "matrix.package.usage.peak", + UsageDisplayFormatter.compactCount(peak.usageEvents), + Self.usageDayFormatter.string(from: peak.dayStart) + )) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } + + UsageTrendBars(buckets: buckets, tint: .accentColor) + + if let first = buckets.first?.dayStart, let last = buckets.last?.dayStart { + HStack { + Text(Self.usageDayFormatter.string(from: first)) + Spacer() + Text(Self.usageDayFormatter.string(from: last)) + } + .font(.caption2.monospacedDigit()) + .foregroundStyle(Color.popTertiaryLabel) + } + } + .padding(9) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + + private func usageTrendBuckets(_ stats: [UsageBucketStat], window: UsageWindowSummary?) -> [UsageBucketStat] { + let indexed = Dictionary(uniqueKeysWithValues: stats.map { + (Self.usageCalendar.startOfDay(for: $0.dayStart), $0) + }) + + guard let window else { + return stats.sorted { $0.dayStart < $1.dayStart } + } + + let endDay = Self.usageCalendar.startOfDay(for: window.endedAt) + let startDay = Self.usageCalendar.date( + byAdding: .day, + value: -(max(1, window.days) - 1), + to: endDay + ) ?? endDay + + return (0.. some View { VStack(alignment: .leading, spacing: 6) { LocalizedText("matrix.package.usage.topComponents") @@ -1398,6 +1477,19 @@ struct InspectorPane: View { return formatter }() + private static let usageDayFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "M-d" + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter + }() + + private static let usageCalendar: Calendar = { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? TimeZone.current + return calendar + }() + private static let actionGridColumns: [GridItem] = [ GridItem(.adaptive(minimum: 132), spacing: 8, alignment: .leading) ] @@ -1454,6 +1546,36 @@ private struct InspectorHeaderChip: Identifiable { let tint: Color } +private struct UsageTrendBars: View { + let buckets: [UsageBucketStat] + let tint: Color + + private var maxEvents: Int { + max(buckets.map(\.usageEvents).max() ?? 0, 1) + } + + var body: some View { + HStack(alignment: .bottom, spacing: 2) { + ForEach(buckets) { bucket in + RoundedRectangle(cornerRadius: 2, style: .continuous) + .fill(bucket.usageEvents > 0 ? tint.opacity(0.72) : Color.popControlFill) + .frame(height: barHeight(for: bucket)) + .frame(maxWidth: .infinity, alignment: .bottom) + .help("\(bucket.usageEvents)") + } + } + .frame(height: 42, alignment: .bottom) + } + + private func barHeight(for bucket: UsageBucketStat) -> CGFloat { + guard bucket.usageEvents > 0 else { + return 4 + } + let ratio = CGFloat(bucket.usageEvents) / CGFloat(maxEvents) + return max(7, ratio * 42) + } +} + private enum InspectorTab: String, CaseIterable, Identifiable { case overview case readme diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 496810d..92126e8 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -420,7 +420,8 @@ struct SkillModelsTests { cacheReadTokens: 0, lastUsedAt: Date(timeIntervalSince1970: 1_700_000_000) ) - let recentStat = SkillUsageStat( + let recentDay = Date(timeIntervalSince1970: 1_799_971_200) + var recentStat = SkillUsageStat( skillID: "baoyu-comic", sourcePlugin: nil, usageEvents: 2, @@ -430,6 +431,16 @@ struct SkillModelsTests { cacheReadTokens: 0, lastUsedAt: Date(timeIntervalSince1970: 1_800_000_000) ) + recentStat.dailyStats = [ + UsageBucketStat( + dayStart: recentDay, + usageEvents: 2, + inputTokens: 4, + outputTokens: 6, + cacheCreationTokens: 0, + cacheReadTokens: 0 + ) + ] var recentWindow = UsageWindowSummary( days: 30, startedAt: Date(timeIntervalSince1970: 1_799_000_000), @@ -454,6 +465,8 @@ struct SkillModelsTests { #expect(snapshot?.usageEvents == 2) #expect(snapshot?.totalTokens == 10) #expect(snapshot?.lastUsedAt == recentStat.lastUsedAt) + #expect(snapshot?.dailyStats.map(\.dayStart) == [recentDay]) + #expect(snapshot?.dailyStats.first?.usageEvents == 2) } @Test @@ -1115,7 +1128,8 @@ struct SkillModelsTests { cacheReadTokens: 0, lastUsedAt: nil ) - let recentStat = SkillUsageStat( + let recentDay = Date(timeIntervalSince1970: 1_799_971_200) + var recentStat = SkillUsageStat( skillID: "baoyu-comic", sourcePlugin: nil, usageEvents: 1, @@ -1125,6 +1139,16 @@ struct SkillModelsTests { cacheReadTokens: 0, lastUsedAt: Date(timeIntervalSince1970: 1_800_000_000) ) + recentStat.dailyStats = [ + UsageBucketStat( + dayStart: recentDay, + usageEvents: 1, + inputTokens: 2, + outputTokens: 3, + cacheCreationTokens: 0, + cacheReadTokens: 0 + ) + ] var recentWindow = UsageWindowSummary( days: 30, startedAt: Date(timeIntervalSince1970: 1_799_000_000), @@ -1149,6 +1173,8 @@ struct SkillModelsTests { #expect(snapshot?.usageEvents == 1) #expect(snapshot?.totalTokens == 5) #expect(snapshot?.componentStats.first?.componentID == "baoyu-comic") + #expect(snapshot?.dailyStats.map(\.dayStart) == [recentDay]) + #expect(snapshot?.componentStats.first?.dailyStats.map(\.usageEvents) == [1]) } @Test diff --git a/swift-app/Tests/PopskillTests/TranscriptUsageScannerTests.swift b/swift-app/Tests/PopskillTests/TranscriptUsageScannerTests.swift index 46da113..ace2186 100644 --- a/swift-app/Tests/PopskillTests/TranscriptUsageScannerTests.swift +++ b/swift-app/Tests/PopskillTests/TranscriptUsageScannerTests.swift @@ -126,6 +126,7 @@ struct TranscriptUsageScannerTests { let transcript = project.appendingPathComponent("session.jsonl") let lines = [ #"{"type":"assistant","sessionId":"recent","timestamp":"2026-05-12T01:01:00.000Z","attributionSkill":"recent-skill","message":{"role":"assistant","model":"claude-opus","usage":{"input_tokens":10,"output_tokens":5}}}"#, + #"{"type":"assistant","sessionId":"recent","timestamp":"2026-05-13T02:01:00.000Z","attributionSkill":"recent-skill","message":{"role":"assistant","model":"claude-opus","usage":{"input_tokens":1,"output_tokens":1}}}"#, #"{"type":"assistant","sessionId":"old","timestamp":"2026-03-01T01:01:00.000Z","attributionSkill":"old-skill","message":{"role":"assistant","model":"claude-sonnet","usage":{"input_tokens":90,"output_tokens":10}}}"# ] try lines.joined(separator: "\n").write(to: transcript, atomically: true, encoding: .utf8) @@ -133,14 +134,22 @@ struct TranscriptUsageScannerTests { let referenceDate = try #require(Self.iso8601.date(from: "2026-05-20T00:00:00Z")) let summary = try TranscriptUsageScanner(projectsURL: root, referenceDate: referenceDate).scan() - #expect(summary.usageEvents == 2) - #expect(summary.totalTokens == 115) + #expect(summary.usageEvents == 3) + #expect(summary.totalTokens == 117) #expect(summary.skillStats.map(\.skillID) == ["old-skill", "recent-skill"]) #expect(summary.recent30Days?.days == 30) - #expect(summary.recent30Days?.usageEvents == 1) - #expect(summary.recent30Days?.totalTokens == 15) + #expect(summary.recent30Days?.usageEvents == 2) + #expect(summary.recent30Days?.totalTokens == 17) #expect(summary.recent30Days?.skillStats.map(\.skillID) == ["recent-skill"]) #expect(summary.recent30Days?.modelStats.map(\.model) == ["claude-opus"]) + + let dailyStats = try #require(summary.recent30Days?.dailyStats) + #expect(dailyStats.map(\.usageEvents) == [1, 1]) + #expect(dailyStats.map(\.totalTokens) == [15, 2]) + + let skillDailyStats = try #require(summary.recent30Days?.skillStats.first?.dailyStats) + #expect(skillDailyStats.map(\.usageEvents) == [1, 1]) + #expect(skillDailyStats.map(\.totalTokens) == [15, 2]) } @Test From 74ec1f4333fc398ab3a4f7313d3e47a64845484d Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 06:02:57 +0800 Subject: [PATCH 31/47] feat(ui): show skill bundle context --- .../Sources/Popskill/Models/SkillModels.swift | 9 ++ .../Resources/en.lproj/Localizable.strings | 4 + .../zh-Hans.lproj/Localizable.strings | 4 + .../Popskill/Views/InspectorPane.swift | 89 +++++++++++++++++++ .../PopskillTests/SkillModelsTests.swift | 17 ++++ 5 files changed, 123 insertions(+) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index 88f7d4c..f76b0cf 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -1393,6 +1393,15 @@ extension PackageComponent { } extension CapabilityPackage { + func containsSkill(_ skill: Skill) -> Bool { + components.skills.contains { $0.matchesSkill(skill) } + } + + func companionInstalledSkills(for skill: Skill, in skills: [Skill]) -> [Skill] { + matchingInstalledSkills(in: skills) + .filter { $0.id != skill.id } + } + func matchingInstalledSkills(in skills: [Skill]) -> [Skill] { var seen: Set = [] var matches: [Skill] = [] diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 6d1cf7e..c560a1d 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -397,6 +397,9 @@ "matrix.package.usage.componentCalls" = "%d calls"; "matrix.skill.usage.empty" = "Usage was scanned, but this skill has no attribution records yet."; "matrix.skill.usage.notScanned" = "Transcript usage has not been scanned yet."; +"matrix.skill.bundle.open" = "Open bundle"; +"matrix.skill.bundle.companions" = "%d companion skills"; +"matrix.skill.bundle.openCompanion" = "Open %@"; "matrix.package.paths.empty" = "No local skill files matched this bundle."; "matrix.package.paths.more" = "%d more matched skills hidden."; @@ -426,6 +429,7 @@ "matrix.inspector.section.coverage" = "COVERAGE"; "matrix.inspector.section.usage" = "USAGE"; "matrix.inspector.section.components" = "COMPONENTS"; +"matrix.inspector.section.bundle" = "BUNDLE"; "matrix.inspector.section.config" = "CONFIG"; "matrix.inspector.section.paths" = "LOCAL PATHS"; "matrix.inspector.section.version" = "VERSION"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index d4d45b3..9b11ae2 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -397,6 +397,9 @@ "matrix.package.usage.componentCalls" = "%d 次调用"; "matrix.skill.usage.empty" = "已扫描用量,但这个 skill 暂无归因记录。"; "matrix.skill.usage.notScanned" = "还没有扫描 transcript,用量暂不可用。"; +"matrix.skill.bundle.open" = "打开套装"; +"matrix.skill.bundle.companions" = "%d 个同伴技能"; +"matrix.skill.bundle.openCompanion" = "打开 %@"; "matrix.package.paths.empty" = "没有匹配到本地 skill 真身。"; "matrix.package.paths.more" = "还有 %d 个匹配 skill 未展开。"; @@ -426,6 +429,7 @@ "matrix.inspector.section.coverage" = "覆盖"; "matrix.inspector.section.usage" = "用量"; "matrix.inspector.section.components" = "组件"; +"matrix.inspector.section.bundle" = "所属套装"; "matrix.inspector.section.config" = "配置"; "matrix.inspector.section.paths" = "本地路径"; "matrix.inspector.section.version" = "版本"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index c9a4287..0da35bc 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -207,6 +207,9 @@ struct InspectorPane: View { if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { triggerSection(scenarios: scenarios) } + if let skill = selectedSkill { + skillBundleSection(skill) + } appsSection case .readme: if let skill = selectedSkill { @@ -1205,6 +1208,84 @@ struct InspectorPane: View { } } + @ViewBuilder + private func skillBundleSection(_ skill: Skill) -> some View { + let packages = containingPackages(for: skill) + if !packages.isEmpty { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.bundle", accent: .popSectionPurple) + ForEach(packages, id: \.id) { package in + skillBundleCard(package, skill: skill) + } + } + } + } + + private func skillBundleCard(_ package: CapabilityPackage, skill: Skill) -> some View { + let companions = package.companionInstalledSkills(for: skill, in: store.skills) + return VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .center, spacing: 8) { + PackageAvatar(name: package.name, identifier: package.id, size: 24) + VStack(alignment: .leading, spacing: 2) { + Text(package.name) + .font(.caption.weight(.semibold)) + .foregroundStyle(Color.popLabel) + .lineLimit(1) + Text(localization.string( + "package.componentSummary", + package.componentCount, + package.installedComponentCount, + package.requiredComponentCount + )) + .font(.caption2) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + } + Spacer(minLength: 8) + Button { + store.selectCapability(MatrixCapability.packageCapabilityID(for: package.id)) + } label: { + Image(systemName: "arrow.right.circle") + .font(.system(size: 13, weight: .semibold)) + } + .buttonStyle(.plain) + .foregroundStyle(Color.accentColor) + .help(localization.string("matrix.skill.bundle.open")) + } + + if !companions.isEmpty { + Text(localization.string("matrix.skill.bundle.companions", companions.count)) + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + + LazyVGrid(columns: Self.companionGridColumns, alignment: .leading, spacing: 5) { + ForEach(companions.prefix(8), id: \.id) { companion in + Button { + store.selectCapability(MatrixCapability.skillCapabilityID(for: companion.id)) + } label: { + Text(companion.name) + .font(.caption2.weight(.semibold)) + .lineLimit(1) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background(Color.popControlFill, in: Capsule()) + } + .buttonStyle(.plain) + .foregroundStyle(Color.popSecondaryLabel) + .help(localization.string("matrix.skill.bundle.openCompanion", companion.name)) + } + if companions.count > 8 { + Text(localization.string("matrix.package.paths.more", companions.count - 8)) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } + } + } + .padding(9) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + private var appsSection: some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.apps") @@ -1494,6 +1575,10 @@ struct InspectorPane: View { GridItem(.adaptive(minimum: 132), spacing: 8, alignment: .leading) ] + private static let companionGridColumns: [GridItem] = [ + GridItem(.adaptive(minimum: 84), spacing: 5, alignment: .leading) + ] + private func firstRevealableSkillURL(for package: CapabilityPackage) -> URL? { package.matchingInstalledSkills(in: store.skills) .first { FileManager.default.fileExists(atPath: $0.localStoreURL.path) }? @@ -1509,6 +1594,10 @@ struct InspectorPane: View { store.updates.filter { package.matchingSkillComponent(for: $0) != nil } } + private func containingPackages(for skill: Skill) -> [CapabilityPackage] { + store.compositePackages.filter { $0.containsSkill(skill) } + } + // MARK: Toggle helpers private func toggleKey(_ app: TargetApp) -> String { diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 92126e8..babaeac 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -1177,6 +1177,23 @@ struct SkillModelsTests { #expect(snapshot?.componentStats.first?.dailyStats.map(\.usageEvents) == [1]) } + @Test + func capabilityPackageFindsContainedAndCompanionSkills() { + let package = self.package(components: [ + component(id: "baoyu-comic", installed: true), + component(id: "baoyu-translate", installed: true), + component(id: "declared-only", installed: false) + ]) + let comic = installedSkill(directory: "baoyu-comic") + let translate = installedSkill(directory: "baoyu-translate") + let unrelated = installedSkill(directory: "other-skill") + + #expect(package.containsSkill(comic)) + #expect(package.containsSkill(translate)) + #expect(!package.containsSkill(unrelated)) + #expect(package.companionInstalledSkills(for: comic, in: [comic, translate, unrelated]).map(\.id) == ["baoyu-translate"]) + } + @Test func matrixUsageIndexCachesSkillPackageAndComponentUsage() { let skill = installedSkill(directory: "baoyu-comic") From fdb52fced0aa8bdac6eab67053ade70dab2d8c70 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 06:12:02 +0800 Subject: [PATCH 32/47] feat(ui): add skill inspector actions --- .../Resources/en.lproj/Localizable.strings | 4 + .../zh-Hans.lproj/Localizable.strings | 4 + .../Popskill/Views/InspectorPane.swift | 74 +++++++++++++++++-- .../PopskillTests/LocalizationTests.swift | 10 +++ 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index c560a1d..9ba05a4 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -440,6 +440,10 @@ "matrix.package.action.checkUpdates" = "Check updates"; "matrix.package.action.openSource" = "Open source"; "matrix.package.action.revealInFinder" = "Reveal local skill"; +"matrix.skill.action.openReadme" = "Edit prompt"; +"matrix.skill.action.checkUpdates" = "Check updates"; +"matrix.skill.action.openSource" = "Open source"; +"matrix.skill.action.revealInFinder" = "Reveal in Finder"; "matrix.skill.paths.readme" = "SKILL.md"; "matrix.package.version.strategy" = "Strategy"; "matrix.package.version.branch" = "Branch"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 9b11ae2..1cae0ae 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -440,6 +440,10 @@ "matrix.package.action.checkUpdates" = "检查更新"; "matrix.package.action.openSource" = "打开来源"; "matrix.package.action.revealInFinder" = "定位本地 Skill"; +"matrix.skill.action.openReadme" = "编辑 prompt"; +"matrix.skill.action.checkUpdates" = "检查更新"; +"matrix.skill.action.openSource" = "打开来源"; +"matrix.skill.action.revealInFinder" = "在 Finder 中显示"; "matrix.skill.paths.readme" = "SKILL.md"; "matrix.package.version.strategy" = "策略"; "matrix.package.version.branch" = "分支"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 0da35bc..d1882dc 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -204,6 +204,9 @@ struct InspectorPane: View { if !primaryDescription.isEmpty { summarySection } + if let skill = selectedSkill { + skillActionsSection(skill) + } if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { triggerSection(scenarios: scenarios) } @@ -267,7 +270,7 @@ struct InspectorPane: View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.actions", accent: .accentColor) LazyVGrid(columns: Self.actionGridColumns, alignment: .leading, spacing: 8) { - packageActionButton( + inspectorActionButton( titleKey: "matrix.package.action.rescanUsage", systemImage: "chart.bar.doc.horizontal", inFlight: store.usageScanInFlight, @@ -276,7 +279,7 @@ struct InspectorPane: View { Task { await store.refreshUsageScan() } } - packageActionButton( + inspectorActionButton( titleKey: "matrix.package.action.checkUpdates", systemImage: "arrow.clockwise", inFlight: store.updatesRefreshInFlight, @@ -287,7 +290,7 @@ struct InspectorPane: View { sourceAction(for: package) - packageActionButton( + inspectorActionButton( titleKey: "matrix.package.action.revealInFinder", systemImage: "folder", disabled: firstRevealableSkillURL(for: package) == nil @@ -304,14 +307,14 @@ struct InspectorPane: View { private func sourceAction(for package: CapabilityPackage) -> some View { if let url = package.sourceURL { Link(destination: url) { - packageActionLabel( + inspectorActionLabel( titleKey: "matrix.package.action.openSource", systemImage: "arrow.up.right.square" ) } .buttonStyle(.plain) } else { - packageActionButton( + inspectorActionButton( titleKey: "matrix.package.action.openSource", systemImage: "arrow.up.right.square", disabled: true @@ -319,7 +322,62 @@ struct InspectorPane: View { } } - private func packageActionButton( + private func skillActionsSection(_ skill: Skill) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.actions", accent: .accentColor) + LazyVGrid(columns: Self.actionGridColumns, alignment: .leading, spacing: 8) { + inspectorActionButton( + titleKey: "matrix.skill.action.openReadme", + systemImage: "square.and.pencil", + disabled: skill.markdownURL == nil + ) { + if let url = skill.markdownURL { + NSWorkspace.shared.open(url) + } + } + + inspectorActionButton( + titleKey: "matrix.skill.action.checkUpdates", + systemImage: "arrow.clockwise", + inFlight: store.updatesRefreshInFlight, + disabled: store.updatesRefreshInFlight + ) { + Task { await store.refreshUpdates(force: true) } + } + + sourceAction(for: skill) + + inspectorActionButton( + titleKey: "matrix.skill.action.revealInFinder", + systemImage: "folder", + disabled: !FileManager.default.fileExists(atPath: skill.localStoreURL.path) + ) { + NSWorkspace.shared.activateFileViewerSelecting([skill.localStoreURL]) + } + } + } + } + + @ViewBuilder + private func sourceAction(for skill: Skill) -> some View { + if let url = skill.sourceURL { + Link(destination: url) { + inspectorActionLabel( + titleKey: "matrix.skill.action.openSource", + systemImage: "arrow.up.right.square" + ) + } + .buttonStyle(.plain) + } else { + inspectorActionButton( + titleKey: "matrix.skill.action.openSource", + systemImage: "arrow.up.right.square", + disabled: true + ) {} + } + } + + private func inspectorActionButton( titleKey: String, systemImage: String, inFlight: Bool = false, @@ -327,14 +385,14 @@ struct InspectorPane: View { action: @escaping () -> Void ) -> some View { Button(action: action) { - packageActionLabel(titleKey: titleKey, systemImage: systemImage, inFlight: inFlight) + inspectorActionLabel(titleKey: titleKey, systemImage: systemImage, inFlight: inFlight) } .buttonStyle(.plain) .disabled(disabled) .opacity(disabled ? 0.52 : 1) } - private func packageActionLabel(titleKey: String, systemImage: String, inFlight: Bool = false) -> some View { + private func inspectorActionLabel(titleKey: String, systemImage: String, inFlight: Bool = false) -> some View { HStack(spacing: 7) { if inFlight { ProgressView() diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 8520bb2..7ed78dc 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -36,4 +36,14 @@ struct LocalizationTests { #expect(localization.string("matrix.inspector.tab.sync") == "同步") #expect(localization.string("matrix.inspector.header.calls", "412") == "412 次调用") } + + @Test + func inspectorSkillActionLabelsAreLocalized() { + let localization = PopskillLocalization(language: .simplifiedChinese) + + #expect(localization.string("matrix.skill.action.openReadme") == "编辑 prompt") + #expect(localization.string("matrix.skill.action.checkUpdates") == "检查更新") + #expect(localization.string("matrix.skill.action.openSource") == "打开来源") + #expect(localization.string("matrix.skill.action.revealInFinder") == "在 Finder 中显示") + } } From f069693959bdac90ef8a96675f9553a5b03b1326 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 06:21:46 +0800 Subject: [PATCH 33/47] feat(ui): activate bundle apps from inspector --- .../Sources/Popskill/Models/SkillModels.swift | 5 ++ .../Resources/en.lproj/Localizable.strings | 7 ++ .../zh-Hans.lproj/Localizable.strings | 7 ++ .../Popskill/Views/InspectorPane.swift | 85 +++++++++++++++++++ .../PopskillTests/LocalizationTests.swift | 2 + .../PopskillTests/SkillModelsTests.swift | 2 + 6 files changed, 108 insertions(+) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index f76b0cf..8fb774f 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -1402,6 +1402,11 @@ extension CapabilityPackage { .filter { $0.id != skill.id } } + func installedSkillsRequiringEnablement(for app: TargetApp, in skills: [Skill]) -> [Skill] { + matchingInstalledSkills(in: skills) + .filter { !$0.apps.isEnabled(app) } + } + func matchingInstalledSkills(in skills: [Skill]) -> [Skill] { var seen: Set = [] var matches: [Skill] = [] diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 9ba05a4..2339f82 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -430,6 +430,7 @@ "matrix.inspector.section.usage" = "USAGE"; "matrix.inspector.section.components" = "COMPONENTS"; "matrix.inspector.section.bundle" = "BUNDLE"; +"matrix.inspector.section.activation" = "ACTIVATION"; "matrix.inspector.section.config" = "CONFIG"; "matrix.inspector.section.paths" = "LOCAL PATHS"; "matrix.inspector.section.version" = "VERSION"; @@ -440,6 +441,12 @@ "matrix.package.action.checkUpdates" = "Check updates"; "matrix.package.action.openSource" = "Open source"; "matrix.package.action.revealInFinder" = "Reveal local skill"; +"matrix.package.action.activateClaude" = "Activate all for Claude"; +"matrix.package.action.activateCodex" = "Activate all for Codex"; +"matrix.package.action.activateApp" = "Activate all"; +"matrix.package.activation.empty" = "No matched local skills can be activated from this bundle."; +"matrix.package.activation.complete" = "All matched local skills are active on Claude and Codex."; +"matrix.package.activation.remaining" = "%d Claude gaps · %d Codex gaps"; "matrix.skill.action.openReadme" = "Edit prompt"; "matrix.skill.action.checkUpdates" = "Check updates"; "matrix.skill.action.openSource" = "Open source"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 1cae0ae..54adb2f 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -430,6 +430,7 @@ "matrix.inspector.section.usage" = "用量"; "matrix.inspector.section.components" = "组件"; "matrix.inspector.section.bundle" = "所属套装"; +"matrix.inspector.section.activation" = "批量启用"; "matrix.inspector.section.config" = "配置"; "matrix.inspector.section.paths" = "本地路径"; "matrix.inspector.section.version" = "版本"; @@ -440,6 +441,12 @@ "matrix.package.action.checkUpdates" = "检查更新"; "matrix.package.action.openSource" = "打开来源"; "matrix.package.action.revealInFinder" = "定位本地 Skill"; +"matrix.package.action.activateClaude" = "全部激活到 Claude"; +"matrix.package.action.activateCodex" = "全部激活到 Codex"; +"matrix.package.action.activateApp" = "全部激活"; +"matrix.package.activation.empty" = "这个套装没有可批量启用的本地 skill。"; +"matrix.package.activation.complete" = "匹配到的本地 skill 已全部激活到 Claude 和 Codex。"; +"matrix.package.activation.remaining" = "Claude 还差 %d 个 · Codex 还差 %d 个"; "matrix.skill.action.openReadme" = "编辑 prompt"; "matrix.skill.action.checkUpdates" = "检查更新"; "matrix.skill.action.openSource" = "打开来源"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index d1882dc..5422bf7 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -170,6 +170,7 @@ struct InspectorPane: View { case .overview: packageSummarySection(package) packageCoverageSection + packageActivationSection(package) packageComponentsSection(package) case .readme: if let skill = readmeSkill(for: package) { @@ -644,6 +645,53 @@ struct InspectorPane: View { .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) } + private func packageActivationSection(_ package: CapabilityPackage) -> some View { + let matchedSkills = package.matchingInstalledSkills(in: store.skills) + let claudeTargets = package.installedSkillsRequiringEnablement(for: .claude, in: store.skills) + let codexTargets = package.installedSkillsRequiringEnablement(for: .codex, in: store.skills) + + return VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.activation", accent: .accentColor) + LazyVGrid(columns: Self.actionGridColumns, alignment: .leading, spacing: 8) { + packageActivationButton(app: .claude, package: package, targets: claudeTargets) + packageActivationButton(app: .codex, package: package, targets: codexTargets) + } + if matchedSkills.isEmpty { + Text(localization.string("matrix.package.activation.empty")) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } else if claudeTargets.isEmpty && codexTargets.isEmpty { + Text(localization.string("matrix.package.activation.complete")) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } else { + Text(localization.string("matrix.package.activation.remaining", claudeTargets.count, codexTargets.count)) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } + } + + private func packageActivationButton(app: TargetApp, package: CapabilityPackage, targets: [Skill]) -> some View { + let inFlight = packageActivationInFlight(for: targets, app: app) + return inspectorActionButton( + titleKey: packageActivationTitleKey(for: app), + systemImage: app.symbolName, + inFlight: inFlight, + disabled: targets.isEmpty || inFlight + ) { + Task { await activatePackage(package, app: app) } + } + } + + private func packageActivationTitleKey(for app: TargetApp) -> String { + switch app { + case .claude: return "matrix.package.action.activateClaude" + case .codex: return "matrix.package.action.activateCodex" + default: return "matrix.package.action.activateApp" + } + } + private func packageComponentsSection(_ package: CapabilityPackage) -> some View { let usageStatsByComponentID = Dictionary( uniqueKeysWithValues: package @@ -1662,6 +1710,14 @@ struct InspectorPane: View { MatrixCapability.toggleKey(capabilityID: capability.id, app: app) } + private func packageToggleKey(skillID: String, app: TargetApp) -> String { + MatrixCapability.toggleKey(capabilityID: MatrixCapability.skillCapabilityID(for: skillID), app: app) + } + + private func packageActivationInFlight(for targets: [Skill], app: TargetApp) -> Bool { + targets.contains { store.pendingToggles.contains(packageToggleKey(skillID: $0.id, app: app)) } + } + @MainActor private func toggle(app: TargetApp, enabled: Bool) async { guard let skillID = capability.underlyingSkillID else { return } @@ -1679,6 +1735,35 @@ struct InspectorPane: View { store.errorMessage = error.localizedDescription } } + + @MainActor + private func activatePackage(_ package: CapabilityPackage, app: TargetApp) async { + let targets = package.installedSkillsRequiringEnablement(for: app, in: store.skills) + guard !targets.isEmpty else { return } + + let keys = Set(targets.map { packageToggleKey(skillID: $0.id, app: app) }) + guard keys.isDisjoint(with: store.pendingToggles) else { return } + store.pendingToggles.formUnion(keys) + defer { store.pendingToggles.subtract(keys) } + + var firstError: Error? + for target in targets { + do { + try await store.client.toggle(skillID: target.id, app: app, enabled: true) + if let idx = store.skills.firstIndex(where: { $0.id == target.id }) { + store.skills[idx].apps.setEnabled(true, for: app) + } + } catch { + if firstError == nil { + firstError = error + } + } + } + + if let firstError { + store.errorMessage = firstError.localizedDescription + } + } } private extension String { diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 7ed78dc..5c55a13 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -45,5 +45,7 @@ struct LocalizationTests { #expect(localization.string("matrix.skill.action.checkUpdates") == "检查更新") #expect(localization.string("matrix.skill.action.openSource") == "打开来源") #expect(localization.string("matrix.skill.action.revealInFinder") == "在 Finder 中显示") + #expect(localization.string("matrix.package.action.activateClaude") == "全部激活到 Claude") + #expect(localization.string("matrix.package.activation.remaining", 2, 1) == "Claude 还差 2 个 · Codex 还差 1 个") } } diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index babaeac..204a4cb 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -1192,6 +1192,8 @@ struct SkillModelsTests { #expect(package.containsSkill(translate)) #expect(!package.containsSkill(unrelated)) #expect(package.companionInstalledSkills(for: comic, in: [comic, translate, unrelated]).map(\.id) == ["baoyu-translate"]) + #expect(package.installedSkillsRequiringEnablement(for: .claude, in: [comic, translate, unrelated]).isEmpty) + #expect(package.installedSkillsRequiringEnablement(for: .codex, in: [comic, translate, unrelated]).map(\.id) == ["baoyu-comic", "baoyu-translate"]) } @Test From 17c95863c0f129662a0a13c7d3a68447741dc080 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 06:31:15 +0800 Subject: [PATCH 34/47] feat(ui): summarize skill machine state --- .../Resources/en.lproj/Localizable.strings | 6 ++ .../zh-Hans.lproj/Localizable.strings | 6 ++ .../Popskill/Views/InspectorPane.swift | 91 +++++++++++++++++++ .../PopskillTests/LocalizationTests.swift | 3 + 4 files changed, 106 insertions(+) diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 2339f82..0d4576e 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -431,6 +431,7 @@ "matrix.inspector.section.components" = "COMPONENTS"; "matrix.inspector.section.bundle" = "BUNDLE"; "matrix.inspector.section.activation" = "ACTIVATION"; +"matrix.inspector.section.machine" = "THIS MACHINE"; "matrix.inspector.section.config" = "CONFIG"; "matrix.inspector.section.paths" = "LOCAL PATHS"; "matrix.inspector.section.version" = "VERSION"; @@ -447,6 +448,11 @@ "matrix.package.activation.empty" = "No matched local skills can be activated from this bundle."; "matrix.package.activation.complete" = "All matched local skills are active on Claude and Codex."; "matrix.package.activation.remaining" = "%d Claude gaps · %d Codex gaps"; +"matrix.machine.firstActivated" = "First active"; +"matrix.machine.lastUsed" = "Last used"; +"matrix.machine.tokens" = "30d tokens"; +"matrix.machine.calls" = "30d calls"; +"matrix.machine.size" = "Disk size"; "matrix.skill.action.openReadme" = "Edit prompt"; "matrix.skill.action.checkUpdates" = "Check updates"; "matrix.skill.action.openSource" = "Open source"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 54adb2f..0e5dc07 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -431,6 +431,7 @@ "matrix.inspector.section.components" = "组件"; "matrix.inspector.section.bundle" = "所属套装"; "matrix.inspector.section.activation" = "批量启用"; +"matrix.inspector.section.machine" = "这台机器"; "matrix.inspector.section.config" = "配置"; "matrix.inspector.section.paths" = "本地路径"; "matrix.inspector.section.version" = "版本"; @@ -447,6 +448,11 @@ "matrix.package.activation.empty" = "这个套装没有可批量启用的本地 skill。"; "matrix.package.activation.complete" = "匹配到的本地 skill 已全部激活到 Claude 和 Codex。"; "matrix.package.activation.remaining" = "Claude 还差 %d 个 · Codex 还差 %d 个"; +"matrix.machine.firstActivated" = "首次激活"; +"matrix.machine.lastUsed" = "最近使用"; +"matrix.machine.tokens" = "近30天 tokens"; +"matrix.machine.calls" = "近30天调用"; +"matrix.machine.size" = "磁盘占用"; "matrix.skill.action.openReadme" = "编辑 prompt"; "matrix.skill.action.checkUpdates" = "检查更新"; "matrix.skill.action.openSource" = "打开来源"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 5422bf7..0bcdfb1 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -207,6 +207,7 @@ struct InspectorPane: View { } if let skill = selectedSkill { skillActionsSection(skill) + skillMachineSection(skill) } if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { triggerSection(scenarios: scenarios) @@ -359,6 +360,85 @@ struct InspectorPane: View { } } + @ViewBuilder + private func skillMachineSection(_ skill: Skill) -> some View { + let metrics = skillMachineMetrics(skill) + if !metrics.isEmpty { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.machine", accent: .popSectionGreen) + LazyVGrid(columns: Self.machineMetricColumns, alignment: .leading, spacing: 8) { + ForEach(metrics) { metric in + machineMetricCard(metric) + } + } + } + } + } + + private func skillMachineMetrics(_ skill: Skill) -> [InspectorMachineMetric] { + var metrics: [InspectorMachineMetric] = [] + let snapshot = skill.usageSnapshot(using: store.usageSummary) + + if let installedAt = skill.installedAt, installedAt > 0 { + metrics.append(InspectorMachineMetric( + id: "installed", + title: localization.string("matrix.machine.firstActivated"), + value: Self.formatTimestamp(installedAt), + tint: Color.popSectionPurple + )) + } + if let lastUsedAt = snapshot?.lastUsedAt { + metrics.append(InspectorMachineMetric( + id: "last-used", + title: localization.string("matrix.machine.lastUsed"), + value: Self.relativeFormatter.localizedString(for: lastUsedAt, relativeTo: Date()), + tint: Color.popSectionGreen + )) + } + if let snapshot, snapshot.hasUsage { + metrics.append(InspectorMachineMetric( + id: "tokens", + title: localization.string("matrix.machine.tokens"), + value: UsageDisplayFormatter.compactTokens(snapshot.totalTokens), + tint: Color.accentColor + )) + metrics.append(InspectorMachineMetric( + id: "calls", + title: localization.string("matrix.machine.calls"), + value: UsageDisplayFormatter.compactCount(snapshot.usageEvents), + tint: Color.popLabel + )) + } + if let size = skill.sizeBytes, size > 0 { + metrics.append(InspectorMachineMetric( + id: "size", + title: localization.string("matrix.machine.size"), + value: Self.formatBytes(size), + tint: Color.popSecondaryLabel + )) + } + + return metrics + } + + private func machineMetricCard(_ metric: InspectorMachineMetric) -> some View { + VStack(alignment: .leading, spacing: 3) { + Text(metric.title) + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + Text(metric.value) + .font(.system(size: 15, weight: .bold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(metric.tint) + .lineLimit(1) + .minimumScaleFactor(0.82) + } + .padding(9) + .frame(maxWidth: .infinity, alignment: .leading) + .background(metric.tint.opacity(0.07), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + @ViewBuilder private func sourceAction(for skill: Skill) -> some View { if let url = skill.sourceURL { @@ -1681,6 +1761,10 @@ struct InspectorPane: View { GridItem(.adaptive(minimum: 132), spacing: 8, alignment: .leading) ] + private static let machineMetricColumns: [GridItem] = [ + GridItem(.adaptive(minimum: 96), spacing: 8, alignment: .leading) + ] + private static let companionGridColumns: [GridItem] = [ GridItem(.adaptive(minimum: 84), spacing: 5, alignment: .leading) ] @@ -1778,6 +1862,13 @@ private struct InspectorHeaderChip: Identifiable { let tint: Color } +private struct InspectorMachineMetric: Identifiable { + let id: String + let title: String + let value: String + let tint: Color +} + private struct UsageTrendBars: View { let buckets: [UsageBucketStat] let tint: Color diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 5c55a13..00c6090 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -47,5 +47,8 @@ struct LocalizationTests { #expect(localization.string("matrix.skill.action.revealInFinder") == "在 Finder 中显示") #expect(localization.string("matrix.package.action.activateClaude") == "全部激活到 Claude") #expect(localization.string("matrix.package.activation.remaining", 2, 1) == "Claude 还差 2 个 · Codex 还差 1 个") + #expect(localization.string("matrix.inspector.section.machine") == "这台机器") + #expect(localization.string("matrix.machine.firstActivated") == "首次激活") + #expect(localization.string("matrix.machine.tokens") == "近30天 tokens") } } From 6459c220703a01205135f360a61bfd0fee8002ab Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 06:39:09 +0800 Subject: [PATCH 35/47] feat(ui): show skill source paths --- .../Resources/en.lproj/Localizable.strings | 6 ++ .../zh-Hans.lproj/Localizable.strings | 6 ++ .../Popskill/Views/InspectorPane.swift | 82 +++++++++++++++++++ .../PopskillTests/LocalizationTests.swift | 3 + 4 files changed, 97 insertions(+) diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 0d4576e..40ba817 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -432,6 +432,7 @@ "matrix.inspector.section.bundle" = "BUNDLE"; "matrix.inspector.section.activation" = "ACTIVATION"; "matrix.inspector.section.machine" = "THIS MACHINE"; +"matrix.inspector.section.source" = "SOURCE & PATHS"; "matrix.inspector.section.config" = "CONFIG"; "matrix.inspector.section.paths" = "LOCAL PATHS"; "matrix.inspector.section.version" = "VERSION"; @@ -453,6 +454,11 @@ "matrix.machine.tokens" = "30d tokens"; "matrix.machine.calls" = "30d calls"; "matrix.machine.size" = "Disk size"; +"matrix.source.repository" = "Repository"; +"matrix.source.directory" = "Directory"; +"matrix.source.localStore" = "Local store"; +"matrix.source.type" = "Source type"; +"matrix.source.readme" = "SKILL.md"; "matrix.skill.action.openReadme" = "Edit prompt"; "matrix.skill.action.checkUpdates" = "Check updates"; "matrix.skill.action.openSource" = "Open source"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 0e5dc07..6b7ffda 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -432,6 +432,7 @@ "matrix.inspector.section.bundle" = "所属套装"; "matrix.inspector.section.activation" = "批量启用"; "matrix.inspector.section.machine" = "这台机器"; +"matrix.inspector.section.source" = "来源与路径"; "matrix.inspector.section.config" = "配置"; "matrix.inspector.section.paths" = "本地路径"; "matrix.inspector.section.version" = "版本"; @@ -453,6 +454,11 @@ "matrix.machine.tokens" = "近30天 tokens"; "matrix.machine.calls" = "近30天调用"; "matrix.machine.size" = "磁盘占用"; +"matrix.source.repository" = "仓库"; +"matrix.source.directory" = "目录"; +"matrix.source.localStore" = "本地真身"; +"matrix.source.type" = "来源类型"; +"matrix.source.readme" = "SKILL.md"; "matrix.skill.action.openReadme" = "编辑 prompt"; "matrix.skill.action.checkUpdates" = "检查更新"; "matrix.skill.action.openSource" = "打开来源"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 0bcdfb1..71a55a7 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -208,6 +208,7 @@ struct InspectorPane: View { if let skill = selectedSkill { skillActionsSection(skill) skillMachineSection(skill) + skillSourceSection(skill) } if let scenarios = capability.triggerScenarios, !scenarios.isEmpty { triggerSection(scenarios: scenarios) @@ -439,6 +440,80 @@ struct InspectorPane: View { .background(metric.tint.opacity(0.07), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) } + private func skillSourceSection(_ skill: Skill) -> some View { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.source", accent: .popSectionPurple) + VStack(alignment: .leading, spacing: 6) { + ForEach(skillSourceFacts(skill)) { fact in + sourceFactRow(fact) + } + } + } + } + + private func skillSourceFacts(_ skill: Skill) -> [InspectorSourceFact] { + var facts: [InspectorSourceFact] = [ + InspectorSourceFact( + id: "repository", + title: localization.string("matrix.source.repository"), + value: skill.sourceLabel, + icon: "chevron.left.forwardslash.chevron.right" + ), + InspectorSourceFact( + id: "directory", + title: localization.string("matrix.source.directory"), + value: skill.directory, + icon: "folder" + ), + InspectorSourceFact( + id: "local-store", + title: localization.string("matrix.source.localStore"), + value: skill.localStoreURL.path.abbreviatingWithTilde, + icon: "externaldrive" + ) + ] + if let sourceType = skill.sourceType, !sourceType.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + facts.append(InspectorSourceFact( + id: "source-type", + title: localization.string("matrix.source.type"), + value: sourceType, + icon: "tag" + )) + } + if let markdownURL = skill.markdownURL { + facts.append(InspectorSourceFact( + id: "readme", + title: localization.string("matrix.source.readme"), + value: markdownURL.path.abbreviatingWithTilde, + icon: "doc.text" + )) + } + return facts + } + + private func sourceFactRow(_ fact: InspectorSourceFact) -> some View { + HStack(alignment: .top, spacing: 8) { + Image(systemName: fact.icon) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(Color.popSecondaryLabel) + .frame(width: 14) + VStack(alignment: .leading, spacing: 2) { + Text(fact.title) + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + Text(fact.value) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(Color.popLabel) + .lineLimit(2) + .truncationMode(.middle) + .textSelection(.enabled) + } + Spacer(minLength: 8) + } + .padding(8) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + @ViewBuilder private func sourceAction(for skill: Skill) -> some View { if let url = skill.sourceURL { @@ -1869,6 +1944,13 @@ private struct InspectorMachineMetric: Identifiable { let tint: Color } +private struct InspectorSourceFact: Identifiable { + let id: String + let title: String + let value: String + let icon: String +} + private struct UsageTrendBars: View { let buckets: [UsageBucketStat] let tint: Color diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 00c6090..85825a4 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -50,5 +50,8 @@ struct LocalizationTests { #expect(localization.string("matrix.inspector.section.machine") == "这台机器") #expect(localization.string("matrix.machine.firstActivated") == "首次激活") #expect(localization.string("matrix.machine.tokens") == "近30天 tokens") + #expect(localization.string("matrix.inspector.section.source") == "来源与路径") + #expect(localization.string("matrix.source.repository") == "仓库") + #expect(localization.string("matrix.source.localStore") == "本地真身") } } From 7a28e9b6c0222eeed329217d70aaf5ada21e588b Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 06:52:19 +0800 Subject: [PATCH 36/47] feat(ui): surface skill manifest metadata --- skill-cli/src/main.rs | 177 +++++++++++++++++- .../Models/MatrixVersionFormatter.swift | 15 ++ .../Sources/Popskill/Models/SkillModels.swift | 96 +++++++++- .../Resources/en.lproj/Localizable.strings | 8 + .../zh-Hans.lproj/Localizable.strings | 8 + .../Popskill/Views/InspectorPane.swift | 70 ++++++- .../Popskill/Views/MatrixPackageRow.swift | 1 + .../Sources/Popskill/Views/MatrixRow.swift | 1 + .../PopskillTests/LocalizationTests.swift | 3 + .../PopskillTests/SkillModelsTests.swift | 79 ++++++++ 10 files changed, 436 insertions(+), 22 deletions(-) diff --git a/skill-cli/src/main.rs b/skill-cli/src/main.rs index d15e1a7..2d3150e 100644 --- a/skill-cli/src/main.rs +++ b/skill-cli/src/main.rs @@ -557,7 +557,7 @@ async fn run() -> Result<()> { .get_installed_skill(&skill_id) .context("failed to read installed skill")? .with_context(|| format!("skill not found: {skill_id}"))?; - print_json(&ApiResponse::ok(skill)) + print_json(&ApiResponse::ok(enrich_installed_skill(skill))) } Commands::CheckUpdates { json: _ } => { let service = SkillService::new(); @@ -726,7 +726,7 @@ async fn run() -> Result<()> { ); } } - print_json(&ApiResponse::ok(installed)) + print_json(&ApiResponse::ok(enrich_installed_skill(installed))) } Commands::Update { skill_id, json: _ } => { let service = SkillService::new(); @@ -734,7 +734,7 @@ async fn run() -> Result<()> { .update_skill(&db, &skill_id) .await .with_context(|| format!("failed to update skill '{skill_id}'"))?; - print_json(&ApiResponse::ok(skill)) + print_json(&ApiResponse::ok(enrich_installed_skill(skill))) } Commands::Uninstall { skill_id, @@ -761,7 +761,7 @@ async fn run() -> Result<()> { let app_type = parse_target_app(&app)?; let skill = rehydrate_stub(&db, &skill_id, &app_type) .with_context(|| format!("failed to rehydrate skill '{skill_id}'"))?; - print_json(&ApiResponse::ok(skill)) + print_json(&ApiResponse::ok(enrich_installed_skill(skill))) } Commands::SecurityScan { skill_dir, @@ -795,7 +795,7 @@ async fn run() -> Result<()> { let app_type = parse_target_app(&app)?; let skill = SkillService::restore_from_backup(&db, &backup_id, &app_type) .with_context(|| format!("failed to restore skill backup '{backup_id}'"))?; - print_json(&ApiResponse::ok(skill)) + print_json(&ApiResponse::ok(enrich_installed_skill(skill))) } Commands::BackupDelete { backup_id, json: _ } => { SkillService::delete_backup(&backup_id) @@ -2183,6 +2183,34 @@ struct EnrichedInstalledSkill { /// "位置与链接" section 以及"链接健康" view。 #[serde(skip_serializing_if = "Option::is_none")] deployment: Option, + /// SKILL.md frontmatter that is useful for the matrix/detail UI. + #[serde(skip_serializing_if = "Option::is_none")] + manifest: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +struct SkillManifest { + #[serde(skip_serializing_if = "Option::is_none")] + version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + author: Option, + #[serde(skip_serializing_if = "Option::is_none")] + license: Option, + #[serde(skip_serializing_if = "Option::is_none")] + homepage: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + required_bins: Vec, +} + +impl SkillManifest { + fn is_empty(&self) -> bool { + self.version.is_none() + && self.author.is_none() + && self.license.is_none() + && self.homepage.is_none() + && self.required_bins.is_empty() + } } /// 描述一个 skill 在中心仓(SSOT)和各 AI 工具目录之间的部署形态。 @@ -2590,13 +2618,17 @@ fn npm_global_mcp_list() -> Vec { fn enrich_installed_skill(skill: InstalledSkill) -> EnrichedInstalledSkill { // CC Switch already parses SKILL.md frontmatter description (including YAML - // multi-line scalars), so reuse it for capability_summary. Only read SKILL.md - // ourselves to extract the `triggers:` field, which CC Switch does not expose. - let trigger_scenarios = installed_skill_dir(&skill) + // multi-line scalars), so reuse it for capability_summary. Read SKILL.md + // once here for UI metadata CC Switch does not expose. + let skill_markdown = installed_skill_dir(&skill) .ok() .and_then(|dir| std::fs::read_to_string(dir.join("SKILL.md")).ok()) - .map(|content| parse_skill_triggers(&content)) + .map(|content| content); + let trigger_scenarios = skill_markdown + .as_deref() + .map(parse_skill_triggers) .unwrap_or_default(); + let manifest = skill_markdown.as_deref().and_then(parse_skill_manifest); let capability_summary = skill.description.as_deref().and_then(first_sentence_of); let source_type = Some(detect_source_type(&skill)); @@ -2608,6 +2640,7 @@ fn enrich_installed_skill(skill: InstalledSkill) -> EnrichedInstalledSkill { trigger_scenarios, source_type, deployment, + manifest, } } @@ -3055,6 +3088,80 @@ fn parse_skill_triggers(content: &str) -> Vec { triggers } +fn parse_skill_manifest(content: &str) -> Option { + let Some((frontmatter, _body)) = split_frontmatter(content) else { + return None; + }; + + let mut manifest = SkillManifest::default(); + let mut in_required_bins_block = false; + + for line in frontmatter.lines() { + let trimmed = line.trim_start(); + + if in_required_bins_block { + if let Some(item) = trimmed.strip_prefix("- ") { + let bin = unquote_frontmatter_value(item.trim()); + if !bin.is_empty() { + manifest.required_bins.push(bin); + } + continue; + } + in_required_bins_block = false; + } + + let Some((key, value)) = trimmed.split_once(':') else { + continue; + }; + let key = key.trim(); + let value = value.trim(); + + match key { + "version" => assign_manifest_scalar(&mut manifest.version, value), + "author" => assign_manifest_scalar(&mut manifest.author, value), + "license" => assign_manifest_scalar(&mut manifest.license, value), + "homepage" => assign_manifest_scalar(&mut manifest.homepage, value), + "anyBins" | "requiredBins" | "bins" => { + if value.is_empty() { + in_required_bins_block = true; + } else { + manifest.required_bins.extend(parse_frontmatter_list(value)); + } + } + _ => {} + } + } + + manifest.required_bins.sort(); + manifest.required_bins.dedup(); + + if manifest.is_empty() { + None + } else { + Some(manifest) + } +} + +fn assign_manifest_scalar(target: &mut Option, value: &str) { + if target.is_some() { + return; + } + let value = unquote_frontmatter_value(value); + if !value.is_empty() { + *target = Some(value); + } +} + +fn parse_frontmatter_list(value: &str) -> Vec { + let trimmed = value.trim().trim_start_matches('[').trim_end_matches(']'); + trimmed + .split(',') + .map(unquote_frontmatter_value) + .map(|item| item.trim().to_string()) + .filter(|item| !item.is_empty()) + .collect() +} + fn first_sentence_of(value: &str) -> Option { let trimmed = value.trim(); if trimmed.is_empty() { @@ -4221,6 +4328,58 @@ Turns fuzzy product ideas into crisp release plans. ); } + #[test] + fn parse_skill_manifest_reads_version_source_and_required_bins() { + let manifest = parse_skill_manifest( + r#"--- +name: baoyu-comic +description: Create knowledge comics. +version: 1.56.1 +author: "@dotey" +license: MIT +metadata: + openclaw: + homepage: https://github.com/JimLiu/baoyu-skills#baoyu-comic + requires: + anyBins: + - bun + - npx +--- +# Knowledge Comic Creator +"#, + ) + .expect("manifest should parse"); + + assert_eq!(manifest.version.as_deref(), Some("1.56.1")); + assert_eq!(manifest.author.as_deref(), Some("@dotey")); + assert_eq!(manifest.license.as_deref(), Some("MIT")); + assert_eq!( + manifest.homepage.as_deref(), + Some("https://github.com/JimLiu/baoyu-skills#baoyu-comic") + ); + assert_eq!(manifest.required_bins, vec!["bun", "npx"]); + } + + #[test] + fn parse_skill_manifest_accepts_inline_required_bins() { + let manifest = parse_skill_manifest( + r#"--- +version: "2.4.1" +homepage: 'https://example.com/demo' +requiredBins: [bun, "npx"] +--- +"#, + ) + .expect("manifest should parse"); + + assert_eq!(manifest.version.as_deref(), Some("2.4.1")); + assert_eq!( + manifest.homepage.as_deref(), + Some("https://example.com/demo") + ); + assert_eq!(manifest.required_bins, vec!["bun", "npx"]); + } + #[test] fn local_agent_from_markdown_infers_category_and_title() { let root = Path::new("/tmp/agents"); diff --git a/swift-app/Sources/Popskill/Models/MatrixVersionFormatter.swift b/swift-app/Sources/Popskill/Models/MatrixVersionFormatter.swift index 39a5b9a..8abd58e 100644 --- a/swift-app/Sources/Popskill/Models/MatrixVersionFormatter.swift +++ b/swift-app/Sources/Popskill/Models/MatrixVersionFormatter.swift @@ -1,6 +1,13 @@ import Foundation enum MatrixVersionFormatter { + static func value(manifestVersion: String?, contentHash: String?, updatedAt: Int?) -> String? { + if let version = semanticVersion(manifestVersion) { + return version + } + return value(contentHash: contentHash, updatedAt: updatedAt) + } + static func value(contentHash: String?, updatedAt: Int?) -> String? { if let hash = shortHash(contentHash) { return hash @@ -19,6 +26,14 @@ enum MatrixVersionFormatter { return String(hash.prefix(7)) } + static func semanticVersion(_ version: String?) -> String? { + guard let version = version?.trimmingCharacters(in: .whitespacesAndNewlines), + !version.isEmpty else { + return nil + } + return version.hasPrefix("v") ? version : "v\(version)" + } + private static func dateString(_ timestamp: Int) -> String { let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) let formatter = ISO8601DateFormatter() diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index 8fb774f..7fa46e7 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -132,6 +132,8 @@ struct Skill: Identifiable, Codable, Equatable { /// 来源类型("github" | "npm" | "brew" | "pip" | "builtin"),sidecar 推断。 /// Swift 端在 ViewModel 里可做更精细的二次推断(如 sourceShort 解析)。 var sourceType: String? = nil + /// SKILL.md frontmatter surfaced by the sidecar for fast matrix/detail UI. + var manifest: SkillManifest? = nil /// 真身 + 各 AI 工具 symlink 状态。喂给 Inspector "位置与链接" 与 链接健康 view。 var deployment: SkillDeployment? = nil var lastUsedAt: Int? = nil @@ -145,7 +147,12 @@ struct Skill: Identifiable, Codable, Equatable { } var sourceURL: URL? { - explicitOrRepositoryURL(readmeUrl: readmeUrl, repoOwner: repoOwner, repoName: repoName) + explicitOrRepositoryURL( + readmeUrl: readmeUrl, + fallbackExplicitURL: manifest?.homepage, + repoOwner: repoOwner, + repoName: repoName + ) } var markdownURL: URL? { @@ -231,6 +238,60 @@ struct Skill: Identifiable, Codable, Equatable { } } +struct SkillManifest: Codable, Equatable { + let version: String? + let author: String? + let license: String? + let homepage: String? + let requiredBins: [String] + + private enum CodingKeys: String, CodingKey { + case version + case author + case license + case homepage + case requiredBins + } + + init( + version: String? = nil, + author: String? = nil, + license: String? = nil, + homepage: String? = nil, + requiredBins: [String] = [] + ) { + self.version = version + self.author = author + self.license = license + self.homepage = homepage + self.requiredBins = requiredBins + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + version = try container.decodeIfPresent(String.self, forKey: .version) + author = try container.decodeIfPresent(String.self, forKey: .author) + license = try container.decodeIfPresent(String.self, forKey: .license) + homepage = try container.decodeIfPresent(String.self, forKey: .homepage) + requiredBins = try container.decodeIfPresent([String].self, forKey: .requiredBins) ?? [] + } + + var semanticVersion: String? { + version?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty + } + + var homepageURL: URL? { + sanitizedExternalURL(homepage) + } + + var requiredBinsLabel: String? { + let bins = requiredBins + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + return bins.isEmpty ? nil : bins.joined(separator: ", ") + } +} + struct LocalAgent: Identifiable, Codable, Equatable { let id: String let name: String @@ -630,6 +691,7 @@ struct CapabilityPackage: Identifiable, Codable, Equatable { var sourceURL: URL? { return explicitOrRepositoryURL( readmeUrl: source.readmeUrl ?? source.location, + fallbackExplicitURL: nil, repoOwner: source.repoOwner, repoName: source.repoName ) @@ -830,15 +892,25 @@ struct CatalogSkill: Identifiable, Codable, Equatable { } var sourceURL: URL? { - explicitOrRepositoryURL(readmeUrl: readmeUrl, repoOwner: repoOwner, repoName: repoName) + explicitOrRepositoryURL( + readmeUrl: readmeUrl, + fallbackExplicitURL: nil, + repoOwner: repoOwner, + repoName: repoName + ) } } -private func explicitOrRepositoryURL(readmeUrl: String?, repoOwner: String?, repoName: String?) -> URL? { - if let readmeUrl, - let url = URL(string: readmeUrl), - let scheme = url.scheme?.lowercased(), - ["http", "https"].contains(scheme) { +private func explicitOrRepositoryURL( + readmeUrl: String?, + fallbackExplicitURL: String?, + repoOwner: String?, + repoName: String? +) -> URL? { + if let url = sanitizedExternalURL(readmeUrl) { + return url + } + if let url = sanitizedExternalURL(fallbackExplicitURL) { return url } @@ -853,6 +925,16 @@ private func explicitOrRepositoryURL(readmeUrl: String?, repoOwner: String?, rep return components.url } +private func sanitizedExternalURL(_ rawValue: String?) -> URL? { + guard let rawValue, + let url = URL(string: rawValue.trimmingCharacters(in: .whitespacesAndNewlines)), + let scheme = url.scheme?.lowercased(), + ["http", "https"].contains(scheme) else { + return nil + } + return url +} + private func githubRepositoryURL(from ownerRepo: String) -> URL? { let parts = ownerRepo.split(separator: "/", omittingEmptySubsequences: true) guard parts.count == 2 else { diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 40ba817..4c59e15 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -458,6 +458,10 @@ "matrix.source.directory" = "Directory"; "matrix.source.localStore" = "Local store"; "matrix.source.type" = "Source type"; +"matrix.source.author" = "Author"; +"matrix.source.license" = "License"; +"matrix.source.homepage" = "Homepage"; +"matrix.source.requires" = "Requires"; "matrix.source.readme" = "SKILL.md"; "matrix.skill.action.openReadme" = "Edit prompt"; "matrix.skill.action.checkUpdates" = "Check updates"; @@ -468,6 +472,10 @@ "matrix.package.version.branch" = "Branch"; "matrix.package.version.hash" = "Hash"; "matrix.package.version.untracked" = "Untracked"; +"matrix.skill.manifest.version" = "Version"; +"matrix.skill.manifest.author" = "Author"; +"matrix.skill.manifest.license" = "License"; +"matrix.skill.manifest.requires" = "Requires"; "matrix.package.sync.upToDate" = "Up to date"; "matrix.package.sync.pending" = "%d pending"; "matrix.package.sync.checking" = "Checking upstream updates..."; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 6b7ffda..b631c64 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -458,6 +458,10 @@ "matrix.source.directory" = "目录"; "matrix.source.localStore" = "本地真身"; "matrix.source.type" = "来源类型"; +"matrix.source.author" = "作者"; +"matrix.source.license" = "License"; +"matrix.source.homepage" = "主页"; +"matrix.source.requires" = "依赖命令"; "matrix.source.readme" = "SKILL.md"; "matrix.skill.action.openReadme" = "编辑 prompt"; "matrix.skill.action.checkUpdates" = "检查更新"; @@ -468,6 +472,10 @@ "matrix.package.version.branch" = "分支"; "matrix.package.version.hash" = "Hash"; "matrix.package.version.untracked" = "未跟踪"; +"matrix.skill.manifest.version" = "版本"; +"matrix.skill.manifest.author" = "作者"; +"matrix.skill.manifest.license" = "License"; +"matrix.skill.manifest.requires" = "依赖命令"; "matrix.package.sync.upToDate" = "已是最新"; "matrix.package.sync.pending" = "%d 个待更新"; "matrix.package.sync.checking" = "正在检查上游更新..."; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 71a55a7..eb5c201 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -480,6 +480,39 @@ struct InspectorPane: View { icon: "tag" )) } + if let author = skill.manifest?.author?.trimmingCharacters(in: .whitespacesAndNewlines), !author.isEmpty { + facts.append(InspectorSourceFact( + id: "author", + title: localization.string("matrix.source.author"), + value: author, + icon: "person" + )) + } + if let license = skill.manifest?.license?.trimmingCharacters(in: .whitespacesAndNewlines), !license.isEmpty { + facts.append(InspectorSourceFact( + id: "license", + title: localization.string("matrix.source.license"), + value: license, + icon: "doc.plaintext" + )) + } + if let homepageURL = skill.manifest?.homepageURL { + facts.append(InspectorSourceFact( + id: "homepage", + title: localization.string("matrix.source.homepage"), + value: homepageURL.absoluteString, + icon: "arrow.up.right.square", + url: homepageURL + )) + } + if let requiredBins = skill.manifest?.requiredBinsLabel { + facts.append(InspectorSourceFact( + id: "requires", + title: localization.string("matrix.source.requires"), + value: requiredBins, + icon: "terminal" + )) + } if let markdownURL = skill.markdownURL { facts.append(InspectorSourceFact( id: "readme", @@ -501,12 +534,21 @@ struct InspectorPane: View { Text(fact.title) .font(.caption2.weight(.semibold)) .foregroundStyle(Color.popSecondaryLabel) - Text(fact.value) - .font(.system(size: 11, design: .monospaced)) - .foregroundStyle(Color.popLabel) - .lineLimit(2) - .truncationMode(.middle) - .textSelection(.enabled) + if let url = fact.url { + Link(fact.value, destination: url) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(Color.accentColor) + .lineLimit(2) + .truncationMode(.middle) + .textSelection(.enabled) + } else { + Text(fact.value) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(Color.popLabel) + .lineLimit(2) + .truncationMode(.middle) + .textSelection(.enabled) + } } Spacer(minLength: 8) } @@ -1313,6 +1355,21 @@ struct InspectorPane: View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.version") VStack(alignment: .leading, spacing: 6) { + if let version = skill.manifest?.semanticVersion { + metaRow( + label: localization.string("matrix.skill.manifest.version"), + value: MatrixVersionFormatter.semanticVersion(version) ?? version + ) + } + if let author = skill.manifest?.author?.trimmingCharacters(in: .whitespacesAndNewlines), !author.isEmpty { + metaRow(label: localization.string("matrix.skill.manifest.author"), value: author) + } + if let license = skill.manifest?.license?.trimmingCharacters(in: .whitespacesAndNewlines), !license.isEmpty { + metaRow(label: localization.string("matrix.skill.manifest.license"), value: license) + } + if let requiredBins = skill.manifest?.requiredBinsLabel { + metaRow(label: localization.string("matrix.skill.manifest.requires"), value: requiredBins) + } metaRow( label: localization.string("matrix.package.version.hash"), value: skill.contentHash.map(Self.shortHash) ?? localization.string("matrix.package.version.untracked") @@ -1949,6 +2006,7 @@ private struct InspectorSourceFact: Identifiable { let title: String let value: String let icon: String + var url: URL? = nil } private struct UsageTrendBars: View { diff --git a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift index 4a66b28..1d90b53 100644 --- a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift @@ -429,6 +429,7 @@ private struct MatrixPackageComponentRow: View { private var versionCell: some View { MatrixVersionValueCell( value: MatrixVersionFormatter.value( + manifestVersion: matchingSkill?.manifest?.semanticVersion, contentHash: matchingSkill?.contentHash, updatedAt: matchingSkill?.updatedAt ), diff --git a/swift-app/Sources/Popskill/Views/MatrixRow.swift b/swift-app/Sources/Popskill/Views/MatrixRow.swift index c2059bb..25a0b57 100644 --- a/swift-app/Sources/Popskill/Views/MatrixRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixRow.swift @@ -211,6 +211,7 @@ struct MatrixRow: View { store.skills.first { $0.id == skillID } } return MatrixVersionValueCell(value: MatrixVersionFormatter.value( + manifestVersion: skill?.manifest?.semanticVersion, contentHash: skill?.contentHash, updatedAt: capability.updatedAt )) diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 85825a4..4944f24 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -53,5 +53,8 @@ struct LocalizationTests { #expect(localization.string("matrix.inspector.section.source") == "来源与路径") #expect(localization.string("matrix.source.repository") == "仓库") #expect(localization.string("matrix.source.localStore") == "本地真身") + #expect(localization.string("matrix.source.license") == "License") + #expect(localization.string("matrix.source.requires") == "依赖命令") + #expect(localization.string("matrix.skill.manifest.version") == "版本") } } diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 204a4cb..e03bc93 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -109,6 +109,83 @@ struct SkillModelsTests { #expect(skill.sourceLabel == "demo") } + @Test + func installedSkillDecodesManifestMetadata() throws { + let data = Data(""" + { + "id": "baoyu-comic", + "name": "baoyu-comic", + "description": "Create knowledge comics.", + "directory": "baoyu-comic", + "repo_owner": "JimLiu", + "repo_name": "baoyu-skills", + "readme_url": null, + "apps": { + "claude": true, + "codex": true, + "gemini": false, + "opencode": false, + "hermes": false + }, + "installed_at": 1, + "updated_at": 2, + "content_hash": "abcdef123456", + "manifest": { + "version": "1.56.1", + "author": "@dotey", + "license": "MIT", + "homepage": "https://github.com/JimLiu/baoyu-skills#baoyu-comic", + "required_bins": ["bun", "npx"] + } + } + """.utf8) + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let skill = try decoder.decode(Skill.self, from: data) + + #expect(skill.manifest?.semanticVersion == "1.56.1") + #expect(skill.manifest?.author == "@dotey") + #expect(skill.manifest?.license == "MIT") + #expect(skill.manifest?.requiredBinsLabel == "bun, npx") + #expect(skill.sourceURL?.absoluteString == "https://github.com/JimLiu/baoyu-skills#baoyu-comic") + } + + @Test + func installedSkillDecodesManifestWithoutRequiredBins() throws { + let data = Data(""" + { + "id": "demo", + "name": "Demo", + "description": "Demo skill", + "directory": "demo", + "repo_owner": null, + "repo_name": null, + "readme_url": null, + "apps": { + "claude": true, + "codex": false, + "gemini": false, + "opencode": false, + "hermes": false + }, + "installed_at": null, + "updated_at": null, + "content_hash": null, + "manifest": { + "version": "2.0.0" + } + } + """.utf8) + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let skill = try decoder.decode(Skill.self, from: data) + + #expect(skill.manifest?.semanticVersion == "2.0.0") + #expect(skill.manifest?.requiredBins == []) + } + @Test func installedSkillLocalStoreURLUsesCCSwitchStore() { let skill = installedSkill(directory: "demo-skill") @@ -1449,6 +1526,8 @@ struct SkillModelsTests { @Test func matrixVersionFormatterPrefersHashThenUpdatedDate() { + #expect(MatrixVersionFormatter.value(manifestVersion: "2.4.1", contentHash: "abcdef123456", updatedAt: 1_700_000_000) == "v2.4.1") + #expect(MatrixVersionFormatter.value(manifestVersion: " v3.0.0 ", contentHash: nil, updatedAt: nil) == "v3.0.0") #expect(MatrixVersionFormatter.value(contentHash: " abcdef123456 ", updatedAt: 1_700_000_000) == "abcdef1") #expect(MatrixVersionFormatter.value(contentHash: nil, updatedAt: 1_700_000_000) == "2023-11-14") #expect(MatrixVersionFormatter.value(contentHash: " ", updatedAt: 0) == nil) From 818bd62caf1a845c79c509669bf69e94a8b57180 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 07:01:42 +0800 Subject: [PATCH 37/47] feat(ui): show skill dependency health --- skill-cli/src/main.rs | 93 ++++++++++++++++++- .../Sources/Popskill/Models/SkillModels.swift | 28 +++++- .../Resources/en.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../Popskill/Views/InspectorPane.swift | 18 +++- .../PopskillTests/LocalizationTests.swift | 2 + .../PopskillTests/SkillModelsTests.swift | 10 +- 7 files changed, 146 insertions(+), 9 deletions(-) diff --git a/skill-cli/src/main.rs b/skill-cli/src/main.rs index 2d3150e..6736ccb 100644 --- a/skill-cli/src/main.rs +++ b/skill-cli/src/main.rs @@ -8,6 +8,8 @@ use serde_json::{Value, json}; use std::collections::BTreeMap; use std::env; use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::Command; use std::process::ExitCode; @@ -2201,6 +2203,15 @@ struct SkillManifest { homepage: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] required_bins: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + required_tools: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +struct SkillManifestTool { + name: String, + available: bool, } impl SkillManifest { @@ -2210,6 +2221,7 @@ impl SkillManifest { && self.license.is_none() && self.homepage.is_none() && self.required_bins.is_empty() + && self.required_tools.is_empty() } } @@ -3134,6 +3146,14 @@ fn parse_skill_manifest(content: &str) -> Option { manifest.required_bins.sort(); manifest.required_bins.dedup(); + manifest.required_tools = manifest + .required_bins + .iter() + .map(|name| SkillManifestTool { + name: name.clone(), + available: command_available(name), + }) + .collect(); if manifest.is_empty() { None @@ -3162,6 +3182,44 @@ fn parse_frontmatter_list(value: &str) -> Vec { .collect() } +fn command_available(command: &str) -> bool { + let command = command.trim(); + if command.is_empty() { + return false; + } + + let explicit_path = Path::new(command); + if explicit_path.components().count() > 1 { + return is_executable(explicit_path); + } + + let Some(paths) = env::var_os("PATH") else { + return false; + }; + + env::split_paths(&paths).any(|path| is_executable(&path.join(command))) +} + +fn is_executable(path: &Path) -> bool { + let Ok(metadata) = fs::metadata(path) else { + return false; + }; + if !metadata.is_file() { + return false; + } + has_execute_bit(&metadata) +} + +#[cfg(unix)] +fn has_execute_bit(metadata: &fs::Metadata) -> bool { + metadata.permissions().mode() & 0o111 != 0 +} + +#[cfg(not(unix))] +fn has_execute_bit(_metadata: &fs::Metadata) -> bool { + true +} + fn first_sentence_of(value: &str) -> Option { let trimmed = value.trim(); if trimmed.is_empty() { @@ -4358,6 +4416,14 @@ metadata: Some("https://github.com/JimLiu/baoyu-skills#baoyu-comic") ); assert_eq!(manifest.required_bins, vec!["bun", "npx"]); + assert_eq!( + manifest + .required_tools + .iter() + .map(|tool| tool.name.as_str()) + .collect::>(), + vec!["bun", "npx"] + ); } #[test] @@ -4366,7 +4432,7 @@ metadata: r#"--- version: "2.4.1" homepage: 'https://example.com/demo' -requiredBins: [bun, "npx"] +requiredBins: [sh, "definitely-popskill-missing-command"] --- "#, ) @@ -4377,7 +4443,30 @@ requiredBins: [bun, "npx"] manifest.homepage.as_deref(), Some("https://example.com/demo") ); - assert_eq!(manifest.required_bins, vec!["bun", "npx"]); + assert_eq!( + manifest.required_bins, + vec!["definitely-popskill-missing-command", "sh"] + ); + assert_eq!( + manifest.required_tools, + vec![ + SkillManifestTool { + name: "definitely-popskill-missing-command".to_string(), + available: false, + }, + SkillManifestTool { + name: "sh".to_string(), + available: true, + } + ] + ); + } + + #[test] + fn command_available_detects_missing_commands() { + assert!(!command_available( + "definitely-popskill-missing-command-for-tests" + )); } #[test] diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index 7fa46e7..a034d83 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -244,6 +244,7 @@ struct SkillManifest: Codable, Equatable { let license: String? let homepage: String? let requiredBins: [String] + let requiredTools: [SkillRequiredTool] private enum CodingKeys: String, CodingKey { case version @@ -251,6 +252,7 @@ struct SkillManifest: Codable, Equatable { case license case homepage case requiredBins + case requiredTools } init( @@ -258,13 +260,15 @@ struct SkillManifest: Codable, Equatable { author: String? = nil, license: String? = nil, homepage: String? = nil, - requiredBins: [String] = [] + requiredBins: [String] = [], + requiredTools: [SkillRequiredTool] = [] ) { self.version = version self.author = author self.license = license self.homepage = homepage self.requiredBins = requiredBins + self.requiredTools = requiredTools } init(from decoder: Decoder) throws { @@ -274,6 +278,7 @@ struct SkillManifest: Codable, Equatable { license = try container.decodeIfPresent(String.self, forKey: .license) homepage = try container.decodeIfPresent(String.self, forKey: .homepage) requiredBins = try container.decodeIfPresent([String].self, forKey: .requiredBins) ?? [] + requiredTools = try container.decodeIfPresent([SkillRequiredTool].self, forKey: .requiredTools) ?? [] } var semanticVersion: String? { @@ -290,6 +295,27 @@ struct SkillManifest: Codable, Equatable { .filter { !$0.isEmpty } return bins.isEmpty ? nil : bins.joined(separator: ", ") } + + var hasMissingRequiredTools: Bool { + requiredTools.contains { !$0.available } + } + + func requiredToolsLabel(availableLabel: String, missingLabel: String) -> String? { + if !requiredTools.isEmpty { + let labels = requiredTools + .filter { !$0.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + .map { tool in + "\(tool.name.trimmingCharacters(in: .whitespacesAndNewlines)) \(tool.available ? availableLabel : missingLabel)" + } + return labels.isEmpty ? nil : labels.joined(separator: " · ") + } + return requiredBinsLabel + } +} + +struct SkillRequiredTool: Codable, Equatable { + let name: String + let available: Bool } struct LocalAgent: Identifiable, Codable, Equatable { diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 4c59e15..9755cce 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -476,6 +476,8 @@ "matrix.skill.manifest.author" = "Author"; "matrix.skill.manifest.license" = "License"; "matrix.skill.manifest.requires" = "Requires"; +"matrix.skill.manifest.requires.available" = "ok"; +"matrix.skill.manifest.requires.missing" = "missing"; "matrix.package.sync.upToDate" = "Up to date"; "matrix.package.sync.pending" = "%d pending"; "matrix.package.sync.checking" = "Checking upstream updates..."; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index b631c64..6ef2e04 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -476,6 +476,8 @@ "matrix.skill.manifest.author" = "作者"; "matrix.skill.manifest.license" = "License"; "matrix.skill.manifest.requires" = "依赖命令"; +"matrix.skill.manifest.requires.available" = "可用"; +"matrix.skill.manifest.requires.missing" = "缺失"; "matrix.package.sync.upToDate" = "已是最新"; "matrix.package.sync.pending" = "%d 个待更新"; "matrix.package.sync.checking" = "正在检查上游更新..."; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index eb5c201..a2289b0 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -505,12 +505,16 @@ struct InspectorPane: View { url: homepageURL )) } - if let requiredBins = skill.manifest?.requiredBinsLabel { + if let manifest = skill.manifest, + let requiredTools = manifest.requiredToolsLabel( + availableLabel: localization.string("matrix.skill.manifest.requires.available"), + missingLabel: localization.string("matrix.skill.manifest.requires.missing") + ) { facts.append(InspectorSourceFact( id: "requires", title: localization.string("matrix.source.requires"), - value: requiredBins, - icon: "terminal" + value: requiredTools, + icon: manifest.hasMissingRequiredTools ? "exclamationmark.triangle.fill" : "checkmark.seal" )) } if let markdownURL = skill.markdownURL { @@ -1367,8 +1371,12 @@ struct InspectorPane: View { if let license = skill.manifest?.license?.trimmingCharacters(in: .whitespacesAndNewlines), !license.isEmpty { metaRow(label: localization.string("matrix.skill.manifest.license"), value: license) } - if let requiredBins = skill.manifest?.requiredBinsLabel { - metaRow(label: localization.string("matrix.skill.manifest.requires"), value: requiredBins) + if let manifest = skill.manifest, + let requiredTools = manifest.requiredToolsLabel( + availableLabel: localization.string("matrix.skill.manifest.requires.available"), + missingLabel: localization.string("matrix.skill.manifest.requires.missing") + ) { + metaRow(label: localization.string("matrix.skill.manifest.requires"), value: requiredTools) } metaRow( label: localization.string("matrix.package.version.hash"), diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 4944f24..1ca1767 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -56,5 +56,7 @@ struct LocalizationTests { #expect(localization.string("matrix.source.license") == "License") #expect(localization.string("matrix.source.requires") == "依赖命令") #expect(localization.string("matrix.skill.manifest.version") == "版本") + #expect(localization.string("matrix.skill.manifest.requires.available") == "可用") + #expect(localization.string("matrix.skill.manifest.requires.missing") == "缺失") } } diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index e03bc93..567827a 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -135,7 +135,11 @@ struct SkillModelsTests { "author": "@dotey", "license": "MIT", "homepage": "https://github.com/JimLiu/baoyu-skills#baoyu-comic", - "required_bins": ["bun", "npx"] + "required_bins": ["bun", "npx"], + "required_tools": [ + {"name": "bun", "available": true}, + {"name": "npx", "available": false} + ] } } """.utf8) @@ -148,6 +152,8 @@ struct SkillModelsTests { #expect(skill.manifest?.author == "@dotey") #expect(skill.manifest?.license == "MIT") #expect(skill.manifest?.requiredBinsLabel == "bun, npx") + #expect(skill.manifest?.hasMissingRequiredTools == true) + #expect(skill.manifest?.requiredToolsLabel(availableLabel: "ok", missingLabel: "missing") == "bun ok · npx missing") #expect(skill.sourceURL?.absoluteString == "https://github.com/JimLiu/baoyu-skills#baoyu-comic") } @@ -184,6 +190,8 @@ struct SkillModelsTests { #expect(skill.manifest?.semanticVersion == "2.0.0") #expect(skill.manifest?.requiredBins == []) + #expect(skill.manifest?.requiredTools == []) + #expect(skill.manifest?.requiredToolsLabel(availableLabel: "ok", missingLabel: "missing") == nil) } @Test From 1335d9c9b00f04d9dbac4cba280c0ee01c00086c Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 07:12:34 +0800 Subject: [PATCH 38/47] feat(ui): summarize package machine usage --- .../Sources/Popskill/Models/SkillModels.swift | 7 +++ .../Resources/en.lproj/Localizable.strings | 1 + .../zh-Hans.lproj/Localizable.strings | 1 + .../Popskill/Views/InspectorPane.swift | 55 +++++++++++++++++++ .../PopskillTests/LocalizationTests.swift | 1 + .../PopskillTests/SkillModelsTests.swift | 18 +++++- 6 files changed, 82 insertions(+), 1 deletion(-) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index a034d83..38028f2 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -767,6 +767,13 @@ struct CapabilityPackage: Identifiable, Codable, Equatable { .trimmingCharacters(in: .whitespacesAndNewlines) .nilIfEmpty } + + func installedSizeBytes(in skills: [Skill]) -> UInt64? { + let total = matchingInstalledSkills(in: skills) + .compactMap(\.sizeBytes) + .reduce(UInt64(0), +) + return total > 0 ? total : nil + } } struct PackageComponentGroupSummary: Identifiable, Equatable { diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 9755cce..353d330 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -453,6 +453,7 @@ "matrix.machine.lastUsed" = "Last used"; "matrix.machine.tokens" = "30d tokens"; "matrix.machine.calls" = "30d calls"; +"matrix.machine.topComponent" = "Top component"; "matrix.machine.size" = "Disk size"; "matrix.source.repository" = "Repository"; "matrix.source.directory" = "Directory"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 6ef2e04..8550e61 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -453,6 +453,7 @@ "matrix.machine.lastUsed" = "最近使用"; "matrix.machine.tokens" = "近30天 tokens"; "matrix.machine.calls" = "近30天调用"; +"matrix.machine.topComponent" = "最常用组件"; "matrix.machine.size" = "磁盘占用"; "matrix.source.repository" = "仓库"; "matrix.source.directory" = "目录"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index a2289b0..854cd13 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -170,6 +170,7 @@ struct InspectorPane: View { case .overview: packageSummarySection(package) packageCoverageSection + packageMachineSection(package) packageActivationSection(package) packageComponentsSection(package) case .readme: @@ -846,6 +847,60 @@ struct InspectorPane: View { .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) } + @ViewBuilder + private func packageMachineSection(_ package: CapabilityPackage) -> some View { + let metrics = packageMachineMetrics(package) + if !metrics.isEmpty { + VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.inspector.section.machine", accent: .popSectionGreen) + LazyVGrid(columns: Self.machineMetricColumns, alignment: .leading, spacing: 8) { + ForEach(metrics) { metric in + machineMetricCard(metric) + } + } + } + } + } + + private func packageMachineMetrics(_ package: CapabilityPackage) -> [InspectorMachineMetric] { + var metrics: [InspectorMachineMetric] = [] + let snapshot = package.usageSnapshot(using: store.usageSummary, skills: store.skills) + + if let snapshot, snapshot.hasUsage { + metrics.append(InspectorMachineMetric( + id: "tokens", + title: localization.string("matrix.machine.tokens"), + value: UsageDisplayFormatter.compactTokens(snapshot.totalTokens), + tint: Color.accentColor + )) + metrics.append(InspectorMachineMetric( + id: "calls", + title: localization.string("matrix.machine.calls"), + value: UsageDisplayFormatter.compactCount(snapshot.usageEvents), + tint: Color.popLabel + )) + if let topComponent = snapshot.componentStats.first { + metrics.append(InspectorMachineMetric( + id: "top-component", + title: localization.string("matrix.machine.topComponent"), + value: topComponent.componentName, + tint: Color.popSectionGreen + )) + } + } + + if let size = package.installedSizeBytes(in: store.skills) { + metrics.append(InspectorMachineMetric( + id: "size", + title: localization.string("matrix.machine.size"), + value: Self.formatBytes(size), + tint: Color.popSecondaryLabel + )) + } + + return metrics + } + private func packageActivationSection(_ package: CapabilityPackage) -> some View { let matchedSkills = package.matchingInstalledSkills(in: store.skills) let claudeTargets = package.installedSkillsRequiringEnablement(for: .claude, in: store.skills) diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 1ca1767..8be681e 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -50,6 +50,7 @@ struct LocalizationTests { #expect(localization.string("matrix.inspector.section.machine") == "这台机器") #expect(localization.string("matrix.machine.firstActivated") == "首次激活") #expect(localization.string("matrix.machine.tokens") == "近30天 tokens") + #expect(localization.string("matrix.machine.topComponent") == "最常用组件") #expect(localization.string("matrix.inspector.section.source") == "来源与路径") #expect(localization.string("matrix.source.repository") == "仓库") #expect(localization.string("matrix.source.localStore") == "本地真身") diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 567827a..c8463aa 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -1199,6 +1199,20 @@ struct SkillModelsTests { #expect(snapshot?.componentStats.first?.totalTokens == 62) } + @Test + func capabilityPackageInstalledSizeBytesAggregatesMatchedSkills() { + let package = self.package(components: [ + component(id: "baoyu-comic", installed: true), + component(id: "baoyu-translate", installed: true) + ]) + let comic = installedSkill(directory: "baoyu-comic", sizeBytes: 1_024) + let translate = installedSkill(directory: "baoyu-translate", sizeBytes: 2_048) + let unrelated = installedSkill(directory: "unrelated", sizeBytes: 8_192) + + #expect(package.installedSizeBytes(in: [comic, translate, unrelated]) == 3_072) + #expect(package.installedSizeBytes(in: [installedSkill(directory: "baoyu-comic")]) == nil) + } + @Test func capabilityPackageUsageSnapshotPrefersRecentThirtyDayWindow() { let package = self.package(components: [component(id: "baoyu-comic", installed: true)]) @@ -1681,6 +1695,7 @@ struct SkillModelsTests { directory: String, installedAt: Int? = nil, updatedAt: Int? = nil, + sizeBytes: UInt64? = nil, apps: SkillApps = SkillApps( claude: true, codex: false, @@ -1700,7 +1715,8 @@ struct SkillModelsTests { apps: apps, installedAt: installedAt, updatedAt: updatedAt, - contentHash: nil + contentHash: nil, + sizeBytes: sizeBytes ) } From 01e217032b403c3547b151b45db417232a4314db Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 07:23:33 +0800 Subject: [PATCH 39/47] feat(ui): summarize package sync health --- .../Sources/Popskill/Models/SkillModels.swift | 62 +++++ .../Resources/en.lproj/Localizable.strings | 11 + .../zh-Hans.lproj/Localizable.strings | 11 + .../Popskill/Views/InspectorPane.swift | 224 +++++++++++++++++- .../PopskillTests/LocalizationTests.swift | 2 + .../PopskillTests/SkillModelsTests.swift | 57 +++++ 6 files changed, 366 insertions(+), 1 deletion(-) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index 38028f2..032edaf 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -1237,6 +1237,30 @@ struct LinkHealthRow: Codable, Equatable { let deployment: SkillDeployment? } +struct PackageLinkHealthSnapshot: Equatable { + let rows: [LinkHealthRow] + + var okCount: Int { + statusCount("ok") + } + + var brokenCount: Int { + statusCount("broken") + } + + var inactiveCount: Int { + statusCount("inactive") + } + + private func statusCount(_ status: String) -> Int { + rows.reduce(0) { count, row in + count + (row.deployment?.appLinks.values.filter { + $0.status.caseInsensitiveCompare(status) == .orderedSame + }.count ?? 0) + } + } +} + /// Envelope returned by `skill-cli onboard-scan --json`. Powers onboarding /// wizard step 3. struct OnboardScanReport: Codable, Equatable { @@ -1542,6 +1566,40 @@ extension CapabilityPackage { matchingInstalledSkills(in: skills).contains { $0.hasBrokenLink } } + func linkHealthSnapshot(using report: LinkHealthReport?, skills: [Skill]) -> PackageLinkHealthSnapshot? { + guard let report else { + return nil + } + + let matchedSkills = matchingInstalledSkills(in: skills) + let matchedSkillIdentifiers = Set(matchedSkills.flatMap { skill in + [ + skill.id, + skill.name, + skill.directory, + skill.id.split(separator: ":", maxSplits: 1).last.map(String.init) ?? skill.id + ].map(Self.normalizedIdentifier).filter { !$0.isEmpty } + }) + + let rows = report.rows.filter { row in + let rowCandidates = [ + row.skillId, + row.skillName, + row.skillId.split(separator: ":", maxSplits: 1).last.map(String.init) ?? row.skillId + ].map(Self.normalizedIdentifier).filter { !$0.isEmpty } + + if !Set(rowCandidates).isDisjoint(with: matchedSkillIdentifiers) { + return true + } + + return components.skills.contains { component in + component.matchesAttributionSkill(row.skillId) || component.matchesAttributionSkill(row.skillName) + } + } + + return PackageLinkHealthSnapshot(rows: rows) + } + func matchingInstalledSkill(for component: PackageComponent, in skills: [Skill]) -> Skill? { skills.first { component.matchesSkill($0) } } @@ -1597,6 +1655,10 @@ extension CapabilityPackage { component.matchesAttributionSkill(stat.skillID) } } + + private static func normalizedIdentifier(_ value: String) -> String { + value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + } } private extension PackageComponent { diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 353d330..5ce6492 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -481,6 +481,17 @@ "matrix.skill.manifest.requires.missing" = "missing"; "matrix.package.sync.upToDate" = "Up to date"; "matrix.package.sync.pending" = "%d pending"; +"matrix.package.sync.status" = "Sync status"; +"matrix.package.sync.upstream" = "Upstream updates"; +"matrix.package.sync.source" = "Source"; +"matrix.package.sync.provider" = "Provider"; +"matrix.package.sync.lastSync" = "Last sync"; +"matrix.package.sync.linkHealth" = "Link health"; +"matrix.package.sync.linkHealthNotScanned" = "Not scanned"; +"matrix.package.sync.linkHealthSummary" = "%d OK · %d broken · %d off"; +"matrix.package.sync.linkHealthEmpty" = "No matching deployed skill links were found for this package."; +"matrix.package.sync.linkHealthNotScannedDetail" = "Run link health to inspect package symlinks across Claude and Codex."; +"matrix.package.sync.noDeployment" = "Deployment info unavailable"; "matrix.package.sync.checking" = "Checking upstream updates..."; "matrix.package.sync.checked" = "Checked %@"; "matrix.package.sync.notChecked" = "Not checked yet"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 8550e61..c615420 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -481,6 +481,17 @@ "matrix.skill.manifest.requires.missing" = "缺失"; "matrix.package.sync.upToDate" = "已是最新"; "matrix.package.sync.pending" = "%d 个待更新"; +"matrix.package.sync.status" = "同步状态"; +"matrix.package.sync.upstream" = "上游更新"; +"matrix.package.sync.source" = "来源"; +"matrix.package.sync.provider" = "同步方式"; +"matrix.package.sync.lastSync" = "上次同步"; +"matrix.package.sync.linkHealth" = "链接健康"; +"matrix.package.sync.linkHealthNotScanned" = "未扫描"; +"matrix.package.sync.linkHealthSummary" = "%d 正常 · %d 断链 · %d 未启用"; +"matrix.package.sync.linkHealthEmpty" = "这个套装还没有匹配到已部署技能链接。"; +"matrix.package.sync.linkHealthNotScannedDetail" = "运行链接健康检查,查看这个套装在 Claude 和 Codex 两端的链接状态。"; +"matrix.package.sync.noDeployment" = "暂无部署信息"; "matrix.package.sync.checking" = "正在检查上游更新..."; "matrix.package.sync.checked" = "已检查 %@"; "matrix.package.sync.notChecked" = "尚未检查"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 854cd13..d82a1f3 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -12,6 +12,7 @@ struct InspectorPane: View { let capability: MatrixCapability @Environment(\.popskillLocalization) private var localization @State private var selectedTab: InspectorTab = .overview + @State private var linkHealthScanInFlight: Bool = false var body: some View { ScrollView { @@ -193,6 +194,7 @@ struct InspectorPane: View { packageLocalPathsSection(package) case .sync: packageActionsSection(package) + packageSyncStatusSection(package) packageSyncSection(package) case .metadata: packageMetadataSection(package) @@ -1463,7 +1465,7 @@ struct InspectorPane: View { private func packageSyncSection(_ package: CapabilityPackage) -> some View { let pendingUpdates = packagePendingUpdates(package) return VStack(alignment: .leading, spacing: 8) { - SectionHeading(title: "matrix.inspector.section.sync") + SectionHeading(title: "matrix.package.sync.upstream") HStack(spacing: 8) { syncStatusPill(for: pendingUpdates) if store.updatesRefreshInFlight { @@ -1503,6 +1505,209 @@ struct InspectorPane: View { } } + private func packageSyncStatusSection(_ package: CapabilityPackage) -> some View { + let snapshot = package.linkHealthSnapshot(using: store.linkHealth, skills: store.skills) + return VStack(alignment: .leading, spacing: 8) { + SectionHeading(title: "matrix.package.sync.status", accent: .popSectionGreen) + LazyVGrid(columns: Self.syncFactColumns, alignment: .leading, spacing: 8) { + syncFactCard( + titleKey: "matrix.package.sync.source", + value: package.source.location, + systemImage: "chevron.left.forwardslash.chevron.right", + tint: Color.accentColor + ) + syncFactCard( + titleKey: "matrix.package.sync.provider", + value: syncProviderLabel, + systemImage: syncProviderSymbol, + tint: Color.popSectionPurple + ) + syncFactCard( + titleKey: "matrix.package.sync.lastSync", + value: lastSyncLabel, + systemImage: "clock.arrow.circlepath", + tint: Color.popSecondaryLabel + ) + syncFactCard( + titleKey: "matrix.package.sync.linkHealth", + value: packageLinkHealthLabel(snapshot), + systemImage: packageLinkHealthSymbol(snapshot), + tint: packageLinkHealthTint(snapshot) + ) + } + + packageLinkHealthRows(snapshot) + } + } + + private var syncProviderLabel: String { + if let provider = SyncProvider(rawValue: store.lastSyncProvider) { + return localization.string(provider.titleKey) + } + return store.lastSyncProvider + } + + private var syncProviderSymbol: String { + SyncProvider(rawValue: store.lastSyncProvider)?.symbol ?? "arrow.triangle.2.circlepath" + } + + private var lastSyncLabel: String { + if let lastSyncAt = store.lastSyncAt { + return Self.relativeFormatter.localizedString(for: lastSyncAt, relativeTo: Date()) + } + return localization.string("sidebar.sync.never") + } + + private func packageLinkHealthLabel(_ snapshot: PackageLinkHealthSnapshot?) -> String { + guard let snapshot else { + return localization.string("matrix.package.sync.linkHealthNotScanned") + } + return localization.string( + "matrix.package.sync.linkHealthSummary", + snapshot.okCount, + snapshot.brokenCount, + snapshot.inactiveCount + ) + } + + private func packageLinkHealthSymbol(_ snapshot: PackageLinkHealthSnapshot?) -> String { + guard let snapshot else { return "checkmark.shield" } + if snapshot.brokenCount > 0 { return "exclamationmark.triangle.fill" } + if snapshot.okCount > 0 { return "link" } + return "circle.dashed" + } + + private func packageLinkHealthTint(_ snapshot: PackageLinkHealthSnapshot?) -> Color { + guard let snapshot else { return Color.popTertiaryLabel } + if snapshot.brokenCount > 0 { return Color.popStatusError } + if snapshot.okCount > 0 { return Color.popStatusOK } + return Color.popTertiaryLabel + } + + private func syncFactCard(titleKey: String, value: String, systemImage: String, tint: Color) -> some View { + HStack(alignment: .top, spacing: 8) { + Image(systemName: systemImage) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(tint) + .frame(width: 14) + VStack(alignment: .leading, spacing: 2) { + LocalizedText(titleKey) + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + Text(value.isEmpty ? "—" : value) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(Color.popLabel) + .lineLimit(2) + .truncationMode(.middle) + .textSelection(.enabled) + } + Spacer(minLength: 4) + } + .padding(9) + .frame(maxWidth: .infinity, alignment: .leading) + .background(tint.opacity(0.07), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + + @ViewBuilder + private func packageLinkHealthRows(_ snapshot: PackageLinkHealthSnapshot?) -> some View { + if let snapshot { + if snapshot.rows.isEmpty { + Text(localization.string("matrix.package.sync.linkHealthEmpty")) + .font(.caption) + .foregroundStyle(Color.popTertiaryLabel) + } else { + VStack(alignment: .leading, spacing: 6) { + ForEach(snapshot.rows.prefix(4), id: \.skillId) { row in + packageLinkHealthRow(row) + } + if snapshot.rows.count > 4 { + Text(localization.string("matrix.package.paths.more", snapshot.rows.count - 4)) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } + } + } else { + HStack(alignment: .center, spacing: 8) { + Text(localization.string("matrix.package.sync.linkHealthNotScannedDetail")) + .font(.caption) + .foregroundStyle(Color.popSecondaryLabel) + Spacer(minLength: 8) + Button { + Task { await refreshPackageLinkHealth() } + } label: { + if linkHealthScanInFlight { + ProgressView().controlSize(.mini) + } else { + Label(localization.string("health.empty.runScan"), systemImage: "play.circle") + } + } + .buttonStyle(.bordered) + .controlSize(.mini) + .disabled(linkHealthScanInFlight) + } + } + } + + private func packageLinkHealthRow(_ row: LinkHealthRow) -> some View { + HStack(alignment: .top, spacing: 8) { + Image(systemName: row.deployment?.hasBrokenLink == true ? "exclamationmark.triangle.fill" : "link") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(row.deployment?.hasBrokenLink == true ? Color.popStatusError : Color.popStatusOK) + .frame(width: 14) + VStack(alignment: .leading, spacing: 2) { + Text(row.skillName) + .font(.caption.weight(.medium)) + .foregroundStyle(Color.popLabel) + .lineLimit(1) + Text(row.deployment?.ssotPath.abbreviatingWithTilde ?? localization.string("matrix.package.sync.noDeployment")) + .font(.system(size: 10.5, design: .monospaced)) + .foregroundStyle(Color.popSecondaryLabel) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } + Spacer(minLength: 8) + HStack(spacing: 4) { + ForEach(sortedAppLinks(row.deployment?.appLinks ?? [:]), id: \.key) { key, link in + packageLinkStatusBadge(appKey: key, status: link.status) + } + } + } + .padding(8) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + + private func packageLinkStatusBadge(appKey: String, status: String) -> some View { + let (label, color): (String, Color) = { + switch status.lowercased() { + case "ok": return (localization.string("matrix.inspector.linkStatus.ok"), Color.popStatusOK) + case "broken": return (localization.string("matrix.inspector.linkStatus.broken"), Color.popStatusError) + case "inactive": return (localization.string("matrix.inspector.linkStatus.inactive"), Color.popTertiaryLabel) + case "na": return (localization.string("matrix.inspector.linkStatus.na"), Color.popTertiaryLabel) + default: return (status, Color.popSecondaryLabel) + } + }() + return HStack(spacing: 3) { + Text(packageLinkAppShortLabel(for: appKey)) + .font(.system(size: 8.5, weight: .bold)) + Text(label) + .font(.system(size: 9.5, weight: .semibold)) + } + .foregroundStyle(color) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background(color.opacity(0.10), in: Capsule()) + } + + private func packageLinkAppShortLabel(for key: String) -> String { + switch key.lowercased() { + case "claude": return "CC" + case "codex": return "CDX" + default: return key.prefix(3).uppercased() + } + } + private func syncStatusPill(for pendingUpdates: [SkillUpdateInfo]) -> some View { let hasPending = !pendingUpdates.isEmpty let color = hasPending ? Color.accentColor : Color.popStatusOK @@ -1960,6 +2165,10 @@ struct InspectorPane: View { GridItem(.adaptive(minimum: 96), spacing: 8, alignment: .leading) ] + private static let syncFactColumns: [GridItem] = [ + GridItem(.adaptive(minimum: 132), spacing: 8, alignment: .leading) + ] + private static let companionGridColumns: [GridItem] = [ GridItem(.adaptive(minimum: 84), spacing: 5, alignment: .leading) ] @@ -2043,6 +2252,19 @@ struct InspectorPane: View { store.errorMessage = firstError.localizedDescription } } + + @MainActor + private func refreshPackageLinkHealth() async { + guard !linkHealthScanInFlight else { return } + linkHealthScanInFlight = true + defer { linkHealthScanInFlight = false } + + do { + store.linkHealth = try await store.client.linkHealth() + } catch { + store.errorMessage = error.localizedDescription + } + } } private extension String { diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 8be681e..e6dc797 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -56,6 +56,8 @@ struct LocalizationTests { #expect(localization.string("matrix.source.localStore") == "本地真身") #expect(localization.string("matrix.source.license") == "License") #expect(localization.string("matrix.source.requires") == "依赖命令") + #expect(localization.string("matrix.package.sync.status") == "同步状态") + #expect(localization.string("matrix.package.sync.linkHealthSummary", 2, 1, 3) == "2 正常 · 1 断链 · 3 未启用") #expect(localization.string("matrix.skill.manifest.version") == "版本") #expect(localization.string("matrix.skill.manifest.requires.available") == "可用") #expect(localization.string("matrix.skill.manifest.requires.missing") == "缺失") diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index c8463aa..82f27ee 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -276,6 +276,63 @@ struct SkillModelsTests { #expect(unrelatedPackage.hasBrokenLinks(in: [skill]) == false) } + @Test + func capabilityPackageLinkHealthSnapshotMatchesInstalledComponents() { + let package = self.package(components: [ + component(id: "demo-skill", installed: true), + component(id: "second-skill", installed: true) + ]) + let demo = installedSkill(directory: "owner/repo:demo-skill") + let second = installedSkill(directory: "second-skill") + let report = LinkHealthReport( + summary: LinkHealthSummary(ok: 5, broken: 3, inactive: 2), + rows: [ + LinkHealthRow( + skillId: "owner/repo:demo-skill", + skillName: "Demo", + deployment: SkillDeployment( + strategy: "symlink", + ssotPath: "/Users/example/.cc-switch/skills/demo-skill", + appLinks: [ + "claude": AppLinkStatus(path: "/Users/example/.claude/skills/demo-skill", status: "ok"), + "codex": AppLinkStatus(path: "/Users/example/.codex/skills/demo-skill", status: "broken") + ] + ) + ), + LinkHealthRow( + skillId: "second-skill", + skillName: "Second", + deployment: SkillDeployment( + strategy: "symlink", + ssotPath: "/Users/example/.cc-switch/skills/second-skill", + appLinks: [ + "claude": AppLinkStatus(path: "/Users/example/.claude/skills/second-skill", status: "inactive") + ] + ) + ), + LinkHealthRow( + skillId: "unrelated", + skillName: "Unrelated", + deployment: SkillDeployment( + strategy: "symlink", + ssotPath: "/Users/example/.cc-switch/skills/unrelated", + appLinks: [ + "claude": AppLinkStatus(path: "/Users/example/.claude/skills/unrelated", status: "ok") + ] + ) + ) + ] + ) + + let snapshot = package.linkHealthSnapshot(using: report, skills: [demo, second]) + + #expect(snapshot?.rows.map(\.skillId) == ["owner/repo:demo-skill", "second-skill"]) + #expect(snapshot?.okCount == 1) + #expect(snapshot?.brokenCount == 1) + #expect(snapshot?.inactiveCount == 1) + #expect(package.linkHealthSnapshot(using: nil, skills: [demo]) == nil) + } + @Test func matrixCapabilityBrokenLinksAggregateDirectAndPackageLinks() { let package = self.package(components: [component(id: "demo-skill", installed: true)]) From bf874b1627dd4da0948e60590ed3cf1439f34ac5 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 07:31:51 +0800 Subject: [PATCH 40/47] feat(ui): show package component versions --- .../Sources/Popskill/Models/SkillModels.swift | 36 +++++++++++++ .../Resources/en.lproj/Localizable.strings | 1 + .../zh-Hans.lproj/Localizable.strings | 1 + .../Popskill/Views/InspectorPane.swift | 50 +++++++++++++++++++ .../PopskillTests/LocalizationTests.swift | 1 + .../PopskillTests/SkillModelsTests.swift | 35 ++++++++++++- 6 files changed, 122 insertions(+), 2 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index 032edaf..e838cbc 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -774,6 +774,15 @@ struct CapabilityPackage: Identifiable, Codable, Equatable { .reduce(UInt64(0), +) return total > 0 ? total : nil } + + func componentVersionSummaries(in skills: [Skill]) -> [PackageComponentVersionSummary] { + components.all.map { component in + PackageComponentVersionSummary( + component: component, + versionLabel: componentVersionLabel(for: component, in: skills) + ) + } + } } struct PackageComponentGroupSummary: Identifiable, Equatable { @@ -798,6 +807,22 @@ struct PackageComponentGroupSummary: Identifiable, Equatable { } } +struct PackageComponentVersionSummary: Identifiable, Equatable { + let id: String + let name: String + let kind: String + let status: String + let versionLabel: String? + + init(component: PackageComponent, versionLabel: String?) { + id = component.displayKey + name = component.name + kind = component.kind + status = component.status + self.versionLabel = versionLabel + } +} + struct PackageSource: Codable, Equatable { let kind: String let location: String @@ -1600,6 +1625,17 @@ extension CapabilityPackage { return PackageLinkHealthSnapshot(rows: rows) } + func componentVersionLabel(for component: PackageComponent, in skills: [Skill]) -> String? { + guard let skill = matchingInstalledSkill(for: component, in: skills) else { + return nil + } + return MatrixVersionFormatter.value( + manifestVersion: skill.manifest?.semanticVersion, + contentHash: skill.contentHash, + updatedAt: skill.updatedAt + ) + } + func matchingInstalledSkill(for component: PackageComponent, in skills: [Skill]) -> Skill? { skills.first { component.matchesSkill($0) } } diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 5ce6492..ebb09ff 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -473,6 +473,7 @@ "matrix.package.version.branch" = "Branch"; "matrix.package.version.hash" = "Hash"; "matrix.package.version.untracked" = "Untracked"; +"matrix.package.version.components" = "Component versions"; "matrix.skill.manifest.version" = "Version"; "matrix.skill.manifest.author" = "Author"; "matrix.skill.manifest.license" = "License"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index c615420..0b32280 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -473,6 +473,7 @@ "matrix.package.version.branch" = "分支"; "matrix.package.version.hash" = "Hash"; "matrix.package.version.untracked" = "未跟踪"; +"matrix.package.version.components" = "组件版本"; "matrix.skill.manifest.version" = "版本"; "matrix.skill.manifest.author" = "作者"; "matrix.skill.manifest.license" = "License"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index d82a1f3..6fc5b7f 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -1205,6 +1205,7 @@ struct InspectorPane: View { usageStat: PackageComponentUsageStat? ) -> some View { let matchedSkill = package.matchingInstalledSkill(for: component, in: store.skills) + let versionLabel = package.componentVersionLabel(for: component, in: store.skills) return HStack(alignment: .top, spacing: 8) { Image(systemName: component.inspectorKindSymbol) @@ -1243,6 +1244,9 @@ struct InspectorPane: View { Text(component.status) .font(.caption2.weight(.semibold)) .foregroundStyle(component.installed ? Color.popStatusOK : Color.popStatusWarning) + Text(versionLabel ?? "—") + .font(.caption2.monospacedDigit().weight(.medium)) + .foregroundStyle(versionLabel == nil ? Color.popTertiaryLabel : Color.accentColor) Text(localization.string( "matrix.package.component.calls", usageStat.map { UsageDisplayFormatter.compactCount($0.usageEvents) } ?? "—" @@ -1409,9 +1413,55 @@ struct InspectorPane: View { value: package.trackedContentHash.map(Self.shortHash) ?? localization.string("matrix.package.version.untracked") ) } + packageComponentVersionsSection(package) } } + @ViewBuilder + private func packageComponentVersionsSection(_ package: CapabilityPackage) -> some View { + let summaries = package.componentVersionSummaries(in: store.skills) + if !summaries.isEmpty { + VStack(alignment: .leading, spacing: 6) { + LocalizedText("matrix.package.version.components") + .font(.caption2.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + ForEach(summaries.prefix(8)) { summary in + packageComponentVersionRow(summary) + } + if summaries.count > 8 { + Text(localization.string("matrix.package.paths.more", summaries.count - 8)) + .font(.caption2) + .foregroundStyle(Color.popTertiaryLabel) + } + } + .padding(.top, 4) + } + } + + private func packageComponentVersionRow(_ summary: PackageComponentVersionSummary) -> some View { + HStack(spacing: 8) { + Image(systemName: summary.kind.usageKindSymbol) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(summary.versionLabel == nil ? Color.popTertiaryLabel : Color.popSecondaryLabel) + .frame(width: 14) + VStack(alignment: .leading, spacing: 1) { + Text(summary.name) + .font(.caption.weight(.medium)) + .foregroundStyle(Color.popLabel) + .lineLimit(1) + Text(summary.status) + .font(.caption2) + .foregroundStyle(Color.popSecondaryLabel) + } + Spacer(minLength: 8) + Text(summary.versionLabel ?? localization.string("matrix.package.version.untracked")) + .font(.caption2.monospacedDigit().weight(.semibold)) + .foregroundStyle(summary.versionLabel == nil ? Color.popTertiaryLabel : Color.accentColor) + } + .padding(8) + .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + private func skillVersionSection(_ skill: Skill) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.version") diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index e6dc797..cd4eba0 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -58,6 +58,7 @@ struct LocalizationTests { #expect(localization.string("matrix.source.requires") == "依赖命令") #expect(localization.string("matrix.package.sync.status") == "同步状态") #expect(localization.string("matrix.package.sync.linkHealthSummary", 2, 1, 3) == "2 正常 · 1 断链 · 3 未启用") + #expect(localization.string("matrix.package.version.components") == "组件版本") #expect(localization.string("matrix.skill.manifest.version") == "版本") #expect(localization.string("matrix.skill.manifest.requires.available") == "可用") #expect(localization.string("matrix.skill.manifest.requires.missing") == "缺失") diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 82f27ee..5474d34 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -1270,6 +1270,33 @@ struct SkillModelsTests { #expect(package.installedSizeBytes(in: [installedSkill(directory: "baoyu-comic")]) == nil) } + @Test + func capabilityPackageComponentVersionSummariesPreferSemanticVersionThenHash() { + let package = self.package(components: [ + component(id: "versioned-skill", installed: true), + component(id: "hashed-skill", installed: true), + component(id: "missing-skill", installed: false) + ]) + let versioned = installedSkill( + directory: "versioned-skill", + contentHash: "abcdef123456", + manifest: SkillManifest(version: "2.4.1") + ) + let hashed = installedSkill( + directory: "hashed-skill", + updatedAt: 1_700_000_000, + contentHash: "1234567890" + ) + + let summaries = package.componentVersionSummaries(in: [versioned, hashed]) + + #expect(summaries.map(\.name) == ["versioned-skill", "hashed-skill", "missing-skill"]) + #expect(summaries[0].versionLabel == "v2.4.1") + #expect(summaries[1].versionLabel == "1234567") + #expect(summaries[2].versionLabel == nil) + #expect(package.componentVersionLabel(for: package.components.skills[0], in: [versioned]) == "v2.4.1") + } + @Test func capabilityPackageUsageSnapshotPrefersRecentThirtyDayWindow() { let package = self.package(components: [component(id: "baoyu-comic", installed: true)]) @@ -1752,7 +1779,9 @@ struct SkillModelsTests { directory: String, installedAt: Int? = nil, updatedAt: Int? = nil, + contentHash: String? = nil, sizeBytes: UInt64? = nil, + manifest: SkillManifest? = nil, apps: SkillApps = SkillApps( claude: true, codex: false, @@ -1761,7 +1790,7 @@ struct SkillModelsTests { hermes: false ) ) -> Skill { - Skill( + var skill = Skill( id: directory, name: "Demo", description: "Demo skill", @@ -1772,9 +1801,11 @@ struct SkillModelsTests { apps: apps, installedAt: installedAt, updatedAt: updatedAt, - contentHash: nil, + contentHash: contentHash, sizeBytes: sizeBytes ) + skill.manifest = manifest + return skill } private func package(components: [PackageComponent]) -> CapabilityPackage { From 4717b9dce427a19cfd4909613dc630bece0326c8 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 07:38:32 +0800 Subject: [PATCH 41/47] feat(ui): mark final package component row --- .../Popskill/Views/MatrixPackageRow.swift | 16 ++++++++++++++-- .../Tests/PopskillTests/CommonViewsTests.swift | 9 +++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift index 1d90b53..ca078f3 100644 --- a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift @@ -28,10 +28,12 @@ struct MatrixPackageRow: View { VStack(spacing: 0) { packageHeader if let package, !isCollapsed { - ForEach(package.components.all, id: \.displayKey) { component in + let components = package.components.all + ForEach(Array(components.enumerated()), id: \.element.displayKey) { index, component in MatrixPackageComponentRow( component: component, packageID: package.id, + treePrefix: PackageComponentTreePrefix.value(index: index, count: components.count), store: store, usageIndex: usageIndex ) @@ -304,6 +306,7 @@ struct MatrixPackageRow: View { private struct MatrixPackageComponentRow: View { let component: PackageComponent let packageID: String + let treePrefix: String @Bindable var store: PopskillStore let usageIndex: MatrixUsageIndex @Environment(\.popskillLocalization) private var localization @@ -388,7 +391,7 @@ private struct MatrixPackageComponentRow: View { } private var componentTreePrefix: String { - "├─" + treePrefix } private var requirementBadge: (key: String, color: Color)? { @@ -460,6 +463,15 @@ private struct MatrixPackageComponentRow: View { } } +enum PackageComponentTreePrefix { + static func value(index: Int, count: Int) -> String { + guard count > 0, index == count - 1 else { + return "├─" + } + return "└─" + } +} + private enum ComponentAppIndicator { case enabled case partial diff --git a/swift-app/Tests/PopskillTests/CommonViewsTests.swift b/swift-app/Tests/PopskillTests/CommonViewsTests.swift index 44fa3cc..ea47053 100644 --- a/swift-app/Tests/PopskillTests/CommonViewsTests.swift +++ b/swift-app/Tests/PopskillTests/CommonViewsTests.swift @@ -42,6 +42,15 @@ struct CommonViewsTests { #expect(PackageAvatar.computeInitials(for: "---") == "S") } + @Test + func packageComponentTreePrefixMarksLastRow() { + #expect(PackageComponentTreePrefix.value(index: 0, count: 1) == "└─") + #expect(PackageComponentTreePrefix.value(index: 0, count: 3) == "├─") + #expect(PackageComponentTreePrefix.value(index: 1, count: 3) == "├─") + #expect(PackageComponentTreePrefix.value(index: 2, count: 3) == "└─") + #expect(PackageComponentTreePrefix.value(index: 0, count: 0) == "├─") + } + @Test func sectionAccentIndexWrapsForwardAndBackward() { #expect(PopskillSectionAccent.index(for: 0) == 0) From 59fa83abd3c55c302a9e22f61f99b4fb1b8d7e4f Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 07:48:19 +0800 Subject: [PATCH 42/47] feat(ui): summarize package composition --- .../Sources/Popskill/Models/SkillModels.swift | 44 +++++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 9 ++++ .../zh-Hans.lproj/Localizable.strings | 9 ++++ .../Popskill/Views/InspectorPane.swift | 14 +----- .../Popskill/Views/MatrixPackageRow.swift | 7 +-- .../Popskill/Views/SpotlightView.swift | 7 +-- .../PopskillTests/LocalizationTests.swift | 1 + .../PopskillTests/SkillModelsTests.swift | 43 ++++++++++++++++++ 8 files changed, 110 insertions(+), 24 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index e838cbc..cd8ab2c 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -751,6 +751,15 @@ struct CapabilityPackage: Identifiable, Codable, Equatable { ].filter { $0.total > 0 } } + var componentCompositionCounts: [PackageComponentCompositionCount] { + [ + PackageComponentCompositionCount(kind: "cli", count: components.cli.count), + PackageComponentCompositionCount(kind: "mcp", count: components.mcp.count), + PackageComponentCompositionCount(kind: "skill", count: components.skills.count), + PackageComponentCompositionCount(kind: "agent", count: components.agents.count) + ].filter { $0.count > 0 } + } + var lastLifecycleTimestamp: Int? { [lifecycle?.installedAt, lifecycle?.updatedAt] .compactMap { value -> Int? in @@ -807,6 +816,41 @@ struct PackageComponentGroupSummary: Identifiable, Equatable { } } +struct PackageComponentCompositionCount: Identifiable, Equatable { + let kind: String + let count: Int + + var id: String { kind } +} + +enum PackageComponentCompositionFormatter { + static func composition(for package: CapabilityPackage, localization: PopskillLocalization) -> String { + let parts = package.componentCompositionCounts.map { count in + localization.string(titleKey(kind: count.kind, count: count.count), count.count) + } + return parts.isEmpty ? localization.string("package.componentComposition.empty") : parts.joined(separator: " + ") + } + + static func label(for package: CapabilityPackage, localization: PopskillLocalization) -> String { + composition(for: package, localization: localization) + } + + static func summary(for package: CapabilityPackage, localization: PopskillLocalization) -> String { + let composition = composition(for: package, localization: localization) + let summary = package.summary.trimmingCharacters(in: .whitespacesAndNewlines) + return summary.isEmpty ? composition : "\(summary) · \(composition)" + } + + private static func titleKey(kind: String, count: Int) -> String { + switch kind.lowercased() { + case "cli": return count == 1 ? "package.componentComposition.cli.one" : "package.componentComposition.cli.other" + case "mcp": return count == 1 ? "package.componentComposition.mcp.one" : "package.componentComposition.mcp.other" + case "agent": return count == 1 ? "package.componentComposition.agent.one" : "package.componentComposition.agent.other" + default: return count == 1 ? "package.componentComposition.skill.one" : "package.componentComposition.skill.other" + } + } +} + struct PackageComponentVersionSummary: Identifiable, Equatable { let id: String let name: String diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index ebb09ff..0f1801f 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -157,6 +157,15 @@ "Installed Components" = "Installed Components"; "Component Tree" = "Component Tree"; "package.componentSummary" = "%d components · %d installed · %d required"; +"package.componentComposition.cli.one" = "%d CLI"; +"package.componentComposition.cli.other" = "%d CLI"; +"package.componentComposition.mcp.one" = "%d MCP"; +"package.componentComposition.mcp.other" = "%d MCP"; +"package.componentComposition.skill.one" = "%d Skill"; +"package.componentComposition.skill.other" = "%d Skills"; +"package.componentComposition.agent.one" = "%d Agent"; +"package.componentComposition.agent.other" = "%d Agents"; +"package.componentComposition.empty" = "No components"; "Directory" = "Directory"; "Identifier" = "Identifier"; "Hash" = "Hash"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 0b32280..d0d8890 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -157,6 +157,15 @@ "Installed Components" = "已安装组件"; "Component Tree" = "组件树"; "package.componentSummary" = "%d 个组件 · %d 个已安装 · %d 个必需"; +"package.componentComposition.cli.one" = "%d 个 CLI"; +"package.componentComposition.cli.other" = "%d 个 CLI"; +"package.componentComposition.mcp.one" = "%d 个 MCP"; +"package.componentComposition.mcp.other" = "%d 个 MCP"; +"package.componentComposition.skill.one" = "%d 项 Skill"; +"package.componentComposition.skill.other" = "%d 项 Skill"; +"package.componentComposition.agent.one" = "%d 个 Agent"; +"package.componentComposition.agent.other" = "%d 个 Agent"; +"package.componentComposition.empty" = "无组件"; "Directory" = "目录"; "Identifier" = "标识符"; "Hash" = "哈希"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 6fc5b7f..6ee7129 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -805,12 +805,7 @@ struct InspectorPane: View { color: package.health.inspectorColor ) packagePill( - localization.string( - "package.componentSummary", - package.componentCount, - package.installedComponentCount, - package.requiredComponentCount - ), + PackageComponentCompositionFormatter.composition(for: package, localization: localization), color: Color.popSecondaryLabel ) } @@ -1867,12 +1862,7 @@ struct InspectorPane: View { .font(.caption.weight(.semibold)) .foregroundStyle(Color.popLabel) .lineLimit(1) - Text(localization.string( - "package.componentSummary", - package.componentCount, - package.installedComponentCount, - package.requiredComponentCount - )) + Text(PackageComponentCompositionFormatter.composition(for: package, localization: localization)) .font(.caption2) .foregroundStyle(Color.popSecondaryLabel) .lineLimit(1) diff --git a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift index ca078f3..1e43809 100644 --- a/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift +++ b/swift-app/Sources/Popskill/Views/MatrixPackageRow.swift @@ -163,12 +163,7 @@ struct MatrixPackageRow: View { private var packageSubtitle: String { guard let package else { return capability.summary ?? localization.string("matrix.row.noSummary") } - return localization.string( - "package.componentSummary", - package.componentCount, - package.installedComponentCount, - package.requiredComponentCount - ) + return PackageComponentCompositionFormatter.summary(for: package, localization: localization) } private func coverageCell(for app: TargetApp) -> some View { diff --git a/swift-app/Sources/Popskill/Views/SpotlightView.swift b/swift-app/Sources/Popskill/Views/SpotlightView.swift index 0d4bc8e..eab7120 100644 --- a/swift-app/Sources/Popskill/Views/SpotlightView.swift +++ b/swift-app/Sources/Popskill/Views/SpotlightView.swift @@ -243,12 +243,7 @@ struct SpotlightView: View { hit.matchedComponents.joined(separator: " · ") ) } - return localization.string( - "package.componentSummary", - package.componentCount, - package.installedComponentCount, - package.requiredComponentCount - ) + return PackageComponentCompositionFormatter.summary(for: package, localization: localization) case let .skill(skill, hit): if !hit.matchedTriggers.isEmpty { return hit.matchedTriggers.prefix(2).joined(separator: " · ") diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index cd4eba0..60f7e13 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -47,6 +47,7 @@ struct LocalizationTests { #expect(localization.string("matrix.skill.action.revealInFinder") == "在 Finder 中显示") #expect(localization.string("matrix.package.action.activateClaude") == "全部激活到 Claude") #expect(localization.string("matrix.package.activation.remaining", 2, 1) == "Claude 还差 2 个 · Codex 还差 1 个") + #expect(localization.string("package.componentComposition.skill.other", 6) == "6 项 Skill") #expect(localization.string("matrix.inspector.section.machine") == "这台机器") #expect(localization.string("matrix.machine.firstActivated") == "首次激活") #expect(localization.string("matrix.machine.tokens") == "近30天 tokens") diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index 5474d34..db8d38a 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -1025,6 +1025,49 @@ struct SkillModelsTests { #expect(package.components.all.map(\.displayKey) == ["cli:lark-cli", "skill:lark-doc"]) } + @Test + func packageComponentCompositionSummarizesKindsInStableOrder() { + let package = CapabilityPackage( + id: "pkg:lark", + type: .composite, + name: "Feishu / Lark", + vendor: "ByteDance", + summary: "Collaboration suite.", + source: PackageSource( + kind: "builtin", + location: "popskill/builtin/lark", + updateStrategy: "manual", + repoOwner: nil, + repoName: nil, + repoBranch: nil, + readmeUrl: nil + ), + components: PackageComponents( + cli: [ + PackageComponent(id: "lark-cli", name: "lark-cli", kind: "cli", required: true, installed: true, status: "detected", location: nil) + ], + skills: [ + PackageComponent(id: "lark-doc", name: "Lark Doc", kind: "skill", required: true, installed: true, status: "installed", location: "lark-doc"), + PackageComponent(id: "lark-base", name: "Lark Base", kind: "skill", required: false, installed: true, status: "installed", location: "lark-base") + ], + mcp: [ + PackageComponent(id: "lark-mcp", name: "Lark MCP", kind: "mcp", required: false, installed: false, status: "registry-reference", location: nil) + ], + agents: [ + PackageComponent(id: "lark-agent", name: "Lark Agent", kind: "agent", required: false, installed: true, status: "installed", location: "~/.claude/agents/lark-agent.md") + ] + ), + configSchema: [], + installed: true, + lifecycle: nil + ) + + #expect(package.componentCompositionCounts.map { "\($0.kind):\($0.count)" } == ["cli:1", "mcp:1", "skill:2", "agent:1"]) + #expect(PackageComponentCompositionFormatter.composition(for: package, localization: PopskillLocalization(language: .english)) == "1 CLI + 1 MCP + 2 Skills + 1 Agent") + #expect(PackageComponentCompositionFormatter.composition(for: package, localization: PopskillLocalization(language: .simplifiedChinese)) == "1 个 CLI + 1 个 MCP + 2 项 Skill + 1 个 Agent") + #expect(PackageComponentCompositionFormatter.summary(for: package, localization: PopskillLocalization(language: .english)) == "Collaboration suite. · 1 CLI + 1 MCP + 2 Skills + 1 Agent") + } + @Test func packageComponentMatchesInstalledSkillByScopedIdentifierAndLocation() { let component = PackageComponent( From a4a534ace0e6adfab55cb767f3297a78f59d2508 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 07:57:17 +0800 Subject: [PATCH 43/47] feat(ui): rescan usage from spotlight --- .../Popskill/Resources/en.lproj/Localizable.strings | 2 ++ .../Resources/zh-Hans.lproj/Localizable.strings | 2 ++ .../Sources/Popskill/Views/SpotlightView.swift | 11 +++++++++++ .../Tests/PopskillTests/LocalizationTests.swift | 8 ++++++++ .../Tests/PopskillTests/SpotlightActionTests.swift | 13 +++++++++++++ 5 files changed, 36 insertions(+) create mode 100644 swift-app/Tests/PopskillTests/SpotlightActionTests.swift diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 0f1801f..eabc485 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -549,6 +549,8 @@ "spotlight.action.refresh.title" = "Refresh data"; "spotlight.action.refresh.subtitle" = "Re-bootstrap skills, sources, and agents from sidecar"; +"spotlight.action.usageScan.title" = "Re-scan usage"; +"spotlight.action.usageScan.subtitle" = "Scan recent transcripts for calls and tokens"; "spotlight.action.linkHealth.title" = "Open Link Health"; "spotlight.action.linkHealth.subtitle" = "Inspect SSOT ↔ symlink integrity"; "spotlight.action.updates.title" = "Open Updates"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index d0d8890..70c1e24 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -549,6 +549,8 @@ "spotlight.action.refresh.title" = "刷新数据"; "spotlight.action.refresh.subtitle" = "重新从 sidecar 拉 skills / 数据源 / agents"; +"spotlight.action.usageScan.title" = "重新扫描用量"; +"spotlight.action.usageScan.subtitle" = "扫描最近 transcript,刷新调用与 tokens"; "spotlight.action.linkHealth.title" = "打开链接健康"; "spotlight.action.linkHealth.subtitle" = "检查 SSOT 与 symlink 的健康"; "spotlight.action.updates.title" = "打开更新列表"; diff --git a/swift-app/Sources/Popskill/Views/SpotlightView.swift b/swift-app/Sources/Popskill/Views/SpotlightView.swift index eab7120..9070ac7 100644 --- a/swift-app/Sources/Popskill/Views/SpotlightView.swift +++ b/swift-app/Sources/Popskill/Views/SpotlightView.swift @@ -478,6 +478,17 @@ struct SpotlightAction: Identifiable { Task { await store.bootstrap() } } ), + SpotlightAction( + id: "usage-scan", + titleKey: "spotlight.action.usageScan.title", + subtitleKey: "spotlight.action.usageScan.subtitle", + symbol: "chart.bar.doc.horizontal", + tint: .green, + perform: { store in + store.currentSelection = .insights + Task { await store.refreshUsageScan() } + } + ), SpotlightAction( id: "link-health", titleKey: "spotlight.action.linkHealth.title", diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 60f7e13..62e7e7a 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -64,4 +64,12 @@ struct LocalizationTests { #expect(localization.string("matrix.skill.manifest.requires.available") == "可用") #expect(localization.string("matrix.skill.manifest.requires.missing") == "缺失") } + + @Test + func spotlightUsageScanActionIsLocalized() { + let localization = PopskillLocalization(language: .simplifiedChinese) + + #expect(localization.string("spotlight.action.usageScan.title") == "重新扫描用量") + #expect(localization.string("spotlight.action.usageScan.subtitle") == "扫描最近 transcript,刷新调用与 tokens") + } } diff --git a/swift-app/Tests/PopskillTests/SpotlightActionTests.swift b/swift-app/Tests/PopskillTests/SpotlightActionTests.swift new file mode 100644 index 0000000..e6378c2 --- /dev/null +++ b/swift-app/Tests/PopskillTests/SpotlightActionTests.swift @@ -0,0 +1,13 @@ +@testable import Popskill +import Testing + +struct SpotlightActionTests { + @Test + func spotlightActionsExposeUsageScan() { + #expect(SpotlightAction.all.map(\.id).contains("usage-scan")) + + let usageScan = SpotlightAction.all.first { $0.id == "usage-scan" } + #expect(usageScan?.titleKey == "spotlight.action.usageScan.title") + #expect(usageScan?.subtitleKey == "spotlight.action.usageScan.subtitle") + } +} From c90586b170c50ae04d7f58f94bf5cae28f91c4ae Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 08:07:44 +0800 Subject: [PATCH 44/47] feat(ui): filter matrix from spotlight --- .../Resources/en.lproj/Localizable.strings | 12 ++++ .../zh-Hans.lproj/Localizable.strings | 12 ++++ .../Popskill/Views/SpotlightView.swift | 60 +++++++++++++++++++ .../PopskillTests/LocalizationTests.swift | 6 ++ .../PopskillTests/SpotlightActionTests.swift | 44 ++++++++++++++ 5 files changed, 134 insertions(+) diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index eabc485..8abfd59 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -551,6 +551,18 @@ "spotlight.action.refresh.subtitle" = "Re-bootstrap skills, sources, and agents from sidecar"; "spotlight.action.usageScan.title" = "Re-scan usage"; "spotlight.action.usageScan.subtitle" = "Scan recent transcripts for calls and tokens"; +"spotlight.action.showBundles.title" = "Show bundles"; +"spotlight.action.showBundles.subtitle" = "Filter the matrix to composite packages"; +"spotlight.action.showSkills.title" = "Show skills"; +"spotlight.action.showSkills.subtitle" = "Filter the matrix to skills"; +"spotlight.action.showCLI.title" = "Show CLI"; +"spotlight.action.showCLI.subtitle" = "Filter the matrix to command-line tools"; +"spotlight.action.showMCP.title" = "Show MCP"; +"spotlight.action.showMCP.subtitle" = "Filter the matrix to MCP servers"; +"spotlight.action.showBrokenLinks.title" = "Show broken links"; +"spotlight.action.showBrokenLinks.subtitle" = "Filter the matrix to capabilities with bad app links"; +"spotlight.action.showInactive.title" = "Show inactive"; +"spotlight.action.showInactive.subtitle" = "Filter the matrix to capabilities not active in Claude or Codex"; "spotlight.action.linkHealth.title" = "Open Link Health"; "spotlight.action.linkHealth.subtitle" = "Inspect SSOT ↔ symlink integrity"; "spotlight.action.updates.title" = "Open Updates"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 70c1e24..ae112a3 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -551,6 +551,18 @@ "spotlight.action.refresh.subtitle" = "重新从 sidecar 拉 skills / 数据源 / agents"; "spotlight.action.usageScan.title" = "重新扫描用量"; "spotlight.action.usageScan.subtitle" = "扫描最近 transcript,刷新调用与 tokens"; +"spotlight.action.showBundles.title" = "查看套装"; +"spotlight.action.showBundles.subtitle" = "把矩阵过滤到复合能力包"; +"spotlight.action.showSkills.title" = "查看 Skill"; +"spotlight.action.showSkills.subtitle" = "把矩阵过滤到 Skill 能力"; +"spotlight.action.showCLI.title" = "查看 CLI"; +"spotlight.action.showCLI.subtitle" = "把矩阵过滤到命令行工具"; +"spotlight.action.showMCP.title" = "查看 MCP"; +"spotlight.action.showMCP.subtitle" = "把矩阵过滤到 MCP 服务"; +"spotlight.action.showBrokenLinks.title" = "查看断链"; +"spotlight.action.showBrokenLinks.subtitle" = "把矩阵过滤到应用链接异常的能力"; +"spotlight.action.showInactive.title" = "查看未启用"; +"spotlight.action.showInactive.subtitle" = "把矩阵过滤到未在 Claude 或 Codex 启用的能力"; "spotlight.action.linkHealth.title" = "打开链接健康"; "spotlight.action.linkHealth.subtitle" = "检查 SSOT 与 symlink 的健康"; "spotlight.action.updates.title" = "打开更新列表"; diff --git a/swift-app/Sources/Popskill/Views/SpotlightView.swift b/swift-app/Sources/Popskill/Views/SpotlightView.swift index 9070ac7..7dd25f5 100644 --- a/swift-app/Sources/Popskill/Views/SpotlightView.swift +++ b/swift-app/Sources/Popskill/Views/SpotlightView.swift @@ -489,6 +489,66 @@ struct SpotlightAction: Identifiable { Task { await store.refreshUsageScan() } } ), + SpotlightAction( + id: "show-bundles", + titleKey: "spotlight.action.showBundles.title", + subtitleKey: "spotlight.action.showBundles.subtitle", + symbol: "shippingbox", + tint: .popSectionPurple, + perform: { store in + store.showMatrix(typeFilter: .bundle) + } + ), + SpotlightAction( + id: "show-skills", + titleKey: "spotlight.action.showSkills.title", + subtitleKey: "spotlight.action.showSkills.subtitle", + symbol: CapabilityKind.skill.symbol, + tint: .accentColor, + perform: { store in + store.showMatrix(typeFilter: .skill) + } + ), + SpotlightAction( + id: "show-cli", + titleKey: "spotlight.action.showCLI.title", + subtitleKey: "spotlight.action.showCLI.subtitle", + symbol: CapabilityKind.cli.symbol, + tint: .blue, + perform: { store in + store.showMatrix(typeFilter: .cli) + } + ), + SpotlightAction( + id: "show-mcp", + titleKey: "spotlight.action.showMCP.title", + subtitleKey: "spotlight.action.showMCP.subtitle", + symbol: CapabilityKind.mcp.symbol, + tint: .mint, + perform: { store in + store.showMatrix(typeFilter: .mcp) + } + ), + SpotlightAction( + id: "show-broken-links", + titleKey: "spotlight.action.showBrokenLinks.title", + subtitleKey: "spotlight.action.showBrokenLinks.subtitle", + symbol: "exclamationmark.triangle", + tint: .red, + perform: { store in + store.showMatrix(filter: .brokenLinks) + } + ), + SpotlightAction( + id: "show-inactive", + titleKey: "spotlight.action.showInactive.title", + subtitleKey: "spotlight.action.showInactive.subtitle", + symbol: "pause.circle", + tint: .orange, + perform: { store in + store.showMatrix(filter: .inactive) + } + ), SpotlightAction( id: "link-health", titleKey: "spotlight.action.linkHealth.title", diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 62e7e7a..482b89f 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -71,5 +71,11 @@ struct LocalizationTests { #expect(localization.string("spotlight.action.usageScan.title") == "重新扫描用量") #expect(localization.string("spotlight.action.usageScan.subtitle") == "扫描最近 transcript,刷新调用与 tokens") + #expect(localization.string("spotlight.action.showBundles.title") == "查看套装") + #expect(localization.string("spotlight.action.showSkills.title") == "查看 Skill") + #expect(localization.string("spotlight.action.showCLI.title") == "查看 CLI") + #expect(localization.string("spotlight.action.showMCP.title") == "查看 MCP") + #expect(localization.string("spotlight.action.showBrokenLinks.title") == "查看断链") + #expect(localization.string("spotlight.action.showInactive.title") == "查看未启用") } } diff --git a/swift-app/Tests/PopskillTests/SpotlightActionTests.swift b/swift-app/Tests/PopskillTests/SpotlightActionTests.swift index e6378c2..0e5510f 100644 --- a/swift-app/Tests/PopskillTests/SpotlightActionTests.swift +++ b/swift-app/Tests/PopskillTests/SpotlightActionTests.swift @@ -5,9 +5,53 @@ struct SpotlightActionTests { @Test func spotlightActionsExposeUsageScan() { #expect(SpotlightAction.all.map(\.id).contains("usage-scan")) + #expect(SpotlightAction.all.map(\.id).contains("show-bundles")) + #expect(SpotlightAction.all.map(\.id).contains("show-skills")) + #expect(SpotlightAction.all.map(\.id).contains("show-cli")) + #expect(SpotlightAction.all.map(\.id).contains("show-mcp")) + #expect(SpotlightAction.all.map(\.id).contains("show-broken-links")) + #expect(SpotlightAction.all.map(\.id).contains("show-inactive")) let usageScan = SpotlightAction.all.first { $0.id == "usage-scan" } #expect(usageScan?.titleKey == "spotlight.action.usageScan.title") #expect(usageScan?.subtitleKey == "spotlight.action.usageScan.subtitle") } + + @MainActor + @Test + func spotlightMatrixFilterActionsOpenExpectedMatrixViews() { + let store = PopskillStore() + + store.searchText = "baoyu" + SpotlightAction.all.first { $0.id == "show-bundles" }?.run(store: store) + #expect(store.currentSelection == .matrix) + #expect(store.matrixTypeFilter == .bundle) + #expect(store.matrixFilter == .all) + #expect(store.searchText.isEmpty) + + SpotlightAction.all.first { $0.id == "show-skills" }?.run(store: store) + #expect(store.currentSelection == .matrix) + #expect(store.matrixTypeFilter == .skill) + #expect(store.matrixFilter == .all) + + SpotlightAction.all.first { $0.id == "show-cli" }?.run(store: store) + #expect(store.currentSelection == .matrix) + #expect(store.matrixTypeFilter == .cli) + #expect(store.matrixFilter == .all) + + SpotlightAction.all.first { $0.id == "show-mcp" }?.run(store: store) + #expect(store.currentSelection == .matrix) + #expect(store.matrixTypeFilter == .mcp) + #expect(store.matrixFilter == .all) + + SpotlightAction.all.first { $0.id == "show-broken-links" }?.run(store: store) + #expect(store.currentSelection == .matrix) + #expect(store.matrixFilter == .brokenLinks) + #expect(store.matrixTypeFilter == .allTypes) + + SpotlightAction.all.first { $0.id == "show-inactive" }?.run(store: store) + #expect(store.currentSelection == .matrix) + #expect(store.matrixFilter == .inactive) + #expect(store.matrixTypeFilter == .allTypes) + } } From f4aa02163daa2f97abf3ff624a98ad601347ec68 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 08:21:28 +0800 Subject: [PATCH 45/47] feat(ui): rank spotlight recents by usage --- .../Resources/en.lproj/Localizable.strings | 5 +- .../zh-Hans.lproj/Localizable.strings | 5 +- .../Popskill/Views/SpotlightView.swift | 146 ++++++++++++++++- .../PopskillTests/LocalizationTests.swift | 1 + .../PopskillTests/SpotlightActionTests.swift | 147 ++++++++++++++++++ 5 files changed, 296 insertions(+), 8 deletions(-) diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 8abfd59..7b58855 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -537,11 +537,14 @@ "spotlight.clear" = "Clear"; "spotlight.section.capabilities" = "CAPABILITIES"; +"spotlight.section.recent" = "RECENTLY USED"; "spotlight.section.actions" = "ACTIONS"; "spotlight.bundle.matchedComponents" = "Matched components: %@"; +"spotlight.recentUsage" = "Used %@ · %@ calls"; +"spotlight.recentUsage.unknown" = "recently"; "spotlight.empty.title" = "No matches"; -"spotlight.empty.body" = "Try a name, trigger word, or use ↑↓ to pick a recent capability."; +"spotlight.empty.body" = "Try a name, trigger word, or use ↑↓ to pick a recently used capability."; "spotlight.hint.navigate" = "↑↓ navigate"; "spotlight.hint.open" = "↩ open"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index ae112a3..525ecf8 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -537,11 +537,14 @@ "spotlight.clear" = "清空"; "spotlight.section.capabilities" = "能力"; +"spotlight.section.recent" = "最近使用"; "spotlight.section.actions" = "操作"; "spotlight.bundle.matchedComponents" = "匹配组件:%@"; +"spotlight.recentUsage" = "最近 %@ · %@ 次调用"; +"spotlight.recentUsage.unknown" = "刚刚"; "spotlight.empty.title" = "没有匹配"; -"spotlight.empty.body" = "试试名字、触发词,或按 ↑↓ 挑一条最近更新的能力。"; +"spotlight.empty.body" = "试试名字、触发词,或按 ↑↓ 挑一条最近使用的能力。"; "spotlight.hint.navigate" = "↑↓ 导航"; "spotlight.hint.open" = "↩ 打开"; diff --git a/swift-app/Sources/Popskill/Views/SpotlightView.swift b/swift-app/Sources/Popskill/Views/SpotlightView.swift index 7dd25f5..cebf057 100644 --- a/swift-app/Sources/Popskill/Views/SpotlightView.swift +++ b/swift-app/Sources/Popskill/Views/SpotlightView.swift @@ -20,6 +20,11 @@ struct SpotlightView: View { private let maxCapabilityHits = 8 private let maxPackageHits = 3 + private static let relativeFormatter: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .short + return formatter + }() var body: some View { ZStack { @@ -133,7 +138,7 @@ struct SpotlightView: View { ) } } header: { - sectionHeader(localization.string("spotlight.section.capabilities")) + sectionHeader(localization.string(capabilitySectionTitleKey)) } } if !actionHits.isEmpty { @@ -243,11 +248,21 @@ struct SpotlightView: View { hit.matchedComponents.joined(separator: " · ") ) } + if isEmptyQuery, + let snapshot = package.usageSnapshot(using: store.usageSummary, skills: store.skills), + let label = recentUsageLabel(calls: snapshot.usageEvents, lastUsedAt: snapshot.lastUsedAt) { + return label + } return PackageComponentCompositionFormatter.summary(for: package, localization: localization) case let .skill(skill, hit): if !hit.matchedTriggers.isEmpty { return hit.matchedTriggers.prefix(2).joined(separator: " · ") } + if isEmptyQuery, + let snapshot = skill.usageSnapshot(using: store.usageSummary), + let label = recentUsageLabel(calls: snapshot.usageEvents, lastUsedAt: snapshot.lastUsedAt) { + return label + } if let summary = skill.capabilitySummary, !summary.isEmpty { return summary } return skill.description case let .action(action): @@ -255,6 +270,16 @@ struct SpotlightView: View { } } + private func recentUsageLabel(calls: Int, lastUsedAt: Date?) -> String? { + guard calls > 0 || lastUsedAt != nil else { + return nil + } + let relative = lastUsedAt.map { + Self.relativeFormatter.localizedString(for: $0, relativeTo: Date()) + } ?? localization.string("spotlight.recentUsage.unknown") + return localization.string("spotlight.recentUsage", relative, UsageDisplayFormatter.compactCount(calls)) + } + @ViewBuilder private func trailing(for item: SpotlightItem) -> some View { switch item { @@ -335,11 +360,22 @@ struct SpotlightView: View { // MARK: Derived + private var isEmptyQuery: Bool { + localQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + + private var capabilitySectionTitleKey: String { + isEmptyQuery ? "spotlight.section.recent" : "spotlight.section.capabilities" + } + private var packageHits: [(package: CapabilityPackage, hit: PackageSearchHit)] { let q = localQuery.trimmingCharacters(in: .whitespacesAndNewlines) guard !q.isEmpty else { - return store.compositePackages - .sorted { ($0.lastLifecycleTimestamp ?? 0) > ($1.lastLifecycleTimestamp ?? 0) } + return SpotlightRecentRanker.recentPackages( + store.compositePackages, + skills: store.skills, + summary: store.usageSummary + ) .prefix(maxPackageHits) .map { ($0, PackageSearchHit.recent) } } @@ -361,9 +397,12 @@ struct SpotlightView: View { let q = localQuery.trimmingCharacters(in: .whitespacesAndNewlines) let remainingSlots = max(0, maxCapabilityHits - packageHits.count) guard !q.isEmpty else { - // Empty query: show recently installed / updated skills (max N). - return store.skills - .sorted { ($0.lastLifecycleTimestamp ?? 0) > ($1.lastLifecycleTimestamp ?? 0) } + // Empty query: mirror the HTML prototype's "recently used" command + // palette, falling back to lifecycle recency before usage has been scanned. + return SpotlightRecentRanker.recentSkills( + store.skills, + summary: store.usageSummary + ) .prefix(remainingSlots) .map { ($0, SkillSearchHit(score: 0, matchedTriggers: [], matchedOnName: false)) } } @@ -443,6 +482,101 @@ struct SpotlightView: View { } } +enum SpotlightRecentRanker { + static func recentPackages( + _ packages: [CapabilityPackage], + skills: [Skill], + summary: UsageSummary? + ) -> [CapabilityPackage] { + packages + .map { package in + ( + package: package, + snapshot: package.usageSnapshot(using: summary, skills: skills) + ) + } + .sorted { lhs, rhs in + isRankedBefore( + lhsLastUsed: lhs.snapshot?.lastUsedAt, + lhsCalls: lhs.snapshot?.usageEvents ?? 0, + lhsTokens: lhs.snapshot?.totalTokens ?? 0, + lhsLifecycle: lhs.package.lastLifecycleTimestamp, + lhsName: lhs.package.name, + rhsLastUsed: rhs.snapshot?.lastUsedAt, + rhsCalls: rhs.snapshot?.usageEvents ?? 0, + rhsTokens: rhs.snapshot?.totalTokens ?? 0, + rhsLifecycle: rhs.package.lastLifecycleTimestamp, + rhsName: rhs.package.name + ) + } + .map(\.package) + } + + static func recentSkills(_ skills: [Skill], summary: UsageSummary?) -> [Skill] { + skills + .map { skill in + ( + skill: skill, + snapshot: skill.usageSnapshot(using: summary) + ) + } + .sorted { lhs, rhs in + isRankedBefore( + lhsLastUsed: lhs.snapshot?.lastUsedAt, + lhsCalls: lhs.snapshot?.usageEvents ?? 0, + lhsTokens: lhs.snapshot?.totalTokens ?? 0, + lhsLifecycle: lhs.skill.lastLifecycleTimestamp, + lhsName: lhs.skill.name, + rhsLastUsed: rhs.snapshot?.lastUsedAt, + rhsCalls: rhs.snapshot?.usageEvents ?? 0, + rhsTokens: rhs.snapshot?.totalTokens ?? 0, + rhsLifecycle: rhs.skill.lastLifecycleTimestamp, + rhsName: rhs.skill.name + ) + } + .map(\.skill) + } + + private static func isRankedBefore( + lhsLastUsed: Date?, + lhsCalls: Int, + lhsTokens: Int64, + lhsLifecycle: Int?, + lhsName: String, + rhsLastUsed: Date?, + rhsCalls: Int, + rhsTokens: Int64, + rhsLifecycle: Int?, + rhsName: String + ) -> Bool { + switch (lhsLastUsed, rhsLastUsed) { + case let (lhs?, rhs?) where lhs != rhs: + return lhs > rhs + case (_?, nil): + return true + case (nil, _?): + return false + default: + break + } + + if lhsCalls != rhsCalls { + return lhsCalls > rhsCalls + } + if lhsTokens != rhsTokens { + return lhsTokens > rhsTokens + } + + let lhsLifecycle = lhsLifecycle ?? 0 + let rhsLifecycle = rhsLifecycle ?? 0 + if lhsLifecycle != rhsLifecycle { + return lhsLifecycle > rhsLifecycle + } + + return lhsName.localizedCaseInsensitiveCompare(rhsName) == .orderedAscending + } +} + // MARK: - Items private enum SpotlightItem { diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 482b89f..8a19704 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -77,5 +77,6 @@ struct LocalizationTests { #expect(localization.string("spotlight.action.showMCP.title") == "查看 MCP") #expect(localization.string("spotlight.action.showBrokenLinks.title") == "查看断链") #expect(localization.string("spotlight.action.showInactive.title") == "查看未启用") + #expect(localization.string("spotlight.section.recent") == "最近使用") } } diff --git a/swift-app/Tests/PopskillTests/SpotlightActionTests.swift b/swift-app/Tests/PopskillTests/SpotlightActionTests.swift index 0e5510f..098ef2a 100644 --- a/swift-app/Tests/PopskillTests/SpotlightActionTests.swift +++ b/swift-app/Tests/PopskillTests/SpotlightActionTests.swift @@ -1,4 +1,5 @@ @testable import Popskill +import Foundation import Testing struct SpotlightActionTests { @@ -17,6 +18,66 @@ struct SpotlightActionTests { #expect(usageScan?.subtitleKey == "spotlight.action.usageScan.subtitle") } + @Test + func spotlightRecentRankerPrefersUsageOverLifecycleForSkills() { + let lifecycleNewer = skillFixture(id: "newer-install", name: "Newer Install", installedAt: 1_900_000_000) + let recentlyUsed = skillFixture(id: "recently-used", name: "Recently Used", installedAt: 1_600_000_000) + let highCalls = skillFixture(id: "high-calls", name: "High Calls", installedAt: 1_500_000_000) + let summary = UsageSummary( + skillStats: [ + usageStat(skillID: "recently-used", calls: 1, tokens: 10, lastUsedAt: 1_800_000_000), + usageStat(skillID: "high-calls", calls: 20, tokens: 200, lastUsedAt: 1_700_000_000) + ] + ) + + let ranked = SpotlightRecentRanker.recentSkills( + [lifecycleNewer, highCalls, recentlyUsed], + summary: summary + ) + + #expect(ranked.map(\.id) == ["recently-used", "high-calls", "newer-install"]) + } + + @Test + func spotlightRecentRankerFallsBackToLifecycleWhenUsageIsMissing() { + let older = skillFixture(id: "older", name: "Older", installedAt: 100) + let newer = skillFixture(id: "newer", name: "Newer", updatedAt: 200) + + let ranked = SpotlightRecentRanker.recentSkills([older, newer], summary: nil) + + #expect(ranked.map(\.id) == ["newer", "older"]) + } + + @Test + func spotlightRecentRankerRanksPackagesByComponentUsage() { + let usedPackage = packageFixture( + id: "pkg-used", + name: "Used Package", + componentID: "used-skill", + lifecycleUpdatedAt: 100 + ) + let lifecycleNewer = packageFixture( + id: "pkg-newer", + name: "Lifecycle Newer", + componentID: "unused-skill", + lifecycleUpdatedAt: 1_900_000_000 + ) + let usedSkill = skillFixture(id: "used-skill", name: "Used Skill") + let summary = UsageSummary( + skillStats: [ + usageStat(skillID: "used-skill", calls: 2, tokens: 20, lastUsedAt: 1_800_000_000) + ] + ) + + let ranked = SpotlightRecentRanker.recentPackages( + [lifecycleNewer, usedPackage], + skills: [usedSkill], + summary: summary + ) + + #expect(ranked.map(\.id) == ["pkg-used", "pkg-newer"]) + } + @MainActor @Test func spotlightMatrixFilterActionsOpenExpectedMatrixViews() { @@ -54,4 +115,90 @@ struct SpotlightActionTests { #expect(store.matrixFilter == .inactive) #expect(store.matrixTypeFilter == .allTypes) } + + private func skillFixture( + id: String, + name: String, + installedAt: Int? = nil, + updatedAt: Int? = nil + ) -> Skill { + Skill( + id: id, + name: name, + description: "\(name) description", + directory: id, + repoOwner: nil, + repoName: nil, + readmeUrl: nil, + apps: SkillApps(claude: true, codex: true, gemini: false, opencode: false, hermes: false), + installedAt: installedAt, + updatedAt: updatedAt, + contentHash: nil + ) + } + + private func packageFixture( + id: String, + name: String, + componentID: String, + lifecycleUpdatedAt: Int + ) -> CapabilityPackage { + CapabilityPackage( + id: id, + type: .composite, + name: name, + vendor: nil, + summary: "\(name) summary", + source: PackageSource( + kind: "builtin", + location: id, + updateStrategy: "manual", + repoOwner: nil, + repoName: nil, + repoBranch: nil, + readmeUrl: nil + ), + components: PackageComponents( + cli: [], + skills: [ + PackageComponent( + id: componentID, + name: componentID, + kind: "skill", + required: true, + installed: true, + status: "installed", + location: componentID + ) + ], + mcp: [], + agents: [] + ), + configSchema: [], + installed: true, + lifecycle: PackageLifecycle( + installedAt: nil, + updatedAt: lifecycleUpdatedAt, + contentHash: nil + ) + ) + } + + private func usageStat( + skillID: String, + calls: Int, + tokens: Int64, + lastUsedAt: TimeInterval + ) -> SkillUsageStat { + SkillUsageStat( + skillID: skillID, + sourcePlugin: nil, + usageEvents: calls, + inputTokens: tokens, + outputTokens: 0, + cacheCreationTokens: 0, + cacheReadTokens: 0, + lastUsedAt: Date(timeIntervalSince1970: lastUsedAt) + ) + } } From b1c8ba46dadf96e11bf387829bc2524fac357421 Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 08:28:39 +0800 Subject: [PATCH 46/47] feat(ui): surface package actions in overview --- swift-app/Sources/Popskill/Views/InspectorPane.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 6ee7129..12b5523 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -170,9 +170,10 @@ struct InspectorPane: View { switch selectedTab { case .overview: packageSummarySection(package) + packageActivationSection(package) + packageActionsSection(package) packageCoverageSection packageMachineSection(package) - packageActivationSection(package) packageComponentsSection(package) case .readme: if let skill = readmeSkill(for: package) { From 9118bdf82fd310a24973fc3188a66990dd54afde Mon Sep 17 00:00:00 2001 From: maojiebc Date: Thu, 21 May 2026 08:40:01 +0800 Subject: [PATCH 47/47] feat(ui): explain package coverage gaps --- .../Sources/Popskill/Models/SkillModels.swift | 56 ++++++++++++++----- .../Resources/en.lproj/Localizable.strings | 4 ++ .../zh-Hans.lproj/Localizable.strings | 4 ++ .../Popskill/Views/InspectorPane.swift | 42 ++++++++++---- .../PopskillTests/LocalizationTests.swift | 2 + .../PopskillTests/SkillModelsTests.swift | 52 +++++++++++++++++ 6 files changed, 136 insertions(+), 24 deletions(-) diff --git a/swift-app/Sources/Popskill/Models/SkillModels.swift b/swift-app/Sources/Popskill/Models/SkillModels.swift index cd8ab2c..3d30e15 100644 --- a/swift-app/Sources/Popskill/Models/SkillModels.swift +++ b/swift-app/Sources/Popskill/Models/SkillModels.swift @@ -816,6 +816,34 @@ struct PackageComponentGroupSummary: Identifiable, Equatable { } } +struct PackageAppCoverageBreakdown: Equatable { + var total: Int + var enabled: Int = 0 + var stubbed: Int = 0 + var off: Int = 0 + var unsupported: Int = 0 + + var label: String { "\(enabled)/\(total)" } + + var percent: Int { + guard total > 0 else { return 0 } + return Int((Double(enabled) / Double(total) * 100).rounded()) + } + + mutating func add(_ state: PackageComponentAppState) { + switch state { + case .active: + enabled += 1 + case .stub: + stubbed += 1 + case .off: + off += 1 + case .unsupported: + unsupported += 1 + } + } +} + struct PackageComponentCompositionCount: Identifiable, Equatable { let kind: String let count: Int @@ -1685,22 +1713,22 @@ extension CapabilityPackage { } func appCoverage(using skills: [Skill]) -> [TargetApp: CapabilityAppCoverage] { - let components = components.all - let total = components.count - guard total > 0 else { - return Dictionary( - uniqueKeysWithValues: TargetApp.supported.map { app in - (app, CapabilityAppCoverage(enabled: 0, total: 0)) - } + Dictionary(uniqueKeysWithValues: TargetApp.supported.map { app in + let breakdown = appCoverageBreakdown(for: app, skills: skills) + return (app, CapabilityAppCoverage(enabled: breakdown.enabled, total: breakdown.total)) + }) + } + + func appCoverageBreakdown(for app: TargetApp, skills: [Skill]) -> PackageAppCoverageBreakdown { + var breakdown = PackageAppCoverageBreakdown(total: components.all.count) + for component in components.all { + let state = component.appState( + for: app, + matching: matchingInstalledSkill(for: component, in: skills) ) + breakdown.add(state) } - - return Dictionary(uniqueKeysWithValues: TargetApp.supported.map { app in - let enabled = components.filter { component in - component.isEnabled(for: app, matching: matchingInstalledSkill(for: component, in: skills)) - }.count - return (app, CapabilityAppCoverage(enabled: enabled, total: total)) - }) + return breakdown } func matchingSkillComponent(for update: SkillUpdateInfo) -> PackageComponent? { diff --git a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings index 7b58855..e5fa881 100644 --- a/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/en.lproj/Localizable.strings @@ -374,6 +374,10 @@ "matrix.row.menu.help" = "More actions"; "matrix.group.coverageHelp" = "%@: %d of %d enabled"; "matrix.package.coverageHelp" = "%@ coverage: %d of %d components"; +"matrix.package.coverage.percent" = "%d%%"; +"matrix.package.coverage.stubbed" = "%d stubbed"; +"matrix.package.coverage.off" = "%d off"; +"matrix.package.coverage.complete" = "Complete"; "matrix.package.expand" = "Expand components"; "matrix.package.collapse" = "Collapse components"; "matrix.package.health.active" = "Complete"; diff --git a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings index 525ecf8..778c885 100644 --- a/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings +++ b/swift-app/Sources/Popskill/Resources/zh-Hans.lproj/Localizable.strings @@ -374,6 +374,10 @@ "matrix.row.menu.help" = "更多操作"; "matrix.group.coverageHelp" = "%@:已启用 %d / %d"; "matrix.package.coverageHelp" = "%@ 覆盖:%d / %d 个组件"; +"matrix.package.coverage.percent" = "%d%%"; +"matrix.package.coverage.stubbed" = "%d 占位"; +"matrix.package.coverage.off" = "%d 未启用"; +"matrix.package.coverage.complete" = "已覆盖"; "matrix.package.expand" = "展开组件"; "matrix.package.collapse" = "收起组件"; "matrix.package.health.active" = "完整"; diff --git a/swift-app/Sources/Popskill/Views/InspectorPane.swift b/swift-app/Sources/Popskill/Views/InspectorPane.swift index 12b5523..de2d246 100644 --- a/swift-app/Sources/Popskill/Views/InspectorPane.swift +++ b/swift-app/Sources/Popskill/Views/InspectorPane.swift @@ -172,7 +172,7 @@ struct InspectorPane: View { packageSummarySection(package) packageActivationSection(package) packageActionsSection(package) - packageCoverageSection + packageCoverageSection(package) packageMachineSection(package) packageComponentsSection(package) case .readme: @@ -813,18 +813,18 @@ struct InspectorPane: View { } } - private var packageCoverageSection: some View { + private func packageCoverageSection(_ package: CapabilityPackage) -> some View { VStack(alignment: .leading, spacing: 8) { SectionHeading(title: "matrix.inspector.section.coverage", accent: .popSectionPurple) HStack(spacing: 10) { - packageCoverageCard(for: .claude) - packageCoverageCard(for: .codex) + packageCoverageCard(for: .claude, package: package) + packageCoverageCard(for: .codex, package: package) } } } - private func packageCoverageCard(for app: TargetApp) -> some View { - let coverage = capability.appCoverage[app] ?? CapabilityAppCoverage(enabled: 0, total: 0) + private func packageCoverageCard(for app: TargetApp, package: CapabilityPackage) -> some View { + let coverage = package.appCoverageBreakdown(for: app, skills: store.skills) return VStack(alignment: .leading, spacing: 5) { HStack(spacing: 5) { Image(systemName: app.symbolName) @@ -833,18 +833,40 @@ struct InspectorPane: View { .font(.caption.weight(.medium)) } .foregroundStyle(Color.popSecondaryLabel) - Text(coverage.label) - .font(.system(size: 24, weight: .bold, design: .rounded)) - .monospacedDigit() - .foregroundStyle(coverage.enabled > 0 ? app.inspectorAccentColor : Color.popTertiaryLabel) + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(coverage.label) + .font(.system(size: 24, weight: .bold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(coverage.enabled > 0 ? app.inspectorAccentColor : Color.popTertiaryLabel) + Text(localization.string("matrix.package.coverage.percent", coverage.percent)) + .font(.caption.weight(.semibold)) + .foregroundStyle(Color.popSecondaryLabel) + } ProgressView(value: Double(coverage.enabled), total: Double(max(coverage.total, 1))) .tint(app.inspectorAccentColor) + Text(packageCoverageStatusLabel(coverage)) + .font(.caption2.weight(.semibold)) + .foregroundStyle(coverage.stubbed > 0 ? Color.popStatusWarning : Color.popTertiaryLabel) } .padding(10) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.popSubtleFill, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) } + private func packageCoverageStatusLabel(_ coverage: PackageAppCoverageBreakdown) -> String { + var parts: [String] = [] + if coverage.stubbed > 0 { + parts.append(localization.string("matrix.package.coverage.stubbed", coverage.stubbed)) + } + if coverage.off > 0 { + parts.append(localization.string("matrix.package.coverage.off", coverage.off)) + } + if !parts.isEmpty { + return parts.joined(separator: " · ") + } + return localization.string("matrix.package.coverage.complete") + } + @ViewBuilder private func packageMachineSection(_ package: CapabilityPackage) -> some View { let metrics = packageMachineMetrics(package) diff --git a/swift-app/Tests/PopskillTests/LocalizationTests.swift b/swift-app/Tests/PopskillTests/LocalizationTests.swift index 8a19704..0e61e82 100644 --- a/swift-app/Tests/PopskillTests/LocalizationTests.swift +++ b/swift-app/Tests/PopskillTests/LocalizationTests.swift @@ -63,6 +63,8 @@ struct LocalizationTests { #expect(localization.string("matrix.skill.manifest.version") == "版本") #expect(localization.string("matrix.skill.manifest.requires.available") == "可用") #expect(localization.string("matrix.skill.manifest.requires.missing") == "缺失") + #expect(localization.string("matrix.package.coverage.stubbed", 1) == "1 占位") + #expect(localization.string("matrix.package.coverage.off", 3) == "3 未启用") } @Test diff --git a/swift-app/Tests/PopskillTests/SkillModelsTests.swift b/swift-app/Tests/PopskillTests/SkillModelsTests.swift index db8d38a..04230b2 100644 --- a/swift-app/Tests/PopskillTests/SkillModelsTests.swift +++ b/swift-app/Tests/PopskillTests/SkillModelsTests.swift @@ -1198,6 +1198,58 @@ struct SkillModelsTests { #expect(installedCLI.appState(for: .gemini, matching: nil) == .unsupported) } + @Test + func capabilityPackageCoverageBreakdownCountsStubbedAndOffComponents() { + let skill = installedSkill( + directory: "baoyu-comic", + apps: SkillApps(claude: true, codex: false, gemini: false, opencode: false, hermes: false) + ) + let package = self.package(components: [ + PackageComponent( + id: "baoyu-comic", + name: "baoyu-comic", + kind: "skill", + required: true, + installed: true, + status: "installed", + location: "skills/baoyu-comic" + ), + PackageComponent( + id: "base-analyst", + name: "base-analyst", + kind: "agent", + required: false, + installed: false, + status: "stub", + location: "~/.claude/agents/base-analyst.md" + ), + PackageComponent( + id: "lark-cli", + name: "lark-cli", + kind: "cli", + required: true, + installed: true, + status: "detected", + location: nil + ) + ]) + + let claude = package.appCoverageBreakdown(for: .claude, skills: [skill]) + let codex = package.appCoverageBreakdown(for: .codex, skills: [skill]) + + #expect(claude.label == "2/3") + #expect(claude.percent == 67) + #expect(claude.enabled == 2) + #expect(claude.stubbed == 1) + #expect(claude.off == 0) + #expect(codex.label == "1/3") + #expect(codex.percent == 33) + #expect(codex.enabled == 1) + #expect(codex.stubbed == 0) + #expect(codex.off == 2) + #expect(package.appCoverage(using: [skill])[.claude]?.label == claude.label) + } + @Test func capabilityPackageUsageSnapshotAggregatesMatchedSkillStats() { let package = CapabilityPackage(