Add Twitch widget with Google Fonts and appearance controls#7
Add Twitch widget with Google Fonts and appearance controls#7nathanialhenniges merged 15 commits intomainfrom
Conversation
- Update CLAUDE.md with Discord service, CI/CD, docs site, and current stack - Update README description and dark/light mode logo - Add Discord Rich Presence to homepage feature grid - Fix Discord support link to mrdwolf.net - Split docs sidebar into Guide and Developers sections
Implement a local WebSocket server (Network.framework NWListener) that broadcasts now-playing JSON to connected clients, and a React widget page on the docs site for use as an OBS browser source overlay. Swift: - WebSocketServerService with multi-client support, progress timer, and auto-retry on failure - Functional WebSocket settings view replacing WIP placeholder - AppDelegate wiring for music playback and artwork URL forwarding - Network server entitlement for App Sandbox - Missing AppConstants (Update, URLs, notifications) that blocked build - MusicPlaybackMonitor delegate updated to pass duration/elapsed Docs: - /widget page with WebSocket client, album art blur background, progress bar with requestAnimationFrame interpolation, auto-reconnect - Supports port, duration (auto-hide), and hideAlbumArt URL params Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename sidebar label from "WebSocket" to "OBS Widget" in settings - Fix App Visibility card to be full width in detail pane - Add Discord and OBS Widget steps to onboarding wizard (4 steps) - Update welcome step with Discord/OBS feature highlights - Update README, features, usage, installation, development, and architecture docs to reflect OBS Widget naming and new onboarding - Fix pre-existing TwitchViewModelTests referencing missing ChannelValidationState enum - Fix double notification in WolfWaveApp WebSocket state callback - Update onboarding tests for 4-step navigation (142 tests pass) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Streamline doc comments across all files to be concise and professional, removing verbose multi-paragraph descriptions and redundant inline comments while keeping MARK headers. Add a WolfWave icon placeholder in the stream overlay widget when album artwork hasn't loaded yet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap setReauthNeeded call in MainActor.run to eliminate the 'no async operations occur within await' compiler warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve all outstanding compliance items: code signing, notarization, and credential security now pass. Add WW-17 WebSocket entitlements and Network.framework dependency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove DispatchQueue.main.asyncAfter delay before opening settings on token expiration — the delayed dispatch could race with notification tap activation, causing duplicate window creation and a crash. Replace the disabled "Connect" button with an orange "Re-auth" button when reauthNeeded is true, giving users a clear action to re-authorize their expired Twitch session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Music Monitor and App Visibility into Advanced, rename Discord to Discord Integration, and fix locale-aware port number formatting (8,765 → 8765) by using Text(verbatim:). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Return false from applicationShouldHandleReopen to prevent AppKit's default window restoration from conflicting with manual openSettings(). Clear settingsWindow reference on close so restoreMenuOnlyIfNeeded correctly detects no visible windows remain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug fixes: - Fix force unwrap crash in DiscordRPCService socket handling - Fix unlimited reconnect loop in TwitchChatService network monitor - Add retry mechanism with exponential backoff for Twitch chat messages - Fix notification observer leak in TwitchViewModel - Replace debug print() calls with Log utility Features: - Add bot command cooldowns with global/per-user limits and mod bypass - Add in-app update banner in settings - Add log file writing and export button in Advanced settings - Add Apple Music permission detection and guidance - Add Copy Widget URL menu item to status bar - Add widget customization (theme, layout, colors, font) with WebSocket broadcast - Add Test Connection buttons for Twitch (token validation) and Discord (IPC check) Code quality: - Extract hardcoded values to AppConstants (Twitch, Widget, WebSocket) - Unify duplicate artwork fetching into shared ArtworkService singleton - Add Color(hex:) extension for hex color parsing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Expand the widget font picker from 4 built-in options to 16 fonts (4 built-in + 12 Google Fonts) with sectioned picker UI. All Google Fonts are preloaded in a single request on widget mount for instant switching. Also adds an "Open in browser" button for the widget URL, hides the widget when WebSocket disconnects, and fixes actor isolation warnings in Logger. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. WalkthroughThis PR introduces an OBS Stream Widget feature that broadcasts now-playing music data via a local WebSocket server. It includes a web-based widget renderer in Next.js, power state monitoring for efficiency, comprehensive logging with file export, Twitch bot command cooldown management, and artwork fetching. Documentation and onboarding flows are updated to reflect the new widget-centric approach. Changes
Sequence Diagram(s)sequenceDiagram
actor User as User
participant OBS as OBS Studio
participant Widget as OBS Widget<br/>(Browser Source)
participant WS as WebSocket Server<br/>(WolfWave)
participant Music as Music App
User->>WolfWave: Enable OBS Widget
activate WS
WS->>WS: Start WebSocket Server<br/>(port 8765)
deactivate WS
User->>OBS: Add Browser Source<br/>(Widget URL)
activate Widget
Widget->>WS: Connect WebSocket
activate WS
WS->>Widget: Send Welcome Message
deactivate WS
Music->>WolfWave: Track Changed
WolfWave->>WolfWave: Fetch Album Artwork<br/>(ArtworkService)
WolfWave->>WS: Broadcast now_playing<br/>(track, artist, artwork)
activate WS
WS->>Widget: Send now_playing
deactivate WS
Widget->>Widget: Render Track Info<br/>& Album Art
loop Every 1 Second
WolfWave->>WS: Broadcast progress<br/>(elapsed time)
activate WS
WS->>Widget: Send progress
deactivate WS
Widget->>Widget: Update Progress Bar
end
sequenceDiagram
participant PowerMon as PowerStateMonitor
participant MusicMon as MusicPlaybackMonitor
participant TwitchChat as TwitchChatService
participant System as System
System->>PowerMon: Power State Changed<br/>(Low Power Mode)
activate PowerMon
PowerMon->>PowerMon: Update isReducedMode
PowerMon->>PowerMon: Post powerStateChanged
deactivate PowerMon
PowerMon->>MusicMon: Notify Power State Change
activate MusicMon
MusicMon->>MusicMon: updateCheckInterval()<br/>(reduced interval)
MusicMon->>MusicMon: Reschedule Timer
deactivate MusicMon
PowerMon->>TwitchChat: Notify Power State Change
activate TwitchChat
TwitchChat->>TwitchChat: Adjust Poll Interval<br/>(reduced)
deactivate TwitchChat
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Resolve build errors from dev merge: - Remove duplicate URLs and Update enums in AppConstants - Add missing NSLock to BotCommandDispatcher - Fix duplicate onReauth and missing channelValidationState in TwitchSettingsView - Add missing state properties to AdvancedSettingsView (update checker) - Add missing now-playing state and helpers to MusicMonitorSettingsView - Fix hasClientID computed property assignment in DiscordSettingsView Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
src/wolfwave/Views/Twitch/TwitchSettingsView.swift (1)
283-289:⚠️ Potential issue | 🔴 Critical
onReauthis declared twice — build-breaking redeclaration confirmed by CI.The property
var onReauth: () -> Voidexists at line 283 (pre-existing) and is re-declared at line 288 (new). Swift does not permit duplicate stored-property names; this is the exact error surfaced by the pipeline (invalid redeclaration of 'onReauth').Remove the duplicate at line 288 and keep only the single declaration, then append
onTestConnectionafter it:🐛 Proposed fix
var onReauth: () -> Void // line 283 — keep this one var onClearCredentials: () -> Void var onJoinChannel: () -> Void var onLeaveChannel: () -> Void var onChannelIDChanged: () -> Void - var onReauth: () -> Void // line 288 — remove duplicate var onTestConnection: () -> Void🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Twitch/TwitchSettingsView.swift` around lines 283 - 289, The property onReauth is declared twice causing a redeclaration error; remove the duplicate declaration of onReauth (the second occurrence) in the TwitchSettingsView where the properties are listed, keep the original onReauth declaration, and ensure the following property list includes onTestConnection right after onReauth so the final sequence contains a single onReauth and then onTestConnection (symbols: onReauth, onTestConnection, onClearCredentials, onJoinChannel, onLeaveChannel, onChannelIDChanged).src/wolfwave/Views/Advanced/AdvancedSettingsView.swift (2)
11-28:⚠️ Potential issue | 🟡 MinorStale DocC comment — still describes the removed software-update UI.
The doc comment on
AdvancedSettingsViewstill lists "Checking for software updates (manual and automatic)" and "Software Update card shows current version…" as features, but that section was replaced by the Diagnostics / Music Monitor / App Visibility cards in this PR.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Advanced/AdvancedSettingsView.swift` around lines 11 - 28, The doc comment above AdvancedSettingsView is stale — it still documents the removed software-update UI and related actions; update the top-level comment for the AdvancedSettingsView type to reflect the current cards (Diagnostics, Music Monitor, App Visibility) and current actions/state (bindings like showingResetAlert, use of AppDelegate context, Reset Onboarding/Reset All behavior) and remove references to software update checking, update availability, and install instructions; edit the comment block that starts the file (the triple-slash summary and bullet lists) so the description and bullet points match the actual UI and methods in AdvancedSettingsView and its resetSettings() usage.
193-199:⚠️ Potential issue | 🔴 CriticalRemove or fix the
onReceivehandler and dead code — both reference undeclared variables.The
onReceiveblock (lines 193–199) assigns tolatestVersion,updateAvailable, andskippedVersion, which have no@Statedeclarations. ThesoftwareUpdateCard@ViewBuilder(lines 204–340) references these same variables pluscurrentVersion,appDelegate,updateCheckEnabled, andisCheckingForUpdates—none of which are declared in this view. TheopenDownloadURL()function (lines 346–353) also references the undeclaredappDelegate.This causes immediate compilation failure. Either restore the missing property declarations or remove the notification handler and dead update-related code entirely.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Advanced/AdvancedSettingsView.swift` around lines 193 - 199, The onReceive handler and related update UI/code reference undeclared state and app delegate variables (latestVersion, updateAvailable, skippedVersion, currentVersion, appDelegate, updateCheckEnabled, isCheckingForUpdates, softwareUpdateCard, openDownloadURL) and must be removed or restored; to fix quickly, delete the onReceive block and remove or comment out the softwareUpdateCard ViewBuilder and the openDownloadURL() function (and any references to the above variables) from AdvancedSettingsView so no undeclared symbols remain, or alternatively reintroduce the missing `@State/`@Environment/@ObservedObject properties with matching names and types and update openDownloadURL() to use a valid app delegate reference if you prefer to keep the update feature.docs/content/docs/installation.mdx (1)
32-32:⚠️ Potential issue | 🟡 Minor"3-step guided setup" is now inaccurate — four steps are described.
With the new OBS Stream Widget step added after Discord (lines 69–79), the wizard now has four steps but the intro still says "a 3-step guided setup."
📝 Proposed fix
-On first launch, WolfWave walks you through a 3-step guided setup: +On first launch, WolfWave walks you through a 4-step guided setup:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/content/docs/installation.mdx` at line 32, Update the introductory sentence that currently reads "On first launch, WolfWave walks you through a 3-step guided setup:" to reflect the added OBS Stream Widget step by changing it to "a 4-step guided setup:" (or "four-step guided setup:") so the intro matches the four steps described later in the document; locate and edit the exact string in the installation.mdx content where that sentence appears.docs/content/docs/development.mdx (1)
29-29:⚠️ Potential issue | 🟡 MinorStale test count in the command table.
The table on line 29 says "124 tests" but summing the Test Coverage table below (17+18+16+14+15+26+22+15) yields 143. Update the count to stay consistent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/content/docs/development.mdx` at line 29, The "make test" row in the commands table shows a stale test count ("124 tests"); update that table entry (the row containing `make test`) to reflect the correct total of 143 tests (sum of the Test Coverage table: 17+18+16+14+15+26+22+15 = 143), and re-run or verify test counts if tests have changed before committing to ensure the number stays accurate.src/wolfwave/Views/Discord/DiscordSettingsView.swift (1)
128-131:⚠️ Potential issue | 🔴 CriticalCompile error: assigning to a computed property.
Line 129 assigns to
hasClientID, but it was changed to a computed property (lines 36–38). This will fail to compile.🐛 Fix: remove the assignment since it's now computed
.onAppear { - hasClientID = DiscordRPCService.resolveClientID() != nil refreshConnectionState() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Discord/DiscordSettingsView.swift` around lines 128 - 131, Remove the assignment to the computed property hasClientID inside the .onAppear closure (the line calling DiscordRPCService.resolveClientID() and assigning to hasClientID) since hasClientID is now a computed property; keep the refreshConnectionState() call so the view still updates on appear. Locate the .onAppear block in DiscordSettingsView where hasClientID is assigned and delete that assignment, leaving only the call to refreshConnectionState().src/wolfwave/Services/Twitch/TwitchChatService.swift (1)
890-911:⚠️ Potential issue | 🟠 MajorRetry counter resets to zero on every send failure — retries are unbounded.
When
sendMessagefails (line 908), it callsqueueMessageForRetry(…, attempts: 0). However,retryPendingMessages(line 956) callssendMessagewhich, on failure, re-queues atattempts: 0again — discarding the accumulated attempt count fromPendingMessage.attempts. This creates an infinite 1-second retry loop for persistently failing messages, never reachingmaxMessageRetries.Contrast with line 949–952 (the "still not connected" path), which correctly preserves
message.attempts.🐛 Proposed fix — propagate the attempt count through sendMessage
One approach: add an internal
attemptsparameter to the send path:func sendMessage(_ message: String, replyTo parentMessageID: String?) { + sendMessage(message, replyTo: parentMessageID, attempts: 0) + } + + private func sendMessage(_ message: String, replyTo parentMessageID: String?, attempts: Int) { guard let broadcasterID = broadcasterID, let botID = botID, let token = oauthToken, let clientID = clientID else { Log.warn("Twitch: Not connected, queuing message for retry", category: "TwitchChat") - queueMessageForRetry(message: message, parentMessageID: parentMessageID, attempts: 0) + queueMessageForRetry(message: message, parentMessageID: parentMessageID, attempts: attempts) return } // ... truncation and body construction unchanged ... sendAPIRequest( // ... unchanged ... ) { [weak self] result in switch result { case .success(let data): // ... unchanged ... case .failure(let error): Log.error( "Twitch: Failed to send message - \(error.localizedDescription)", category: "TwitchChat") - self?.queueMessageForRetry( - message: message, parentMessageID: parentMessageID, attempts: 0) + self?.queueMessageForRetry( + message: message, parentMessageID: parentMessageID, attempts: attempts) } } }Then in
retryPendingMessages:- sendMessage(message.message, replyTo: message.parentMessageID) + sendMessage(message.message, replyTo: message.parentMessageID, attempts: message.attempts)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/Twitch/TwitchChatService.swift` around lines 890 - 911, The retry logic resets attempts to zero on every send failure because sendMessage calls queueMessageForRetry(message:parentMessageID:attempts: 0) instead of preserving/incrementing the PendingMessage.attempts, so messages never reach maxMessageRetries; fix by propagating and incrementing the attempt count through the send path—either add an internal attempts parameter to sendMessage or, in the failure branch of sendMessage, call queueMessageForRetry with attempts: (message.attempts ?? 0) + 1 (or message.attempts + 1) so retryPendingMessages and queueMessageForRetry use and respect PendingMessage.attempts and maxMessageRetries consistently (update references in sendMessage, retryPendingMessages, and queueMessageForRetry).src/wolfwave/Views/SettingsView.swift (1)
58-73:⚠️ Potential issue | 🟡 MinorAdd missing Discord sidebar icon.
Discord section has no sidebar icon. The code comment on line 62 states
.discord: return nil // Uses custom image, indicating intent to use a custom icon. However, thecustomIconproperty (lines 68–73) only includes a case for.twitchIntegration, leaving Discord withnilfor bothsystemIconandcustomIcon. ThesidebarIconview builder (lines 243–254) renders nothing when both arenil, resulting in an empty icon slot. TheDiscordLogoasset already exists in the project and should be referenced.Fix
var customIcon: String? { switch self { case .twitchIntegration: return "TwitchLogo" + case .discord: return "DiscordLogo" default: return nil } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/SettingsView.swift` around lines 58 - 73, The Discord sidebar icon is missing because customIcon only handles .twitchIntegration; update the Custom icon provider by adding a case for .discord in the customIcon computed property (alongside the existing .twitchIntegration case) to return the existing "DiscordLogo" asset (leave systemIcon's .discord as nil); this ensures the sidebarIcon view builder will render the DiscordLogo image when building the sidebar.
🧹 Nitpick comments (31)
src/wolfwave/Views/Twitch/TwitchDeviceAuthDialog.swift (1)
149-156: Consider migratingDispatchQueue.main.asyncAftertoTask.sleep/ async/await.Both delayed callbacks use
DispatchQueue.main.asyncAfter, which is inconsistent with the project's guideline to preferasync/awaitoverDispatchQueuefor async work. These are pre-existing but worth cleaning up opportunistically.♻️ Suggested async/await replacements
private func handleAuthorizePressed() { withAnimation(.easeInOut(duration: 0.3)) { isWaiting = true } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if let encodedCode = deviceCode.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), - let url = URL(string: "https://www.twitch.tv/activate?device_code=\(encodedCode)") { - NSWorkspace.shared.open(url) - } - onAuthorizePressed() - } + Task { `@MainActor` in + try? await Task.sleep(for: .milliseconds(100)) + if let encodedCode = deviceCode.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: "https://www.twitch.tv/activate?device_code=\(encodedCode)") { + NSWorkspace.shared.open(url) + } + onAuthorizePressed() + } }private func copyDeviceCode() { // … - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - withAnimation(.easeInOut(duration: 0.2)) { - showCopyFeedback = false - isCodeCopied = false - } - } + Task { `@MainActor` in + try? await Task.sleep(for: .seconds(2)) + withAnimation(.easeInOut(duration: 0.2)) { + showCopyFeedback = false + isCodeCopied = false + } + } }As per coding guidelines: "Use async/await concurrency instead of DispatchQueue for new async work in Swift."
Also applies to: 171-176
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Twitch/TwitchDeviceAuthDialog.swift` around lines 149 - 156, Replace the DispatchQueue.main.asyncAfter usages with Swift concurrency: wrap the delayed work in a Task, mark UI work `@MainActor`, and use try await Task.sleep(nanoseconds: 100_000_000) for the 0.1s delay; specifically replace the block that uses DispatchQueue.main.asyncAfter around deviceCode, NSWorkspace.shared.open(...) and onAuthorizePressed() with Task { `@MainActor` in try await Task.sleep(nanoseconds: 100_000_000); if let encodedCode = deviceCode.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: "https://www.twitch.tv/activate?device_code=\(encodedCode)") { NSWorkspace.shared.open(url) } onAuthorizePressed() }, and make the same change for the other occurrence that currently uses DispatchQueue.main.asyncAfter so both follow async/await and run on the main actor.src/wolfwave/Views/Shared/ViewModifiers.swift (1)
138-151: MoveColorextension to a dedicated file
ViewModifiers.swiftis scoped toViewModifier-conforming types and theirViewhelper extensions. TheColor(hex:)initializer (lines 139–152) is a general utility extension that doesn't belong here. Createsrc/wolfwave/Views/Shared/Color+Hex.swiftand move the extension there to maintain file cohesion, especially as color-customization features (e.g., alpha-channel support) expand.♻️ Suggested file split
Create
src/wolfwave/Views/Shared/Color+Hex.swift:import SwiftUI // MARK: - Color Hex Initializer extension Color { /// Creates a Color from a hex string (e.g. "#FF0000" or "FF0000"). init?(hex: String) { var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines) if hexString.hasPrefix("#") { hexString.removeFirst() } guard hexString.count == 6, let value = UInt64(hexString, radix: 16) else { return nil } let r = Double((value >> 16) & 0xFF) / 255.0 let g = Double((value >> 8) & 0xFF) / 255.0 let b = Double(value & 0xFF) / 255.0 self.init(red: r, green: g, blue: b) } }Remove lines 139–152 from
ViewModifiers.swift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Shared/ViewModifiers.swift` around lines 138 - 151, Move the Color hex initializer into its own file: create a new Swift file named Color+Hex.swift, import SwiftUI, and paste the extension containing init?(hex:) (the Color extension/initializer shown) into that file; then remove the init?(hex:) extension block from ViewModifiers.swift so ViewModifiers.swift only contains ViewModifier-related code and the Color hex utility lives in Color+Hex.swift for future expansion (e.g., alpha support).src/wolfwave/Monitors/MusicPlaybackMonitor.swift (2)
93-105: Consider annotatingMusicPlaybackMonitor(orupdateCheckInterval) with@MainActorto encode the implicit thread contract.The class is clearly designed for main-thread use — delegate callbacks are dispatched to
DispatchQueue.main,DistributedNotificationCenterdelivers on the main thread, andstartTracking/stopTrackingare expected to be called from UI code.updateCheckIntervalmutatestimer,isTracking, andcurrentCheckIntervalwithout any synchronization. Today this is safe because all callers happen to be on the main thread, but a future caller dispatching from a background queue would introduce a data race with no compile-time warning.Marking the class
@MainActor(or at minimum annotatingupdateCheckIntervaland the other lifecycle methods) would enforce this invariant at compile time.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Monitors/MusicPlaybackMonitor.swift` around lines 93 - 105, MusicPlaybackMonitor mutates shared state (timer, isTracking, currentCheckInterval) on the implicit assumption callers are on the main thread; annotate the class or its lifecycle methods to enforce that contract. Add `@MainActor` to the MusicPlaybackMonitor declaration (or at minimum to updateCheckInterval, startTracking, stopTracking and any methods touching timer/isTracking/currentCheckInterval like setupFallbackTimer) so the compiler enforces main‑thread access and prevents data races.
154-154: Pre-existing delimiter injection risk is now more likely to produce silently wrong data rather than fail fast.The guard was relaxed from
== 3to>= 3so the 5-component format works, and the numeric fields (duration, elapsed) are safe since Double strings cannot contain|. However, if an artist or album name legitimately contains|, the old== 3guard would have silently dropped the track; the new>= 3guard lets it proceed withcomponents[1]/[2]containing wrong data andDouble(components[3])returning nil → duration/elapsed both zero with no diagnostic.This is a pre-existing design limitation (using a freeform delimiter in a combined string built from arbitrary user-supplied metadata), but worth addressing in a follow-up by switching to a safe serialisation — e.g. a null-byte joined string, or passing the five values as a small struct directly through
handleTrackInfotoprocessTrackInfoStringto avoid any delimiter parsing entirely.Also applies to: 187-198
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Monitors/MusicPlaybackMonitor.swift` at line 154, The combined string construction using the delimiter (variable combined) in MusicPlaybackMonitor allows delimiter injection and the relaxed guard (>= 3) causes silent misparses; update the code to stop using a freeform delimiter: change handleTrackInfo/processTrackInfoString to pass a small struct (e.g., TrackInfo with name, artist, album, duration, elapsed) instead of a single combined string, adjust callers to build and pass TrackInfo, and remove delimiter splitting logic; if you must keep string parsing as a short-term fix, enforce an exact component count (restore strict check for the expected number of components), validate Double parsing for duration/elapsed with error logging on failure, and return early on parse errors to avoid silent zero values.src/wolfwave/Views/Twitch/TwitchDeviceAuthWindowController.swift (1)
89-89:@Stateis misused in a non-Viewstruct
@Stateis designed exclusively for types conforming toSwiftUI.View. In a plain struct likeTwitchDeviceAuthWindow, the heap-allocated backing storage happens to work as a side-effect of the implementation, but this is undocumented and not guaranteed across Swift/SwiftUI versions. It also makes themutatingkeyword misleading (the setter goes through the reference-type backing store, so struct mutation is not actually required).A plain optional property is correct, idiomatic, and equally effective here:
♻️ Proposed refactor
struct TwitchDeviceAuthWindow { let deviceCode: String let onAuthorize: () -> Void let onCancel: () -> Void - `@State` private var retainedController: TwitchDeviceAuthWindowController? + private var retainedController: TwitchDeviceAuthWindowController?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Twitch/TwitchDeviceAuthWindowController.swift` at line 89, The property retainedController is incorrectly declared with `@State` inside the plain struct TwitchDeviceAuthWindow; remove the `@State` wrapper and change it to a normal optional property (e.g., private var retainedController: TwitchDeviceAuthWindowController?) so the struct uses ordinary storage; update any places that relied on SwiftUI state behavior (mutating setters or bindings) to directly set the optional on the struct instance or make the setter mutating if needed, and ensure TwitchDeviceAuthWindowController references remain unchanged.src/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift (1)
105-119: Combine the two.onAppearmodifiers into one.Having two
.onAppearmodifiers on the sameVStackworks (both fire), but it is fragile and non-idiomatic. Merge them into a single closure.♻️ Proposed fix
-.onAppear { - loadCurrentTrack() -} .onReceive(...) { ... } +.onAppear { + loadCurrentTrack() + checkMusicPermission() +} -.onAppear { - checkMusicPermission() -}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift` around lines 105 - 119, There are two .onAppear modifiers; merge them into a single .onAppear closure so both startup actions run from one place: call loadCurrentTrack() and checkMusicPermission() in the same closure (preserve order), leaving the existing .onReceive(NotificationCenter.default.publisher(...)) block intact; update the VStack (or the view that currently has the two .onAppear modifiers) to use one .onAppear that invokes loadCurrentTrack() and checkMusicPermission().src/wolfwave/Views/Advanced/AdvancedSettingsView.swift (1)
147-150: Log Export card uses inline card styling instead of.cardStyle().The Onboarding card directly above uses
.cardStyle(), but the new Log Export card manually applies.padding(),.background(), and.clipShape()without an.overlay()stroke, resulting in a visually inconsistent card. Apply.cardStyle()for consistency (or align the inline styling to match the Danger Zone card pattern if a distinct look is intended).♻️ Proposed fix
} -.padding(AppConstants.SettingsUI.cardPadding) -.background(Color(nsColor: .controlBackgroundColor)) -.clipShape(RoundedRectangle(cornerRadius: AppConstants.SettingsUI.cardCornerRadius)) +.cardStyle()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Advanced/AdvancedSettingsView.swift` around lines 147 - 150, The Log Export card currently applies inline styling (.padding, .background, .clipShape) producing a visual mismatch with other cards; update the view that builds the Log Export card to use the reusable .cardStyle() modifier (or, if a distinct look is required, mirror the Danger Zone card by adding the same .overlay(stroke) and corner radius used there) so its padding, background, corner radius and stroke match the rest of the UI; locate the card view in AdvancedSettingsView (the Log Export card block) and replace the inline styling with a call to .cardStyle() or adjust the inline styling to exactly match the Danger Zone card’s overlay/stroke and AppConstants.SettingsUI values.docs/app/widget/page.tsx (3)
214-260:nowPlaying!non-null assertions are safe but fragile.The render functions (
renderHorizontal,renderVertical,renderCompact) usenowPlaying!extensively (e.g., lines 218, 220, 250, 301, 332, 381, 412). These are safe today becauserenderLayout()is only called inside{nowPlaying && (...)}on line 451, but a future refactor could easily break this guard.Consider passing
nowPlayingas a parameter so the type system enforces non-nullability:Suggested approach
- const renderHorizontal = () => ( + const renderHorizontal = (np: NowPlayingData) => ( <div className="relative flex h-full"> {!hideAlbumArt && ( <div className="flex-shrink-0 p-[5px]"> - {nowPlaying!.artworkURL ? ( + {np.artworkURL ? (Then call as
renderHorizontal(nowPlaying)fromrenderLayout, which itself takes the non-null value from the guard.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/app/widget/page.tsx` around lines 214 - 260, The render helpers use fragile non-null assertions on nowPlaying; change renderHorizontal, renderVertical, and renderCompact to accept a non-null nowPlaying parameter (e.g., renderHorizontal(nowPlaying: NowPlayingType)) and update their references to use the local parameter instead of nowPlaying!, then update renderLayout to take the non-null nowPlaying from the existing guard and call the helpers as renderHorizontal(nowPlaying) / renderVertical(nowPlaying) / renderCompact(nowPlaying) so the type system enforces non-nullability.
44-46:statusstate is set but never read.
ConnectionStatusis tracked viasetStatuson lines 161, 168, 179 but never referenced in render output or any conditional logic. This is dead state that adds unnecessary re-renders on every connect/disconnect cycle.Either remove it or use it (e.g., to show a connection indicator).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/app/widget/page.tsx` around lines 44 - 46, The ConnectionStatus state variable (status) is never read but is updated via setStatus in the component (e.g., calls to setStatus when connection changes), causing unnecessary re-renders; either remove the state and all setStatus calls (cleanup the handlers that call setStatus) or actually use the state in render (for example render a connection indicator using status) so it serves a purpose—locate the useState declaration for status and the setStatus usages and either delete both the state and its setters or add JSX/conditional logic that references status to display connection state.
148-151: Silent catch on unknown message types.The empty
catch {}on line 151 swallows all JSON parse errors silently. For an OBS overlay this is arguably correct (don't show errors to viewers), but aconsole.debugfor non-production troubleshooting would help widget developers diagnose issues.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/app/widget/page.tsx` around lines 148 - 151, The empty catch after the switch (the block that follows case "welcome") is swallowing JSON parse errors; update that catch to accept the error (e.g., catch (err)) and emit a non-production debug log with the error and the raw message payload (use console.debug or similar) so developers can troubleshoot — ensure the log only fires in non-production environments or is gated by an env check to avoid leaking to viewers.src/wolfwave/Views/Onboarding/OnboardingView.swift (1)
14-14: Missing#Previewblock.The coding guidelines recommend including a
#Previewblock for SwiftUI views. A simple preview with a no-op closure would suffice:`#Preview` { OnboardingView(onComplete: {}) }As per coding guidelines: "When creating SwiftUI views, include
@Stateproperties for UI state, a body property with View components, and a#Previewblock for previews"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Onboarding/OnboardingView.swift` at line 14, Add a SwiftUI preview for OnboardingView by adding a `#Preview` block that instantiates OnboardingView with a no-op closure (e.g., OnboardingView(onComplete: {})); place the block near the view declaration so Xcode previews compile and follow the project's guideline to include a preview for the OnboardingView type.src/wolfwave/Services/ArtworkService.swift (2)
81-82: Artwork URL upscaling via string replacement is fragile.
replacingOccurrences(of: "100x100", with: "512x512")silently produces the original 100×100 URL if Apple changes their URL format. This won't break anything (the URL still works), but the widget would get low-res artwork. Consider a regex or checking whether the replacement actually changed the string, and logging if it didn't.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/ArtworkService.swift` around lines 81 - 82, The current string-replacement in ArtworkService (line creating highRes from artworkUrl) is fragile; modify the logic in the method that builds highRes (referencing artworkUrl and highRes variables in ArtworkService) to use a regex that matches common Apple artwork size tokens (e.g., \d+x\d+), or at minimum check whether replacing "100x100" actually changed the URL and, if not, emit a warning/error log (via your service logger) so we can detect when the expected pattern no longer applies; update the code to fall back to the original URL only after logging the mismatch.
29-36: In-memory cache grows without bound.The
cachedictionary is never evicted. For a desktop music app this is unlikely to be a problem in practice, but long-running sessions with diverse libraries could accumulate thousands of entries. Consider adding a cap (e.g.,NSCacheor an LRU eviction policy) if this becomes a concern.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/ArtworkService.swift` around lines 29 - 36, The in-memory cache (private var cache) grows without bound; replace it with a capacity-capped cache (e.g., use NSCache<String, NSString> or implement a simple LRU ring buffer) and preserve thread-safety via the existing cacheQueue: change the storage from Dictionary to an NSCache or an LRU class, set a sensible maximum (e.g., maxEntries or totalCostLimit), and update all get/set/remove usages in ArtworkService to call into the new cache API under cacheQueue.async/ sync as appropriate so eviction happens automatically when the cap is reached.src/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift (1)
119-141: Hardcoded trigger strings duplicate knowledge from the command definitions.The
switchon trigger strings ("!song","!currentsong", etc.) must be kept in sync withSongCommand.triggersandLastSongCommand.triggers. If a trigger is added or renamed in either command, this map silently falls through todefault.Consider deriving the mapping from the command instances rather than duplicating the trigger literals.
♻️ Example approach
private func cooldownValues(for trigger: String, command: BotCommand) -> (TimeInterval, TimeInterval) { let defaults = Foundation.UserDefaults.standard if command is SongCommand { let globalCD = defaults.object(forKey: AppConstants.UserDefaults.songCommandGlobalCooldown) as? TimeInterval ?? command.globalCooldown let userCD = defaults.object(forKey: AppConstants.UserDefaults.songCommandUserCooldown) as? TimeInterval ?? command.userCooldown return (globalCD, userCD) } else if command is LastSongCommand { let globalCD = defaults.object(forKey: AppConstants.UserDefaults.lastSongCommandGlobalCooldown) as? TimeInterval ?? command.globalCooldown let userCD = defaults.object(forKey: AppConstants.UserDefaults.lastSongCommandUserCooldown) as? TimeInterval ?? command.userCooldown return (globalCD, userCD) } return (command.globalCooldown, command.userCooldown) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift` around lines 119 - 141, The switch over hardcoded trigger strings in cooldownValues(for:command:) duplicates trigger definitions and can drift out of sync; instead detect the command type (e.g., SongCommand vs LastSongCommand) from the provided command instance and use that to select the appropriate UserDefaults keys (AppConstants.UserDefaults.songCommandGlobalCooldown / songCommandUserCooldown or AppConstants.UserDefaults.lastSongCommandGlobalCooldown / lastSongCommandUserCooldown), falling back to command.globalCooldown and command.userCooldown for other command types; update cooldownValues to check "if command is SongCommand" and "else if command is LastSongCommand" and read defaults accordingly so trigger literals are not duplicated.src/wolfwave/Views/WebSocket/WebSocketSettingsView.swift (3)
153-167: PreferTask.sleepoverDispatchQueue.main.asyncAfterfor transient UI feedback.The coding guidelines specify using async/await over DispatchQueue for new async work. The same pattern is repeated for the widget URL copy button (lines 200–205).
♻️ Proposed fix
Button { copyToClipboard(connectionURL) copiedConnectionURL = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - copiedConnectionURL = false - } + Task { + try? await Task.sleep(for: .seconds(2)) + copiedConnectionURL = false + } } label: {Apply the same change to the widget URL copy button (lines 200–205).
As per coding guidelines: "Use async/await concurrency instead of DispatchQueue for new async work in Swift"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/WebSocket/WebSocketSettingsView.swift` around lines 153 - 167, Replace the DispatchQueue.main.asyncAfter usage with async/await Task.sleep to handle the transient UI reset: inside the Button action that calls copyToClipboard(connectionURL) and sets copiedConnectionURL = true, start a Task and await Task.sleep(for: .seconds(2)) (or Task.sleep(nanoseconds: 2_000_000_000)) before setting copiedConnectionURL = false; do the same change for the analogous widget URL copy button (the other Button that sets copiedWidgetURL), ensuring both use Task { await Task.sleep(...) ; copied... = false } instead of DispatchQueue.main.asyncAfter.
434-438:refreshServerState()directly accessesAppDelegate.sharedsingleton.This couples the view tightly to the app delegate. The MVVM guideline for views under
Views/recommends using a ViewModel as an intermediary. For the "Chill" review mode this isn't blocking, but worth noting for future refactoring — it also makes this view untestable in isolation.Based on learnings: "Use MVVM pattern for views with ViewModels as
ObservableObjectwithPublishedproperties in SwiftUI"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/WebSocket/WebSocketSettingsView.swift` around lines 434 - 438, refreshServerState() currently reads AppDelegate.shared directly, coupling the view to the app delegate; move this logic into a ViewModel (e.g., WebSocketSettingsViewModel : ObservableObject) that exposes `@Published` properties serverState and clientCount and a refreshServerState() method that reads websocketServer?.state and connectionCount (or accepts an injectable dependency instead of directly referencing AppDelegate.shared). Change WebSocketSettingsView to own an `@StateObject` (or accept an `@ObservedObject`) of WebSocketSettingsViewModel and read serverState and clientCount from the ViewModel instead of AppDelegate.shared.websocketServer; update calls that invoked the view's refreshServerState() to call the ViewModel's method.
43-49: Widget URL computation is duplicated withOnboardingOBSWidgetStepView.The same
#if DEBUG/#elseURL logic appears identically inOnboardingOBSWidgetStepView.swift(lines 20–28). If the production URL or query parameter format changes, both must be updated in lockstep.Consider extracting this into a static method on
AppConstants.Widgetor a shared helper.♻️ Example: centralize in AppConstants
// In AppConstants.Widget static func widgetURL(port: Int) -> String { `#if` DEBUG "http://localhost:3000/widget/?port=\(port)" `#else` "https://mrdemonwolf.github.io/wolfwave/widget/?port=\(port)" `#endif` }Then in both views:
private var widgetURL: String { AppConstants.Widget.widgetURL(port: storedPort) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/WebSocket/WebSocketSettingsView.swift` around lines 43 - 49, Duplicate conditional widget URL logic in WebSocketSettingsView.private var widgetURL and OnboardingOBSWidgetStepView must be centralized; add a static helper like AppConstants.Widget.widgetURL(port:) that contains the `#if` DEBUG / `#else` URL selection using the passed port, then replace both private var widgetURL computed properties to return AppConstants.Widget.widgetURL(port: storedPort). Ensure the helper uses the same query parameter format and update both views to call it so future URL changes are made in one place.src/wolfwave/Views/Twitch/TwitchViewModel.swift (1)
692-717:testConnection()looks correct; minor:MainActor.runis redundant here.Since
TwitchViewModelis@MainActorandTask { }inherits the actor context, theawait MainActor.run { }on line 711 is unnecessary — the closure is already on@MainActor. Not harmful, just slightly verbose.♻️ Simplify by removing redundant MainActor.run
Task { let isValid = await service.validateToken(token) - await MainActor.run { - self.statusMessage = isValid - ? "✅ Token is valid — scopes OK" - : "❌ Token is invalid or expired" - } + self.statusMessage = isValid + ? "✅ Token is valid — scopes OK" + : "❌ Token is invalid or expired" }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Twitch/TwitchViewModel.swift` around lines 692 - 717, The MainActor.run call inside testConnection is redundant because TwitchViewModel is `@MainActor` and Task inherits that actor context; remove the await MainActor.run { ... } wrapper and directly assign self.statusMessage within the Task after await service.validateToken(token) (update the closure in testConnection to set statusMessage based on isValid without calling MainActor.run).src/wolfwave/Services/WebSocket/WebSocketServerService.swift (2)
323-324: Hardcoded version"1.0.0"in the welcome message.Consider reading this from
Bundle.mainto keep it in sync with the app version automatically.♻️ Use bundle version
private func sendWelcome(to connection: NWConnection) { - sendJSON(["type": "welcome", "server": "WolfWave", "version": "1.0.0"], to: connection) + let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown" + sendJSON(["type": "welcome", "server": "WolfWave", "version": version], to: connection) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/WebSocket/WebSocketServerService.swift` around lines 323 - 324, The welcome payload in sendWelcome currently hardcodes "1.0.0"; update sendWelcome to read the app version from Bundle.main (e.g., Bundle.main.infoDictionary?["CFBundleShortVersionString"] or CFBundleVersion as fallback) and pass that value into sendJSON instead of the literal; ensure you handle the optional safely (provide a sensible default like "0.0.0" if missing) and keep the call site (sendWelcome(to:) and the sendJSON invocation) unchanged otherwise.
132-146: Duplicate widget config construction inbroadcastWidgetConfig()andsendWidgetConfig(to:).Both methods build the same dictionary from
UserDefaults. Extract a shared helper to keep them in sync.♻️ Extract a shared config builder
+ private func buildWidgetConfigMessage() -> [String: Any] { + let defaults = UserDefaults.standard + return [ + "type": "widget_config", + "data": [ + "theme": defaults.string(forKey: AppConstants.UserDefaults.widgetTheme) ?? "Default", + "layout": defaults.string(forKey: AppConstants.UserDefaults.widgetLayout) ?? "Horizontal", + "textColor": defaults.string(forKey: AppConstants.UserDefaults.widgetTextColor) ?? "#FFFFFF", + "backgroundColor": defaults.string(forKey: AppConstants.UserDefaults.widgetBackgroundColor) ?? "#1A1A2E", + "fontFamily": defaults.string(forKey: AppConstants.UserDefaults.widgetFontFamily) ?? "System", + ], + ] + } + func broadcastWidgetConfig() { - let defaults = UserDefaults.standard - let config: [String: Any] = [...] - broadcastJSON(config) + broadcastJSON(buildWidgetConfigMessage()) } private func sendWidgetConfig(to connection: NWConnection) { - let defaults = UserDefaults.standard - let config: [String: Any] = [...] - sendJSON(config, to: connection) + sendJSON(buildWidgetConfigMessage(), to: connection) }Also applies to: 328-341
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/WebSocket/WebSocketServerService.swift` around lines 132 - 146, Both broadcastWidgetConfig() and sendWidgetConfig(to:) build the same widget config from UserDefaults; extract a single helper (e.g., buildWidgetConfig() or widgetConfigDictionary()) that reads UserDefaults keys (AppConstants.UserDefaults.widgetTheme, widgetLayout, widgetTextColor, widgetBackgroundColor, widgetFontFamily) and returns the [String: Any] payload (with "type":"widget_config" and "data": ...); then replace the duplicate dictionary constructions in broadcastWidgetConfig() and sendWidgetConfig(to:) to call that helper and pass its result to broadcastJSON/send as before so both methods remain in sync.src/wolfwave/Views/Shared/UpdateBannerView.swift (2)
76-110: Thelistening()modifier pattern is fragile — callers must remember to chain it.If a consumer writes
UpdateBannerView()without.listening(), the banner silently never activates. Consider moving theonAppear/onReceivelogic directly into thebody(e.g., via a background modifier or.onAppear/.onReceiveinside the top-level view builder), or document this requirement prominently.♻️ Inline the listening logic into the body
var body: some View { - if isUpdateAvailable && !isDismissed { + Group { + if isUpdateAvailable && !isDismissed { HStack(spacing: 10) { // ... existing content ... } - } - } - - /// Modifier that wires up update state listening. - func listening() -> some View { - self - .onAppear { + } + } + .onAppear { if let appDelegate = AppDelegate.shared, let info = appDelegate.updateChecker?.latestUpdateInfo, info.isUpdateAvailable { latestVersion = info.latestVersion releaseURL = info.releaseURL isUpdateAvailable = true } - } - .onReceive( + } + .onReceive( NotificationCenter.default.publisher( for: NSNotification.Name(AppConstants.Notifications.updateStateChanged) ) - ) { notification in + ) { notification in // ... existing handling ... - } + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Shared/UpdateBannerView.swift` around lines 76 - 110, The listening() View modifier is fragile because callers can omit it; move its onAppear/onReceive logic into UpdateBannerView's body so the update-checking runs whenever the view is used (no extra chaining required). Specifically, remove or stop relying on the listening() function and place the same onAppear block and NotificationCenter publisher handling directly in the top-level view builder of UpdateBannerView (updating latestVersion, releaseURL, isUpdateAvailable and isDismissed the same way). Keep the same guard/unwrapping logic and fallback URL behavior for releaseURL, and ensure you still read AppDelegate.shared.updateChecker?.latestUpdateInfo on appear and update the state properties accordingly.
16-111: Missing#Previewblock.As per coding guidelines, SwiftUI views should include a
#Previewblock.Add preview
`#Preview` { UpdateBannerView() .listening() .padding() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Shared/UpdateBannerView.swift` around lines 16 - 111, Add a SwiftUI preview for UpdateBannerView by appending a `#Preview` block that instantiates UpdateBannerView() with the listening() modifier (and .padding() if desired) so the view and its update-state wiring can be inspected in Xcode; place the block after the UpdateBannerView declaration (referencing UpdateBannerView and its listening() method) to satisfy the guideline requiring a preview.src/wolfwave/Views/Discord/DiscordSettingsView.swift (1)
111-116: Fragile color logic relying on emoji content in the message string.
testResultMessage.contains("✅")couples the UI color to the exact emoji used in the message. Consider a dedicated boolean or enum to track success/failure state instead.♻️ Example: use a separate state for the result
+ `@State` private var testSucceeded: Bool? // ... if !testResultMessage.isEmpty { Text(testResultMessage) .font(.system(size: 12)) - .foregroundStyle(testResultMessage.contains("✅") ? .green : .red) + .foregroundStyle(testSucceeded == true ? .green : .red) .transition(.opacity) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/Discord/DiscordSettingsView.swift` around lines 111 - 116, The view currently decides color by checking testResultMessage.contains("✅"), which is fragile; introduce a dedicated result state (e.g., a Bool like testSucceeded or an enum like TestResult { case success, failure, none }) in the DiscordSettingsView and set that state where the testResultMessage is produced, then change the Text modifier to use that state (e.g., .foregroundStyle(testSucceeded ? .green : .red) or switch on TestResult) so the UI color is driven by the explicit result state rather than emoji content; update any code paths that set testResultMessage to also set the new state and remove the emoji-based color check.docs/app/widget/themes.ts (2)
35-69:fontMapandgoogleFontscan drift out of sync.The Google Font names are duplicated between
fontMap(lines 42–53) andgoogleFonts(lines 57–68). Adding a font to one but not the other would cause either a broken CSS reference or a missing preload.♻️ Derive googleFonts from fontMap to keep them in sync
-const googleFonts = new Set([ - "Montserrat", - "Roboto", - "Open Sans", - "Lato", - "Poppins", - "Fira Code", - "JetBrains Mono", - "Oswald", - "Bebas Neue", - "Raleway", - "Press Start 2P", - "Permanent Marker", -]); +const builtInFonts = new Set(["System", "Monospaced", "Rounded", "Serif"]); +const googleFonts = new Set( + Object.keys(fontMap).filter((f) => !builtInFonts.has(f)) +);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/app/widget/themes.ts` around lines 35 - 69, fontMap and googleFonts are duplicated and can drift; replace the hardcoded googleFonts list by deriving it from fontMap so they stay in sync: keep fontMap as the single source of truth and construct googleFonts (e.g., new Set(Object.keys(fontMap).filter(name => !["System","Monospaced","Rounded","Serif"].includes(name)))) so built-in names (System, Monospaced, Rounded, Serif) are excluded and all other keys in fontMap are treated as Google fonts.
245-254: Default-theme color override sets primary, secondary, and progress fill to the same value.When
textColor !== "#FFFFFF", lines 247–249 assign the same color totextPrimary,textSecondary, andprogressFillBg. This eliminates contrast between primary and secondary text, which may hurt readability. Consider applying an opacity modifier fortextSecondary(e.g.,${config.textColor}CC) to preserve hierarchy.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/app/widget/themes.ts` around lines 245 - 254, In the Default-theme branch, instead of assigning config.textColor verbatim to textPrimary, textSecondary, and progressFillBg, keep resolved.textPrimary = config.textColor but set resolved.textSecondary to a semi-transparent version (e.g., append an 8-bit alpha like "CC" to the hex or convert to rgba at ~80% opacity) to preserve visual hierarchy, and leave resolved.progressFillBg as the primary color (or a slightly dimmed variant if needed); update the assignment logic that touches config.textColor, resolved.textPrimary, resolved.textSecondary, and resolved.progressFillBg to produce the semi-transparent secondary color reliably (use an existing color helper or add a small utility to convert hex to hex+alpha or rgba).src/wolfwave/Core/AppConstants.swift (1)
208-208: Unnecessarynonisolatedqualifier on static constant.
nonisolatedis superfluous on astatic letin a non-actorenum. No other constant in this file uses it. This likely came from silencing a compiler warning in a different context.Suggested fix
- nonisolated static let networkReconnectCooldown: TimeInterval = 60.0 + static let networkReconnectCooldown: TimeInterval = 60.0🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Core/AppConstants.swift` at line 208, Remove the unnecessary nonisolated qualifier from the static constant declaration for networkReconnectCooldown: locate the declaration of nonisolated static let networkReconnectCooldown: TimeInterval = 60.0 (in AppConstants) and change it to a plain static let networkReconnectCooldown: TimeInterval = 60.0 so the qualifier is not used on a static constant in a non-actor enum; no other logic changes required.src/wolfwave/Services/Twitch/TwitchChatService.swift (1)
1178-1191: Minor race window betweencancelSessionWelcomeTimeoutand async timer creation.
cancelSessionWelcomeTimeout()runs synchronously on the calling thread (line 1179), but the new timer is created insideDispatchQueue.main.async(line 1180). IfstartSessionWelcomeTimeoutis called twice in quick succession, the secondcancelcould run before the firstasyncblock creates its timer, leaving the first timer uncancelled. Low likelihood in practice sinceconnectToEventSubis the only caller, but wrapping the cancel inside the same main-queue dispatch would eliminate the window.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Services/Twitch/TwitchChatService.swift` around lines 1178 - 1191, Move the synchronous cancel call onto the main queue to eliminate the race: instead of calling cancelSessionWelcomeTimeout() before DispatchQueue.main.async, perform the cancel inside the same DispatchQueue.main.async block (before scheduling the new Timer) so both cancellation and creation happen on the main thread; update startSessionWelcomeTimeout to acquire sessionTimerLock and cancel sessionWelcomeTimer inside that async block prior to calling Timer.scheduledTimer to ensure no uncancelled timer remains.src/wolfwave/Views/SettingsView.swift (1)
378-418: Cooldown sliders lackaccessibilityIdentifier/accessibilityLabel.The command toggle rows have proper accessibility identifiers and labels, but the cooldown sliders (lines 396 and 404) don't. For testing and accessibility compliance, consider adding identifiers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/Views/SettingsView.swift` around lines 378 - 418, The cooldownRow view is missing accessibility identifiers/labels for its two Slider controls (the ones bound to globalCooldown and userCooldown); update the Slider for globalCooldown and the Slider for userCooldown inside cooldownRow to include descriptive accessibilityIdentifier and accessibilityLabel (e.g., "globalCooldownSlider" / "Global cooldown slider" and "userCooldownSlider" / "Per-user cooldown slider") so tests and assistive technologies can target them; ensure you attach these via the Slider modifiers (accessibilityIdentifier and accessibilityLabel) so they reference the correct Binding variables (globalCooldown and userCooldown) and remain unique.src/wolfwave/WolfWaveApp.swift (3)
440-457: Normal music check interval is a magic number.The reduced-mode interval uses
AppConstants.PowerManagement.reducedMusicCheckIntervalbut the normal-mode fallback is hardcoded as5.0(line 445). Consider adding a constant likeAppConstants.Music.defaultCheckIntervalfor consistency and to avoid drift.Suggested change
In
AppConstants.swift, add:enum Music { // ... existing ... /// Default polling interval for music playback checks (seconds) static let defaultCheckInterval: TimeInterval = 5.0 }Then in
WolfWaveApp.swift:musicMonitor?.updateCheckInterval( - reduced ? AppConstants.PowerManagement.reducedMusicCheckInterval : 5.0 + reduced ? AppConstants.PowerManagement.reducedMusicCheckInterval : AppConstants.Music.defaultCheckInterval )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/WolfWaveApp.swift` around lines 440 - 457, The powerStateChanged(_:) method in WolfWaveApp.swift uses a hardcoded 5.0 for the normal music check interval instead of a named constant; add a new constant (e.g. AppConstants.Music.defaultCheckInterval: TimeInterval = 5.0) in AppConstants.swift and replace the literal 5.0 in powerStateChanged(_:) with AppConstants.Music.defaultCheckInterval so the musicMonitor?.updateCheckInterval call uses the new constant (refer to PowerStateMonitor.shared.isReducedMode and AppConstants.PowerManagement.reducedMusicCheckInterval when applying the change).
429-431: Side-effect-only initialization could be more explicit.
_ = PowerStateMonitor.sharedrelies on the singleton'sinitto register system observers. A brief comment or a dedicated method likePowerStateMonitor.shared.activate()would make the intent clearer to future readers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/WolfWaveApp.swift` around lines 429 - 431, setupPowerStateMonitor currently forces the singleton init via `_ = PowerStateMonitor.shared`, which is a side-effect-only pattern; replace this with an explicit activation to make intent clear: add a public method on the PowerStateMonitor type such as `activate()` (or `startMonitoring()`), have its initializer avoid side effects, and call `PowerStateMonitor.shared.activate()` from `setupPowerStateMonitor()` (or alternatively add a short clarifying comment if you prefer not to change API). Ensure the symbol `PowerStateMonitor.shared` remains the singleton accessor and update `setupPowerStateMonitor()` to call the new explicit activation method.
510-512: Artwork URL updated from two paths — potential redundant broadcast.
discordService?.onArtworkResolved(line 511) andfetchArtworkForWidget(line 906) both callwebsocketServer?.updateArtworkURL. If Discord resolves artwork first, the widget fetch may overwrite it redundantly (or vice versa). Not a bug, but worth noting — if the WebSocket server broadcasts on everyupdateArtworkURLcall, clients may receive duplicate artwork updates for the same track.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/wolfwave/WolfWaveApp.swift` around lines 510 - 512, discordService?.onArtworkResolved and fetchArtworkForWidget both call websocketServer?.updateArtworkURL leading to duplicate broadcasts; update one or both call sites to guard against redundant updates by comparing the new artwork URL to the currently known URL before invoking websocketServer?.updateArtworkURL (or delegate deduplication into websocketServer by adding an idempotent check there). Specifically, in the discordService?.onArtworkResolved closure and/or in fetchArtworkForWidget, check the existing artwork URL/state and only call updateArtworkURL when the URL is non-nil and different from the last-sent URL, or implement the same check inside websocketServer.updateArtworkURL to ignore identical successive URLs.
|
|
||
| Last audited: 2026-02-14 | ||
| Bundle ID: `com.mrdemonwolf.wolfwave` | ||
| Deployment target: macOS 15.6 |
There was a problem hiding this comment.
Deployment target macOS 15.6 is inconsistent with the installation docs.
docs/content/docs/installation.mdx states "macOS 15.0 or later," but COMPLIANCE.md lists 15.6. macOS 15.6 has not shipped; this is likely a typo. If the true minimum is 15.0, update this line to match.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@COMPLIANCE.md` at line 5, Update the deployment target line in COMPLIANCE.md
that currently reads "Deployment target: macOS 15.6" to match the installation
docs; change it to "Deployment target: macOS 15.0" (or "macOS 15.0 or later" if
you prefer consistency with docs/content/docs/installation.mdx) so both sources
specify the same minimum macOS requirement.
| A guided 3-step onboarding wizard walks new users through initial configuration on first launch: | ||
|
|
||
| 1. **Welcome** — Feature overview highlighting Music Monitoring, Twitch Chat Bot, Discord Rich Presence, Stream Overlays, and Menu Bar integration | ||
| 2. **Twitch Connection** — Optional OAuth Device Code flow to connect your Twitch account (skippable) | ||
| 3. **Discord Rich Presence** — Visual preview of what your Discord profile will look like, with a toggle to enable (skippable) | ||
| 1. **Welcome** — App overview with feature highlights | ||
| 2. **Twitch Connection** — Optional OAuth Device Code flow to link your Twitch account | ||
| 3. **Discord Rich Presence** — Optional toggle to enable listening status on Discord | ||
| 4. **OBS Stream Widget** — Optional toggle to enable the WebSocket server with widget URL |
There was a problem hiding this comment.
Step count mismatch: "3-step" but 4 steps listed.
Line 99 says "A guided 3-step onboarding wizard" but four steps are listed immediately below (Welcome, Twitch, Discord, OBS Widget). Update to "4-step".
Fix
-A guided 3-step onboarding wizard walks new users through initial configuration on first launch:
+A guided 4-step onboarding wizard walks new users through initial configuration on first launch:📝 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.
| A guided 3-step onboarding wizard walks new users through initial configuration on first launch: | |
| 1. **Welcome** — Feature overview highlighting Music Monitoring, Twitch Chat Bot, Discord Rich Presence, Stream Overlays, and Menu Bar integration | |
| 2. **Twitch Connection** — Optional OAuth Device Code flow to connect your Twitch account (skippable) | |
| 3. **Discord Rich Presence** — Visual preview of what your Discord profile will look like, with a toggle to enable (skippable) | |
| 1. **Welcome** — App overview with feature highlights | |
| 2. **Twitch Connection** — Optional OAuth Device Code flow to link your Twitch account | |
| 3. **Discord Rich Presence** — Optional toggle to enable listening status on Discord | |
| 4. **OBS Stream Widget** — Optional toggle to enable the WebSocket server with widget URL | |
| A guided 4-step onboarding wizard walks new users through initial configuration on first launch: | |
| 1. **Welcome** — App overview with feature highlights | |
| 2. **Twitch Connection** — Optional OAuth Device Code flow to link your Twitch account | |
| 3. **Discord Rich Presence** — Optional toggle to enable listening status on Discord | |
| 4. **OBS Stream Widget** — Optional toggle to enable the WebSocket server with widget URL |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/content/docs/features.mdx` around lines 99 - 104, Update the onboarding
description header so the step count matches the list: change the phrase "A
guided 3-step onboarding wizard" to "A guided 4-step onboarding wizard" in the
content that introduces the steps (the sentence starting with "A guided 3-step
onboarding wizard walks new users...") so the header aligns with the four listed
steps (Welcome, Twitch Connection, Discord Rich Presence, OBS Stream Widget).
| nonisolated static func exportLogFile() -> URL? { | ||
| let url = logFileURL | ||
| // Flush any pending writes | ||
| fileLock.lock() | ||
| fileHandle?.synchronizeFile() | ||
| fileLock.unlock() | ||
| return FileManager.default.fileExists(atPath: url.path) ? url : nil | ||
| } |
There was a problem hiding this comment.
Data race: _logFileURL is read outside fileLock in exportLogFile(), and the lock pattern is inconsistent with writeToFile.
logFileURL (line 160) reads and potentially writes the nonisolated(unsafe) stored property _logFileURL before fileLock is acquired (line 162). writeToFile always accesses _logFileURL (via logFileURL) while holding fileLock. If exportLogFile() and the first-ever log() call execute concurrently, there is an unguarded concurrent read-write on _logFileURL.
Additionally, writeToFile uses defer { fileLock.unlock() } for safe release, while exportLogFile() does a manual lock()/unlock() pair — an early return or future change between the two calls risks a deadlock.
🔒 Proposed fix
nonisolated static func exportLogFile() -> URL? {
- let url = logFileURL
- // Flush any pending writes
fileLock.lock()
+ defer { fileLock.unlock() }
+ let url = logFileURL
+ // Flush any pending writes
fileHandle?.synchronizeFile()
- fileLock.unlock()
return FileManager.default.fileExists(atPath: url.path) ? url : nil
}📝 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.
| nonisolated static func exportLogFile() -> URL? { | |
| let url = logFileURL | |
| // Flush any pending writes | |
| fileLock.lock() | |
| fileHandle?.synchronizeFile() | |
| fileLock.unlock() | |
| return FileManager.default.fileExists(atPath: url.path) ? url : nil | |
| } | |
| nonisolated static func exportLogFile() -> URL? { | |
| fileLock.lock() | |
| defer { fileLock.unlock() } | |
| let url = logFileURL | |
| // Flush any pending writes | |
| fileHandle?.synchronizeFile() | |
| return FileManager.default.fileExists(atPath: url.path) ? url : nil | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/wolfwave/Core/Logger.swift` around lines 159 - 166, exportLogFile()
currently reads logFileURL (which accesses the nonisolated(unsafe) stored
property _logFileURL) before acquiring fileLock and uses manual lock/unlock,
risking a race with writeToFile and inconsistent unlock behavior; fix by
acquiring fileLock at the start of nonisolated static func exportLogFile(), then
read logFileURL while the lock is held, call fileHandle?.synchronizeFile() and
determine existence, and always release the lock with defer { fileLock.unlock()
} (matching writeToFile's pattern) so reads/writes of _logFileURL are serialized
and unlock is guaranteed.
| final class PowerStateMonitor { | ||
| static let shared = PowerStateMonitor() | ||
|
|
||
| /// Whether the system is in a reduced-power state (Low Power Mode or serious/critical thermal pressure). | ||
| private(set) var isReducedMode: Bool = false | ||
|
|
||
| private init() { | ||
| updateState() | ||
|
|
||
| NotificationCenter.default.addObserver( | ||
| self, | ||
| selector: #selector(handleChange), | ||
| name: .NSProcessInfoPowerStateDidChange, | ||
| object: nil | ||
| ) | ||
| NotificationCenter.default.addObserver( | ||
| self, | ||
| selector: #selector(handleChange), | ||
| name: ProcessInfo.thermalStateDidChangeNotification, | ||
| object: nil | ||
| ) | ||
| } | ||
|
|
||
| @objc private func handleChange() { | ||
| updateState() | ||
| } | ||
|
|
||
| private func updateState() { | ||
| let info = ProcessInfo.processInfo | ||
| let newValue = info.isLowPowerModeEnabled | ||
| || info.thermalState == .serious | ||
| || info.thermalState == .critical | ||
|
|
||
| guard newValue != isReducedMode else { return } | ||
| isReducedMode = newValue | ||
|
|
||
| NotificationCenter.default.post( | ||
| name: NSNotification.Name(AppConstants.Notifications.powerStateChanged), | ||
| object: nil, | ||
| userInfo: ["isReducedMode": newValue] | ||
| ) | ||
| } |
There was a problem hiding this comment.
isReducedMode can be read/written from multiple threads without synchronization.
System power notifications can fire on arbitrary threads, and consumers (e.g., UpdateCheckerService) may read isReducedMode from yet another thread. Without a lock, concurrent reads and writes to isReducedMode constitute a data race.
🔒 Proposed fix: add NSLock
final class PowerStateMonitor {
static let shared = PowerStateMonitor()
- /// Whether the system is in a reduced-power state (Low Power Mode or serious/critical thermal pressure).
- private(set) var isReducedMode: Bool = false
+ private var _isReducedMode: Bool = false
+ private let lock = NSLock()
+
+ /// Whether the system is in a reduced-power state (Low Power Mode or serious/critical thermal pressure).
+ var isReducedMode: Bool {
+ lock.withLock { _isReducedMode }
+ }
// ...
private func updateState() {
let info = ProcessInfo.processInfo
let newValue = info.isLowPowerModeEnabled
|| info.thermalState == .serious
|| info.thermalState == .critical
- guard newValue != isReducedMode else { return }
- isReducedMode = newValue
+ let changed: Bool = lock.withLock {
+ guard newValue != _isReducedMode else { return false }
+ _isReducedMode = newValue
+ return true
+ }
+ guard changed else { return }
NotificationCenter.default.post(
name: NSNotification.Name(AppConstants.Notifications.powerStateChanged),
object: nil,
userInfo: ["isReducedMode": newValue]
)
}
}As per coding guidelines: "Use NSLock for thread safety when mutating shared state in services"
📝 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.
| final class PowerStateMonitor { | |
| static let shared = PowerStateMonitor() | |
| /// Whether the system is in a reduced-power state (Low Power Mode or serious/critical thermal pressure). | |
| private(set) var isReducedMode: Bool = false | |
| private init() { | |
| updateState() | |
| NotificationCenter.default.addObserver( | |
| self, | |
| selector: #selector(handleChange), | |
| name: .NSProcessInfoPowerStateDidChange, | |
| object: nil | |
| ) | |
| NotificationCenter.default.addObserver( | |
| self, | |
| selector: #selector(handleChange), | |
| name: ProcessInfo.thermalStateDidChangeNotification, | |
| object: nil | |
| ) | |
| } | |
| @objc private func handleChange() { | |
| updateState() | |
| } | |
| private func updateState() { | |
| let info = ProcessInfo.processInfo | |
| let newValue = info.isLowPowerModeEnabled | |
| || info.thermalState == .serious | |
| || info.thermalState == .critical | |
| guard newValue != isReducedMode else { return } | |
| isReducedMode = newValue | |
| NotificationCenter.default.post( | |
| name: NSNotification.Name(AppConstants.Notifications.powerStateChanged), | |
| object: nil, | |
| userInfo: ["isReducedMode": newValue] | |
| ) | |
| } | |
| final class PowerStateMonitor { | |
| static let shared = PowerStateMonitor() | |
| private var _isReducedMode: Bool = false | |
| private let lock = NSLock() | |
| /// Whether the system is in a reduced-power state (Low Power Mode or serious/critical thermal pressure). | |
| var isReducedMode: Bool { | |
| lock.withLock { _isReducedMode } | |
| } | |
| private init() { | |
| updateState() | |
| NotificationCenter.default.addObserver( | |
| self, | |
| selector: `#selector`(handleChange), | |
| name: .NSProcessInfoPowerStateDidChange, | |
| object: nil | |
| ) | |
| NotificationCenter.default.addObserver( | |
| self, | |
| selector: `#selector`(handleChange), | |
| name: ProcessInfo.thermalStateDidChangeNotification, | |
| object: nil | |
| ) | |
| } | |
| `@objc` private func handleChange() { | |
| updateState() | |
| } | |
| private func updateState() { | |
| let info = ProcessInfo.processInfo | |
| let newValue = info.isLowPowerModeEnabled | |
| || info.thermalState == .serious | |
| || info.thermalState == .critical | |
| let changed: Bool = lock.withLock { | |
| guard newValue != _isReducedMode else { return false } | |
| _isReducedMode = newValue | |
| return true | |
| } | |
| guard changed else { return } | |
| NotificationCenter.default.post( | |
| name: NSNotification.Name(AppConstants.Notifications.powerStateChanged), | |
| object: nil, | |
| userInfo: ["isReducedMode": newValue] | |
| ) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/wolfwave/Core/PowerStateMonitor.swift` around lines 26 - 67,
isReducedMode is accessed from multiple threads without synchronization; add a
private NSLock (e.g., private let lock = NSLock()) plus a private backing
storage (e.g., private var _isReducedMode: Bool) and make isReducedMode a
computed property that returns _isReducedMode while holding the lock; in
updateState (and handleChange if it touches state) acquire the lock when
comparing newValue to _isReducedMode and when assigning _isReducedMode, then
release before posting the notification. This ensures thread-safe reads/writes
for PowerStateMonitor's isReducedMode while keeping the NotificationCenter logic
in init, updateState, handleChange unchanged.
| <key>com.apple.security.network.server</key> | ||
| <true/> |
There was a problem hiding this comment.
Inconsistency: ENABLE_INCOMING_NETWORK_CONNECTIONS = NO in both Debug and Release build configs contradicts this entitlement.
project.pbxproj has ENABLE_INCOMING_NETWORK_CONNECTIONS = NO set for both build configurations (Debug line 354, Release line 413), while both wolfwave.dev.entitlements and wolfwave.entitlements now declare com.apple.security.network.server = true. The entitlements file is authoritative at signing time, so the WebSocket server should work — but the stale build setting is misleading and could confuse future Capabilities UI edits (which sync the setting with the entitlement).
Consider updating the build setting to ENABLE_INCOMING_NETWORK_CONNECTIONS = YES to keep it consistent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/wolfwave/wolfwave.dev.entitlements` around lines 9 - 10, The build
setting ENABLE_INCOMING_NETWORK_CONNECTIONS is set to NO while your entitlements
(com.apple.security.network.server = true in wolfwave.dev.entitlements and
wolfwave.entitlements) declare incoming network permissions; update the Xcode
project build setting named ENABLE_INCOMING_NETWORK_CONNECTIONS to YES for both
Debug and Release configurations (the entries currently in project.pbxproj) so
the project setting matches the entitlements and won’t confuse the Capabilities
UI or future edits.
| private func addCopyWidgetURLItem(to menu: NSMenu) { | ||
| let item = NSMenuItem( | ||
| title: "Copy Widget URL", | ||
| action: #selector(copyWidgetURL), | ||
| keyEquivalent: "" | ||
| ) | ||
| item.image = NSImage( | ||
| systemSymbolName: "link", | ||
| accessibilityDescription: "Copy Widget URL" | ||
| ) | ||
| menu.addItem(item) | ||
| } | ||
|
|
||
| @objc private func copyWidgetURL() { | ||
| let port = UserDefaults.standard.object(forKey: AppConstants.UserDefaults.websocketServerPort) as? UInt16 | ||
| ?? AppConstants.WebSocketServer.defaultPort | ||
| let url = "http://localhost:\(port)" | ||
| NSPasteboard.general.clearContents() | ||
| NSPasteboard.general.setString(url, forType: .string) | ||
| Log.info("Widget URL copied to clipboard: \(url)", category: "App") | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check how WebSocketServerService serves the widget / what URL the widget expects
rg -n "localhost" --type swift -C 3 -g '!WolfWaveApp.swift'
echo "---"
# Check widget/overlay source for its expected URL
fd -e tsx -e ts -e html --exec grep -l "localhost" {} \;Repository: MrDemonWolf/wolfwave
Length of output: 1973
🏁 Script executed:
# Check the widget page source code
cat -n ./docs/app/widget/page.tsx | head -50Repository: MrDemonWolf/wolfwave
Length of output: 1797
🏁 Script executed:
# Check how the widget connects with the port parameter
rg -n "port\|websocket\|ws://" ./docs/app/widget/page.tsx -C 2Repository: MrDemonWolf/wolfwave
Length of output: 46
🏁 Script executed:
# Check more of the widget page to see WebSocket connection
cat -n ./docs/app/widget/page.tsx | head -100Repository: MrDemonWolf/wolfwave
Length of output: 3757
🏁 Script executed:
# Search for the WebSocket connection initialization
rg -n "new WebSocket\|ws://\|wss://" ./docs/app/widget/page.tsx -A 5Repository: MrDemonWolf/wolfwave
Length of output: 46
🏁 Script executed:
# Search for WebSocket initialization more broadly
grep -n "WebSocket\|ws" ./docs/app/widget/page.tsxRepository: MrDemonWolf/wolfwave
Length of output: 488
🏁 Script executed:
# Get line numbers for WebSocketSettingsView to reference in the rewrite
rg -n "widgetURL" src/wolfwave/Views/WebSocket/WebSocketSettingsView.swiftRepository: MrDemonWolf/wolfwave
Length of output: 271
Update copyWidgetURL() to include widget path and port parameter.
copyWidgetURL() returns http://localhost:\(port) but should match the pattern used in WebSocketSettingsView and OnboardingOBSWidgetStepView: http://localhost:3000/widget/?port=\(port) for DEBUG builds. The missing /widget path and ?port= query parameter prevent the widget from loading and connecting properly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/wolfwave/WolfWaveApp.swift` around lines 395 - 415, The Copy Widget URL
currently produced by copyWidgetURL() is missing the widget path and port query
parameter; update copyWidgetURL() so in DEBUG builds it constructs the URL as
"http://localhost:3000/widget/?port=\(port)" (or use the resolved port variable
instead of hardcoding 3000 if needed) and writes that to the pasteboard and log;
implement this with a platform conditional (`#if` DEBUG / `#else` / `#endif`) inside
copyWidgetURL() (referencing copyWidgetURL and addCopyWidgetURLItem to locate
the code) so the pasteboard and Log.info contain the new "/widget/?port="
pattern.
| func testServiceInitializesWithDefaultPort() { | ||
| let service = WebSocketServerService() | ||
| XCTAssertEqual(service.state, .stopped) | ||
| XCTAssertEqual(service.connectionCount, 0) | ||
| } | ||
|
|
||
| func testServiceInitializesWithCustomPort() { | ||
| let service = WebSocketServerService(port: 9999) | ||
| XCTAssertEqual(service.state, .stopped) | ||
| XCTAssertEqual(service.connectionCount, 0) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find WebSocketServerService file and check for port property
fd WebSocketServerService --type f
# Search for the port property definition
rg -n 'var port' --type swift | head -20
# Search for AppConstants WebSocketServer configuration
rg -n 'defaultPort' --type swift | head -20Repository: MrDemonWolf/wolfwave
Length of output: 2180
🏁 Script executed:
#!/bin/bash
# Get the WebSocketServerService class definition to verify port property
fd -t f 'WebSocketServerService\.swift$' -x cat {} | head -100Repository: MrDemonWolf/wolfwave
Length of output: 3118
Service initialization tests don't assert on the port value.
testServiceInitializesWithDefaultPort and testServiceInitializesWithCustomPort verify state and connectionCount but never assert that the port is actually set to the expected value (8765 and 9999 respectively). Add port assertions:
Suggested addition
func testServiceInitializesWithDefaultPort() {
let service = WebSocketServerService()
+ XCTAssertEqual(service.port, AppConstants.WebSocketServer.defaultPort)
XCTAssertEqual(service.state, .stopped)
XCTAssertEqual(service.connectionCount, 0)
}
func testServiceInitializesWithCustomPort() {
let service = WebSocketServerService(port: 9999)
+ XCTAssertEqual(service.port, 9999)
XCTAssertEqual(service.state, .stopped)
XCTAssertEqual(service.connectionCount, 0)
}📝 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.
| func testServiceInitializesWithDefaultPort() { | |
| let service = WebSocketServerService() | |
| XCTAssertEqual(service.state, .stopped) | |
| XCTAssertEqual(service.connectionCount, 0) | |
| } | |
| func testServiceInitializesWithCustomPort() { | |
| let service = WebSocketServerService(port: 9999) | |
| XCTAssertEqual(service.state, .stopped) | |
| XCTAssertEqual(service.connectionCount, 0) | |
| } | |
| func testServiceInitializesWithDefaultPort() { | |
| let service = WebSocketServerService() | |
| XCTAssertEqual(service.port, AppConstants.WebSocketServer.defaultPort) | |
| XCTAssertEqual(service.state, .stopped) | |
| XCTAssertEqual(service.connectionCount, 0) | |
| } | |
| func testServiceInitializesWithCustomPort() { | |
| let service = WebSocketServerService(port: 9999) | |
| XCTAssertEqual(service.port, 9999) | |
| XCTAssertEqual(service.state, .stopped) | |
| XCTAssertEqual(service.connectionCount, 0) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/WolfWaveTests/WebSocketServerServiceTests.swift` around lines 63 - 73,
Add assertions in testServiceInitializesWithDefaultPort and
testServiceInitializesWithCustomPort to verify the WebSocketServerService.port
property is set correctly: in testServiceInitializesWithDefaultPort assert
service.port == 8765, and in testServiceInitializesWithCustomPort assert
service.port == 9999; locate these tests (functions
testServiceInitializesWithDefaultPort and testServiceInitializesWithCustomPort)
and add the port equality assertions alongside the existing state and
connectionCount checks.
Summary
Test plan
make build) with zero warningsmake test)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Documentation