Skip to content

Land the five fixes that lived only on the integration branch - #22

Merged
havokentity merged 5 commits into
mainfrom
fix/integration-orphans
Jul 21, 2026
Merged

Land the five fixes that lived only on the integration branch#22
havokentity merged 5 commits into
mainfrom
fix/integration-orphans

Conversation

@havokentity

Copy link
Copy Markdown
Owner

These five commits were made directly on integration/wave1-test while testing the
Wave 1 build, so they belonged to no feature PR. Merging the 14 feature PRs would
have dropped them silently — including two fixes that were verified by hand this
morning and two TCC/entitlement fixes for bugs reported during testing.

  • Edit menu wired into every text-entry tool, so ⌘C/⌘V/⌘A/⌘Z work in the
    helpers (LSUIElement apps get no main menu by default).
  • Audio Router: audio-input entitlement on the outer app. TCC attributes a
    nested helper's request to the container, so without it the mic prompt never
    resolves.
  • Calendar: calendars entitlement + usage string on the container, same
    reasoning — this was the reported "opens Settings but I can't add an app" bug.
  • Volume Mixer: quit no longer persists a stop for every app that merely
    happened to be playing. stopProcessing() is the footer gesture and marks the
    overrides; teardown now uses shutdownProcessing(), which touches nothing
    persisted. Also collapses duplicate persisted keys instead of trapping on them.
  • Audio Switcher: the volume slider no longer stutters mid-drag (same-value
    @Published writes were republishing on every CoreAudio callback), and the drag
    latch is released on disappear so dismissing mid-drag no longer freezes volume
    sync for the rest of the process.

Verified: swift build clean, full suite 772 tests / 0 failures. The Volume Mixer
and Audio Switcher fixes were also confirmed by hand against a signed local build.

🤖 Generated with Claude Code

havokentity and others added 5 commits July 21, 2026 12:06
Scratchpad, Dev Tools, QR, Clipboard, Network Info and Image Converter all
present text fields and all shipped without a main menu, so ⌘C/⌘V/⌘A/⌘Z were
dead in each of them for the same reason they were dead in Snippets.

Staged here so the integration build is worth testing. This needs to land as a
proper commit on main once the feature PRs merge — HelperMainMenu itself
arrives with the Snippets PR, so the per-tool calls cannot go on their own
branches until that is in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 13cfc12)
"Listen to an input" never worked from a normal toolbox launch: clicking
Listen created no monitor, showed no consent dialog, and added nothing to
System Settings ▸ Privacy & Security ▸ Microphone.

macOS attributes a nested helper's microphone use to the containing app.
PR #4 diagnosed that correctly and put NSMicrophoneUsageDescription on the
outer Info.plist for exactly that reason — but the audio-input entitlement
only ever went on the Audio Router helper. Under the Hardened Runtime TCC
requires it on the subject it attributes to, and when absent it does not
merely deny, it refuses to prompt and writes a permanent denial:

  requires entitlement com.apple.security.device.audio-input but it is
  missing ... Policy disallows prompt for Sub:{com.havokentity.mactools}

Unrecoverable from the UI, because the Microphone list has no "+" — it is
only ever populated by a successful request.

Sign the outer app with new DMonteApp.entitlements (the helper baseline
plus audio-input) rather than the shared helper baseline.

Two supporting changes, both of which this bug needed to be findable:

- The permission gate logs via os_log instead of stderr. Under
  NSWorkspace.openApplication stderr goes nowhere, so MonitorDebug output
  was invisible on the only launch path users have — which is why every
  Terminal-launched reproduction succeeded (inheriting Terminal's own TCC
  grant) and hid the defect.
- ensureMicrophoneAccess returns MicrophoneAccess instead of Bool, so
  .restricted (MDM/Screen Time, not user-grantable) stops masquerading as
  .denied and sending people to a settings pane with nothing listed.

Existing TCC denial records from before this fix must be cleared with
`tccutil reset Microphone com.havokentity.mactools`; the stored denial
survives re-signing.

741 tests pass. Verified on a packaged, Developer ID signed build launched
from the toolbox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 6d2857e)
Granting Calendar access was impossible: the tool asked, no consent dialog
appeared, nothing was added to System Settings ▸ Privacy & Security ▸
Calendars, and that list has no "+" to add an app by hand — so there was no
way to recover from the UI.

This is the microphone bug from 6d2857e, one service over. tccd attributes a
nested helper's request to the containing app, so the Calendar helper's
request is recorded against Sub:{com.havokentity.mactools}. Under the Hardened
Runtime TCC then requires com.apple.security.personal-information.calendars on
that subject, and when it is missing it does not deny — it refuses to prompt
at all.

The container had neither the entitlement nor NSCalendarsFullAccessUsageDescription;
it carries the microphone equivalents of both because that fix was made
service-by-service rather than as a rule. Both are added here, plus a
Calendar.entitlements for the helper itself, mirroring AudioRouter.

Verified on the signed bundle: the entitlement is present on the container and
on the helper.

Not fixed here, but the same shape and worth testing: Volume Mixer declares
NSAudioCaptureUsageDescription while the container does not, so system-audio
capture may fail the same silent way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 73795c79211f1bd38d8cb1686d4d134e40a5113e)
`stopProcessing()` is the footer gesture: it marks every live engine force-off
so the 2-second reconciler will not restart them. Since stops became persistent,
that write also goes to disk — and the app delegate was calling the same method
from `cleanup()` on quit and terminate.

So quitting wrote a force-off for every app that merely happened to be playing,
and the next launch restored all of them as stops the user never pressed: the
row's slider still reading 40% while the audio played at full volume, until each
one was nudged by hand.

Split the two intents. `stopProcessing()` keeps marking the overrides; the new
`shutdownProcessing()` releases the taps and touches nothing persisted, and that
is what teardown now calls.

Also collapse duplicate keys when reading the stopped list back instead of
trapping on them. `stoppedKeys(from:)` reads a dictionary and cannot emit a
repeat, but the list comes back off disk, where a hand-edited or half-merged
prefs plist is outside our control — and `Dictionary(uniqueKeysWithValues:)`
would crash the tool on launch over a value whose meaning is unambiguous.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 2a8f1cef1766c163057c56c750033154cdf802e5)
`refreshVolumeAndMute()` wrote `volumeSupported`, `muteSupported` and `isMuted`
unconditionally. They are `@Published`, which does not dedupe, so a same-value
write still fires `objectWillChange` and re-evaluates the whole popover.
CoreAudio posts a VolumeScalar notification for every step of a drag, so the
popover re-rendered on each one — the `isAdjustingVolume` guard held `volume`
steady, but the redundant publishes alone were enough to make the drag stutter.
Every write is now guarded on an actual change.

The drag latch also had no way back. Only the slider's `onEditingChanged(false)`
cleared it, so dismissing the popover mid-drag left it set — and because the
controller is a `@StateObject` that outlives an open/close cycle, volume stopped
tracking the hardware for the rest of the process. Release it on disappear,
which also re-reads the device so the next open shows the true level.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 577907a36c7c18fac5ff341d530f4e94038432b0)
@havokentity
havokentity merged commit f0baacf into main Jul 21, 2026
1 check passed
@havokentity
havokentity deleted the fix/integration-orphans branch July 21, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant