Files
scarf/scarf/Scarf iOS/Skills/Installed/InstalledSkillsListView.swift
T
Alan Wizemann ee3791a1b2 feat(hermes-v12): Skills v0.12 surface — URL install + reload + pin/disable badges (Phase E)
Hermes v0.12 added three skills surfaces Scarf can now reach:

- Direct-URL install: `hermes skills install <https://...>` lets users
  pull a one-off skill without going through a registry. Mac SkillsView
  grew an "Install from URL…" toolbar button (capability-gated on
  HermesCapabilities.hasSkillURLInstall) opening a sheet with the URL
  field plus optional --category / --name overrides.
- Reload: `hermes skills audit` rescans `~/.hermes/skills/` and refreshes
  the agent's view of available skills without restarting. Wired to a
  "Reload" toolbar button next to the install button on Mac.
- Enabled state: skills.disabled in config.yaml is now read at scan time
  (SkillsViewModel.readDisabledSkillNames). Disabled skills render
  strikethrough + an "OFF" pill on Mac and iOS rows so users see what
  Hermes won't load. iOS detail view explains the state in plain text.
- Curator pin badge: pinned-skill names from
  `~/.hermes/skills/.curator_state` (SkillsViewModel.readPinnedSkillNames)
  surface as a pin glyph on each row. Mac sidebar + iOS list both show
  it; iOS detail view explains "pinned by curator — won't auto-archive."

Model + scanner:

- HermesSkill gains `enabled: Bool` (default true) and `pinned: Bool`
  (default false). Both default to backwards-compatible values so
  unmodified call sites keep compiling.
- SkillsScanner.scan now takes optional `disabledNames` and
  `pinnedNames` sets and applies them per skill at scan time.
- SkillsViewModel.load auto-fetches both sets internally so Mac/iOS
  callers don't have to plumb curator state manually; an opt-in
  `pinnedNames` override is available for the Curator screen which
  has a fresher snapshot in hand.

Tests: 215 ScarfCore tests pass; both Mac and iOS schemes build clean.

Note: the disable-toggle path (writing the array back into
config.yaml) is deferred to v2.7 — Hermes ships
`hermes skills config` as an interactive verb only, and we'd rather
read accurately than risk clobbering the user's list with a
half-tested write path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:44:15 +02:00

80 lines
3.6 KiB
Swift

import SwiftUI
import ScarfCore
import ScarfDesign
/// Installed skills sub-tab. Category-grouped list; tapping a row
/// pushes `SkillDetailView` for that skill. Filtering uses the VM's
/// `filteredCategories` derivation so the search field works against
/// the same model the Mac uses.
struct InstalledSkillsListView: View {
@Bindable var vm: SkillsViewModel
var body: some View {
Group {
if vm.isLoading && vm.categories.isEmpty {
ProgressView("Scanning skills…")
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if vm.categories.isEmpty {
ContentUnavailableView {
Label("No skills installed", systemImage: "lightbulb")
} description: {
Text("Browse the Hub tab to install one, or run `hermes skills install <name>` on the remote.")
.font(.caption)
}
} else {
listContent
}
}
}
@ViewBuilder
private var listContent: some View {
List {
ForEach(vm.filteredCategories) { category in
Section(category.name) {
ForEach(category.skills) { skill in
NavigationLink {
SkillDetailView(skill: skill, vm: vm)
} label: {
HStack(spacing: 8) {
VStack(alignment: .leading, spacing: 2) {
Text(skill.name)
.font(.body)
.foregroundStyle(skill.enabled ? .primary : .secondary)
.strikethrough(!skill.enabled, color: .secondary)
Text("\(skill.files.count) file\(skill.files.count == 1 ? "" : "s")")
.font(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
}
Spacer(minLength: 0)
if skill.pinned {
Image(systemName: "pin.fill")
.font(.caption2)
.foregroundStyle(ScarfColor.accent)
.accessibilityLabel("Pinned by curator")
}
if !skill.enabled {
Text("OFF")
.font(.caption2.weight(.semibold))
.padding(.horizontal, 4)
.padding(.vertical, 1)
.background(ScarfColor.backgroundTertiary)
.clipShape(RoundedRectangle(cornerRadius: 3))
.foregroundStyle(ScarfColor.foregroundMuted)
.accessibilityLabel("Disabled — Hermes won't load this skill")
}
}
}
.scarfGoCompactListRow()
.listRowBackground(ScarfColor.backgroundSecondary)
}
}
}
}
.scarfGoListDensity()
.scrollContentBackground(.hidden)
.background(ScarfColor.backgroundPrimary)
.searchable(text: $vm.searchText, placement: .navigationBarDrawer(displayMode: .always))
}
}