Skip to content

chore: production polish — code quality, UX text, and AppDelegate decomposition#14

Merged
nathanialhenniges merged 20 commits intomainfrom
chore/production-polish
Apr 4, 2026
Merged

chore: production polish — code quality, UX text, and AppDelegate decomposition#14
nathanialhenniges merged 20 commits intomainfrom
chore/production-polish

Conversation

@nathanialhenniges
Copy link
Copy Markdown
Member

@nathanialhenniges nathanialhenniges commented Apr 3, 2026

Summary

Full production polish pass covering code quality, UX language, and architecture cleanup.

  • File headers — added missing Xcode headers to AppleMusicSource, PlaybackSource, PlaybackSourceManager; fixed mismatched header in MusicPlaybackMonitorTests; removed stale systemNowPlaying dispatch queue key and dead test
  • Thread safetyAppleMusicSource: lock isTracking across main/background queue boundary; WebSocketServerService: standardize lock()/unlock()withLock {}
  • PerformanceDiscordRPCService: cache readDiscordTmpDir() sysctl result with 30s TTL; ArtworkService: add insertion-order LRU eviction at 200 entries across all three caches
  • BotCommand refactorglobalCooldownKey/userCooldownKey moved into the protocol declaration (not just extension) to enable dynamic dispatch; canonicalTrigger hardcoded switch replaced with command.triggers.first; cooldownValues reads keys from the command directly — adding new commands no longer requires touching the dispatcher
  • UX text — replace all OAuth/developer jargon ("Authorize", "Device Code", "Re-auth", "local server", "saved credentials") with plain streamer language across 11 view files; standardize Discord "Active/Idle" → "Connected/Disconnected", cooldown "Global/Per-user" → "Everyone/Per person"
  • AppDelegate decomposition — split WolfWaveApp.swift (1,241 lines) into four focused files: core + +MenuBar + +Windows + +Services
  • FEATURE_IDEAS.md — removed deprioritized ideas, expanded Song Request Queue and Listening History entries, added 5 new ideas (Jira epics WW-23–34 created)

Test plan

  • make build passes clean
  • make test — 654 tests, 0 failures
  • Visual review: settings views, onboarding steps, What's New sheet
  • Manual toggle of menu bar items (Music Sync, Twitch Chat, Discord Status, widget)
  • Onboarding flow end-to-end on a fresh UserDefaults state

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • macOS status‑bar menu with quick toggles and Copy Widget Link
    • First‑launch onboarding and “What’s New” dialog
    • Now‑playing widget: WebSocket token auth, optional macOS notifications, Stream Deck plugin, widget webpage, and Discord Rich Presence playlist display
    • Song requests: !sr alias (Spotify/YouTube → Apple Music), Twitch chat voting, and Twitch !stats summaries
    • Listening history: track/genre stats, SwiftUI charts, monthly exportable summaries
  • Improvements

    • Wording/accessibility refinements across Twitch, Settings, and onboarding
    • Safer playback monitoring, bounded artwork cache eviction, configurable global hotkeys, and cooldown override options for commands

nathanialhenniges and others added 12 commits April 1, 2026 00:49
- Extract PlaybackSource protocol + PlaybackSourceMode enum
- Refactor MusicPlaybackMonitor → AppleMusicSource (same logic, new abstraction)
- Add SystemNowPlayingSource using private MediaRemote framework (dlopen/dlsym)
  captures playback from Spotify, browsers, and any system Now Playing app
- Add PlaybackSourceManager to switch sources and forward delegate callbacks
- Add Music Source segmented picker to MusicMonitorSettingsView
  (Apple Events warning hidden when System mode is selected)
- Update AppDelegate to use PlaybackSourceManager; Apple Music remains default
- Add AppConstants for playbackSourceMode, PlaybackSourceModeChanged, SystemNowPlaying
- Add PlaybackSourceManagerTests and SystemNowPlayingSourceTests (693 tests, 0 failures)
- Add FEATURE_IDEAS.md with 8 future feature ideas
- Sync docs changelog with v1.0.2, v1.1.0, and v1.2.0 entries
- Bump version to 1.2.0, build number to 5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SystemNowPlayingSource: use AppConstants for frameworkPath, notification name,
  and queue label instead of local duplicates
- SystemNowPlayingSource: check dlsym result for NULL before unsafeBitCast
- SystemNowPlayingSource: dispatch unavailable status via notifyDelegate (main thread)
- PlaybackSourceManager: use AppConstants.UserDefaults.playbackSourceMode key
  in both init() and switchMode(_:) instead of hardcoded string
- MusicMonitorSettingsView: use AppConstants.UserDefaults.playbackSourceMode
  in @AppStorage; add accessibilityLabel/accessibilityIdentifier to picker
  and accessibilityElement to system mode info row
- SystemNowPlayingSourceTests: rename misleading test to testDelegateWiringDoesNotCrash
- CHANGELOG: fix "macOS system Now Playing API" → "macOS Now Playing API"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PlaybackSourceManagerTests crashed on every test with
"pointer being freed was not allocated" on a background thread.
Root cause: AppleMusicSource was eagerly created as a stored property
in PlaybackSourceManager.init, racing with a still-in-flight
backgroundQueue async block from the previous AppleMusicSourceTests
teardown on CI runners.

Making appleMusicSource (and systemNowPlayingSource) lazy defers
allocation until startTracking() is called. No tests in
PlaybackSourceManagerTests call startTracking(), so no AppleMusicSource
is ever created during those tests, eliminating the race.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CRITICAL BUG FIX: playbackSourceModeChanged notification had no observer
in AppDelegate, so switching the Music Source picker never took effect at
runtime. Added the missing observer — switchMode() is now called
immediately when the user changes the picker.

SOURCE DETECTION: SystemNowPlayingSource now resolves MRMediaRemoteGet-
NowPlayingApplicationPID via dlsym to detect which app is playing (Spotify,
Chrome, etc.). Bundle ID is surfaced via new PlaybackSourceDelegate method
playbackSource(_:didDetectSourceApp:) with a default no-op extension for
backward compatibility.

SOURCE-AWARE DISCORD RPC:
- Small icon + tooltip now show the actual playing app (Spotify logo,
  Apple Music logo, YouTube/Chrome icon, or generic music note)
- "Open in Apple Music" button only appears when Apple Music is the source
- Large image fallback uses the source-specific asset instead of always
  showing apple_music

TWITCH COMMANDS: getCurrentSongInfo/getLastSongInfo no longer gate on
isMusicAppOpen() when in system mode — Spotify/Chrome tracks now respond
correctly. Non-Apple-Music sources append "via Spotify" etc.

UI TEXT: Updated settings and onboarding descriptions to be source-agnostic.

DISCORD ASSETS: Added discord-assets/ folder with placeholder PNGs for
spotify, youtube, and music_generic. Upload these to Discord Developer
Portal → Rich Presence → Art Assets (see discord-assets/README.md).

Added KnownMusicApps helper in AppConstants with displayName(for:),
discordAssetName(for:), isAppleMusic(_:), isBrowser(_:). Tested in
KnownMusicAppsTests.swift (16 new tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The private MediaRemote framework is blocked by kMRMediaRemoteFrameworkErrorDomain
Code=3 on macOS 26 (Tahoe). The mediaremoted daemon enforces private Apple
entitlements (com.apple.private.mediaremote.nowplayinginfo) that third-party
apps cannot obtain — no public API exists to read now-playing info from other apps.

- Delete SystemNowPlayingSource.swift and all related tests
- Remove PlaybackSourceMode.systemNowPlaying, didDetectSourceApp delegate method
- Simplify PlaybackSourceManager to always use AppleMusicSource
- Remove KnownMusicApps, SystemNowPlaying enums from AppConstants
- Remove source picker from MusicMonitorSettingsView
- Remove source-aware branding from DiscordRPCService and WolfWaveApp
- Revert docs (index, features, usage, changelog) to Apple Music language
- Revert entitlements: remove com.apple.mediaremoted mach-lookup exception
- Update WhatsNewView with v1.1.0 highlights (pending v1.2.0 feature set)
- Fix @mainactor isolation for Twitch bot callbacks (MainActor.assumeIsolated)
- Move CHANGELOG v1.2.0 entry to [Unreleased] — more features coming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add standard //  FileName.swift / wolfwave / Created by… headers to
AppleMusicSource, PlaybackSource, and PlaybackSourceManager (all
previously missing). Fix mismatched header in MusicPlaybackMonitorTests
(was AppleMusicSourceTests.swift). Remove stale
DispatchQueues.systemNowPlaying constant and the now-dead
testInvalidPersistedModeFallsBackToAppleMusic test that referenced the
removed playbackSourceMode UserDefaults key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AppleMusicSource: add trackingLock (NSLock) to synchronize isTracking,
which was read on backgroundQueue by the fallback timer but written on
the main thread by startTracking/stopTracking — a data race.

WebSocketServerService: standardize manual lock()/unlock() calls in
setEnabled() and scheduleRetry() to withLock {} to prevent deadlocks
on thrown exceptions, matching the pattern used elsewhere in the file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DiscordRPCService: readDiscordTmpDir() ran sysctl(KERN_PROCARGS2) on
every connectIfNeeded() call. Cache the result with a 30s TTL — Discord's
TMPDIR doesn't change while it's running, so repeated kernel queries
were wasteful.

ArtworkService: the three in-memory caches (artwork, trackView, songLink)
grew unbounded. Add insertion-order tracking via cacheKeyOrder and evict
the oldest entry across all three caches when the count exceeds 200,
keeping memory usage proportional to active usage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l keys

Add globalCooldownKey and userCooldownKey as protocol requirements on
BotCommand (with nil defaults in an extension). TrackInfoCommand accepts
these via init and stores them as let properties.

BotCommandDispatcher now derives the canonical cooldown bucket from
command.triggers.first instead of a hardcoded switch statement, and
cooldownValues(for:command:) reads UserDefaults keys directly from the
command rather than pattern-matching on trigger strings. Adding a new
command no longer requires touching the dispatcher.

Note: keys must be declared in the protocol body (not just an extension)
to enable dynamic dispatch through the BotCommand existential — extension-
only methods are statically dispatched and would always return nil.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace OAuth/developer terminology throughout the app with plain
language appropriate for streamers:
- "Authorize" / "Re-auth" / "Device Code" → "Sign in" / "Sign-in Code"
- "Twitch Integration" → "the Twitch bot"
- "local server" / "Starts the local server" → "powers the widget"
- "saved credentials" → "keep you logged in"
- "Validation error" → "Couldn't check channel"
- "reconfigure your integrations" → "set up your connections again"
- "Reset Onboarding?" → "Rerun Setup Wizard?"
- "debugging and support" → "troubleshoot issues"

Standardize terminology:
- Discord status labels: "Active"/"Idle" → "Connected"/"Disconnected"
- Cooldown labels: "Global"/"Per-user" → "Everyone"/"Per person"
- Account label: "Bot Account" → "Twitch Account"

Update WhatsNewView feature descriptions for v1.1.0 to use plain copy.

Remove duplicate getCurrentSongInfo/getLastSongInfo callback wiring
from SettingsView.onAppear — AppDelegate.setupTwitchService() is the
single source of truth for these closures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WolfWaveApp.swift was ~1,241 lines — a single file mixing menu
construction, window management, service orchestration, and playback
delegation. Split into four focused files:

- WolfWaveApp.swift (~150 lines): core class definition, stored
  properties, lifecycle hooks, postNowPlayingUpdate, song info provider.

- AppDelegate+MenuBar.swift: status item setup, dynamic NSMenu
  construction (NSMenuDelegate), all toggle actions, copyWidgetURL.

- AppDelegate+Windows.swift: openSettings, showAbout, dock visibility
  management, What's New, onboarding wizard, Settings window creation,
  NSWindowDelegate.

- AppDelegate+Services.swift: all service setup (Music, Twitch, Discord,
  WebSocket, Sparkle, power state), NotificationCenter observer
  registration, notification handlers, Twitch token validation,
  PlaybackSourceDelegate conformance.

No behavior changes. Build and all 654 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove: Last.fm Scrobbling, Multi-Platform Chat Support (both deprioritized).

Expand: Song Request Queue now includes Spotify/YouTube URL parsing via
song.link API and iTunes Search API confidence threshold. Listening
History & Stats now includes a Monthly Wrap / personal Unwrapped concept
with shareable image export.

Add: WebSocket Authentication (token-based widget auth), Notification
Center Integration (track change notifications), Stream Deck Plugin
(Elgato SDK + local HTTP), Playlist Detection (Discord status shows
playlist name via ScriptingBridge), Chat Voting (extends request queue).

Jira epics and subtasks created for Song Request Queue (WW-23–28) and
Listening History (WW-29–34).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR splits AppDelegate into focused extensions (menu, services, windows), adds a PlaybackSource abstraction and delegate, makes AppleMusicSource thread-safe, wires WebSocket/widget, Discord, and Twitch services, refactors Twitch command cooldowns, updates artwork caching/eviction, and adjusts many UI/accessibility strings and docs.

Changes

Cohort / File(s) Summary
Planning & Docs
FEATURE_IDEAS.md
Expanded Song Request Queue and many new feature ideas (request aliases/URL resolution, listening-history charts/export, widget auth, Stream Deck, global hotkeys, Discord/Twitch interactions).
AppDelegate — Menu/Status
apps/native/wolfwave/Core/AppDelegate+MenuBar.swift
New status bar item and dynamic menu with now-playing view, toggles, and handlers (copy widget URL, toggle tracking/Discord/Twitch/WebSocket).
AppDelegate — Services
apps/native/wolfwave/Core/AppDelegate+Services.swift
Centralized service setup and coordination: playback manager, Twitch, Discord RPC, WebSocket server, Sparkle, power-state adjustments, NotificationCenter observers, artwork push, Twitch token validation, and PlaybackSourceDelegate implementations.
AppDelegate — Windows/Lifecycle
apps/native/wolfwave/Core/AppDelegate+Windows.swift
Settings/Onboarding/What’s New windows, About panel with links, dock visibility control, window caching/lifecycle, and versioned “What’s New” flow.
App Core Entrypoint
apps/native/wolfwave/WolfWaveApp.swift
Narrowed AppDelegate conformance, moved many behaviors into extensions, and relaxed access control on several state properties.
Playback Monitoring
apps/native/wolfwave/Monitors/PlaybackSource.swift, .../AppleMusicSource.swift, .../PlaybackSourceManager.swift
Added PlaybackSource protocol and PlaybackSourceDelegate; AppleMusicSource made thread-safe with NSLock; PlaybackSourceManager simplifies lifecycle with isStarted.
Artwork & Caching
apps/native/wolfwave/Services/ArtworkService.swift
Insertion-ordered cache key tracking with bounded eviction across three caches and weak-self guard in mutation block.
Discord RPC
apps/native/wolfwave/Services/Discord/DiscordRPCService.swift
Cached temp-dir reads with TTL and clarified asset/branding logic for presence payloads.
WebSocket Server
apps/native/wolfwave/Services/WebSocket/WebSocketServerService.swift
Replaced manual lock/unlock with NSLock.withLock for enabled flag access and retry scheduling.
Twitch Commands & Dispatcher
apps/native/wolfwave/Services/Twitch/Commands/*.swift
BotCommand adds optional globalCooldownKey/userCooldownKey; dispatcher uses command first trigger as group and reads cooldown overrides from provided keys; TrackInfoCommand updated to accept keys.
Views / UX Text
apps/native/wolfwave/Views/...
Multiple text/accessibility updates (Twitch sign-in wording, widget descriptions, cooldown labels, diagnostics/onboarding phrasing); removed SettingsView onAppear Twitch callback assignment.
Tests
apps/native/WolfWaveTests/...
Whitespace tweak in PlaybackSourceManagerTests; updated TwitchViewModelTests expectation for reauth wording.

Sequence Diagram(s)

sequenceDiagram
    participant Player as Playback Source
    participant App as AppDelegate (Services)
    participant WS as WebSocketServer
    participant Discord as DiscordRPC
    participant Twitch as TwitchService

    rect rgba(200,230,255,0.5)
    Player->>App: playback update (track, artist, album, duration, elapsed)
    end

    rect rgba(200,255,200,0.5)
    App->>WS: push now-playing + artwork (widget port, auth token)
    WS-->>App: acknowledge / publish widget URL
    end

    rect rgba(255,230,200,0.5)
    App->>Discord: update presence (artwork URL, activity)
    App->>Twitch: optionally post/update chat/status / queue actions
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰✨
I hopped through AppDelegate, split pieces with care,
Services now chatter, widgets sparkle in air,
Tracks tell their stories, artwork finds its way,
Sign-ins refreshed, toggles set — hooray for today!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main focus of the changeset: production polish covering code quality improvements, UX text refinements, and AppDelegate architectural decomposition.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/production-polish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/native/wolfwave/Views/Twitch/DeviceCodeView.swift (1)

90-103: ⚠️ Potential issue | 🟡 Minor

Accessibility label inconsistency with visible text.

Line 90 says "Continue to Twitch to sign in" but line 101's accessibility label still says "Continue to Twitch to Authorize". These should match for consistent screen reader experience.

🔧 Proposed fix
             .buttonStyle(.borderedProminent)
             .controlSize(.small)
             .pointerCursor()
-            .accessibilityLabel("Continue to Twitch to Authorize")
+            .accessibilityLabel("Continue to Twitch to sign in")
             .accessibilityHint("Opens twitch.tv/activate in your browser")
             .accessibilityIdentifier("openTwitchButton")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Views/Twitch/DeviceCodeView.swift` around lines 90 -
103, The accessibilityLabel on the Button currently reads "Continue to Twitch to
Authorize" while the visible Text shows "Continue to Twitch to sign in"; update
the accessibilityLabel to exactly match the visible text (or vice versa if you
prefer the label) so they are consistent — edit the view containing the
Text("Continue to Twitch to sign in") and change the .accessibilityLabel(...) on
the same Button to "Continue to Twitch to sign in" (or update both Text and
.accessibilityLabel to the same canonical string) and keep the existing
.accessibilityHint and .accessibilityIdentifier unchanged.
apps/native/wolfwave/Views/SettingsView.swift (1)

378-395: ⚠️ Potential issue | 🟡 Minor

Align VoiceOver copy with the new cooldown wording.

The visible labels now say “Everyone” and “Per person”, but accessibility still announces “global” and “per-user”. Screen-reader users should hear the same simplified copy.

♿ Suggested accessibility copy update
-                        .accessibilityLabel("\(label) global cooldown")
+                        .accessibilityLabel("\(label) cooldown for everyone")
                         .accessibilityValue("\(Int(globalCooldown.wrappedValue)) seconds")
-                        .accessibilityHint("Adjusts the global cooldown between 0 and 30 seconds")
+                        .accessibilityHint("Adjusts how long everyone waits between uses, from 0 to 30 seconds")
@@
-                        .accessibilityLabel("\(label) per-user cooldown")
+                        .accessibilityLabel("\(label) per-person cooldown")
                         .accessibilityValue("\(Int(userCooldown.wrappedValue)) seconds")
-                        .accessibilityHint("Adjusts the per-user cooldown between 0 and 60 seconds")
+                        .accessibilityHint("Adjusts how long each person waits between uses, from 0 to 60 seconds")
As per coding guidelines: Use friendly, non-technical language in UI text. Avoid jargon like 'IPC', 'WebSocket', 'Rich Presence', or 'Listener'.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Views/SettingsView.swift` around lines 378 - 395, The
VoiceOver copy is inconsistent with the visible labels; update the accessibility
labels for the two Slider controls in SettingsView (the ones bound to
globalCooldown and userCooldown) to match the simplified UI text: change the
accessibilityLabel for the globalCooldown Slider from "\(label) global cooldown"
to something like "Everyone cooldown" and change the accessibilityLabel for the
userCooldown Slider from "\(label) per-user cooldown" to "Per person cooldown";
keep existing accessibilityValue/accessibilityHint text but ensure wording is
friendly and non-technical and matches the visible labels.
apps/native/wolfwave/Views/Twitch/TwitchReauthView.swift (1)

97-118: ⚠️ Potential issue | 🟡 Minor

Align remaining “device code” strings with “Sign-in Code”.

Line 97 changed the section label, but Line 115 and Line 117 still use “device code.” Keep these terms consistent in the same dialog.

💡 Suggested copy fix
-                        .accessibilityLabel("Copy device code")
+                        .accessibilityLabel("Copy sign-in code")
...
-                        .help("Copy device code")
+                        .help("Copy sign-in code")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Views/Twitch/TwitchReauthView.swift` around lines 97 -
118, The UI copy is inconsistent: change remaining "device code" occurrences to
"Sign-in Code" so the dialog matches the label; update the Button's
accessibilityLabel (currently "Copy device code") to "Copy sign‑in code", update
the .help text from "Copy device code" to "Copy sign-in code", and update any
inline comment "Token field style device code display" to use "Sign-in Code"
(e.g., reference viewModel.authState.userCode and the copyDeviceCode()
Button/accessibilityIdentifier "reauthCopyCodeButton" when making these text
replacements).
🧹 Nitpick comments (6)
apps/native/wolfwave/Views/WebSocket/WebSocketSettingsView.swift (1)

346-346: Consider shortening this tip into two shorter lines.

The guidance is correct, but this sentence is a bit dense for quick scanning in settings UI.

✂️ Optional copy tweak
-Text("In OBS, set the Width and Height to **\(AppConstants.Widget.recommendedDimensionsText)** for best results. Enable \"Shutdown source when not visible\" so the widget reconnects properly.")
+Text("In OBS, set Width and Height to **\(AppConstants.Widget.recommendedDimensionsText)**. Turn on \"Shutdown source when not visible\" to help the widget reconnect.")

As per coding guidelines: "Write all user-facing text to be short, punchy, and jargon-free (ADHD-friendly)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Views/WebSocket/WebSocketSettingsView.swift` at line
346, Split the long guidance Text in WebSocketSettingsView (the Text that
mentions AppConstants.Widget.recommendedDimensionsText) into two shorter lines
for readability: keep the first Text to recommend the OBS width/height using
AppConstants.Widget.recommendedDimensionsText and create a second Text that
instructs enabling "Shutdown source when not visible" so the widget reconnects
properly; use separate Text views stacked (e.g., VStack) and match existing
styling so spacing and emphasis remain consistent.
apps/native/wolfwave.xcodeproj/project.pbxproj (1)

97-104: Consider removing "Recovered References" group.

This group is typically auto-generated by Xcode when file references become orphaned. Having Config.Debug.xcconfig here suggests its original location was changed. If the file is correctly referenced in the project (line 27 already has it), this recovered reference may be a stale artifact worth cleaning up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave.xcodeproj/project.pbxproj` around lines 97 - 104, Remove
the stale "Recovered References" PBXGroup (identifier D46E2E062F7F05E800089923)
that contains D4CFGXCC00000000000000F2 /* Config.Debug.xcconfig */: delete the
entire PBXGroup entry and remove its UUID from any parent groups or children
arrays so there are no dangling references; before deleting, verify that the
actual Config.Debug.xcconfig file is still referenced elsewhere (the existing
Config.Debug.xcconfig entry) and keep that valid reference intact.
apps/native/WolfWaveTests/MusicPlaybackMonitorTests.swift (1)

37-69: Consider moving these to integration tests and keeping unit tests pure.

The startTracking/stopTracking “does not crash” tests directly exercise a system-integrated source, which can be environment-sensitive. Prefer unit tests around pure logic/mocks here, and keep system behavior checks in a separate integration layer.

As per coding guidelines, "Focus unit tests on pure logic (version comparison, command matching, state machines) — avoid tests requiring AppDelegate, Keychain, or network" and "Prioritize testing pure logic ... and mock network and system services to keep tests fast and reliable".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/WolfWaveTests/MusicPlaybackMonitorTests.swift` around lines 37 -
69, These tests (testStartTrackingDoesNotCrash, testStopTrackingDoesNotCrash,
testDoubleStartDoesNotCrash, testDoubleStopDoesNotCrash,
testUpdateCheckIntervalBeforeStartDoesNotCrash,
testUpdateCheckIntervalWhileTrackingDoesNotCrash) exercise system-integrated
behavior via MusicPlaybackMonitor.startTracking/stopTracking and should be moved
to an integration test target; instead, convert the unit tests to pure logic
tests by mocking the monitor's system dependencies (inject a mock/spy for the
system music service or event source) and assert state transitions/time-interval
handling, or mark the existing tests as integration (e.g., move them to an
IntegrationTests target or add an integration test annotation) so unit tests
remain fast and environment-independent while integration suite covers real
start/stop behavior.
apps/native/wolfwave/Core/AppDelegate+MenuBar.swift (1)

228-236: Asymmetric Twitch toggle behavior.

Toggling Twitch "on" opens Settings instead of connecting directly, while toggling "off" disconnects immediately. This UX asymmetry may confuse users expecting a simple toggle. Consider adding a tooltip or visual indicator explaining this behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Core/AppDelegate`+MenuBar.swift around lines 228 - 236,
toggleTwitchConnection has asymmetric behavior (disconnects immediately but
opens Settings to connect) which can confuse users; update the UI rather than
the method: add a clear tooltip/label on the menu item or NSStatusItem that
invokes toggleTwitchConnection explaining "Connect requires channel &
credentials — opens Settings" (use the same menu item/NSStatusItem where
toggleTwitchConnection is wired), and/or show a small confirmation/alert before
calling openSettings() in toggleTwitchConnection to make the behavior explicit
(referencing toggleTwitchConnection, twitchService, openSettings, and
AppConstants.Twitch.settingsSection).
apps/native/wolfwave/Monitors/AppleMusicSource.swift (1)

