Skip to content

feat(docs): 2026 marketing redesign of landing page#17

Merged
nathanialhenniges merged 3 commits intomainfrom
claude/agitated-chandrasekhar
Apr 15, 2026
Merged

feat(docs): 2026 marketing redesign of landing page#17
nathanialhenniges merged 3 commits intomainfrom
claude/agitated-chandrasekhar

Conversation

@nathanialhenniges
Copy link
Copy Markdown
Member

@nathanialhenniges nathanialhenniges commented Apr 15, 2026

Summary

  • Hero — Kinetic oversized headline (YOUR MUSIC, live everywhere.) using Unbounded + Instrument Serif italic + animated gradient clip, mesh background with grid lines + noise overlay, SVG live-wire waveform, mono kicker metadata row, liquid-glass now-playing frame with corner brackets and 3-output badges, staggered page-load reveal.
  • Integration ticker — Infinite edge-to-edge marquee (APPLE MUSIC · TWITCH · DISCORD · OBS · SONG REQUESTS · WEBSOCKET API · MENU BAR · OPEN SOURCE) with mask-fade edges.
  • Bento features — Asymmetric 12-col grid: wide Twitch card with animated chat mock, tall Discord RPC card with mock Rich Presence, three small cards including a v2.0 Song Requests teaser. Gradient-border on hover.
  • Flow — Diagonal 3-step staircase (downloaded → connect → stream) with oversized outline numbers.
  • Dev terminal — macOS traffic-light window with syntax-highlighted WebSocket snippet in JetBrains Mono.
  • Giant CTAPress play. We handle the rest. at kinetic size, shimmer primary button, mono proof pills.
  • New fonts: JetBrains Mono (labels/code) + Instrument Serif (editorial italic accents). Unbounded + Instrument Sans retained.
  • Fully reduced-motion aware. Light + dark themes both covered.

Why

The previous landing read like a 2023 SaaS template — centered hero, 4 equal feature cards, generic 3-step grid. With v1.2.0 shipped and v2.0 song requests incoming, the docs landing needs to sell WolfWave in the first two seconds and feel distinctively 2026. No new dependencies — pure CSS + existing stack (Fumadocs + Tailwind v4).

Files

  • apps/docs/app/(home)/page.tsx — full rewrite into 6 sections
  • apps/docs/app/global.css — +~400 lines (mesh, grid-lines, noise, kinetic, liquid-glass, ticker, shimmer, bento, terminal, reduced-motion)
  • apps/docs/app/layout.tsx — add JetBrains Mono + Instrument Serif fonts, expose as --font-mono + --font-serif

Test plan

  • bun run build --filter docs passes (verified locally — 30 pages, clean)
  • bun run types:check passes (verified locally)
  • Visit / — hero renders with fluid kinetic headline, mesh bg, now-playing pulses
  • Ticker scrolls smoothly with edge fade
  • Bento grid animates chat lines, no overflow on mobile
  • Light/dark theme switch flips mesh + text colors correctly
  • /docs link still resolves and Fumadocs shell untouched
  • CTA shimmer button animates; prefers-reduced-motion disables all motion

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Twitch song request handling with !sr, !queue, !myqueue, !skip, !clearqueue, and !hold/!resume commands.
    • Full queue UI displaying now-playing track and pending requests with requester names.
    • Hold/resume mode for buffering requests when Music.app is closed.
    • Song and artist blocklist to prevent specific requests.
    • Fallback Apple Music playlist when queue empties.
    • Apple Music authorization step in onboarding wizard.
    • Configurable settings: per-user request limits, subscriber-only mode, custom command aliases, and per-command toggles.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

Warning

Rate limit exceeded

@nathanialhenniges has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 3 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 44 minutes and 3 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a4ada8f6-79d2-487b-8b17-01ea21d5b483

📥 Commits

Reviewing files that changed from the base of the PR and between 97d0033 and 6e58283.

📒 Files selected for processing (4)
  • .github/workflows/test.yml
  • apps/docs/app/(home)/page.tsx
  • apps/docs/app/global.css
  • apps/docs/app/layout.tsx

Walkthrough

This PR introduces comprehensive song request functionality for Twitch chat, enabling users to request songs via !sr with queue management, playback control, and a blocklist system. It includes new bot commands, Apple Music integration via AppleScript and MusicKit, UI views for queue management and settings, onboarding steps for authorization, and test coverage.

Changes

Cohort / File(s) Summary
Documentation & Changelog
CHANGELOG.md, apps/docs/content/docs/changelog.mdx
Added v2.0.0 unreleased section documenting new Twitch song request commands, queue behaviors, Apple Music onboarding, and expanded governance/configuration features.
Landing Page Redesign
apps/docs/app/(home)/page.tsx, apps/docs/app/global.css, apps/docs/app/layout.tsx
Redesigned landing page with new mesh/grid hero, animated ticker, bento-grid features, and vertical flow layout. Added extensive CSS utilities for animations (shimmer, marquee, stagger reveal, chat-line entrance, wire-dash). Imported new fonts (Instrument Serif, JetBrains Mono) with CSS variables.
Core Constants & Infrastructure
apps/native/wolfwave/Core/AppConstants.swift, apps/native/wolfwave/Core/AppConstants+Notifications.swift, apps/native/wolfwave/Core/BlocklistItem.swift
Extended AppConstants with song-request-related notification and UserDefaults keys for queue limits, cooldowns, command toggles, and aliases. Added typed notification name extension and BlocklistItem model for managing blocked songs/artists.
Queue & Item Models
apps/native/wolfwave/Services/SongRequest/SongRequestItem.swift, apps/native/wolfwave/Services/SongRequest/SongRequestQueue.swift
Introduced SongRequestItem model with metadata and SongRequestQueue managing thread-safe queue operations including add/dequeue/skip/clear with configurable per-user and global limits read from UserDefaults.
Apple Music Integration
apps/native/wolfwave/Services/SongRequest/AppleMusicController.swift, apps/native/wolfwave/Services/SongRequest/LinkResolverService.swift, apps/native/wolfwave/Services/SongRequest/SongSearchResolver.swift
Implemented AppleMusicController using AppleScript for playback control and MusicKit for search/resolve; added LinkResolverService for extracting metadata from Spotify/YouTube/Apple Music links via oEmbed; created SongSearchResolver to orchestrate link vs. text query routing.
Blocklist & Song Request Services
apps/native/wolfwave/Services/SongRequest/SongBlocklist.swift, apps/native/wolfwave/Services/SongRequest/SongRequestService.swift
Added SongBlocklist for case-insensitive song/artist blocking with UserDefaults persistence. Created main SongRequestService orchestrating request processing, playback monitoring, hold mode, auto-advance, and fallback playlist with support for Music.app launch detection and polling-based queue advancement.
Bot Command Protocol & Context
apps/native/wolfwave/Services/Twitch/Commands/AsyncBotCommand.swift, apps/native/wolfwave/Services/Twitch/Commands/BotCommand.swift, apps/native/wolfwave/Services/Twitch/Commands/BotCommandContext.swift
Introduced AsyncBotCommand protocol for non-blocking command execution with callback replies; extended BotCommand with enabledKey/aliasesKey for feature flags and custom aliases; added BotCommandContext struct capturing user identity and privilege level; added ServiceBoundCommand for commands requiring song request service access.
Song Request Bot Commands
apps/native/wolfwave/Services/Twitch/Commands/SongRequestCommand.swift, apps/native/wolfwave/Services/Twitch/Commands/QueueCommand.swift, apps/native/wolfwave/Services/Twitch/Commands/MyQueueCommand.swift, apps/native/wolfwave/Services/Twitch/Commands/SkipCommand.swift, apps/native/wolfwave/Services/Twitch/Commands/ClearQueueCommand.swift, apps/native/wolfwave/Services/Twitch/Commands/HoldCommand.swift
Implemented six new bot commands: SongRequestCommand (!sr) for requesting songs, QueueCommand (!queue) displaying full queue, MyQueueCommand (!myqueue) showing user's requests, SkipCommand (!skip) advancing queue, ClearQueueCommand (!clearqueue) removing all requests, and HoldCommand (!hold/!resume) pausing auto-play.
Command Dispatcher & Twitch Integration
apps/native/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift, apps/native/wolfwave/Services/Twitch/TwitchChatService.swift
Extended BotCommandDispatcher with overloaded processMessage supporting async commands via context and reply callback; wired in new song request commands; updated trigger matching to use allTriggers with aliases. Enhanced TwitchChatService to detect subscriber badge and construct BotCommandContext for async dispatch.
Onboarding Views
apps/native/wolfwave/Views/Onboarding/OnboardingAppleMusicStepView.swift, apps/native/wolfwave/Views/Onboarding/OnboardingView.swift, apps/native/wolfwave/Views/Onboarding/OnboardingViewModel.swift
Added OnboardingAppleMusicStepView for MusicKit authorization during first-launch; integrated into OnboardingView and OnboardingViewModel as a new .appleMusicAccess step; updated navigation to conditionally show Back/Skip buttons.
Song Request UI Views
apps/native/wolfwave/Views/SongRequest/SongRequestQueueView.swift, apps/native/wolfwave/Views/SongRequest/SongRequestSettingsView.swift
Created SongRequestQueueView displaying now-playing card, queue list with per-item removal, and skip/hold/clear actions. Built comprehensive SongRequestSettingsView with queue configuration, per-command toggles, custom aliases, cooldowns, blocklist editor, and Twitch connectivity checks.
Settings & Updates
apps/native/wolfwave/Views/SettingsView.swift, apps/native/wolfwave/Views/Shared/WhatsNewView.swift
Added .songRequests section to settings sidebar routing to SongRequestSettingsView. Updated WhatsNewView feature list highlighting song requests, hold mode, queue controls, and fallback playlist functionality.
App Lifecycle Integration
apps/native/wolfwave/Core/AppDelegate+MenuBar.swift, apps/native/wolfwave/Core/AppDelegate+Services.swift, apps/native/wolfwave/WolfWaveApp.swift
Wired SongRequestService setup in AppDelegate with initialization of queue/blocklist/music controller; added menu bar items for now-playing/queue status with hold/skip/clear actions; integrated playback monitoring startup; added notification observer for song request setting changes.
Build Configuration & Testing
apps/native/wolfwave.xcodeproj/project.pbxproj, apps/native/WolfWaveTests/OnboardingViewModelEdgeCaseTests.swift, apps/native/WolfWaveTests/OnboardingViewModelTests.swift, apps/native/WolfWaveTests/SongRequestCommandTests.swift, apps/native/WolfWaveTests/SongRequestQueueTests.swift, apps/native/WolfWaveTests/SongRequestServiceTests.swift
Updated MARKETING_VERSION to 2.0.0 and CURRENT_PROJECT_VERSION to 7. Updated onboarding tests to account for new AppleMusicAccess step. Added comprehensive test suites covering song request commands, queue operations, blocklist behavior, and service integration with mocking.

Sequence Diagram

sequenceDiagram
    participant User as Twitch User
    participant TwitchChat as TwitchChatService
    participant Dispatcher as BotCommandDispatcher
    participant SongCmd as SongRequestCommand
    participant Service as SongRequestService
    participant Resolver as SongSearchResolver
    participant LinkResolver as LinkResolverService
    participant MusicKit as AppleMusicController
    participant Queue as SongRequestQueue
    participant AppleScript as Music.app

    User->>TwitchChat: !sr song name
    TwitchChat->>Dispatcher: processMessage(text, context, asyncReply)
    Dispatcher->>SongCmd: execute(message, context, reply)
    SongCmd->>Service: processRequest(query, username, context)
    
    Service->>Resolver: resolve(query)
    alt Query is link
        Resolver->>LinkResolver: resolve(url)
        LinkResolver-->>Resolver: ResolveResult
    else Query is text
        Resolver->>MusicKit: search(query)
        MusicKit-->>Resolver: SearchResult
    end
    
    Resolver-->>Service: Result.found(Song)
    Service->>Queue: add(SongRequestItem)
    Queue-->>Service: AddResult
    
    Service->>MusicKit: playNow(song)
    MusicKit->>AppleScript: AppleScript command
    AppleScript-->>MusicKit: Playback started
    
    Service-->>SongCmd: RequestResult.added
    SongCmd->>TwitchChat: reply callback
    TwitchChat-->>User: Chat message response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A humble rabbit hops with glee,
Through song requests that flow so free!
With queues and holds and skips sublime,
Now Music.app keeps perfect time—
No focus-steal, just background play,
Apple's way! Sparkles ✨🎵

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.38% 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 PR title clearly and concisely summarizes the primary change: a 2026 marketing redesign of the landing page with visual/stylistic updates.

✏️ 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 claude/agitated-chandrasekhar

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: 14

🧹 Nitpick comments (15)
apps/native/wolfwave/Views/Shared/WhatsNewView.swift (2)

21-26: Use stable feature IDs instead of titles for accessibility identifiers.

Because Line 93 builds identifiers from feature.title, changing display text here changes test selectors. Add a stable id field to each feature and derive identifiers from that.

🧪 Suggested stabilization
-private let features: [(icon: String, iconColor: Color, title: String, description: String)] = [
-    ("music.mic", .pink, "Song Requests", "..."),
+private let features: [(id: String, icon: String, iconColor: Color, title: String, description: String)] = [
+    ("songRequests", "music.mic", .pink, "Song Requests", "..."),
     ...
 ]

-private func featureRow(_ feature: (icon: String, iconColor: Color, title: String, description: String)) -> some View {
+private func featureRow(_ feature: (id: String, icon: String, iconColor: Color, title: String, description: String)) -> some View {
     ...
-    .accessibilityIdentifier("whatsNew.feature.\(feature.title)")
+    .accessibilityIdentifier("whatsNew.feature.\(feature.id)")
 }

Also applies to: 93-93

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

In `@apps/native/wolfwave/Views/Shared/WhatsNewView.swift` around lines 21 - 26,
The feature list in WhatsNewView uses display titles which makes accessibility
identifiers brittle; add a stable id field to each feature tuple/struct (e.g.,
change the array entries to include an "id" like "song_requests", "hold_mode",
etc.), update the model used by WhatsNewView (the features collection) to
include that id, and change the code that currently derives identifiers from
feature.title (referenced in WhatsNewView and at the identifier construction
site around where feature.title is used) to use feature.id when building
accessibility identifiers and test selectors so identifiers remain stable when
titles change.

21-26: Tighten copy length for faster scanability.

The descriptions are clear, but several are long for a compact “What’s New” sheet. Consider shortening each to one crisp clause.

✂️ Suggested copy polish
-("music.mic", .pink, "Song Requests", "Viewers request songs with !sr in Twitch chat — plays through your Music.app"),
+("music.mic", .pink, "Song Requests", "Viewers request songs in chat with !sr."),

-("pause.fill", .orange, "Hold Mode", "Pause the queue to curate requests, then release when you're ready"),
+("pause.fill", .orange, "Hold Mode", "Pause requests while you curate the queue."),

-("forward.fill", .blue, "Queue Controls", "Skip, clear, or hold from the app, menu bar, or Twitch chat (mods only)"),
+("forward.fill", .blue, "Queue Controls", "Skip, clear, or hold from app, menu bar, or chat."),

-("list.number", .green, "Live Queue View", "See what's playing, what's next, and who requested each song"),
+("list.number", .green, "Live Queue View", "See now playing, up next, and requester names."),

-("music.note.list", .purple, "Fallback Playlist", "Pick an Apple Music playlist to play when the request queue runs dry"),
+("music.note.list", .purple, "Fallback Playlist", "Auto-play a playlist when requests run out."),

-("eye.slash", .cyan, "No Focus-Steal", "Music.app stays in the background — your stream tools keep focus"),
+("eye.slash", .cyan, "No Focus-Steal", "Music.app stays in back while your stream tools stay front."),

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/Shared/WhatsNewView.swift` around lines 21 - 26,
In WhatsNewView.swift update the short-description strings in the tuples used by
WhatsNewView (the list entries like the ("music.mic", .pink, "Song Requests",
"...") tuples) to be single, punchy clauses instead of full sentences—e.g.
condense each description to one fragment (3–6 words) that summarizes the
feature—so replace the long fourth-field strings for Song Requests, Hold Mode,
Queue Controls, Live Queue View, Fallback Playlist, and No Focus-Steal with much
shorter phrases to improve scanability.
apps/docs/app/(home)/page.tsx (1)

224-234: Consider a more descriptive key for the ticker items.

Using array index as key works here since the list is static, but a compound key improves clarity.

💡 Optional improvement
-          {[...tickerItems, ...tickerItems, ...tickerItems].map((item, i) => (
-            <span key={i} className="ticker-item">
+          {[...tickerItems, ...tickerItems, ...tickerItems].map((item, i) => (
+            <span key={`${item}-${i}`} className="ticker-item">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/docs/app/`(home)/page.tsx around lines 224 - 234, The map over
tickerItems uses the array index as the React key which is unclear; update the
key in the .map callback that renders .ticker-item to a more
descriptive/compound key (e.g., derive from tickerItems' value plus the index or
a unique id) so each <span key=... className="ticker-item"> uses a stable,
meaningful key instead of just i; locate the map over [...tickerItems,
...tickerItems, ...tickerItems] and replace key={i} with a compound key like
`${item}-${i}` or an actual unique identifier from the ticker data.
apps/native/wolfwave/Services/SongRequest/SongBlocklist.swift (1)

84-96: Recover from corrupt persisted blocklists instead of failing silently.

A decode failure currently leaves the bad payload in UserDefaults, so every future launch keeps failing the same way. Logging and clearing/quarantining the stored blob would make this self-healing.

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

In `@apps/native/wolfwave/Services/SongRequest/SongBlocklist.swift` around lines
84 - 96, The load() currently swallows JSON decode failures and leaves a corrupt
blob under storageKey so subsequent loads always fail; modify load() to catch
and log the decoding error (capture the thrown Error from JSONDecoder.decode),
and if decoding fails take remedial action such as removing or moving the bad
data (e.g. removeObject(forKey: storageKey) or copy it to a quarantine key like
"corrupt_\(storageKey)") and then set entries = [] under the same lock used by
save() (lock.withLock { entries = [] }) so the app can recover; also ensure
save() still writes the new empty or repaired entries normally.
apps/native/WolfWaveTests/OnboardingViewModelTests.swift (1)

139-159: Consider extending the full navigation cycle test to cover .appleMusicAccess.

The testFullNavigationCycleForwardAndBack test stops at .obsWidget and doesn't navigate to the new .appleMusicAccess step. While the other tests cover this step individually, extending this test would provide more comprehensive round-trip coverage.

♻️ Optional: Extend the navigation cycle test
         viewModel.goToNextStep()
         XCTAssertEqual(viewModel.currentStep, .obsWidget)

+        viewModel.goToNextStep()
+        XCTAssertEqual(viewModel.currentStep, .appleMusicAccess)
+
+        viewModel.goToPreviousStep()
+        XCTAssertEqual(viewModel.currentStep, .obsWidget)
+
         viewModel.goToPreviousStep()
         XCTAssertEqual(viewModel.currentStep, .discordConnect)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/WolfWaveTests/OnboardingViewModelTests.swift` around lines 139 -
159, Extend the testFullNavigationCycleForwardAndBack test to include the new
.appleMusicAccess step: after the current assertion for .obsWidget call
viewModel.goToNextStep() and assert viewModel.currentStep == .appleMusicAccess,
then continue the backward assertions by calling viewModel.goToPreviousStep()
and asserting it returns to .obsWidget before continuing back to
.discordConnect, .twitchConnect, and .welcome; update the sequence of
viewModel.goToNextStep()/goToPreviousStep() calls and their XCTAssertEqual
checks to cover the full forward-and-back round trip including
.appleMusicAccess.
apps/native/wolfwave/Services/Twitch/Commands/AsyncBotCommand.swift (1)

33-33: Consider marking the reply callback as @Sendable.

Since async commands may perform network requests and call reply from different isolation contexts, marking the callback as @Sendable would ensure thread-safety guarantees and satisfy Swift strict concurrency checking.

♻️ Optional: Add `@Sendable` annotation
-    func execute(message: String, context: BotCommandContext, reply: `@escaping` (String) -> Void)
+    func execute(message: String, context: BotCommandContext, reply: `@escaping` `@Sendable` (String) -> Void)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Services/Twitch/Commands/AsyncBotCommand.swift` at line
33, The execute signature's reply closure should be annotated as `@Sendable` to
satisfy Swift concurrency safety: update the AsyncBotCommand.execute declaration
so the reply parameter is declared as reply: `@Sendable` `@escaping` (String) ->
Void (and propagate this change to any protocol/type declarations and callers
that conform to or call AsyncBotCommand.execute), then fix any calling sites to
pass a `@Sendable-compatible` closure (or wrap non-sendable captures) so the code
compiles under strict concurrency checking.
apps/native/wolfwave/Services/Twitch/Commands/HoldCommand.swift (1)

14-24: Missing enabledKey and aliasesKey properties.

Unlike SkipCommand and ClearQueueCommand which define enabledKey and aliasesKey for user-configurable enable/disable and custom aliases, HoldCommand lacks these properties. This means the command cannot be disabled or aliased via settings.

Consider adding for consistency:

♻️ Proposed fix to add enable/alias support
     var userCooldown: TimeInterval { 3.0 }
 
+    var enabledKey: String? { AppConstants.UserDefaults.songRequestHoldEnabled }
+
+    var aliasesKey: String? { AppConstants.UserDefaults.holdCommandAliases }
+
     // MARK: - Properties

Note: You'd also need to add holdCommandAliases to AppConstants.UserDefaults if you want alias support.

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

In `@apps/native/wolfwave/Services/Twitch/Commands/HoldCommand.swift` around lines
14 - 24, HoldCommand is missing user-configurable keys so it can't be disabled
or aliased; add the same properties used by SkipCommand/ClearQueueCommand:
implement var enabledKey: String and var aliasesKey: String in the HoldCommand
class, returning the appropriate UserDefaults keys (e.g.
AppConstants.UserDefaults.holdCommandEnabled and
AppConstants.UserDefaults.holdCommandAliases), and add the new
holdCommandAliases and holdCommandEnabled entries to AppConstants.UserDefaults
to expose alias and enable/disable settings.
apps/native/wolfwave/Views/SongRequest/SongRequestQueueView.swift (2)

202-251: Missing accessibility identifiers on action buttons.

The action buttons (Skip, Hold/Resume, Clear Queue) lack accessibilityIdentifier modifiers, which are required per coding guidelines for UI testing.

♻️ Proposed fix to add accessibility identifiers
             Button {
                 Task {
                     _ = await service?.skip()
                     refreshState()
                 }
             } label: {
                 Label("Skip", systemImage: "forward.fill")
                     .font(.system(size: 11))
             }
             .buttonStyle(.bordered)
             .controlSize(.small)
             .disabled(nowPlaying == nil)
+            .accessibilityIdentifier("songRequestQueue.skipButton")
 
             Button {
                 Task {
                     await service?.setHold(!isHeld)
                     refreshState()
                 }
             } label: {
                 Label(isHeld ? "Resume" : "Hold", systemImage: isHeld ? "play.fill" : "pause.fill")
                     .font(.system(size: 11))
             }
             .buttonStyle(.bordered)
             .controlSize(.small)
             .tint(isHeld ? .green : .orange)
+            .accessibilityIdentifier("songRequestQueue.holdButton")
 
             Button {
                 showingClearConfirm = true
             } label: {
                 Label("Clear Queue", systemImage: "trash")
                     .font(.system(size: 11))
             }
             .buttonStyle(.bordered)
             .controlSize(.small)
             .disabled(items.isEmpty && nowPlaying == nil)
+            .accessibilityIdentifier("songRequestQueue.clearQueueButton")

As per coding guidelines: "Ensure all UI elements have accessibilityLabel and accessibilityIdentifier for UI testing"

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

In `@apps/native/wolfwave/Views/SongRequest/SongRequestQueueView.swift` around
lines 202 - 251, The action buttons in the actionButtons view (the Skip Button,
the Hold/Resume Button and the Clear Queue Button plus the confirmation dialog
action) are missing accessibility identifiers/labels; update each Button (the
one that calls service?.skip(), the one that calls service?.setHold(!isHeld),
and the Clear Queue Button and its destructive confirmation Button) to add
.accessibilityIdentifier("<unique_id>") and .accessibilityLabel("<readable
label>") (use clear ids like "skipButton", "holdResumeButton",
"clearQueueButton", "confirmClearQueueButton") so UI tests can reliably find
them while keeping existing behavior intact.

279-284: Prefer async/await over DispatchQueue for timer callback.

Per coding guidelines, async/await should be preferred over DispatchQueue for new async work. The timer callback uses DispatchQueue.main.async.

♻️ Proposed fix using MainActor
     private func startRefresh() {
         refreshState()
         refreshTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { _ in
-            DispatchQueue.main.async { refreshState() }
+            Task { `@MainActor` in
+                refreshState()
+            }
         }
     }

Based on learnings: "Use Swift 5.9+ with async/await concurrency instead of DispatchQueue for new async work"

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

In `@apps/native/wolfwave/Views/SongRequest/SongRequestQueueView.swift` around
lines 279 - 284, The timer callback uses DispatchQueue.main.async; replace it
with Swift concurrency by running the refresh on the main actor via Task. In
startRefresh (and the scheduledTimer closure) call Task { await MainActor.run {
refreshState() } } instead of DispatchQueue.main.async { refreshState() }; if
refreshState must be async or touch UI, consider annotating it `@MainActor` or
making it async so the await/MainActor.run usage is appropriate. Ensure
refreshTimer and startRefresh keep the Timer.scheduledTimer invocation but
invoke refreshState through Task+MainActor.
apps/native/wolfwave/Views/SongRequest/SongRequestSettingsView.swift (1)

181-194: Prefer Task.sleep over DispatchQueue.main.asyncAfter.

Per coding guidelines, async/await should be preferred over DispatchQueue for new async work.

♻️ Proposed fix using Task.sleep
         .onAppear {
             blocklist = songBlocklist?.allEntries ?? []
             musicAuthStatus = MusicAuthorization.currentStatus
             refreshTwitchState()
-            // Delayed re-check to catch late connections
-            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
-                refreshTwitchState()
-            }
+            // Delayed re-check to catch late connections
+            Task {
+                try? await Task.sleep(for: .seconds(1))
+                await MainActor.run { refreshTwitchState() }
+            }
         }

Based on learnings: "Use Swift 5.9+ with async/await concurrency instead of DispatchQueue for new async work"

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

In `@apps/native/wolfwave/Views/SongRequest/SongRequestSettingsView.swift` around
lines 181 - 194, Replace the DispatchQueue delayed re-check in the .onAppear
handler with an async/await Task using Task.sleep to wait 1 second and then call
refreshTwitchState; specifically, inside the .onAppear block (where blocklist,
musicAuthStatus = MusicAuthorization.currentStatus and refreshTwitchState() are
set) start a Task that awaits Task.sleep(nanoseconds: 1_000_000_000) and then
calls refreshTwitchState(), ensuring the call runs on the main actor if
refreshTwitchState mutates UI state (e.g., mark the Task `@MainActor` or call
await MainActor.run { refreshTwitchState() } as appropriate); leave the
NotificationCenter .onReceive handling with updateTwitchState untouched.
apps/native/WolfWaveTests/SongRequestServiceTests.swift (1)

304-317: Avoid the real 2.5s sleep in this unit test.

This makes the suite slow and timing-sensitive. Inject the polling interval / clock, or expose a single monitoring tick so the paused-state branch can be asserted synchronously.

Based on learnings "Prioritize testing pure logic (Bot command parsing, version comparison, state machines) 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/SongRequestServiceTests.swift` around lines 304 -
317, The test testAutoAdvanceDoesNotFireWhenPaused currently uses a real 2.5s
sleep; change the design so the test can run synchronously by either (A) making
SongRequestService accept an injectable polling interval or clock (e.g., add a
pollingInterval parameter to the initializer and use that in
startPlaybackMonitoring()) so the test can set a tiny interval, or (B) expose a
single monitoring tick API on SongRequestService (e.g., monitorTick() or
processPlaybackTick()) that startPlaybackMonitoring() calls repeatedly; update
the test to call that single tick directly after setting
mockController.isPlaying = false and mockController.isPaused = true and assert
no dequeue/advance occurred, then call stopPlaybackMonitoring() as before.
apps/native/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift (1)

144-145: Minor: Redundant lowercased() call on trigger.

The allTriggers property (from BotCommand extension in context snippet 1, line 71) already returns lowercased triggers. The trigger.lowercased() call on line 145 is redundant.

♻️ Suggested simplification
             for trigger in triggers {
-                let triggerLowered = trigger.lowercased()
                 // Match: message starts with the trigger (original hasPrefix behavior)
-                if lowered.hasPrefix(triggerLowered) {
+                if lowered.hasPrefix(trigger) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift`
around lines 144 - 145, The loop in BotCommandDispatcher that does `for trigger
in triggers { let triggerLowered = trigger.lowercased() }` performs a redundant
lowercasing because the triggers come from the BotCommand extension's
allTriggers property which already returns lowercased strings; remove the extra
`lowercased()` call and use `trigger` directly (or rename `triggerLowered` to
`trigger` where it's referenced) to simplify the code and avoid unnecessary
allocations while keeping references to the loop over `triggers`,
`triggerLowered`, and the `allTriggers` property in mind.
apps/native/wolfwave/Services/SongRequest/AppleMusicController.swift (1)

210-217: URL interpolation in AppleScript is safe here, but consider validation.

The song.url is a system-provided Apple Music URL, so it shouldn't contain AppleScript-breaking characters. However, for defense-in-depth, you could apply sanitizeForAppleScript to the URL string as well:

let urlString = sanitizeForAppleScript(musicURL.absoluteString)

This is a minor hardening suggestion since MusicKit URLs are well-formed.

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

In `@apps/native/wolfwave/Services/SongRequest/AppleMusicController.swift` around
lines 210 - 217, The AppleScript URL is currently interpolated directly from
song.url (musicURL.absoluteString) before calling runAppleScriptPreservingFocus;
to harden it, pass the URL string through sanitizeForAppleScript (e.g., use
sanitizeForAppleScript(musicURL.absoluteString)) and then interpolate that
sanitized value into the script so the Music open location string is escaped
safely; update the code around song.url, musicURL, and
runAppleScriptPreservingFocus to use the sanitized urlString.
apps/native/wolfwave/Services/SongRequest/LinkResolverService.swift (2)

66-75: URL extraction is fragile with punctuation.

extractURL splits only on spaces, so URLs followed by punctuation (common in chat messages like check this out: https://open.spotify.com/track/abc!) would include the trailing !. Consider trimming common trailing punctuation:

♻️ Suggested improvement
 static func extractURL(from text: String) -> String? {
     let words = text.split(separator: " ")
     for word in words {
-        let str = String(word)
+        var str = String(word)
+        // Strip common trailing punctuation
+        while let last = str.last, ["!", "?", ",", ".", ";", ":"].contains(String(last)) {
+            str.removeLast()
+        }
         if str.hasPrefix("http://") || str.hasPrefix("https://") {
             return str
         }
     }
     return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Services/SongRequest/LinkResolverService.swift` around
lines 66 - 75, extractURL(from:) is fragile because it splits on spaces and can
return URLs with trailing punctuation; replace the simple split logic with a
robust URL detector: use NSDataDetector configured for .link to find the first
URL in the input text and return its absoluteString (or, if you prefer to keep
manual parsing, trim common trailing punctuation characters like . , ! ? : ; ) ]
} and surrounding quotes from the detected token). Update the extractURL(from:)
implementation to use NSDataDetector (NSTextCheckingResult) and return the
detector's first match URL to avoid including trailing punctuation.

45-47: spotify.link/ short URLs may resolve to non-track content.

The spotify.link/ domain is Spotify's universal short link service that can redirect to albums, playlists, artists, or tracks. Treating all spotify.link/ URLs as track links may cause oEmbed to return unexpected metadata (e.g., album/playlist info) or fail silently.

Consider either:

  1. Following the redirect to get the canonical URL before checking, or
  2. Adding a note that only track links are reliably supported
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Services/SongRequest/LinkResolverService.swift` around
lines 45 - 47, The isSpotifyLink(_:) helper currently treats any spotify.link/
URL as a track, which is incorrect; update LinkResolverService.isSpotifyLink to
resolve short URLs (follow HTTP redirect) and then inspect the final URL's path
for the explicit track pattern (e.g., contains "open.spotify.com/track/") before
returning true, or alternatively limit the check to the canonical track pattern
only and add a comment/note that only track links are supported; use a simple
URLSession HEAD/GET to follow redirects and perform the canonical check inside
isSpotifyLink so callers get a reliable true/false for tracks.
🤖 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/docs/app/global.css`:
- Around line 419-427: The CSS uses the keyword "currentColor" with an uppercase
C in the selector .kinetic-headline .outline (specifically in
-webkit-text-stroke: 2px currentColor;), which violates stylelint's
value-keyword-case rule; update that property (and any other occurrences of the
keyword) to use lowercase "currentcolor" so the rule reads -webkit-text-stroke:
2px currentcolor; preserving all other declarations and selectors unchanged.

In `@apps/native/wolfwave.xcodeproj/project.pbxproj`:
- Around line 433-435: The Release build configuration is using
CODE_SIGN_IDENTITY = "Apple Development" which signs release artifacts with a
dev identity; locate the Release configuration block in the project.pbxproj
where CODE_SIGN_IDENTITY is set and change the value to "Developer ID
Application" so Release builds are signed with the Developer ID Application
identity (keep CODE_SIGN_ENTITLEMENTS and CODE_SIGN_STYLE unchanged and ensure
this change only applies to the Release configuration).

In `@apps/native/wolfwave/Services/SongRequest/SongBlocklist.swift`:
- Around line 55-60: In add(_ item: BlocklistItem) normalize the incoming value
by trimming whitespace/newlines and then lowercasing before doing the duplicate
check and before appending; specifically, compute a normalizedValue =
item.value.trimmingCharacters(in: .whitespacesAndNewlines) and ensure it's
non-empty (reject/return if empty) and use normalizedValue.lowercased() in the
entries.contains(...) comparison and when creating/persisting the stored
BlocklistItem so duplicates like "Taylor Swift " are caught and empty values are
not stored.

In `@apps/native/wolfwave/Services/SongRequest/SongRequestService.swift`:
- Around line 218-234: The recursive call in playNextInQueue on a generic
playback error can cause unbounded recursion or tight loops if many items fail;
modify playNextInQueue to avoid recursion by implementing a bounded retry/skip
mechanism: add a failure counter or maxRetries parameter tracked per invocation
or per queue item (e.g., attach a retryCount to the dequeued item), increment it
when musicController.playNow throws (except PlaybackError.musicAppNotRunning),
and if retryCount exceeds a configurable max (or after N consecutive failures)
either skip the item (log and continue) or stop iterating; alternatively convert
the recursive follow-up (await playNextInQueue()) into a loop that processes the
next item until successful or limit reached, and preserve current behavior for
PlaybackError.musicAppNotRunning by re-queuing with insertAtHead and
clearNowPlaying as before.

In `@apps/native/wolfwave/Services/SongRequest/SongSearchResolver.swift`:
- Around line 67-100: The logs in SongSearchResolver (inside resolve(url:) and
resolveText(_:)) currently emit raw user-controlled input (urlString and query);
update these Log.debug calls to redact PII by replacing the raw values with a
sanitized/stable identifier (e.g., a hashed or truncated fingerprint) and/or a
constant token, and use the app's structured logging helpers (emoji prefixes/PII
redaction) from Logger.swift; specifically change the Log.debug calls that
reference urlString in resolve(url:) and the one that logs query in
resolveText(_:) to log only the redacted identifier and any non-sensitive
context.
- Around line 84-88: The match arm for case .found in SongSearchResolver
currently force-unwraps artist when building searchQuery; change this to safe
optional binding (e.g., use if let artist = artist { ... } else { ... } or guard
let) to construct searchQuery without using !, then call
resolveText(searchQuery) as before; update the case .found(let title, let
artist) block to use the safe unwrap instead of the ternary with artist!.

In `@apps/native/wolfwave/Services/Twitch/Commands/QueueCommand.swift`:
- Around line 38-66: The code in QueueCommand.swift reads queue.isEmpty,
queue.nowPlaying and queue.items separately which can race with concurrent
mutations; add a single locked snapshot API on SongRequestQueue (e.g.,
SongRequestQueue.snapshot() or currentSnapshot()) that returns an immutable
struct containing nowPlaying, items (and derived isEmpty/count) while holding
the existing lock used by positions(for:). Update QueueCommand (the handler that
currently reads queue.nowPlaying and queue.items) to call this snapshot API once
and build the response from that snapshot instead of accessing queue properties
individually.

In `@apps/native/wolfwave/Views/Onboarding/OnboardingAppleMusicStepView.swift`:
- Around line 45-85: authStatus currently falls through to the request button
for the .restricted case; add an explicit else if authStatus == .restricted
branch (next to the .authorized and .denied branches) that mirrors the denied UI
pattern (e.g., a VStack/HStack with Image(systemName: "lock.shield.fill") or
similar, explanatory Text that the device policy restricts Apple Music access
and how to contact an administrator or check device management settings), ensure
the restricted UI does not show the Grant button or call
MusicAuthorization.request(), and include appropriate
accessibilityLabel/accessibilityIdentifier similar to the denied branch to keep
behavior consistent with SuccessFeedbackRow and isRequesting usage.

In `@apps/native/wolfwave/Views/Onboarding/OnboardingView.swift`:
- Around line 119-140: Add accessibility identifiers to the footer buttons so UI
tests can target them: for the Back button (inside the if !viewModel.isFirstStep
block around Button("Back")) add
.accessibilityIdentifier("onboarding_back_button") alongside the existing
.accessibilityLabel and .accessibilityHint, and for the Skip All button (inside
the if !viewModel.isLastStep block around Button("Skip All")) add
.accessibilityIdentifier("onboarding_skip_all_button") alongside its label and
hint; keep the existing calls to cancelTwitchOAuthIfNeeded() and
finishOnboarding() unchanged.
- Around line 196-197: shouldShowSkip currently reads
MusicAuthorization.currentStatus directly which SwiftUI doesn't observe; lift
the authorization into shared parent state so the footer updates when
authorization changes. Add an `@State` property (e.g.,
appleMusicAuthorizationStatus) in OnboardingView initialized from
MusicAuthorization.currentStatus, change shouldShowSkip to read that `@State`
instead of MusicAuthorization.currentStatus, pass a Binding of that state into
OnboardingAppleMusicStepView, and update that binding from the child when
calling MusicAuthorization.request() (or update the parent state in the request
completion) so both the footer (Skip button) and the child stay in sync.

In `@apps/native/WolfWaveTests/SongRequestCommandTests.swift`:
- Around line 353-379: The tests currently pass because SkipCommand and
ClearQueueCommand no-op when their songRequestService is nil; bind a stub/mocked
SongRequestService to the command instances (e.g., set the songRequestService
property on SkipCommand and ClearQueueCommand) before calling execute, and
assert that a viewer (context.isModerator/isBroadcaster false) does not invoke
the stub while a privileged user (isModerator or isBroadcaster true) does; the
stub should record whether its skip/clear method was called so you can assert
replyCalled or service method invocation for both viewer and
moderator/broadcaster cases.
- Around line 128-133: The test testDispatcherSkipsDisabledCommand currently can
pass for the wrong reason because a fresh BotCommandDispatcher may return nil
when no queue provider is wired; update the test to deterministically exercise
the enabled/disabled gate by either (a) registering a stub command/handler on
BotCommandDispatcher that returns a known non-nil result for "!queue" and then
assert that setting UserDefaults.standard.set(false, forKey:
AppConstants.UserDefaults.queueCommandEnabled) causes processMessage("!queue")
to return nil, or (b) explicitly bind a test queue provider to the dispatcher
before toggling AppConstants.UserDefaults.queueCommandEnabled so the only
variable is the enabled flag; reference BotCommandDispatcher,
processMessage(_:), testDispatcherSkipsDisabledCommand, and
AppConstants.UserDefaults.queueCommandEnabled when making the change.

In `@apps/native/WolfWaveTests/SongRequestServiceTests.swift`:
- Around line 32-33: The mock AppleMusicController currently hardcodes
search(query:) and resolve(url:) to return .notFound, preventing processRequest
tests (e.g., testRequestWhileMusicAppClosedBuffers,
testPlayNextInQueueRequeuesItemWhenMusicAppNotRunning,
testHoldBlocksAutoPlayOnRequest) from exercising buffering/hold/playback
branches; change the mock to allow configuring the search result (either by
adding an initializer parameter or a mutable property on the mock) and/or accept
an injectable resolver/stub so tests can set it to return a found song
(AppleMusicController.SearchResult.found) when needed, and update affected tests
to set that configured result before calling processRequest(...).

In `@CHANGELOG.md`:
- Line 15: The CHANGELOG.md entry lists the commands `!hold`, `!resume`, and
`!unhold` but the docs changelog only lists `!hold` and `!resume`; update the
docs changelog to include `!unhold` so both changelogs are consistent (add
`!unhold` to the same list/line that currently contains `!hold / !resume` in the
changelog.mdx entry), or alternatively remove `!unhold` from CHANGELOG.md if
that command shouldn’t be documented; ensure the final text uses the exact
command token `!unhold` so both files match.

---

Nitpick comments:
In `@apps/docs/app/`(home)/page.tsx:
- Around line 224-234: The map over tickerItems uses the array index as the
React key which is unclear; update the key in the .map callback that renders
.ticker-item to a more descriptive/compound key (e.g., derive from tickerItems'
value plus the index or a unique id) so each <span key=...
className="ticker-item"> uses a stable, meaningful key instead of just i; locate
the map over [...tickerItems, ...tickerItems, ...tickerItems] and replace
key={i} with a compound key like `${item}-${i}` or an actual unique identifier
from the ticker data.

In `@apps/native/wolfwave/Services/SongRequest/AppleMusicController.swift`:
- Around line 210-217: The AppleScript URL is currently interpolated directly
from song.url (musicURL.absoluteString) before calling
runAppleScriptPreservingFocus; to harden it, pass the URL string through
sanitizeForAppleScript (e.g., use
sanitizeForAppleScript(musicURL.absoluteString)) and then interpolate that
sanitized value into the script so the Music open location string is escaped
safely; update the code around song.url, musicURL, and
runAppleScriptPreservingFocus to use the sanitized urlString.

In `@apps/native/wolfwave/Services/SongRequest/LinkResolverService.swift`:
- Around line 66-75: extractURL(from:) is fragile because it splits on spaces
and can return URLs with trailing punctuation; replace the simple split logic
with a robust URL detector: use NSDataDetector configured for .link to find the
first URL in the input text and return its absoluteString (or, if you prefer to
keep manual parsing, trim common trailing punctuation characters like . , ! ? :
; ) ] } and surrounding quotes from the detected token). Update the
extractURL(from:) implementation to use NSDataDetector (NSTextCheckingResult)
and return the detector's first match URL to avoid including trailing
punctuation.
- Around line 45-47: The isSpotifyLink(_:) helper currently treats any
spotify.link/ URL as a track, which is incorrect; update
LinkResolverService.isSpotifyLink to resolve short URLs (follow HTTP redirect)
and then inspect the final URL's path for the explicit track pattern (e.g.,
contains "open.spotify.com/track/") before returning true, or alternatively
limit the check to the canonical track pattern only and add a comment/note that
only track links are supported; use a simple URLSession HEAD/GET to follow
redirects and perform the canonical check inside isSpotifyLink so callers get a
reliable true/false for tracks.

In `@apps/native/wolfwave/Services/SongRequest/SongBlocklist.swift`:
- Around line 84-96: The load() currently swallows JSON decode failures and
leaves a corrupt blob under storageKey so subsequent loads always fail; modify
load() to catch and log the decoding error (capture the thrown Error from
JSONDecoder.decode), and if decoding fails take remedial action such as removing
or moving the bad data (e.g. removeObject(forKey: storageKey) or copy it to a
quarantine key like "corrupt_\(storageKey)") and then set entries = [] under the
same lock used by save() (lock.withLock { entries = [] }) so the app can
recover; also ensure save() still writes the new empty or repaired entries
normally.

In `@apps/native/wolfwave/Services/Twitch/Commands/AsyncBotCommand.swift`:
- Line 33: The execute signature's reply closure should be annotated as
`@Sendable` to satisfy Swift concurrency safety: update the
AsyncBotCommand.execute declaration so the reply parameter is declared as reply:
`@Sendable` `@escaping` (String) -> Void (and propagate this change to any
protocol/type declarations and callers that conform to or call
AsyncBotCommand.execute), then fix any calling sites to pass a
`@Sendable-compatible` closure (or wrap non-sendable captures) so the code
compiles under strict concurrency checking.

