feat(audio): add mute option for playback during recording - #952
feat(audio): add mute option for playback during recording#952emilevictorportenart wants to merge 3 commits into
Conversation
Pausing media only reaches apps that publish Now Playing state, so audio from browser tabs, games, and conferencing apps kept playing over recordings with no way to stop it (altic-dev#908). Replace the "Pause Media During Transcription" toggle with a "Playback When Recording" choice of Keep Playing, Pause, or Mute. Mute silences the default output device through CoreAudio, which covers any audio reaching the speakers. - Add SystemAudioMuteService, which takes ownership of the mute only when the output is audible, restores the exact device it silenced even if the default output changed mid-recording, and falls back to zeroing the volume on devices without a mute switch - Mute just before the start cue and restore as capture ends, so the Mac is not held silent during transcription - Migrate existing users: a stored pause preference maps to Pause, and backups keep the legacy field so older builds still restore Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The PR Policy check is blocking this PR because required template information is missing. Please update the PR description with:
Visual files detected:
Screenshots or video are required for UI, UX, settings, onboarding, overlay, menu bar, or visual behavior changes. If this PR has no visual changes, check the no-visual-change box in the template. If this remains incomplete for 48 hours after opening, the PR may be closed. |
Settings > Dictation showing the new dropdown in its default state and with Mute selected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Greptile SummaryThis PR replaces the recording-time media pause toggle with Keep Playing, Pause, and Mute choices. It adds CoreAudio output muting with device-aware ownership and restoration, integrates the behavior with recording lifecycle boundaries and sound cues, migrates settings and backups, updates settings search and UI, and adds focused service tests. Since the previous review, restoration now retries when a connected output is temporarily unreadable in both the mute service and cue-volume restoration path.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe57065ab3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1bb229b481
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d689cd52a7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 503bcb2f7d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0068fe2ad9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7cb4371e68
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 60a51a70ff
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 80bd41606a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: db12916c4c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e3dfbf3dd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c23d44d10c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c86b71b06e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
c86b71b to
45fd283
Compare
|
I have rewritten this branch to make it reviewable. The tree is byte for byte what it was before the rewrite, so nothing about the code changed. It was fifteen commits, including an add-and-revert pair, because I worked through the automated review findings one at a time. That history is a record of how I got here, not something worth reading. It is now three commits:
Side effect worth knowing: the resolved review threads now point at commits that no longer exist, so their diff context will render oddly. The discussion on each is intact and each explains what changed and why. Twenty three threads, all resolved. Two things I deliberately did not fix, both predating this PR and neither caused by the mute feature. In Also unresolved and outside my control: Build and Test is sitting at |
|
Want your agent to iterate on Greptile's feedback? Start a greploop in Codex and it will work through the open comments and keep going until this PR reviews clean. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 45fd2839f1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
45fd283 to
75dddb1
Compare
f309d71 to
4bbb6d7
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4bbb6d71c9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
4bbb6d7 to
9504244
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9504244fe3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
9504244 to
148f13b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 148f13bdba
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
59de7eb to
c2974cc
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c2974cc61d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
c2974cc to
580a038
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 580a038a5c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
580a038 to
4e4d50c
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4e4d50c3f3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
4e4d50c to
1ddfd63
Compare
1ddfd63 to
aa5fd60
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa5fd601b6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Review surfaced a series of edge cases where the mute either leaked audio into a recording or failed to hand a device back. They shared two causes: the mute was applied and released at the wrong points relative to the capture boundary, and ownership was tracked by a value that does not survive a device disconnecting. Capture boundary - Silence the output before the pipeline starts retaining PCM, not after the first packet, so playback cannot reach the opening of a transcript. - Restore only after the acquisition boundary is frozen, so re-enabling the speakers cannot contaminate the final packet. Both stop paths and the termination path follow the same rule. Device identity and lifetime - Hold a record only while a device is silent because of this service. A device that was already quiet, rejected the write, cannot be read, or has since been changed by the user is not ours, so nothing is written to it and no stale value survives to be handed back later. - Silence only outputs that publish a UID. Doing it is a promise to put the device back, an object id can be reissued to other hardware once the original goes away, and without a stable identity that promise cannot be kept. - Follow the default output for the length of the recording, and keep observing while any device is still owed a restore, so one unplugged during teardown is handed back when it returns rather than at the next recording. Observation starts before the first sample, since CoreAudio does not replay a change that predates its listener. - Re-attempt a device that would not be silenced, or would not be handed back, after a short delay as well as on any output change. Hardware that is still initialising refuses reads and writes, and becoming ready again raises no notification of its own. The attempts are bounded, so a device that simply will not take the write is not polled for the session, and each one reads the device afresh, so the value captured is always the one the user has at that moment. Refused reads are re-attempted on the same terms as refused writes, in both directions. A change of output gets its own allowance, and an output whose UID is not readable yet is re-attempted rather than taken as permanently unidentifiable. - Re-register every listener after the audio service restarts, which discards them, re-resolving each held device rather than only the current default, and treat each as recreated, since a restart can reset what we applied and the user has not touched anything. - Watch a held device's own mute and volume, so a change made from elsewhere releases it as it happens. The record and its listener follow the device when it returns under a new object id. Comparing only at teardown cannot see a round trip: unmuting and muting again leaves the device looking exactly as we left it, and undoing that would reverse the user's latest choice. - Decide whether a device is still ours by whether it still holds exactly what we applied, rather than by its object id. A device that reconnects still carrying our mute keeps its record, and one that comes back reset is silenced again with its own current state captured. - Tell a reconnect reset apart from a user's own change by whether the device has been seen absent or has come back under a different object id. Either is proof on its own: the id covers a reconnect whose disappearance was never observed because the notifications coalesced, and the absence covers one that returned under the same id. A device that no longer holds what we applied is released before being silenced again, since nothing of ours is in effect on it and keeping the record would let teardown undo a silence the user created afterwards. Deferring to the user - Release ownership, without writing, when the device is no longer in the state FluidVoice left it in. A mute the user lifts or a volume they set during a recording is newer than the value captured at the start. - Keep ownership when a write fails and retry it, rather than forgetting a device that is briefly unreachable. Recording cues - Suppress the start cue under Mute. It fires after the output is silenced, so it cannot be heard, and playing it put the cue's independent-volume handling in contention with the mute over the same system volume. The stop cue still plays, because the output is restored before it. - Settle a pending cue volume restore before muting, so the value captured is the one the user had rather than the level a cue lowered it to. - Only the first cue of an overlapping run records the volume, so the value handed back is the one from before any of them played. Applied policy - Read the setting once per recording. The mute and the pause are applied either side of the wait for first PCM, so reading it twice let a change made during that wait produce a recording with both policies or neither. - Keep a device whose write was rejected, marked as not silenced, so the recording retries it rather than giving up on hardware that was merely busy initialising. A device that cannot be read yet is indeterminate, not silent, so it is retried rather than recorded as done, and one that reads as silent after a rejected write was silenced by the user rather than by us, so it is not claimed. - Track whether a device is silent right now separately from whether its state is still owed a restore. A rejected write leaves the device owned but audible, so the original value survives for a retry, an audible reading is not mistaken for the user taking over, and teardown does not write to a device nothing of ours was applied to. - Put a cue's saved volume back on the output it was read from, identified by UID and resolved to a current object id at the time of the write. Writing to whichever device is current would overwrite a setting that never belonged to it, skipping the write would strand the original device at the cue's temporary volume, and an object id can be reissued to other hardware once the original is unplugged. A cue whose output publishes no UID plays at the requested volume rather than taking the system volume over, since there would be no way to promise it back. - Decide the start cue on whether the recording in progress is silencing the output, rather than on the setting, which can change between a recording starting and the cue firing, or on whether any device is still owed a restore, which a leftover from an earlier session would satisfy. - Resolve the cue's output once and read, lower and restore that same device, so a route change between separate lookups cannot read one device's volume and write it to another. The saved value is kept when a write is rejected, rather than discarded before the write is known to have landed. - Hold a pending cue volume per output rather than one shared slot, so cues that overlap across an output change each hand their own device back, and restore one only while the device still reads the level the cue applied, which each cue updates so overlapping cues asking for different levels do not read as somebody else having changed the device. That level is recorded only once the device accepts the write, since a rejected one leaves it where it was, and the level recorded is the one the device settled on rather than the one requested, since some outputs quantise it. An output that exposes no volume at all, common for HDMI and other fixed-volume devices, plays the cue at the requested level rather than suppressing it. A device that refuses to take its volume back is re-attempted a bounded number of times, since becoming writable raises no notification. Delayed callbacks carry the run of cues they were scheduled for, so one left over cannot hand back a volume a later cue is still using. - Watch the device list while a cue volume is owed back, so an output unplugged before its cue finished is restored when it returns rather than at some later cue. - Leave the termination restore to a stop already in flight, which restores after freezing its own capture boundary. Running it here would make playback audible while that pipeline was still retaining audio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aa5fd60 to
8a0a496
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a0a496084
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Ready for review. What it does. Replaces the Pause Media During Transcription toggle with a Playback When Recording choice of Keep Playing, Pause or Mute, which is the shape #908 asks for. Pause only reaches apps that publish Now Playing state, so a browser tab, a game or a call keeps playing over a recording with nothing able to stop it. Mute silences the default output through CoreAudio instead, which covers anything reaching the speakers, and works on Intel as well. Existing users who had pausing on get Pause; everyone else gets Keep Playing. Backups still write the old boolean, so a backup made by this build restores correctly on an older one. Three commits. The feature, the screenshots, then one commit folding in everything the automated review surfaced. The branch was rewritten partway through to make it readable, so the resolved threads point at commits that no longer exist and their diff context renders oddly. Each thread still carries the discussion and the reasoning. Review status. Greptile is at 5/5, PR Policy passes, and all 68 threads are resolved. Locally: 480 tests pass, SwiftLint strict adds no violations over One decision I would rather you made than me. Both automated reviewers asked for opposite things on whether unresolved mute ownership should survive the app quitting. I implemented persistence, Greptile raised it as a P1 because restoring at launch silently overrides whatever the user chose in the meantime, and I reverted it. The provenance of a device's audio state is unknowable across a process boundary, so the two positions cannot both be satisfied. I left it out, because the feature's rule everywhere else is that a choice the user makes wins. The residual gap is a device that is absent when the app quits and never returns while it runs. If you want that closed, I think asking the user beats guessing, and that belongs in its own change. Two things I deliberately did not fix, both predating this PR and neither caused by the mute feature. In One thing I cannot do myself. Build and Test is sitting at Happy to split this up, drop parts of the hardening, or take it in a different direction if any of it goes further than you want in a settings change. |
|
This pull request has been marked stale because it has had no activity for 5 days. It will be closed in 2 days if there is no further activity. Add the |
Description
Pausing media only reaches apps that publish Now Playing state. Audio from a browser tab, a game, or a conferencing app keeps playing over a recording with no way to stop it, which is what #908 reports.
This replaces the Pause Media During Transcription toggle with a Playback When Recording choice of Keep Playing, Pause, or Mute, matching the shape of the setting the issue asks for. Mute silences the default output device through CoreAudio, so it covers any audio reaching the speakers regardless of whether the source exposes media controls. It works on Intel too, unlike
Pause, which needsMediaRemoteAdapteron Apple Silicon.How mute behaves
One rule holds the whole thing together: a device is recorded only while it is silent because of this service. Everything else follows from that.
AudioObjectID, which is not stable across a disconnect and can be reissued to different hardware. An output with no UID is left alone, since silencing it would be a promise that could not be kept.Migration
Existing users who had pausing enabled get
Pause; everyone else getsKeep Playing. Backups still write the old boolean field, so a backup made by this build restores correctly on an older build, and older backups restore through the boolean.Type of Change
Related Issue or Discussion
Closes #908
Testing
swiftlint --strict --config .swiftlint.yml Sources Tests Package.swiftswiftformat --config .swiftformat Sourcesxcodebuild test -project Fluid.xcodeproj -scheme Fluid -destination 'platform=macOS,arch=arm64'SystemAudioMuteServiceTestsruns 44 tests against a fake CoreAudio layer. They cover ownership and user takeover, the volume fallback, output changes mid-recording, reconnects under a new object id, refused reads and writes and their bounded retries, devices without a UID, audio-service restarts, and the interaction with the recording cues. 480 tests pass overall.The linter reports the same 7
legacy_swiftui_aspect_ratioviolations on this branch and onmain; they come from a newer local SwiftLint (0.65.1) than CI pins (0.63.2) and are untouched by this change. The formatter was run at--swift-version 5.0and reports no changes to any file in this diff.One pre-existing test,
HotkeyShortcutTests.testKeyboardPayloadIgnoresStrayMouseButtonField, fails identically on this branch and on a cleanmaincheckout on my machine. It asserts key code 0 renders asA, which isQon the AZERTY layout this Mac uses. It is unrelated to this change and passes on QWERTY CI runners.Screenshots / Video
Settings > Dictation, with the new dropdown in its default state:
And with Mute selected, reached through settings search:
And a video showing the 3 states with Music and video in the background
CleanShot.2026-09-08.at.09.54.11.mp4
How this PR got here
Worth reading before the diff, because the shape of the branch reflects it.
The feature itself is small: a setting, a CoreAudio service, and four call sites in the dictation lifecycle. Almost everything after that came from twelve rounds of automated review, which surfaced real edge cases in how the mute behaves against actual hardware: outputs changing mid-recording, devices reconnecting under a new id or with their state reset, CoreAudio refusing reads and writes while a device initialises, the audio service restarting, and the recording cues contending for the same system volume.
Three things a reviewer should know about that process:
If any of this reads as more machinery than a settings change should carry, I am happy to scale it back. The hardening is separable from the feature.
Notes
TranscriptionSoundPlayerare left alone. With Mute off, overlapping cues hand the volume back when the first finishes rather than the last. Neither can bite under Mute, since the start cue no longer plays while the output is silenced. Both predate this PR and are not caused by the mute feature, so I would rather raise them separately than widen this further. Say the word and I will fold them in.PauseandMuteare mutually exclusive by design, which is why this is a dropdown rather than a second toggle. Happy to split it back into two toggles if you would rather leave the existing setting untouched.Lowermode, even though the app in the issue offers one, to keep this focused on what [FEATURE] Mute sounds instead of Pause #908 asks for. It is a small addition on top of the volume fallback if you want it later.action_required. That is GitHub holding workflows for a first-time fork contributor, and approving it needs write access to this repo, so it needs a maintainer to click Approve and run.🤖 Generated with Claude Code