Skip to content

Design-fidelity follow-ups: cosmetic alignment, sidebar breakdown, mic-test banner#19

Merged
jtn0123 merged 3 commits into
masterfrom
design-fidelity-followups
May 16, 2026
Merged

Design-fidelity follow-ups: cosmetic alignment, sidebar breakdown, mic-test banner#19
jtn0123 merged 3 commits into
masterfrom
design-fidelity-followups

Conversation

@jtn0123
Copy link
Copy Markdown
Owner

@jtn0123 jtn0123 commented May 16, 2026

Summary

Follow-ups from the round-2 fidelity audit of the design redesign. Three focused commits:

  1. Cosmetic alignment — pulls six values that drifted between the prototype and the Swift handoff back to the prototype's exact numbers: Welcome window 600×720→580×700, status menu width 300→320, "Help"→"Help / Welcome", Welcome tile height 70→64, recording-window corner radius 18→22, Overview hero numeral 52pt→48pt, Classic bar count 64→56.

  2. Sidebar lifetime-breakdown popover — the "Total recorded" sidebar footer now shows a month-over-month delta line and, on hover, a popover breaking down this month / last 7 days / avg per day / time saved / sessions. All figures from real UsageMetricsStore data. (The prototype's "installed N days ago" row is omitted — the app doesn't persist an install date and fabricating one wasn't warranted.)

  3. "Test with my voice" mic-test banner — ports the deferred Visuals feature: a banner that captures live mic audio (AVAudioEngine tap → RMS level + 64 samples + 8 FFT bands) and feeds the shared LivePreviewSampler so every waveform preview reacts to the user's real voice. Off/live/denied/error states with permission handling. Capture is visualization-only — nothing recorded; the mic is released on stop/error/tab-dismiss.

Test plan

  • swift build + swift build --build-tests clean
  • swiftlint --strict clean
  • Full suite: 2776 tests, 0 failures, 37 skipped
  • Manual: the mic-test banner needs a real mic — verify capture, level meter, denied/error states (can't be tested headlessly)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added live microphone test card displaying real-time audio input visualization
    • Added dashboard sidebar breakdown popover showing monthly statistics
  • Style

    • Updated help menu label to include "Welcome"
    • Refined welcome window dimensions, spacing, and typography
    • Adjusted layout metrics and visual hierarchy across the interface
  • Tests

    • Added comprehensive test coverage for new microphone testing functionality

Review Change Stack

jtn0123 and others added 3 commits May 15, 2026 21:06
Pulls six values that drifted between the design prototype and the Swift
handoff back to the prototype's numbers:

- Welcome window 600x720 -> 580x700
- Status menu width 300 -> 320; "Help" item -> "Help / Welcome"
- Welcome style-tile height 70 -> 64
- Recording window corner radius 18 -> 22
- Overview hero numeral 52pt -> 48pt
- Classic waveform bar count 64 -> 56

Updates the affected tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports the deferred sidebar enhancement from the design prototype: the
"Total recorded" footer now shows a month-over-month delta line and, on
hover, a popover with a lifetime breakdown — this month, last 7 days,
average per day, time saved, and total sessions.

All figures are computed from real UsageMetricsStore data. The
prototype's "installed N days ago" row is omitted — the app does not
persist an install date and fabricating one was not warranted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports the deferred mic-test feature from the design prototype. A banner
at the top of the Visuals tab captures live microphone audio and feeds
it into the shared LivePreviewSampler, so every waveform preview reacts
to the user's real voice.

- MicTestCapture: AVAudioEngine input tap; RMS level, 64 time-domain
  samples, and 8 FFT bands (via the existing FFTProcessor).
- LivePreviewSampler gains a mic mode that suppresses the synthetic
  envelope and publishes real frames.
- MicTestBanner: live preview, state-driven copy, input level meter,
  and off/live/denied/error states with permission handling.

Capture is visualization-only — nothing is recorded or persisted; the
mic is released on stop, on error, and when the tab disappears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

📝 Walkthrough

Walkthrough

This PR introduces live microphone testing with real-time waveform visualization and level metering, alongside dashboard enhancements including a hover-activated breakdown popover for word count statistics. Welcome screen dimensions are refined, menu labels updated, and waveform styling constants tuned.

Changes

Microphone Test Feature

Layer / File(s) Summary
Mic Test Capture Audio Processing
Sources/Views/Components/Waveform/MicTestCapture.swift, Tests/Views/Dashboard/DashboardVisualsViewTests.swift
MicTestCapture manages AVAudioEngine with input taps, processes audio buffers to extract RMS-based smoothed levels, FFT-derived frequency bands, and downsampled time-domain samples. Handles microphone permission requests and publishes metrics to LivePreviewSampler. Includes downsampling and Logger utilities.
Live Sampler Mic Mode Integration
Sources/Views/Components/Waveform/WaveformPreviewHelpers.swift
LivePreviewSampler gains isMicMode published flag and methods setMicMode(_:) and publishMicFrame(level:samples:bands:) to accept real microphone frames and skip synthetic waveform generation when mic mode is active.
Mic Test Banner SwiftUI Component
Sources/Views/Components/Waveform/MicTestBanner.swift
MicTestBanner displays a three-column card with waveform preview, real-time level meter (shown only during capture), and action button. Styling and copy adapt dynamically based on capture state (.off, .live, .denied, .error) and speaking detection.
Dashboard Mic Test Wiring and Tests
Sources/Views/Dashboard/DashboardVisualsView.swift
DashboardVisualsView instantiates LivePreviewSampler and MicTestCapture as shared @StateObjects in custom initializer and stops both on onDisappear. New test section covers mic mode toggling, frame publishing, and state reset.

Dashboard and UI Refinements

Layer / File(s) Summary
Dashboard Breakdown Popover
Sources/Views/Dashboard/DashboardView.swift
Adds showBreakdown state to control hover-activated sidebar popover displaying word counts and monthly delta percentage. Includes breakdownPopover view, popRow helper, and stat computation methods (wordsInLast, dailyAverage, monthDeltaPercent).
Welcome Window and Badge Layout Updates
Sources/Design/LayoutMetrics.swift, Sources/Views/WelcomeView.swift, Tests/Design/LayoutMetricsTests.swift, Tests/Views/WelcomeViewTests.swift
Welcome window size reduced from 600×720 to 580×700. StyleTile "NEW" badge height adjusted from 70 to 64. Layout metric and welcome view tests updated to verify new dimensions.
Menu and Waveform Styling Adjustments
Sources/App/AppDelegate+Menu.swift, Sources/Views/Components/Waveform/ClassicWaveformView.swift, Sources/Views/Components/Waveform/WaveformContainer.swift, Tests/AppDelegate/AppDelegateMenuTests.swift, Tests/AppDelegateExtensionTests.swift
Status menu Help item renamed "Help / Welcome" with menuWidth increased from 300 to 320. ClassicWaveformView bar count reduced from 64 to 56. WaveformContainer corner radius increased from 18 to 22. Menu tests updated to expect new Help label.
Dashboard Typography Refinement
Sources/Views/Dashboard/DashboardHomeView.swift
Hero "Words this month" metric serif font size reduced from 52pt to 48pt.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jtn0123/AudioWhisper#17: Previous PR updated the same status menu Help label and Welcome layout dimensions, suggesting continued refinement of menu structure and welcome screen presentation.

Poem

A whisper of the mic so true,
Waveforms dancing, levels new,
Dashboard breakdown, stats align,
Layouts tuned, a design so fine. 🎙️✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the three main change categories: cosmetic alignment adjustments, sidebar breakdown feature, and mic-test banner implementation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch design-fidelity-followups

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.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
11.4% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Views/Components/Waveform/MicTestBanner.swift`:
- Around line 224-229: The preview instantiates two separate LivePreviewSampler
instances so MicTestBanner and MicTestCapture don't share updates; create a
single sampler variable (e.g., let sampler = LivePreviewSampler()) in the
`#Preview` block and pass that same sampler to both MicTestBanner(sampler:
sampler) and MicTestCapture(sampler: sampler) so the live capture drives the
rendered waveform.
- Around line 19-23: The MicTestBanner SwiftUI view lacks main-actor isolation;
annotate the MicTestBanner type with `@MainActor` to ensure UI access is
main-thread safe — update the declaration of struct MicTestBanner (which holds
WaveformStyle, `@ObservedObject` var sampler: LivePreviewSampler, and
`@ObservedObject` var capture: MicTestCapture) by adding the `@MainActor` attribute
so the entire view and its lifecycle are main-actor isolated.

In `@Sources/Views/Components/Waveform/MicTestCapture.swift`:
- Around line 67-75: Add an explicit "captureRequested" Bool flag on the
MicTestCapture instance, set it true immediately before calling
AVCaptureDevice.requestAccess(for: .audio), and clear it in stop(); in the
requestAccess callback (the Task { `@MainActor` ... } block) only call
beginCapture() if granted && captureRequested is still true, otherwise set state
= .denied if !granted; apply the same guard to the other requestAccess callback
(the similar block around beginCapture() at the second occurrence) so late
permission responses cannot restart capture after stop() was called.
- Line 158: The code uses optional chaining on a non-optional property
`fftProcessor` when assigning `rawBands`; remove the invalid `?` and call
`fftProcessor.process(mono)` directly in `MicTestCapture.swift` (line with
`rawBands`) and either drop the fallback Array(repeating:0, count:8) or adapt to
the actual return type of `process` (e.g., handle an optional return from
`process` if needed) so the call compiles without optional chaining.
- Around line 41-42: smoothedLevel is mutated from the audio callback thread
while being read/reset on the main actor, but stateLock is unused—introduce
explicit locking around every access to smoothedLevel: wrap writes in the audio
callback with stateLock.lock()/defer { stateLock.unlock() } and wrap
reads/resets on the main actor (where smoothedLevel is inspected/reset) the same
way so all reads and writes use stateLock; keep the nonisolated(unsafe)
declaration but ensure stateLock protects smoothedLevel accesses (reference
smoothedLevel and stateLock) so there is no data race.

In `@Sources/Views/Dashboard/DashboardVisualsView.swift`:
- Around line 13-17: The DashboardVisualsView initializes MainActor-isolated
objects (LivePreviewSampler and MicTestCapture) from a non-isolated context;
annotate the DashboardVisualsView declaration with `@MainActor` so its init and
property wrappers (_sampler: StateObject, _micCapture: StateObject) run on the
main actor. Locate the DashboardVisualsView type (the struct/class that defines
init() shown) and add the `@MainActor` attribute to its declaration so the
LivePreviewSampler and MicTestCapture initializations occur under main-thread
isolation.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 1b553181-c2d1-4bed-bb6c-49c327c86dbe

📥 Commits

Reviewing files that changed from the base of the PR and between 4968504 and 7e5e69f.

📒 Files selected for processing (16)
  • Sources/App/AppDelegate+Menu.swift
  • Sources/Design/LayoutMetrics.swift
  • Sources/Views/Components/Waveform/ClassicWaveformView.swift
  • Sources/Views/Components/Waveform/MicTestBanner.swift
  • Sources/Views/Components/Waveform/MicTestCapture.swift
  • Sources/Views/Components/Waveform/WaveformContainer.swift
  • Sources/Views/Components/Waveform/WaveformPreviewHelpers.swift
  • Sources/Views/Dashboard/DashboardHomeView.swift
  • Sources/Views/Dashboard/DashboardView.swift
  • Sources/Views/Dashboard/DashboardVisualsView.swift
  • Sources/Views/WelcomeView.swift
  • Tests/AppDelegate/AppDelegateMenuTests.swift
  • Tests/AppDelegateExtensionTests.swift
  • Tests/Design/LayoutMetricsTests.swift
  • Tests/Views/Dashboard/DashboardVisualsViewTests.swift
  • Tests/Views/WelcomeViewTests.swift

Comment on lines +19 to +23
internal struct MicTestBanner: View {
let style: WaveformStyle
@ObservedObject var sampler: LivePreviewSampler
@ObservedObject var capture: MicTestCapture

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "MicTestBanner.swift" | head -20

Repository: jtn0123/AudioWhisper

Length of output: 120


🏁 Script executed:

cat -n ./Sources/Views/Components/Waveform/MicTestBanner.swift | head -30

Repository: jtn0123/AudioWhisper

Length of output: 1417


Annotate MicTestBanner with @MainActor.

This view is a UI component in Sources/**/*.swift and should be explicitly main-actor isolated.

💡 Proposed fix
-internal struct MicTestBanner: View {
+@MainActor
+internal struct MicTestBanner: View {

As per coding guidelines, Sources/**/*.swift: Use @MainActor annotation for UI components in Swift.

📝 Committable suggestion

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

Suggested change
internal struct MicTestBanner: View {
let style: WaveformStyle
@ObservedObject var sampler: LivePreviewSampler
@ObservedObject var capture: MicTestCapture
`@MainActor`
internal struct MicTestBanner: View {
let style: WaveformStyle
`@ObservedObject` var sampler: LivePreviewSampler
`@ObservedObject` var capture: MicTestCapture
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Views/Components/Waveform/MicTestBanner.swift` around lines 19 - 23,
The MicTestBanner SwiftUI view lacks main-actor isolation; annotate the
MicTestBanner type with `@MainActor` to ensure UI access is main-thread safe —
update the declaration of struct MicTestBanner (which holds WaveformStyle,
`@ObservedObject` var sampler: LivePreviewSampler, and `@ObservedObject` var
capture: MicTestCapture) by adding the `@MainActor` attribute so the entire view
and its lifecycle are main-actor isolated.

Comment on lines +224 to +229
#Preview("Mic test banner") {
MicTestBanner(
style: .classic,
sampler: LivePreviewSampler(),
capture: MicTestCapture(sampler: LivePreviewSampler())
)
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 | ⚡ Quick win

Use one shared sampler in #Preview wiring.

The preview currently gives MicTestBanner one sampler and MicTestCapture a different sampler, so live capture updates won’t drive the rendered preview waveform.

💡 Proposed fix
 `#Preview`("Mic test banner") {
+    let sampler = LivePreviewSampler()
     MicTestBanner(
         style: .classic,
-        sampler: LivePreviewSampler(),
-        capture: MicTestCapture(sampler: LivePreviewSampler())
+        sampler: sampler,
+        capture: MicTestCapture(sampler: sampler)
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Views/Components/Waveform/MicTestBanner.swift` around lines 224 -
229, The preview instantiates two separate LivePreviewSampler instances so
MicTestBanner and MicTestCapture don't share updates; create a single sampler
variable (e.g., let sampler = LivePreviewSampler()) in the `#Preview` block and
pass that same sampler to both MicTestBanner(sampler: sampler) and
MicTestCapture(sampler: sampler) so the live capture drives the rendered
waveform.

Comment on lines +41 to +42
private nonisolated(unsafe) var smoothedLevel: Float = 0
private let stateLock = NSLock()
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n Sources/Views/Components/Waveform/MicTestCapture.swift | head -200

Repository: jtn0123/AudioWhisper

Length of output: 7511


🏁 Script executed:

rg "stateLock" Sources/Views/Components/Waveform/MicTestCapture.swift

Repository: jtn0123/AudioWhisper

Length of output: 101


Synchronize smoothedLevel access with the existing stateLock across audio and main threads.

smoothedLevel is marked nonisolated(unsafe) to allow mutation from the audio callback thread (line 139) while reading/resetting on the main actor. The stateLock is declared but never used. Without synchronization, concurrent access creates a data race.

Proposed fix
    func stop() {
        teardownEngine()
+       stateLock.lock()
        smoothedLevel = 0
+       stateLock.unlock()
        level = 0
+       stateLock.lock()
        let smooth = smoothedLevel * 0.55 + target * 0.45
        smoothedLevel = smooth
+       stateLock.unlock()

Applies to lines 85, 151–152.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Views/Components/Waveform/MicTestCapture.swift` around lines 41 - 42,
smoothedLevel is mutated from the audio callback thread while being read/reset
on the main actor, but stateLock is unused—introduce explicit locking around
every access to smoothedLevel: wrap writes in the audio callback with
stateLock.lock()/defer { stateLock.unlock() } and wrap reads/resets on the main
actor (where smoothedLevel is inspected/reset) the same way so all reads and
writes use stateLock; keep the nonisolated(unsafe) declaration but ensure
stateLock protects smoothedLevel accesses (reference smoothedLevel and
stateLock) so there is no data race.

Comment on lines +67 to +75
AVCaptureDevice.requestAccess(for: .audio) { [weak self] granted in
Task { @MainActor [weak self] in
guard let self = self else { return }
if granted {
self.beginCapture()
} else {
self.state = .denied
}
}
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 | ⚡ Quick win

Prevent late permission callbacks from restarting capture after stop().

If the permission dialog resolves after the banner/tab is dismissed, the callback can still call beginCapture(), re-enabling mic unexpectedly. Gate async completion with an explicit “capture requested” flag that stop() clears.

💡 Suggested direction
 final class MicTestCapture: ObservableObject {
+    private var shouldBeCapturing = false
@@
     func start() {
         guard state != .live else { return }
+        shouldBeCapturing = true
@@
         case .notDetermined:
             AVCaptureDevice.requestAccess(for: .audio) { [weak self] granted in
                 Task { `@MainActor` [weak self] in
                     guard let self = self else { return }
+                    guard self.shouldBeCapturing else { return }
                     if granted {
                         self.beginCapture()
                     } else {
                         self.state = .denied
                     }
                 }
             }
@@
     func stop() {
+        shouldBeCapturing = false
         teardownEngine()

Also applies to: 83-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Views/Components/Waveform/MicTestCapture.swift` around lines 67 - 75,
Add an explicit "captureRequested" Bool flag on the MicTestCapture instance, set
it true immediately before calling AVCaptureDevice.requestAccess(for: .audio),
and clear it in stop(); in the requestAccess callback (the Task { `@MainActor` ...
} block) only call beginCapture() if granted && captureRequested is still true,
otherwise set state = .denied if !granted; apply the same guard to the other
requestAccess callback (the similar block around beginCapture() at the second
occurrence) so late permission responses cannot restart capture after stop() was
called.

let samples = Self.downsample(mono, count: 64).map { $0 * 0.9 }

// 8 frequency bands, ignoring the top ~35% of the range as noise.
let rawBands = fftProcessor?.process(mono) ?? Array(repeating: 0, count: 8)
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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the non-optional declaration and optional-chaining use are both present.
rg -n -C2 'private let fftProcessor = FFTProcessor\(\)|fftProcessor\?\.' Sources/Views/Components/Waveform/MicTestCapture.swift

Repository: jtn0123/AudioWhisper

Length of output: 501


Remove invalid optional chaining on fftProcessor.

Line 158 uses optional chaining on a non-optional value, which is a compile-time error.

💡 Proposed fix
-        let rawBands = fftProcessor?.process(mono) ?? Array(repeating: 0, count: 8)
+        let rawBands = fftProcessor.process(mono) ?? Array(repeating: 0, count: 8)
📝 Committable suggestion

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

Suggested change
let rawBands = fftProcessor?.process(mono) ?? Array(repeating: 0, count: 8)
let rawBands = fftProcessor.process(mono) ?? Array(repeating: 0, count: 8)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Views/Components/Waveform/MicTestCapture.swift` at line 158, The code
uses optional chaining on a non-optional property `fftProcessor` when assigning
`rawBands`; remove the invalid `?` and call `fftProcessor.process(mono)`
directly in `MicTestCapture.swift` (line with `rawBands`) and either drop the
fallback Array(repeating:0, count:8) or adapt to the actual return type of
`process` (e.g., handle an optional return from `process` if needed) so the call
compiles without optional chaining.

Comment on lines +13 to +17
init() {
let sharedSampler = LivePreviewSampler()
_sampler = StateObject(wrappedValue: sharedSampler)
_micCapture = StateObject(wrappedValue: MicTestCapture(sampler: sharedSampler))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "DashboardVisualsView.swift" | head -5

Repository: jtn0123/AudioWhisper

Length of output: 117


🏁 Script executed:

git ls-files | grep -E "(DashboardVisualsView|LivePreviewSampler|MicTestCapture)" | head -20

Repository: jtn0123/AudioWhisper

Length of output: 224


🏁 Script executed:

cat -n Sources/Views/Dashboard/DashboardVisualsView.swift

Repository: jtn0123/AudioWhisper

Length of output: 14629


🏁 Script executed:

cat -n Sources/Views/Components/Waveform/MicTestCapture.swift

Repository: jtn0123/AudioWhisper

Length of output: 7511


🏁 Script executed:

rg -n "class LivePreviewSampler|struct LivePreviewSampler" --type swift

Repository: jtn0123/AudioWhisper

Length of output: 181


🏁 Script executed:

sed -n '1,50p' Sources/Views/Components/Waveform/WaveformPreviewHelpers.swift | cat -n

Repository: jtn0123/AudioWhisper

Length of output: 2083


Annotate DashboardVisualsView with @MainActor.

The view initializes @MainActor-isolated objects (LivePreviewSampler and MicTestCapture) from a non-isolated context. Marking the view @MainActor ensures proper actor isolation and prevents concurrency boundary ambiguity when instantiating main-thread-only dependencies.

Proposed fix
+@MainActor
 internal struct DashboardVisualsView: View {

Per coding guidelines: Sources/**/*.swift requires @MainActor annotation for UI components.

📝 Committable suggestion

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

Suggested change
init() {
let sharedSampler = LivePreviewSampler()
_sampler = StateObject(wrappedValue: sharedSampler)
_micCapture = StateObject(wrappedValue: MicTestCapture(sampler: sharedSampler))
}
`@MainActor`
internal struct DashboardVisualsView: View {
init() {
let sharedSampler = LivePreviewSampler()
_sampler = StateObject(wrappedValue: sharedSampler)
_micCapture = StateObject(wrappedValue: MicTestCapture(sampler: sharedSampler))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Views/Dashboard/DashboardVisualsView.swift` around lines 13 - 17, The
DashboardVisualsView initializes MainActor-isolated objects (LivePreviewSampler
and MicTestCapture) from a non-isolated context; annotate the
DashboardVisualsView declaration with `@MainActor` so its init and property
wrappers (_sampler: StateObject, _micCapture: StateObject) run on the main
actor. Locate the DashboardVisualsView type (the struct/class that defines
init() shown) and add the `@MainActor` attribute to its declaration so the
LivePreviewSampler and MicTestCapture initializations occur under main-thread
isolation.

@jtn0123 jtn0123 merged commit b254122 into master May 16, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant