Land the five fixes that lived only on the integration branch - #22
Merged
Conversation
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
These five commits were made directly on
integration/wave1-testwhile testing theWave 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.
helpers (LSUIElement apps get no main menu by default).
nested helper's request to the container, so without it the mic prompt never
resolves.
reasoning — this was the reported "opens Settings but I can't add an app" bug.
happened to be playing.
stopProcessing()is the footer gesture and marks theoverrides; teardown now uses
shutdownProcessing(), which touches nothingpersisted. Also collapses duplicate persisted keys instead of trapping on them.
@Publishedwrites were republishing on every CoreAudio callback), and the draglatch is released on disappear so dismissing mid-drag no longer freezes volume
sync for the rest of the process.
Verified:
swift buildclean, full suite 772 tests / 0 failures. The Volume Mixerand Audio Switcher fixes were also confirmed by hand against a signed local build.
🤖 Generated with Claude Code