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>
This commit is contained in:
Alan Wizemann
2026-05-01 12:44:15 +02:00
parent 686fb37630
commit ee3791a1b2
7 changed files with 375 additions and 13 deletions
@@ -36,12 +36,33 @@ struct InstalledSkillsListView: View {
NavigationLink {
SkillDetailView(skill: skill, vm: vm)
} label: {
VStack(alignment: .leading, spacing: 2) {
Text(skill.name)
.font(.body)
Text("\(skill.files.count) file\(skill.files.count == 1 ? "" : "s")")
.font(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
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()
@@ -31,6 +31,34 @@ struct SkillDetailView: View {
.font(.caption.monospaced())
.foregroundStyle(ScarfColor.foregroundMuted)
.textSelection(.enabled)
if !skill.enabled {
Label {
VStack(alignment: .leading, spacing: 2) {
Text("Disabled").font(.callout.weight(.medium))
Text("This skill is in `skills.disabled` in `~/.hermes/config.yaml`. Hermes won't load it. Re-enable from the Mac app's Skills config UI or with `hermes skills config`.")
.font(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
.fixedSize(horizontal: false, vertical: true)
}
} icon: {
Image(systemName: "circle.slash")
.foregroundStyle(.secondary)
}
}
if skill.pinned {
Label {
VStack(alignment: .leading, spacing: 2) {
Text("Pinned by curator").font(.callout.weight(.medium))
Text("The autonomous curator won't auto-archive or rewrite this skill. Unpin from the Curator screen.")
.font(.caption)
.foregroundStyle(ScarfColor.foregroundMuted)
.fixedSize(horizontal: false, vertical: true)
}
} icon: {
Image(systemName: "pin.fill")
.foregroundStyle(ScarfColor.accent)
}
}
}
.listRowBackground(ScarfColor.backgroundSecondary)