In `@apps/native/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift`:
- Around line 144-145: The loop in BotCommandDispatcher that does `for trigger
in triggers { let triggerLowered = trigger.lowercased() }` performs a redundant
lowercasing because the triggers come from the BotCommand extension's
allTriggers property which already returns lowercased strings; remove the extra
`lowercased()` call and use `trigger` directly (or rename `triggerLowered` to
`trigger` where it's referenced) to simplify the code and avoid unnecessary
allocations while keeping references to the loop over `triggers`,
`triggerLowered`, and the `allTriggers` property in mind.

In `@apps/native/wolfwave/Services/Twitch/Commands/HoldCommand.swift`:
- Around line 14-24: HoldCommand is missing user-configurable keys so it can't
be disabled or aliased; add the same properties used by
SkipCommand/ClearQueueCommand: implement var enabledKey: String and var
aliasesKey: String in the HoldCommand class, returning the appropriate
UserDefaults keys (e.g. AppConstants.UserDefaults.holdCommandEnabled and
AppConstants.UserDefaults.holdCommandAliases), and add the new
holdCommandAliases and holdCommandEnabled entries to AppConstants.UserDefaults
to expose alias and enable/disable settings.

In `@apps/native/wolfwave/Views/Shared/WhatsNewView.swift`:
- Around line 21-26: The feature list in WhatsNewView uses display titles which
makes accessibility identifiers brittle; add a stable id field to each feature
tuple/struct (e.g., change the array entries to include an "id" like
"song_requests", "hold_mode", etc.), update the model used by WhatsNewView (the
features collection) to include that id, and change the code that currently
derives identifiers from feature.title (referenced in WhatsNewView and at the
identifier construction site around where feature.title is used) to use
feature.id when building accessibility identifiers and test selectors so
identifiers remain stable when titles change.
- Around line 21-26: In WhatsNewView.swift update the short-description strings
in the tuples used by WhatsNewView (the list entries like the ("music.mic",
.pink, "Song Requests", "...") tuples) to be single, punchy clauses instead of
full sentences—e.g. condense each description to one fragment (3–6 words) that
summarizes the feature—so replace the long fourth-field strings for Song
Requests, Hold Mode, Queue Controls, Live Queue View, Fallback Playlist, and No
Focus-Steal with much shorter phrases to improve scanability.

In `@apps/native/wolfwave/Views/SongRequest/SongRequestQueueView.swift`:
- Around line 202-251: The action buttons in the actionButtons view (the Skip
Button, the Hold/Resume Button and the Clear Queue Button plus the confirmation
dialog action) are missing accessibility identifiers/labels; update each Button
(the one that calls service?.skip(), the one that calls
service?.setHold(!isHeld), and the Clear Queue Button and its destructive
confirmation Button) to add .accessibilityIdentifier("<unique_id>") and
.accessibilityLabel("<readable label>") (use clear ids like "skipButton",
"holdResumeButton", "clearQueueButton", "confirmClearQueueButton") so UI tests
can reliably find them while keeping existing behavior intact.
- Around line 279-284: The timer callback uses DispatchQueue.main.async; replace
it with Swift concurrency by running the refresh on the main actor via Task. In
startRefresh (and the scheduledTimer closure) call Task { await MainActor.run {
refreshState() } } instead of DispatchQueue.main.async { refreshState() }; if
refreshState must be async or touch UI, consider annotating it `@MainActor` or
making it async so the await/MainActor.run usage is appropriate. Ensure
refreshTimer and startRefresh keep the Timer.scheduledTimer invocation but
invoke refreshState through Task+MainActor.

In `@apps/native/wolfwave/Views/SongRequest/SongRequestSettingsView.swift`:
- Around line 181-194: Replace the DispatchQueue delayed re-check in the
.onAppear handler with an async/await Task using Task.sleep to wait 1 second and
then call refreshTwitchState; specifically, inside the .onAppear block (where
blocklist, musicAuthStatus = MusicAuthorization.currentStatus and
refreshTwitchState() are set) start a Task that awaits Task.sleep(nanoseconds:
1_000_000_000) and then calls refreshTwitchState(), ensuring the call runs on
the main actor if refreshTwitchState mutates UI state (e.g., mark the Task
`@MainActor` or call await MainActor.run { refreshTwitchState() } as appropriate);
leave the NotificationCenter .onReceive handling with updateTwitchState
untouched.

In `@apps/native/WolfWaveTests/OnboardingViewModelTests.swift`:
- Around line 139-159: Extend the testFullNavigationCycleForwardAndBack test to
include the new .appleMusicAccess step: after the current assertion for
.obsWidget call viewModel.goToNextStep() and assert viewModel.currentStep ==
.appleMusicAccess, then continue the backward assertions by calling
viewModel.goToPreviousStep() and asserting it returns to .obsWidget before
continuing back to .discordConnect, .twitchConnect, and .welcome; update the
sequence of viewModel.goToNextStep()/goToPreviousStep() calls and their
XCTAssertEqual checks to cover the full forward-and-back round trip including
.appleMusicAccess.

In `@apps/native/WolfWaveTests/SongRequestServiceTests.swift`:
- Around line 304-317: The test testAutoAdvanceDoesNotFireWhenPaused currently
uses a real 2.5s sleep; change the design so the test can run synchronously by
either (A) making SongRequestService accept an injectable polling interval or
clock (e.g., add a pollingInterval parameter to the initializer and use that in
startPlaybackMonitoring()) so the test can set a tiny interval, or (B) expose a
single monitoring tick API on SongRequestService (e.g., monitorTick() or
processPlaybackTick()) that startPlaybackMonitoring() calls repeatedly; update
the test to call that single tick directly after setting
mockController.isPlaying = false and mockController.isPaused = true and assert
no dequeue/advance occurred, then call stopPlaybackMonitoring() as before.
🪄 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: a6919454-feac-479f-a2fe-da5cddd055e0

📥 Commits

Reviewing files that changed from the base of the PR and between 43d990e and 97d0033.

📒 Files selected for processing (42)
  • CHANGELOG.md
  • apps/docs/app/(home)/page.tsx
  • apps/docs/app/global.css
  • apps/docs/app/layout.tsx
  • apps/docs/content/docs/changelog.mdx
  • apps/native/WolfWaveTests/OnboardingViewModelEdgeCaseTests.swift
  • apps/native/WolfWaveTests/OnboardingViewModelTests.swift
  • apps/native/WolfWaveTests/SongRequestCommandTests.swift
  • apps/native/WolfWaveTests/SongRequestQueueTests.swift
  • apps/native/WolfWaveTests/SongRequestServiceTests.swift
  • apps/native/wolfwave.xcodeproj/project.pbxproj
  • apps/native/wolfwave/Core/AppConstants+Notifications.swift
  • apps/native/wolfwave/Core/AppConstants.swift
  • apps/native/wolfwave/Core/AppDelegate+MenuBar.swift
  • apps/native/wolfwave/Core/AppDelegate+Services.swift
  • apps/native/wolfwave/Core/BlocklistItem.swift
  • apps/native/wolfwave/Core/SongRequestItem.swift
  • apps/native/wolfwave/Services/SongRequest/AppleMusicController.swift
  • apps/native/wolfwave/Services/SongRequest/LinkResolverService.swift
  • apps/native/wolfwave/Services/SongRequest/SongBlocklist.swift
  • apps/native/wolfwave/Services/SongRequest/SongRequestQueue.swift
  • apps/native/wolfwave/Services/SongRequest/SongRequestService.swift
  • apps/native/wolfwave/Services/SongRequest/SongSearchResolver.swift
  • apps/native/wolfwave/Services/Twitch/Commands/AsyncBotCommand.swift
  • apps/native/wolfwave/Services/Twitch/Commands/BotCommand.swift
  • apps/native/wolfwave/Services/Twitch/Commands/BotCommandContext.swift
  • apps/native/wolfwave/Services/Twitch/Commands/BotCommandDispatcher.swift
  • apps/native/wolfwave/Services/Twitch/Commands/ClearQueueCommand.swift
  • apps/native/wolfwave/Services/Twitch/Commands/HoldCommand.swift
  • apps/native/wolfwave/Services/Twitch/Commands/MyQueueCommand.swift
  • apps/native/wolfwave/Services/Twitch/Commands/QueueCommand.swift
  • apps/native/wolfwave/Services/Twitch/Commands/SkipCommand.swift
  • apps/native/wolfwave/Services/Twitch/Commands/SongRequestCommand.swift
  • apps/native/wolfwave/Services/Twitch/TwitchChatService.swift
  • apps/native/wolfwave/Views/Onboarding/OnboardingAppleMusicStepView.swift
  • apps/native/wolfwave/Views/Onboarding/OnboardingView.swift
  • apps/native/wolfwave/Views/Onboarding/OnboardingViewModel.swift
  • apps/native/wolfwave/Views/SettingsView.swift
  • apps/native/wolfwave/Views/Shared/WhatsNewView.swift
  • apps/native/wolfwave/Views/SongRequest/SongRequestQueueView.swift
  • apps/native/wolfwave/Views/SongRequest/SongRequestSettingsView.swift
  • apps/native/wolfwave/WolfWaveApp.swift

Comment thread apps/docs/app/global.css
Comment on lines 433 to 435
CODE_SIGN_ENTITLEMENTS = wolfwave/wolfwave.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For a macOS app distributed outside the App Store with Sparkle auto-updates, what code-signing identity should the Xcode Release configuration use for archiving and notarization: Apple Development, Apple Distribution, or Developer ID Application?

💡 Result:

For a macOS app distributed outside the App Store with Sparkle auto-updates, the Xcode Release configuration should use Developer ID Application for archiving and notarization.

Citations:


Change Release target signing identity from "Apple Development" to "Developer ID Application".

For a macOS app distributed outside the App Store with Sparkle auto-updates, using "Apple Development" in the Release configuration will produce a dev-signed app instead of a release-signed artifact suitable for archiving, notarization, and distribution.

🤖 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 433 - 435, The
Release build configuration is using CODE_SIGN_IDENTITY = "Apple Development"
which signs release artifacts with a dev identity; locate the Release
configuration block in the project.pbxproj where CODE_SIGN_IDENTITY is set and
change the value to "Developer ID Application" so Release builds are signed with
the Developer ID Application identity (keep CODE_SIGN_ENTITLEMENTS and
CODE_SIGN_STYLE unchanged and ensure this change only applies to the Release
configuration).

Comment on lines +55 to +60
func add(_ item: BlocklistItem) {
lock.withLock {
// Avoid duplicates
guard !entries.contains(where: {
$0.type == item.type && $0.value.lowercased() == item.value.lowercased()
}) else { return }
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

Normalize blocklist values before duplicate checks.

lowercased() alone still treats leading/trailing whitespace as distinct, so "Taylor Swift " can be saved as a separate entry and then never match normal metadata. Trim whitespace/newlines and reject empty values before storing.

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

In `@apps/native/wolfwave/Services/SongRequest/SongBlocklist.swift` around lines
55 - 60, In add(_ item: BlocklistItem) normalize the incoming value by trimming
whitespace/newlines and then lowercasing before doing the duplicate check and
before appending; specifically, compute a normalizedValue =
item.value.trimmingCharacters(in: .whitespacesAndNewlines) and ensure it's
non-empty (reject/return if empty) and use normalizedValue.lowercased() in the
entries.contains(...) comparison and when creating/persisting the stored
BlocklistItem so duplicates like "Taylor Swift " are caught and empty values are
not stored.

Comment on lines +218 to +234
private func playNextInQueue() async {
guard let item = queue.dequeue(), let song = item.song else { return }

do {
try await musicController.playNow(song: song)
isPlayingFallback = false
Log.debug("SongRequestService: Now playing \"\(item.title)\" by \(item.artist) (requested by \(item.requesterUsername))", category: "SongRequest")
} catch PlaybackError.musicAppNotRunning {
// Music.app closed — put the item back at the front so it plays first when Music.app re-opens
queue.insertAtHead(item)
queue.clearNowPlaying()
Log.debug("SongRequestService: Music.app closed — \"\(item.title)\" re-queued at head", category: "SongRequest")
} catch {
Log.debug("SongRequestService: Failed to play \"\(item.title)\": \(error)", category: "SongRequest")
await playNextInQueue()
}
}
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 unbounded recursion on repeated playback failures.

If playNow throws an error other than musicAppNotRunning (line 230-233), the method recursively calls itself. If every song in the queue fails to play (e.g., due to a persistent network issue or invalid songs), this could drain the queue in a tight loop or, in extreme cases, cause a stack overflow.

Consider adding a retry limit or breaking the recursion:

🛡️ Suggested fix to prevent unbounded recursion
-    private func playNextInQueue() async {
+    private func playNextInQueue(retryCount: Int = 0) async {
+        guard retryCount < 3 else {
+            Log.debug("SongRequestService: Max retries reached, stopping auto-advance", category: "SongRequest")
+            return
+        }
         guard let item = queue.dequeue(), let song = item.song else { return }

         do {
             try await musicController.playNow(song: song)
             isPlayingFallback = false
             Log.debug("SongRequestService: Now playing \"\(item.title)\" by \(item.artist) (requested by \(item.requesterUsername))", category: "SongRequest")
         } catch PlaybackError.musicAppNotRunning {
             queue.insertAtHead(item)
             queue.clearNowPlaying()
             Log.debug("SongRequestService: Music.app closed — \"\(item.title)\" re-queued at head", category: "SongRequest")
         } catch {
             Log.debug("SongRequestService: Failed to play \"\(item.title)\": \(error)", category: "SongRequest")
-            await playNextInQueue()
+            await playNextInQueue(retryCount: retryCount + 1)
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/native/wolfwave/Services/SongRequest/SongRequestService.swift` around
lines 218 - 234, The recursive call in playNextInQueue on a generic playback
error can cause unbounded recursion or tight loops if many items fail; modify
playNextInQueue to avoid recursion by implementing a bounded retry/skip
mechanism: add a failure counter or maxRetries parameter tracked per invocation
or per queue item (e.g., attach a retryCount to the dequeued item), increment it
when musicController.playNow throws (except PlaybackError.musicAppNotRunning),
and if retryCount exceeds a configurable max (or after N consecutive failures)
either skip the item (log and continue) or stop iterating; alternatively convert
the recursive follow-up (await playNextInQueue()) into a loop that processes the
next item until successful or limit reached, and preserve current behavior for
PlaybackError.musicAppNotRunning by re-queuing with insertAtHead and
clearNowPlaying as before.

Comment on lines +67 to +100
Log.debug("SongSearchResolver: Resolving link: \(urlString)", category: "SongRequest")

let result = await linkResolver.resolve(url: urlString)

switch result {
case .appleMusicURL(let url):
// Resolve Apple Music URL directly via MusicKit
let musicResult = await musicController.resolve(url: url)
switch musicResult {
case .found(let song):
return .found(song)
case .notFound:
return .linkNotFound
case .error(let message):
return .error(message)
}

case .found(let title, let artist):
// oEmbed gave us title/artist — search MusicKit
let searchQuery = artist != nil ? "\(title) \(artist!)" : title
Log.debug("SongSearchResolver: oEmbed resolved to: \(searchQuery)", category: "SongRequest")
return await resolveText(searchQuery)

case .notFound:
return .linkNotFound

case .error(let message):
return .error(message)
}
}

private func resolveText(_ query: String) async -> Result {
Log.debug("SongSearchResolver: Searching Apple Music for: \(query)", category: "SongRequest")

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 | 🟠 Major

Redact user-controlled input before logging it.

Line 67 and Line 99 log raw URLs / search text coming straight from chat input. That will persist arbitrary user content in the app logs, including anything sensitive embedded in the query string. Please log a redacted form or a stable identifier instead.

As per coding guidelines "Use structured logging with emoji prefixes, file rotation (5MB cap), and PII redaction in Logger.swift".

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

In `@apps/native/wolfwave/Services/SongRequest/SongSearchResolver.swift` around
lines 67 - 100, The logs in SongSearchResolver (inside resolve(url:) and
resolveText(_:)) currently emit raw user-controlled input (urlString and query);
update these Log.debug calls to redact PII by replacing the raw values with a
sanitized/stable identifier (e.g., a hashed or truncated fingerprint) and/or a
constant token, and use the app's structured logging helpers (emoji prefixes/PII
redaction) from Logger.swift; specifically change the Log.debug calls that
reference urlString in resolve(url:) and the one that logs query in
resolveText(_:) to log only the redacted identifier and any non-sensitive
context.

Comment on lines +196 to +197
case .appleMusicAccess:
return MusicAuthorization.currentStatus != .authorized
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

🧩 Analysis chain

🌐 Web query:

In SwiftUI, if a child view updates its own @StateafterMusicAuthorization.request(), will a parent sibling that reads MusicAuthorization.currentStatus directly recompute automatically, or should that authorization state be lifted into shared state?

💡 Result:

No, a parent sibling view that reads MusicAuthorization.currentStatus directly will not automatically recompute when a child view calls MusicAuthorization.request and the status changes. MusicAuthorization.currentStatus is a static property that is not observable by SwiftUI. Reading it directly in a view's body does not trigger recomputation when its value changes system-wide, as SwiftUI only tracks changes to @State, @ObservedObject, or @Bindable properties. The child's @State update only affects that child view's recomputation. Sibling views do not share state through the parent's recompute unless the parent has shared observable state that both depend on. To ensure the parent sibling updates reliably, lift the authorization state into shared parent state, such as an @State or @Observable property that is manually updated after request (using the returned status or by polling currentStatus), following Apple and community best practices for MusicKit in SwiftUI.

Citations:


🏁 Script executed:

# First, locate the OnboardingView.swift file
find . -name "OnboardingView.swift" -type f

Repository: MrDemonWolf/wolfwave

Length of output: 124


🏁 Script executed:

# Read the OnboardingView.swift file around lines 196-197 and get context
wc -l apps/native/wolfwave/Views/Onboarding/OnboardingView.swift

Repository: MrDemonWolf/wolfwave

Length of output: 126


🏁 Script executed:

# Read the relevant section with more context (around lines 196-197)
sed -n '180,220p' apps/native/wolfwave/Views/Onboarding/OnboardingView.swift

Repository: MrDemonWolf/wolfwave

Length of output: 1167


🏁 Script executed:

# Search for shouldShowSkip definition
rg "shouldShowSkip" apps/native/wolfwave/Views/Onboarding/ -A 5 -B 2

Repository: MrDemonWolf/wolfwave

Length of output: 1732


🏁 Script executed:

# Search for OnboardingAppleMusicStepView to understand its structure
rg "OnboardingAppleMusicStepView" apps/native/wolfwave/Views/Onboarding/ -A 10 -B 2

Repository: MrDemonWolf/wolfwave

Length of output: 3901


🏁 Script executed:

# Find the footer usage to understand how it reads the state
rg "footer|shouldShowSkip" apps/native/wolfwave/Views/Onboarding/OnboardingView.swift -B 2 -A 2

Repository: MrDemonWolf/wolfwave

Length of output: 529


🏁 Script executed:

# Read the full OnboardingAppleMusicStepView.swift
cat -n apps/native/wolfwave/Views/Onboarding/OnboardingAppleMusicStepView.swift

Repository: MrDemonWolf/wolfwave

Length of output: 4471


🏁 Script executed:

# Search for MusicAuthorization.request() in the onboarding views
rg "MusicAuthorization.request" apps/native/wolfwave/Views/Onboarding/ -A 5 -B 2

Repository: MrDemonWolf/wolfwave

Length of output: 1014


🏁 Script executed:

# Check the overall structure of OnboardingView to see the footer and step view hierarchy
sed -n '1,100p' apps/native/wolfwave/Views/Onboarding/OnboardingView.swift

Repository: MrDemonWolf/wolfwave

Length of output: 3540


🏁 Script executed:

# Check the navigationBar section where shouldShowSkip is used
sed -n '120,180p' apps/native/wolfwave/Views/Onboarding/OnboardingView.swift

Repository: MrDemonWolf/wolfwave

Length of output: 2467


🏁 Script executed:

# Check the footer/navigation bar more specifically
sed -n '140,165p' apps/native/wolfwave/Views/Onboarding/OnboardingView.swift

Repository: MrDemonWolf/wolfwave

Length of output: 995


Lift Apple Music auth into shared parent state so the footer updates reliably.

The shouldShowSkip computed property reads MusicAuthorization.currentStatus directly, which SwiftUI does not track for changes. When OnboardingAppleMusicStepView calls MusicAuthorization.request() and updates its local @State, the parent's footer (Skip button) will not recompute automatically. The button can remain disabled even after authorization succeeds until an unrelated state change triggers the parent to refresh.

Move the authorization status into shared parent state (e.g., @State in OnboardingView) and pass it down to the child view as a binding so both the footer and step view stay in sync.

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

In `@apps/native/wolfwave/Views/Onboarding/OnboardingView.swift` around lines 196
- 197, shouldShowSkip currently reads MusicAuthorization.currentStatus directly
which SwiftUI doesn't observe; lift the authorization into shared parent state
so the footer updates when authorization changes. Add an `@State` property (e.g.,
appleMusicAuthorizationStatus) in OnboardingView initialized from
MusicAuthorization.currentStatus, change shouldShowSkip to read that `@State`
instead of MusicAuthorization.currentStatus, pass a Binding of that state into
OnboardingAppleMusicStepView, and update that binding from the child when
calling MusicAuthorization.request() (or update the parent state in the request
completion) so both the footer (Skip button) and the child stay in sync.

Comment on lines +128 to +133
func testDispatcherSkipsDisabledCommand() {
UserDefaults.standard.set(false, forKey: AppConstants.UserDefaults.queueCommandEnabled)
let dispatcher = BotCommandDispatcher()
let result = dispatcher.processMessage("!queue")
XCTAssertNil(result)
}
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 | 🟠 Major

This does not actually isolate the enabled/disabled gate.

A fresh BotCommandDispatcher can still return nil for !queue when no queue provider is wired, so this passes even if queueCommandEnabled is ignored. Please use a stub command with a deterministic response, or bind the queue before asserting the disabled path.

Based on learnings "Prioritize testing pure logic (Bot command parsing, version comparison, state machines) 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/SongRequestCommandTests.swift` around lines 128 -
133, The test testDispatcherSkipsDisabledCommand currently can pass for the
wrong reason because a fresh BotCommandDispatcher may return nil when no queue
provider is wired; update the test to deterministically exercise the
enabled/disabled gate by either (a) registering a stub command/handler on
BotCommandDispatcher that returns a known non-nil result for "!queue" and then
assert that setting UserDefaults.standard.set(false, forKey:
AppConstants.UserDefaults.queueCommandEnabled) causes processMessage("!queue")
to return nil, or (b) explicitly bind a test queue provider to the dispatcher
before toggling AppConstants.UserDefaults.queueCommandEnabled so the only
variable is the enabled flag; reference BotCommandDispatcher,
processMessage(_:), testDispatcherSkipsDisabledCommand, and
AppConstants.UserDefaults.queueCommandEnabled when making the change.

Comment on lines +353 to +379
func testSkipCommandSilentlyIgnoresNonPrivileged() {
let command = SkipCommand()
var replyCalled = false
command.execute(
message: "!skip",
context: BotCommandContext(
userID: "1", username: "viewer",
isModerator: false, isBroadcaster: false,
isSubscriber: false, messageID: "m"
)
) { _ in replyCalled = true }
XCTAssertFalse(replyCalled)
}

func testClearQueueCommandSilentlyIgnoresNonPrivileged() {
let command = ClearQueueCommand()
var replyCalled = false
command.execute(
message: "!clearqueue",
context: BotCommandContext(
userID: "1", username: "viewer",
isModerator: false, isBroadcaster: false,
isSubscriber: false, messageID: "m"
)
) { _ in replyCalled = true }
XCTAssertFalse(replyCalled)
}
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 | 🟠 Major

Bind a service before asserting the privilege check.

SkipCommand and ClearQueueCommand can no-op when songRequestService is nil, so these tests pass even if context.isPrivileged is never consulted. Inject a stub service and compare viewer vs moderator/broadcaster behavior instead.

Based on learnings "Prioritize testing pure logic (Bot command parsing, version comparison, state machines) 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/SongRequestCommandTests.swift` around lines 353 -
379, The tests currently pass because SkipCommand and ClearQueueCommand no-op
when their songRequestService is nil; bind a stub/mocked SongRequestService to
the command instances (e.g., set the songRequestService property on SkipCommand
and ClearQueueCommand) before calling execute, and assert that a viewer
(context.isModerator/isBroadcaster false) does not invoke the stub while a
privileged user (isModerator or isBroadcaster true) does; the stub should record
whether its skip/clear method was called so you can assert replyCalled or
service method invocation for both viewer and moderator/broadcaster cases.

Comment on lines +32 to +33
func search(query: String) async -> AppleMusicController.SearchResult { .notFound }
func resolve(url: URL) async -> AppleMusicController.SearchResult { .notFound }
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 | 🟠 Major

Make the mock search path configurable.

With search(query:) and resolve(url:) hardcoded to .notFound, the processRequest(...) tests below never reach buffering / hold / playback branches. That makes cases like testRequestWhileMusicAppClosedBuffers, testPlayNextInQueueRequeuesItemWhenMusicAppNotRunning, and testHoldBlocksAutoPlayOnRequest pass even if the post-search logic is broken. Please make the mock return a found song for those scenarios, or inject a stub resolver that does.

Based on learnings "Prioritize testing pure logic (Bot command parsing, version comparison, state machines) 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/SongRequestServiceTests.swift` around lines 32 -
33, The mock AppleMusicController currently hardcodes search(query:) and
resolve(url:) to return .notFound, preventing processRequest tests (e.g.,
testRequestWhileMusicAppClosedBuffers,
testPlayNextInQueueRequeuesItemWhenMusicAppNotRunning,
testHoldBlocksAutoPlayOnRequest) from exercising buffering/hold/playback
branches; change the mock to allow configuring the search result (either by
adding an initializer parameter or a mutable property on the mock) and/or accept
an injectable resolver/stub so tests can set it to return a found song
(AppleMusicController.SearchResult.found) when needed, and update affected tests
to set that configured result before calling processRequest(...).

Comment thread CHANGELOG.md Outdated
- **`!queue` / `!myqueue`** — show the full request queue or a viewer's own requests in chat.
- **`!skip` / `!next`** — mod/broadcaster-only command to skip the current request.
- **`!clearqueue`** — mod/broadcaster-only command to wipe the queue (with in-app confirmation dialog).
- **`!hold` / `!resume` / `!unhold`** — mod/broadcaster-only hold mode; new requests buffer without auto-playing so the streamer can curate before releasing the queue.
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

Minor inconsistency: !unhold command documented here but not in changelog.mdx.

This file lists !hold / !resume / !unhold but the docs changelog (apps/docs/content/docs/changelog.mdx line 18) only lists !hold / !resume. Consider aligning both changelogs for consistency.

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

In `@CHANGELOG.md` at line 15, The CHANGELOG.md entry lists the commands `!hold`,
`!resume`, and `!unhold` but the docs changelog only lists `!hold` and
`!resume`; update the docs changelog to include `!unhold` so both changelogs are
consistent (add `!unhold` to the same list/line that currently contains `!hold /
!resume` in the changelog.mdx entry), or alternatively remove `!unhold` from
CHANGELOG.md if that command shouldn’t be documented; ensure the final text uses
the exact command token `!unhold` so both files match.

nathanialhenniges and others added 3 commits April 15, 2026 03:57
Editorial tech-luxe aesthetic — kinetic hero, liquid-glass now-playing
frame, infinite integration ticker, asymmetric bento features with live
Twitch chat mock, diagonal 3-step flow, terminal dev card, giant CTA.

- layout.tsx: add JetBrains Mono + Instrument Serif fonts
- global.css: mesh bg, grid lines, noise, kinetic headline, liquid-glass,
  ticker marquee, shimmer btn, bento cards, terminal window, reduced-motion
- page.tsx: full rewrite into 6 sections, v2.0 song-requests teaser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use dorny/paths-filter to detect native changes. When a PR only touches
apps/docs/** or apps/marketing/**, the xcodebuild step is skipped and
the job reports pass. Unblocks docs-only PRs while the song-requests
test malloc crash is investigated separately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nathanialhenniges nathanialhenniges force-pushed the claude/agitated-chandrasekhar branch from f53aba5 to 6e58283 Compare April 15, 2026 08:57
@nathanialhenniges nathanialhenniges merged commit 5ac9fcd into main Apr 15, 2026
3 checks passed
@nathanialhenniges nathanialhenniges deleted the claude/agitated-chandrasekhar branch April 15, 2026 08:58
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