feat(profiles): add --no-skills toggle to create-profile sheet

Adds an "Empty profile (no skills)" toggle to the Mac create-profile
sheet, gated on `hasProfileNoSkills` (v0.13+). When ON, the create
flow appends `--no-skills` to `hermes profile create`. The toggle is
disabled (greyed out) when "Full copy of active profile" is on, per
WS-7 plan Decision H — a full clone copies skills wholesale, so
`--no-skills` would be a contradiction at the UX layer. The wire
itself stays permissive: a user can stack `--clone --no-skills` to
clone config but skip skills, which is a plausible workflow.

Defensive write-strip: even though the toggle is hidden on pre-v0.13
hosts, the call site reads `createNoSkills` through the capability
gate so a stale state value can't sneak `--no-skills` past argparse
on a CLI that doesn't know it.

iOS Profiles is read-only (per CLAUDE.md "v0.12 iOS catch-up
Phase H") so no toggle there.

TODO marker (WS-7-Q8) flags the assumed `--clone-all` interaction —
verify Hermes's behaviour with both flags during integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-05-09 19:03:06 +02:00
parent 6c96fcfa43
commit 0070441243
2 changed files with 35 additions and 3 deletions
@@ -112,10 +112,17 @@ final class ProfilesViewModel {
}
}
func create(name: String, cloneConfig: Bool, cloneAll: Bool) {
func create(name: String, cloneConfig: Bool, cloneAll: Bool, noSkills: Bool = false) {
var args = ["profile", "create", name]
if cloneAll { args.append("--clone-all") }
else if cloneConfig { args.append("--clone") }
// v0.13+: Empty-profile creation. The wire is independent of
// --clone / --clone-all per the v0.13 release notes the user
// can stack `--clone --no-skills` to clone config but skip
// skills, which is a plausible workflow. The UI still disables
// the toggle under --clone-all (Decision H, see ProfilesView)
// but the wire is permissive.
if noSkills { args.append("--no-skills") }
runAndReload(args, success: "Profile '\(name)' created")
}
@@ -11,7 +11,12 @@ struct ProfilesView: View {
@State private var createName = ""
@State private var createCloneConfig = true
@State private var createCloneAll = false
/// v0.13+ `--no-skills` toggle. Mutually exclusive with `--clone-all`
/// at the UX layer (Decision H from the WS-7 plan): a full clone
/// copies skills wholesale `--no-skills` would be a contradiction.
@State private var createNoSkills = false
@State private var showRename = false
@Environment(\.hermesCapabilities) private var capabilitiesStore
init(context: ServerContext) {
_viewModel = State(initialValue: ProfilesViewModel(context: context))
@@ -123,7 +128,7 @@ struct ProfilesView: View {
}
Spacer()
Button {
createName = ""; createCloneConfig = true; createCloneAll = false
createName = ""; createCloneConfig = true; createCloneAll = false; createNoSkills = false
showCreate = true
} label: {
Label("Create", systemImage: "plus")
@@ -300,11 +305,31 @@ struct ProfilesView: View {
Toggle("Clone config, .env, SOUL.md from active profile", isOn: $createCloneConfig)
.disabled(createCloneAll)
Toggle("Full copy of active profile (all state)", isOn: $createCloneAll)
// TODO(WS-7-Q8): Decision H disable --no-skills when --clone-all
// is on. A full clone copies skills wholesale; --no-skills would
// be a contradiction. Verify Hermes's behaviour with both flags
// (argparse mutual exclusion vs. last-flag-wins vs. clone-but-
// skip-skills) and relax the disabled state if Hermes does
// something useful with the combination.
if capabilitiesStore?.capabilities.hasProfileNoSkills ?? false {
Toggle("Empty profile (no skills)", isOn: $createNoSkills)
.disabled(createCloneAll)
}
HStack {
Spacer()
Button("Cancel") { showCreate = false }
Button("Create") {
viewModel.create(name: createName, cloneConfig: createCloneConfig, cloneAll: createCloneAll)
viewModel.create(
name: createName,
cloneConfig: createCloneConfig,
cloneAll: createCloneAll,
// Defensive: if the toggle isn't visible (pre-v0.13)
// the state is always `false`, but read it through
// the capability gate anyway so a stale state value
// can't sneak `--no-skills` to a CLI that doesn't
// know it.
noSkills: (capabilitiesStore?.capabilities.hasProfileNoSkills ?? false) ? createNoSkills : false
)
showCreate = false
}
.buttonStyle(.borderedProminent)