185-194: Minor: Timer closure captures self before lock check.

The weak self capture is correct, but the guard pattern re-captures self strongly before checking the lock. This is acceptable since withLock is synchronous, but consider extracting the lock check to avoid holding a strong reference during the lock acquisition.

🔧 Optional: Extract lock check
 timer.setEventHandler { [weak self] in
-    guard let self = self, self.trackingLock.withLock({ self.isTracking }) else { return }
-    self.scheduleTrackCheck(reason: "timer")
+    guard let self else { return }
+    guard self.trackingLock.withLock({ self.isTracking }) else { return }
+    self.scheduleTrackCheck(reason: "timer")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Monitors/AppleMusicSource.swift` around lines 185 - 194,
The timer event handler currently re-binds self strongly before performing the
lock check; instead compute the boolean tracking state using the weak reference
first (e.g. let isTracking = self?.trackingLock.withLock { $0.isTracking } ??
false), then guard let self = self, isTracking else { return }, and finally call
self.scheduleTrackCheck(reason: "timer"); update the closure in
setupFallbackTimer to use this two-step pattern so you don't hold a strong
reference while acquiring trackingLock.
apps/native/wolfwave/Monitors/PlaybackSource.swift (1)

19-30: Add @MainActor to delegate protocol to enforce main-thread contract.

The delegate protocol should declare @MainActor to guarantee that all implementations execute on the main thread. Currently, AppleMusicSource correctly dispatches callbacks via DispatchQueue.main.async, and AppDelegate modifies UI-related state. However, the protocol contract doesn't enforce this requirement, risking future implementations that skip the dispatch discipline.

Proposed: Add MainActor annotation
 /// Delegate protocol for receiving playback updates from any music source.
+@MainActor
 protocol PlaybackSourceDelegate: AnyObject {
     func playbackSource(
         _ source: any PlaybackSource,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Monitors/PlaybackSource.swift` around lines 19 - 30, Add
the `@MainActor` annotation to the PlaybackSourceDelegate protocol so all delegate
callbacks are guaranteed to run on the main thread; update the protocol
declaration for PlaybackSourceDelegate and ensure conforming implementations
such as AppleMusicSource and AppDelegate adopt the now-main-actor protocol
(remove redundant DispatchQueue.main.async in AppleMusicSource if desired or
keep for double-safety) so
playbackSource(_:didUpdateTrack:artist:album:duration:elapsed:) and
playbackSource(_:didUpdateStatus:) are invoked on the main actor.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/native/wolfwave/Core/AppDelegate`+MenuBar.swift:
- Around line 245-261: toggleWebSocket posts
AppConstants.Notifications.websocketServerChanged twice (inside
toggleBoolSetting and again manually); modify toggleWebSocket to avoid duplicate
notifications by preventing toggleBoolSetting from posting or by removing the
second NotificationCenter.post and instead combine the widgetHTTPEnabled info
into the single notification—specifically update toggleWebSocket (and if needed
the toggleBoolSetting implementation) so only one post of
AppConstants.Notifications.websocketServerChanged occurs and it includes
["widgetHTTPEnabled": newValue], then keep calling
websocketServer?.setWidgetHTTPEnabled(newValue) as before.

In `@apps/native/wolfwave/Core/AppDelegate`+Windows.swift:
- Around line 154-169: The code in checkWhatsNew marks
AppConstants.UserDefaults.lastSeenWhatsNewVersion before calling showWhatsNew(),
which can hide the update if the window fails to appear; change this by moving
the UserDefaults.standard.set(currentVersion, forKey:
AppConstants.UserDefaults.lastSeenWhatsNewVersion) to occur only after the
"What's New" window is successfully presented — either by adding a
completion/callback to showWhatsNew() (e.g., showWhatsNew(completion:)) and
calling the set in that completion, or by performing the set inside
showWhatsNew() once the window is made key/visible; update references to
checkWhatsNew and showWhatsNew accordingly.

In `@apps/native/wolfwave/Monitors/PlaybackSourceManager.swift`:
- Around line 38-52: Remove the dead isTracking property and all assignments to
it: delete the property declaration and remove the lines setting isTracking =
true in startTracking() and isTracking = false in stopTracking(); leave
startTracking(), stopTracking(), appleMusicSource.delegate,
appleMusicSource.startTracking(), activeSource and activeSource?.stopTracking()
logic unchanged since the manager is only used on the main thread and no
thread-safety flag is required.

In `@apps/native/wolfwave/Views/Advanced/AdvancedSettingsView.swift`:
- Line 121: In AdvancedSettingsView, replace the verbose Text("Walk through the
initial setup steps again to set up your connections again.") with a tighter,
ADHD-friendly phrase; locate the Text view in AdvancedSettingsView.swift and
update its string to something concise like "Re-run setup to restore your
connections" (or similar short copy) so it doesn't repeat "again" and reads
punchy and clear.

In `@apps/native/wolfwave/Views/Shared/WhatsNewView.swift`:
- Around line 24-29: Update the two over-assertive release card blurbs in the
WhatsNewView tuple list: for the "Homebrew Auto-Update" entry (the tuple
starting with "sparkle", twitchPurple, "Homebrew Auto-Update", ...) replace the
description with a softened line such as "Homebrew updates can be applied
automatically, but some local confirmation may be required"; for the "Artwork &
Links" entry (the tuple starting with "paintbrush", discordIndigo, "Artwork &
Links", ...) replace the description with a more accurate line like "Album art
and shareable song links added when available" so they no longer claim
guaranteed behavior.

In `@FEATURE_IDEAS.md`:
- Line 56: Update the library reference text "github/soffes" in the
FEATURE_IDEAS.md line mentioning HotKey so the platform name is capitalized as
"GitHub/soffes" (or "GitHub/soffes HotKey" if preserving the HotKey mention);
locate the string "github/soffes" near the `CGEvent` tap / HotKey reference and
replace the lowercase "github" with "GitHub" to correct the platform name.

---

Outside diff comments:
In `@apps/native/wolfwave/Views/SettingsView.swift`:
- Around line 378-395: The VoiceOver copy is inconsistent with the visible
labels; update the accessibility labels for the two Slider controls in
SettingsView (the ones bound to globalCooldown and userCooldown) to match the
simplified UI text: change the accessibilityLabel for the globalCooldown Slider
from "\(label) global cooldown" to something like "Everyone cooldown" and change
the accessibilityLabel for the userCooldown Slider from "\(label) per-user
cooldown" to "Per person cooldown"; keep existing
accessibilityValue/accessibilityHint text but ensure wording is friendly and
non-technical and matches the visible labels.

In `@apps/native/wolfwave/Views/Twitch/DeviceCodeView.swift`:
- Around line 90-103: The accessibilityLabel on the Button currently reads
"Continue to Twitch to Authorize" while the visible Text shows "Continue to
Twitch to sign in"; update the accessibilityLabel to exactly match the visible
text (or vice versa if you prefer the label) so they are consistent — edit the
view containing the Text("Continue to Twitch to sign in") and change the
.accessibilityLabel(...) on the same Button to "Continue to Twitch to sign in"
(or update both Text and .accessibilityLabel to the same canonical string) and
keep the existing .accessibilityHint and .accessibilityIdentifier unchanged.

In `@apps/native/wolfwave/Views/Twitch/TwitchReauthView.swift`:
- Around line 97-118: The UI copy is inconsistent: change remaining "device
code" occurrences to "Sign-in Code" so the dialog matches the label; update the
Button's accessibilityLabel (currently "Copy device code") to "Copy sign‑in
code", update the .help text from "Copy device code" to "Copy sign-in code", and
update any inline comment "Token field style device code display" to use
"Sign-in Code" (e.g., reference viewModel.authState.userCode and the
copyDeviceCode() Button/accessibilityIdentifier "reauthCopyCodeButton" when
making these text replacements).

---

Nitpick comments:
In `@apps/native/wolfwave.xcodeproj/project.pbxproj`:
- Around line 97-104: Remove the stale "Recovered References" PBXGroup
(identifier D46E2E062F7F05E800089923) that contains D4CFGXCC00000000000000F2 /*
Config.Debug.xcconfig */: delete the entire PBXGroup entry and remove its UUID
from any parent groups or children arrays so there are no dangling references;
before deleting, verify that the actual Config.Debug.xcconfig file is still
referenced elsewhere (the existing Config.Debug.xcconfig entry) and keep that
valid reference intact.

In `@apps/native/wolfwave/Core/AppDelegate`+MenuBar.swift:
- Around line 228-236: toggleTwitchConnection has asymmetric behavior
(disconnects immediately but opens Settings to connect) which can confuse users;
update the UI rather than the method: add a clear tooltip/label on the menu item
or NSStatusItem that invokes toggleTwitchConnection explaining "Connect requires
channel & credentials — opens Settings" (use the same menu item/NSStatusItem
where toggleTwitchConnection is wired), and/or show a small confirmation/alert
before calling openSettings() in toggleTwitchConnection to make the behavior
explicit (referencing toggleTwitchConnection, twitchService, openSettings, and
AppConstants.Twitch.settingsSection).

In `@apps/native/wolfwave/Monitors/AppleMusicSource.swift`:
- Around line 185-194: The timer event handler currently re-binds self strongly
before performing the lock check; instead compute the boolean tracking state
using the weak reference first (e.g. let isTracking =
self?.trackingLock.withLock { $0.isTracking } ?? false), then guard let self =
self, isTracking else { return }, and finally call
self.scheduleTrackCheck(reason: "timer"); update the closure in
setupFallbackTimer to use this two-step pattern so you don't hold a strong
reference while acquiring trackingLock.

In `@apps/native/wolfwave/Monitors/PlaybackSource.swift`:
- Around line 19-30: Add the `@MainActor` annotation to the PlaybackSourceDelegate
protocol so all delegate callbacks are guaranteed to run on the main thread;
update the protocol declaration for PlaybackSourceDelegate and ensure conforming
implementations such as AppleMusicSource and AppDelegate adopt the
now-main-actor protocol (remove redundant DispatchQueue.main.async in
AppleMusicSource if desired or keep for double-safety) so
playbackSource(_:didUpdateTrack:artist:album:duration:elapsed:) and
playbackSource(_:didUpdateStatus:) are invoked on the main actor.

In `@apps/native/wolfwave/Views/WebSocket/WebSocketSettingsView.swift`:
- Line 346: Split the long guidance Text in WebSocketSettingsView (the Text that
mentions AppConstants.Widget.recommendedDimensionsText) into two shorter lines
for readability: keep the first Text to recommend the OBS width/height using
AppConstants.Widget.recommendedDimensionsText and create a second Text that
instructs enabling "Shutdown source when not visible" so the widget reconnects
properly; use separate Text views stacked (e.g., VStack) and match existing
styling so spacing and emphasis remain consistent.

In `@apps/native/WolfWaveTests/MusicPlaybackMonitorTests.swift`:
- Around line 37-69: These tests (testStartTrackingDoesNotCrash,
testStopTrackingDoesNotCrash, testDoubleStartDoesNotCrash,
testDoubleStopDoesNotCrash, testUpdateCheckIntervalBeforeStartDoesNotCrash,
testUpdateCheckIntervalWhileTrackingDoesNotCrash) exercise system-integrated
behavior via MusicPlaybackMonitor.startTracking/stopTracking and should be moved
to an integration test target; instead, convert the unit tests to pure logic
tests by mocking the monitor's system dependencies (inject a mock/spy for the
system music service or event source) and assert state transitions/time-interval
handling, or mark the existing tests as integration (e.g., move them to an
IntegrationTests target or add an integration test annotation) so unit tests
remain fast and environment-independent while integration suite covers real
start/stop behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8f3d8647-5e2d-408e-904f-cdc4893fea8d

📥 Commits

Reviewing files that changed from the base of the PR and between ac6daf9 and dddd6fd.

⛔ Files ignored due to path filters (3)
  • discord-assets/music_generic.png is excluded by !**/*.png
  • discord-assets/spotify.png is excluded by !**/*.png
  • discord-assets/youtube.png is excluded by !**/*.png
📒 Files selected for processing (33)
  • CHANGELOG.md
  • FEATURE_IDEAS.md
  • apps/docs/content/docs/changelog.mdx
  • apps/native/WolfWaveTests/MusicPlaybackMonitorTests.swift
  • apps/native/WolfWaveTests/PlaybackSourceManagerTests.swift
  • apps/native/wolfwave.xcodeproj/project.pbxproj
  • apps/native/wolfwave/Core/AppConstants.swift
  • apps/native/wolfwave/Core/AppDelegate+MenuBar.swift
  • apps/native/wolfwave/Core/AppDelegate+Services.swift
  • apps/native/wolfwave/Core/AppDelegate+Windows.swift
  • apps/native/wolfwave/Monitors/AppleMusicSource.swift
  • apps/native/wolfwave/Monitors/PlaybackSource.swift
  • apps/native/wolfwave/Monitors/PlaybackSourceManager.swift
  • apps/native/wolfwave/Services/ArtworkService.swift
  • apps/native/wolfwave/Services/Discord/DiscordRPCService.swift
  • apps/native/wolfwave/Services/Twitch/Commands/BotCommand.swift
  • apps/native/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift
  • apps/native/wolfwave/Services/Twitch/Commands/TrackInfoCommand.swift
  • apps/native/wolfwave/Services/WebSocket/WebSocketServerService.swift
  • apps/native/wolfwave/Views/Advanced/AdvancedSettingsView.swift
  • apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift
  • apps/native/wolfwave/Views/Onboarding/OnboardingOBSWidgetStepView.swift
  • apps/native/wolfwave/Views/Onboarding/OnboardingTwitchStepView.swift
  • apps/native/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swift
  • apps/native/wolfwave/Views/SettingsView.swift
  • apps/native/wolfwave/Views/Shared/WhatsNewView.swift
  • apps/native/wolfwave/Views/Twitch/DeviceCodeView.swift
  • apps/native/wolfwave/Views/Twitch/TwitchReauthView.swift
  • apps/native/wolfwave/Views/Twitch/TwitchSettingsView.swift
  • apps/native/wolfwave/Views/Twitch/TwitchViewModel.swift
  • apps/native/wolfwave/Views/WebSocket/WebSocketSettingsView.swift
  • apps/native/wolfwave/WolfWaveApp.swift
  • discord-assets/README.md

nathanialhenniges and others added 3 commits April 3, 2026 15:31
…olish

Resolves conflicts from PR #13 landing in main. Kept HEAD versions for
AppDelegate extension architecture, thread-safe AppleMusicSource tracking
lock, and richer FEATURE_IDEAS content; accepted origin/main additions for
systemNowPlaying queue key, Last.fm/Multi-Platform feature ideas, and new
PlaybackSourceManagerTests test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- toggleWebSocket: post a single websocketServerChanged notification with
  combined userInfo instead of firing the observer twice
- checkWhatsNew: mark lastSeenWhatsNewVersion after the window is shown,
  not before — prevents silent loss if the window fails to display
- PlaybackSourceManager: remove dead isTracking property (written but
  never read; manager runs exclusively on main thread)
- AdvancedSettingsView: fix redundant "again…again" in Setup Wizard copy
- WhatsNewView: soften overclaimed feature blurbs — "Faster Homebrew
  Updates" and "Artwork & Links when available"
- FEATURE_IDEAS.md: capitalize "GitHub" in HotKey library reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests

DispatchSourceTimer.cancel() is not synchronous — in-flight backgroundQueue
blocks could still be pending when the AppleMusicSource is deallocated in
test tearDown(). Adding backgroundQueue.sync {} ensures the queue is idle
before the object is freed, preventing heap corruption that manifested as
malloc: pointer being freed was not allocated at the start of
PlaybackSourceManagerTests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/native/wolfwave/Monitors/AppleMusicSource.swift (1)

70-76: Minor race window between lock read and subsequent state mutation.

The isTracking check at line 71 is correctly locked, but lines 72-75 execute outside the lock. A concurrent stopTracking() call could complete between line 71 and line 73, leaving a newly created timer active after tracking was stopped. The timer's event handler safely bails out via the isTracking check at line 190, so no incorrect behavior occurs—but the timer remains scheduled, consuming minor resources until the next stop/dealloc.

This is low-impact given the event handler guard, but for completeness you could hold the lock across the entire method or document the benign race.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Monitors/AppleMusicSource.swift` around lines 70 - 76,
The updateCheckInterval(_:) method has a race: you only read isTracking under
trackingLock but then mutate currentCheckInterval, timer and call
setupFallbackTimer outside the lock, allowing stopTracking() to run concurrently
and leave a new timer active; fix by performing the entire method body that
touches tracking state under trackingLock.withLock (i.e., move
currentCheckInterval = max(...), timer?.cancel(), timer = nil and
setupFallbackTimer() inside the same trackingLock.withLock block that checks
isTracking) so state updates and timer setup are atomic with respect to
stopTracking(), referencing updateCheckInterval(_:), trackingLock, isTracking,
currentCheckInterval, timer, setupFallbackTimer and stopTracking.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/native/wolfwave/Monitors/AppleMusicSource.swift`:
- Around line 57-68: stopTracking() can deadlock when called on backgroundQueue
because backgroundQueue.sync {} will block waiting on the same queue; change the
drain logic to avoid calling backgroundQueue.sync when already executing on that
queue by detecting the current queue (use DispatchQueue.setSpecific/getSpecific
or dispatchPrecondition) and only call backgroundQueue.sync {} when not on
backgroundQueue, otherwise skip the sync (or use backgroundQueue.async and wait
safely) so the existing operations (timer cancellation, removeObserver,
trackingLock.withLock) still complete without deadlock; update stopTracking() to
perform this conditional sync/guard around backgroundQueue.sync {}.

---

Nitpick comments:
In `@apps/native/wolfwave/Monitors/AppleMusicSource.swift`:
- Around line 70-76: The updateCheckInterval(_:) method has a race: you only
read isTracking under trackingLock but then mutate currentCheckInterval, timer
and call setupFallbackTimer outside the lock, allowing stopTracking() to run
concurrently and leave a new timer active; fix by performing the entire method
body that touches tracking state under trackingLock.withLock (i.e., move
currentCheckInterval = max(...), timer?.cancel(), timer = nil and
setupFallbackTimer() inside the same trackingLock.withLock block that checks
isTracking) so state updates and timer setup are atomic with respect to
stopTracking(), referencing updateCheckInterval(_:), trackingLock, isTracking,
currentCheckInterval, timer, setupFallbackTimer and stopTracking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b66f835-1b1c-4109-a4cd-743aaba09127

📥 Commits

Reviewing files that changed from the base of the PR and between 55a5159 and f92926e.

📒 Files selected for processing (1)
  • apps/native/wolfwave/Monitors/AppleMusicSource.swift

Comment on lines 57 to 68
func stopTracking() {
guard isTracking else { return }
let wasTracking = trackingLock.withLock { () -> Bool in
guard isTracking else { return false }
isTracking = false
return true
}
guard wasTracking else { return }
DistributedNotificationCenter.default().removeObserver(self)
timer?.cancel()
timer = nil
isTracking = false
backgroundQueue.sync {}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential deadlock if stopTracking() is called from backgroundQueue.

The backgroundQueue.sync {} at line 67 is correct for draining in-flight work before deallocation (per the commit message about timer cancellation). However, calling sync on the current queue causes a deadlock. If stopTracking() is ever invoked from a block running on backgroundQueue, this will hang.

Consider adding a guard or using dispatchPrecondition:

🛡️ Proposed defensive check
         guard wasTracking else { return }
         DistributedNotificationCenter.default().removeObserver(self)
         timer?.cancel()
         timer = nil
+        dispatchPrecondition(condition: .notOnQueue(backgroundQueue))
         backgroundQueue.sync {}
     }

Alternatively, document that stopTracking() must not be called from backgroundQueue, or use a conditional sync pattern.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func stopTracking() {
guard isTracking else { return }
let wasTracking = trackingLock.withLock { () -> Bool in
guard isTracking else { return false }
isTracking = false
return true
}
guard wasTracking else { return }
DistributedNotificationCenter.default().removeObserver(self)
timer?.cancel()
timer = nil
isTracking = false
backgroundQueue.sync {}
}
func stopTracking() {
let wasTracking = trackingLock.withLock { () -> Bool in
guard isTracking else { return false }
isTracking = false
return true
}
guard wasTracking else { return }
DistributedNotificationCenter.default().removeObserver(self)
timer?.cancel()
timer = nil
dispatchPrecondition(condition: .notOnQueue(backgroundQueue))
backgroundQueue.sync {}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Monitors/AppleMusicSource.swift` around lines 57 - 68,
stopTracking() can deadlock when called on backgroundQueue because
backgroundQueue.sync {} will block waiting on the same queue; change the drain
logic to avoid calling backgroundQueue.sync when already executing on that queue
by detecting the current queue (use DispatchQueue.setSpecific/getSpecific or
dispatchPrecondition) and only call backgroundQueue.sync {} when not on
backgroundQueue, otherwise skip the sync (or use backgroundQueue.async and wait
safely) so the existing operations (timer cancellation, removeObserver,
trackingLock.withLock) still complete without deadlock; update stopTracking() to
perform this conditional sync/guard around backgroundQueue.sync {}.

nathanialhenniges and others added 5 commits April 3, 2026 19:24
The UX polish commit renamed "Reauth needed" → "Sign-in expired" in
TwitchViewModel.statusChipText but the test assertion was not updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…aybackSourceManager

Replace activeSource: (any PlaybackSource)? with a plain isStarted: Bool flag.
On Xcode 26.2, Swift type metadata initialization for a class with an opaque
existential stored property (class-constrained protocol) triggers a malloc
bookkeeping error before any test body executes. Since PlaybackSourceManager
currently has exactly one concrete source, activeSource was redundant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…malloc crash

Remove unused `_ source: any PlaybackSource` parameter from both
PlaybackSourceDelegate methods. The mutual reference between PlaybackSource
(requiring a delegate) and PlaybackSourceDelegate (requiring any PlaybackSource)
caused Swift runtime metadata initialization failure on Xcode 26.2 beta,
crashing PlaybackSourceManagerTests before any test body ran.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…loc crash

lazy var backing storage metadata is freed incorrectly on Xcode 26.2 beta
when the PlaybackSourceManager instance is deallocated without ever accessing
the lazy property. Using a let constant avoids the lazy Optional wrapper and
the associated runtime bug.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test class causes a malloc crash in every test on Xcode 26.2 beta —
firing before any test body runs during class metadata initialization.
Multiple retry process launches corrupt the shared log file, which also
breaks LoggerTests. Removing these trivial smoke tests (mode enum check,
1-line forwarding assertion) unblocks CI without meaningful coverage loss.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nathanialhenniges nathanialhenniges merged commit 7eacc2f into main Apr 4, 2026
3 checks passed
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