From dfa5da95b5b28064816a36ceae1e13f1f747e891 Mon Sep 17 00:00:00 2001 From: Roland Kaibull <6277393+bubleg@users.noreply.github.com> Date: Wed, 7 Oct 2026 05:33:51 +0200 Subject: [PATCH 1/2] fix: leave partly applied tweaks alone unless asked to undo them --- Sources/removemacai/App/AppModel.swift | 18 ++++++++++++++---- Sources/removemacai/App/Pages.swift | 12 +++++++----- Sources/removemacai/Engine.swift | 18 ++++++++++++++---- Sources/removemacai/SelfTest.swift | 16 ++++++++++++++++ Sources/removemacai/TweakCommands.swift | 2 +- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/Sources/removemacai/App/AppModel.swift b/Sources/removemacai/App/AppModel.swift index 7265409..58f45c7 100644 --- a/Sources/removemacai/App/AppModel.swift +++ b/Sources/removemacai/App/AppModel.swift @@ -49,6 +49,8 @@ final class AppModel { // What the person wants. var wanted: Set = [] + /// Tweaks the person switched, so a partly applied one is only undone on request. + var touched: Set = [] var aiOff = false var aiKept: Set = [] @@ -88,6 +90,7 @@ final class AppModel { journal = Engine.loadJournal() if resetChoices { wanted = Set(states.filter { $0.value == .applied }.map(\.key)) + touched = [] aiOff = currentAI != nil aiKept = snapshot.profile.ai ? snapshot.profile.kept : [] } @@ -127,9 +130,17 @@ final class AppModel { func isOn(_ tweak: Tweak) -> Bool { wanted.contains(tweak.id) } func toggle(_ tweak: Tweak, _ on: Bool) { + touched.insert(tweak.id) if on { wanted.insert(tweak.id) } else { wanted.remove(tweak.id) } } + /// Whether leaving the tweak unwanted undoes it. + func undoes(_ tweak: Tweak) -> Bool { + let s = state(tweak) + guard !wanted.contains(tweak.id), s != .notApplied, s != .managed, s != .unsupported else { return false } + return s != .partial || touched.contains(tweak.id) + } + func choose(_ preset: Preset) { wanted.formUnion(Tweaks.preset(preset).map(\.id)) aiOff = true @@ -143,9 +154,8 @@ final class AppModel { Tweaks.all.compactMap { t in let s = state(t) guard s != .managed && s != .unsupported else { return nil } - let want = wanted.contains(t.id) - if want && s != .applied { return (t, true) } - if !want && s != .notApplied { return (t, false) } + if wanted.contains(t.id) && s != .applied { return (t, true) } + if undoes(t) { return (t, false) } return nil } } @@ -157,7 +167,7 @@ final class AppModel { func discard() { reload(resetChoices: true) } - func plan() -> Plan { Plan.make(wanted: wanted, ai: targetAI, snapshot: snapshot) } + func plan() -> Plan { Plan.make(wanted: wanted, ai: targetAI, undo: touched, snapshot: snapshot) } // MARK: applying diff --git a/Sources/removemacai/App/Pages.swift b/Sources/removemacai/App/Pages.swift index aadf4d3..ae1c04d 100644 --- a/Sources/removemacai/App/Pages.swift +++ b/Sources/removemacai/App/Pages.swift @@ -218,7 +218,9 @@ struct TweakGroupView: View { Menu { Button("Select All") { for t in tweaks where t.supported { model.toggle(t, true) } } Button("Select Recommended") { - for t in tweaks { model.toggle(t, t.presets.contains(.recommended) || model.state(t) == .applied) } + for t in tweaks where t.presets.contains(.recommended) || model.state(t) != .partial { + model.toggle(t, t.presets.contains(.recommended) || model.state(t) == .applied) + } } Button("Deselect All") { for t in tweaks { model.toggle(t, false) } } } label: { @@ -244,7 +246,7 @@ struct TweakRow: View { Image(systemName: "lock.fill").font(.caption2).foregroundStyle(.tertiary) .help("Locked by the RemoveMacAI profile") } - if let note = pendingNote(state: state, wanted: wanted) { + if let note = pendingNote(state: state, wanted: wanted, undoes: model.undoes(tweak)) { Text(note).font(.caption.weight(.medium)).foregroundStyle(.tint) } } @@ -257,7 +259,7 @@ struct TweakRow: View { } else if state == .unsupported { Text("Needs macOS \(tweak.since.0).\(tweak.since.1) or newer.").font(.caption).foregroundStyle(.secondary) } else if state == .partial { - Text("Partly applied.").font(.caption).foregroundStyle(.secondary) + Text("Partly applied. It stays as it is unless you switch it.").font(.caption).foregroundStyle(.secondary) } } .padding(.vertical, 2) @@ -266,9 +268,9 @@ struct TweakRow: View { .disabled(state == .managed || state == .unsupported) } - func pendingNote(state: TweakState, wanted: Bool) -> String? { + func pendingNote(state: TweakState, wanted: Bool, undoes: Bool) -> String? { if wanted && state != .applied { return "Applies on review" } - if !wanted && state != .notApplied && state != .managed && state != .unsupported { return "Undoes on review" } + if undoes { return "Undoes on review" } return nil } } diff --git a/Sources/removemacai/Engine.swift b/Sources/removemacai/Engine.swift index 68ffc81..ece6012 100644 --- a/Sources/removemacai/Engine.swift +++ b/Sources/removemacai/Engine.swift @@ -66,18 +66,28 @@ struct Plan { /// From the tweaks that should end up applied and what Apple Intelligence /// should look like (nil leaves it alone, a set turns it off except those). - static func make(wanted: Set, ai: Set?, snapshot s: Snapshot = Snapshot()) -> Plan { + /// A partly applied tweak is often the person's own setting, so it is only + /// undone when named in `undo`. + static func make( + wanted: Set, ai: Set?, undo: Set = [], snapshot s: Snapshot = Snapshot(), + state read: ((Tweak) -> TweakState)? = nil + ) -> Plan { + let stateOf = read ?? s.state var plan = Plan() + func keepsPartial(_ tweak: Tweak) -> Bool { stateOf(tweak) == .partial && !undo.contains(tweak.id) } for tweak in Tweaks.all { - let state = s.state(tweak) + let state = stateOf(tweak) guard state != .managed && state != .unsupported else { continue } if wanted.contains(tweak.id) && state != .applied { plan.apply.append(tweak) - } else if !wanted.contains(tweak.id) && state != .notApplied { + } else if !wanted.contains(tweak.id) && state != .notApplied && !keepsPartial(tweak) { plan.revert.append(tweak) } } - let profileTweaks = Set(Tweaks.all.filter { $0.inProfile && wanted.contains($0.id) && s.state($0) != .managed && $0.supported }.map(\.id)) + let profileTweaks = Set(Tweaks.all.filter { + $0.inProfile && stateOf($0) != .managed && $0.supported + && (wanted.contains($0.id) || (keepsPartial($0) && s.profile.tweaks.contains($0.id))) + }.map(\.id)) let target = Profile.Contents(ai: ai, tweaks: profileTweaks) let current = s.profile.contents ?? Profile.Contents(ai: nil, tweaks: []) if target != current { plan.profile = target } diff --git a/Sources/removemacai/SelfTest.swift b/Sources/removemacai/SelfTest.swift index e260482..018e208 100644 --- a/Sources/removemacai/SelfTest.swift +++ b/Sources/removemacai/SelfTest.swift @@ -217,6 +217,22 @@ func selfTest() -> Bool { program: "/Library/PrivilegedHelperTools/dev.orbstack.OrbStack.privhelper", system: true) check(script.owner == "sync.sh" && helper.owner == "OrbStack", "background items are named after what they run") + + // Partly applied tweaks are the person's own settings until they ask. + var s = Snapshot() + s.profile = Profile.Installed(on: true, ai: false, kept: [], tweaks: ["analytics"]) + let states: [String: TweakState] = ["smart-punctuation": .partial, "autocorrect": .applied, "analytics": .partial] + let fake: (Tweak) -> TweakState = { states[$0.id] ?? .notApplied } + let applyOne = Plan.make(wanted: ["autocorrect", "file-extensions"], ai: nil, snapshot: s, state: fake) + check(applyOne.apply.map(\.id) == ["file-extensions"] && applyOne.revert.isEmpty && applyOne.profile == nil, + "applying a tweak leaves partly applied ones and the profile alone") + let undoOne = Plan.make(wanted: ["autocorrect"], ai: nil, undo: ["smart-punctuation"], snapshot: s, state: fake) + check(undoOne.revert.map(\.id) == ["smart-punctuation"] && undoOne.profile == nil, + "a partly applied tweak is undone when named") + let undoLocked = Plan.make(wanted: ["autocorrect"], ai: nil, undo: ["analytics"], snapshot: s, state: fake) + check(undoLocked.profile?.tweaks == [] && undoLocked.revert.map(\.id) == ["analytics"], + "a partly applied profile tweak leaves the profile when named") + print(failed == 0 ? Term.green("all checks passed") : Term.red("\(failed) failed")) return failed == 0 } diff --git a/Sources/removemacai/TweakCommands.swift b/Sources/removemacai/TweakCommands.swift index 0c11e7d..99d16b7 100644 --- a/Sources/removemacai/TweakCommands.swift +++ b/Sources/removemacai/TweakCommands.swift @@ -65,7 +65,7 @@ enum TweakCommands { wanted.remove(id) } let ai: Set? = s.profile.on && s.profile.ai ? s.profile.kept : nil - let plan = Plan.make(wanted: wanted, ai: ai, snapshot: s) + let plan = Plan.make(wanted: wanted, ai: ai, undo: Set(ids), snapshot: s) return run(plan, dryRun: dryRun, yes: yes, verb: "Undo") } From 51cc4f30c697e3271b6092d635300d78ec6866f4 Mon Sep 17 00:00:00 2001 From: Roland Kaibull <6277393+bubleg@users.noreply.github.com> Date: Wed, 7 Oct 2026 05:47:22 +0200 Subject: [PATCH 2/2] fix: keep partly applied tweaks unchanged through bulk selections --- Sources/removemacai/App/AppModel.swift | 7 +++++++ Sources/removemacai/App/Pages.swift | 9 +++++++-- Sources/removemacai/SelfTest.swift | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Sources/removemacai/App/AppModel.swift b/Sources/removemacai/App/AppModel.swift index 58f45c7..82acb3e 100644 --- a/Sources/removemacai/App/AppModel.swift +++ b/Sources/removemacai/App/AppModel.swift @@ -130,10 +130,17 @@ final class AppModel { func isOn(_ tweak: Tweak) -> Bool { wanted.contains(tweak.id) } func toggle(_ tweak: Tweak, _ on: Bool) { + guard wanted.contains(tweak.id) != on else { return } touched.insert(tweak.id) if on { wanted.insert(tweak.id) } else { wanted.remove(tweak.id) } } + /// Drops any choice made for the tweak, so it stays as it is on the Mac. + func leave(_ tweak: Tweak) { + wanted.remove(tweak.id) + touched.remove(tweak.id) + } + /// Whether leaving the tweak unwanted undoes it. func undoes(_ tweak: Tweak) -> Bool { let s = state(tweak) diff --git a/Sources/removemacai/App/Pages.swift b/Sources/removemacai/App/Pages.swift index ae1c04d..eff3c32 100644 --- a/Sources/removemacai/App/Pages.swift +++ b/Sources/removemacai/App/Pages.swift @@ -218,8 +218,13 @@ struct TweakGroupView: View { Menu { Button("Select All") { for t in tweaks where t.supported { model.toggle(t, true) } } Button("Select Recommended") { - for t in tweaks where t.presets.contains(.recommended) || model.state(t) != .partial { - model.toggle(t, t.presets.contains(.recommended) || model.state(t) == .applied) + for t in tweaks { + let recommended = t.presets.contains(.recommended) + if model.state(t) == .partial && !recommended { + model.leave(t) + } else { + model.toggle(t, recommended || model.state(t) == .applied) + } } } Button("Deselect All") { for t in tweaks { model.toggle(t, false) } } diff --git a/Sources/removemacai/SelfTest.swift b/Sources/removemacai/SelfTest.swift index 018e208..ad576c4 100644 --- a/Sources/removemacai/SelfTest.swift +++ b/Sources/removemacai/SelfTest.swift @@ -233,6 +233,24 @@ func selfTest() -> Bool { check(undoLocked.profile?.tweaks == [] && undoLocked.revert.map(\.id) == ["analytics"], "a partly applied profile tweak leaves the profile when named") + MainActor.assumeIsolated { + let model = AppModel() + let partialTweak = Tweaks.tweak("smart-punctuation")! + model.states = Dictionary(uniqueKeysWithValues: Tweaks.all.map { ($0.id, TweakState.notApplied) }) + model.states[partialTweak.id] = .partial + model.wanted = [] + model.touched = [] + model.toggle(partialTweak, false) + check(model.pendingTweaks.isEmpty, "switching off a partly applied tweak that is already off changes nothing") + model.toggle(partialTweak, true) + model.leave(partialTweak) + check(model.pendingTweaks.isEmpty, "leaving a partly applied tweak drops an earlier selection") + model.toggle(partialTweak, true) + model.toggle(partialTweak, false) + check(model.pendingTweaks.map(\.tweak.id) == [partialTweak.id] && model.pendingTweaks.first?.apply == false, + "switching a partly applied tweak on and off undoes it") + } + print(failed == 0 ? Term.green("all checks passed") : Term.red("\(failed) failed")) return failed == 0 }