mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
feat(skills): SkillSnapshotService + 'What's New' pill (Phase 3.4)
Per-server snapshot of skill signatures so the Skills tab can show "2 new, 4 updated since you last looked" — same pattern Hermes's `hermes skills update` CLI shows on the host. ScarfCore SkillSnapshotService: - [skillId: signature] map, signature is `<fileCount>:<sorted-files>`. New / removed / files-changed all show up as a delta. - diff(against:) returns SkillSnapshotDiff with counts + a label string for the pill. - markSeen(_:) persists the current set. - Backend abstraction: file-based on Mac, UserDefaults on iOS, in-memory for tests. - previousSnapshotEmpty silently primes first-load so users don't see "everything is new!" noise. Mac SkillsView: - whatsNewPill(diff:) tinted pill at the top with "Mark as seen". - recomputeSnapshotDiff() on .task and on totalSkillCount change. iOS SkillsListView: - Same pill rendered as a Section row with "Seen" button. - Recompute on .task + .refreshable. Verified: Mac + iOS builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ struct SkillsListView: View {
|
||||
let config: IOSServerConfig
|
||||
|
||||
@State private var vm: IOSSkillsViewModel
|
||||
@State private var snapshotDiff: SkillSnapshotDiff?
|
||||
|
||||
private static let sharedContextID: ServerID = ServerID(
|
||||
uuidString: "00000000-0000-0000-0000-0000000000A1"
|
||||
@@ -21,6 +22,28 @@ struct SkillsListView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if let diff = snapshotDiff,
|
||||
diff.hasChanges,
|
||||
!diff.previousSnapshotEmpty {
|
||||
Section {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "sparkles")
|
||||
.foregroundStyle(.tint)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(diff.label)
|
||||
.font(.callout)
|
||||
}
|
||||
Spacer()
|
||||
Button("Seen") {
|
||||
SkillSnapshotService(serverID: Self.sharedContextID)
|
||||
.markSeen(vm.categories.flatMap(\.skills))
|
||||
snapshotDiff = nil
|
||||
}
|
||||
.controlSize(.small)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let err = vm.lastError {
|
||||
Section {
|
||||
Label(err, systemImage: "exclamationmark.triangle.fill")
|
||||
@@ -71,8 +94,29 @@ struct SkillsListView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
.refreshable { await vm.load() }
|
||||
.task { await vm.load() }
|
||||
.refreshable {
|
||||
await vm.load()
|
||||
recomputeSnapshotDiff()
|
||||
}
|
||||
.task {
|
||||
await vm.load()
|
||||
recomputeSnapshotDiff()
|
||||
}
|
||||
}
|
||||
|
||||
/// v2.5 "What's New" diff against the last-seen snapshot for this
|
||||
/// server. First-time users get a silent prime — the pill only
|
||||
/// renders on subsequent loads when something actually changed.
|
||||
private func recomputeSnapshotDiff() {
|
||||
let allSkills = vm.categories.flatMap(\.skills)
|
||||
let svc = SkillSnapshotService(serverID: Self.sharedContextID)
|
||||
let diff = svc.diff(against: allSkills)
|
||||
if diff.previousSnapshotEmpty {
|
||||
svc.markSeen(allSkills)
|
||||
snapshotDiff = nil
|
||||
} else {
|
||||
snapshotDiff = diff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user