Skip to content

Implement Claude Design handoff: waveform, dashboard, welcome & menu bar redesign#17

Merged
jtn0123 merged 5 commits into
masterfrom
design-handoff-prototype
May 15, 2026
Merged

Implement Claude Design handoff: waveform, dashboard, welcome & menu bar redesign#17
jtn0123 merged 5 commits into
masterfrom
design-handoff-prototype

Conversation

@jtn0123
Copy link
Copy Markdown
Owner

@jtn0123 jtn0123 commented May 15, 2026

Summary

Implements the AudioWhisper visual redesign from the Claude Design handoff bundle (AudioWhisper Prototype.html + 17-file Swift handoff). Delivered as four phased commits, one per surface.

  • Waveform system — replaces the 6-style menu (classic/neon/spectrum/circular/pulseRings/particles) with 8 styles sharing one coral/cream/sage palette: classic, neon, spectrum + 5 new renderers (stream, constellation, halo, dial, heartbeat). WaveformContainer adopts a frameless "glow" chrome with a state-aware glow, vignette, floating status row (live timer / hotkey hint / success recap) and a scanning shimmer for processing.
  • Dashboard — native NSVisualEffectView sidebar (replacing the blue gradient), coral accent, real serif numerals, 10pt card radius. The Overview tab leads with one hero metric + sparkline + delta, a 28-day waveform timeline (replacing the GitHub heatmap), and source usage bars. DashboardHomeView consolidated from 5 extension files into one.
  • Visuals picker — the dropdown becomes a 2×4 grid of live previews driven by a synthetic LivePreviewSampler.
  • Welcome — a single-page hero screen replacing the stacked multi-section onboarding.
  • Menu bar — custom 5-bar waveform icon with a smooth coral pulse (replacing the 1Hz blink), and a branded status menu with an inline "Recent transcripts" section and visible shortcuts.

Drops CircularSpectrumView, PulseRingsView, ParticleFieldView, the blink animation, and DashboardLogo.jpg. All affected tests were migrated.

Test plan

  • swift build — Sources compile clean
  • swift build --build-tests — test target compiles clean
  • Full suite: 2772 tests, 0 failures, 37 skipped
  • Manual: open the recording window across all 8 styles and 5 states
  • Manual: Dashboard → Visuals picker selection flows to the recording window
  • Manual: menu bar icon pulse + Recent section populate after a recording

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added recent transcript previews to menu bar with copy-to-clipboard action
    • Introduced five new animated waveform visualizations for audio recording display
    • Redesigned dashboard with activity timeline, updated stats layout, and streamlined metrics
  • UI/Design Updates

    • Updated app theme with warm coral accent colors
    • Redesigned welcome screen with interactive waveform style selection grid
    • Enhanced menu bar icon with animated states for recording, processing, success, and error feedback
    • Increased welcome window height for improved layout
  • Refactor

    • Rebuilt menu system with dynamic SwiftUI components
    • Modernized waveform rendering architecture with five new visualization types
    • Restructured dashboard home view with new helper components and improved data handling

Review Change Stack

jtn0123 and others added 4 commits May 15, 2026 15:02
Replaces the 6-style waveform menu (classic, neon, spectrum, circular,
pulseRings, particles) with 8 styles that all share the app's
coral/cream/sage palette: classic, neon, spectrum, plus five new
renderers — stream, constellation, halo, dial, heartbeat.

- WaveformContainer adopts the frameless "glow" chrome: no border stroke,
  state-aware colored glow, inner vignette, floating status row with a
  live timer / hotkey hint / success recap, and a scanning shimmer for
  the processing state.
- Neon and Spectrum refined to drop their rainbow palettes for a single
  coral / ember-to-cream hue family.
- DashboardVisualsView replaces the dropdown picker with a 2x4 grid of
  live previews driven by a synthetic LivePreviewSampler.
- Drops CircularSpectrumView, PulseRingsView, ParticleFieldView and
  migrates the tests that referenced the removed styles/types.

From the Claude Design handoff bundle (AudioWhisper Prototype).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the blue-gradient sidebar with an NSVisualEffectView .sidebar
material (the standard Finder/Mail/Settings treatment), moves the accent
from system blue to the app's coral, and switches headings/numerals to a
real serif. Card radius goes from 2pt to 10pt.

The Overview tab leads with one hero metric (words this month + sparkline
+ month-over-month delta) instead of three equal numbers, swaps the 4x7
GitHub-style heatmap for a 28-day waveform timeline, and drops the source
rank column in favor of a usage bar behind each row.

- DashboardHomeView consolidated from five extension files into one;
  pure computations exposed as static helpers for testing.
- Drops the bundled DashboardLogo.jpg — the masthead is now drawn.
- Migrates the dashboard tests for the removed heatmap/week-grid helpers.

From the Claude Design handoff bundle (AudioWhisper Prototype).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the stacked multi-section onboarding (header + privacy card +
feature grid + setup section + smart-paste instructions) for one screen:
a live hero recording window that renders the selected waveform style, a
decisive headline, the 8-style picker grid, a selected-style readout, and
a single coral CTA.

- Welcome window grows to 600x720 to fit the consolidated layout.
- "Get started" preserves the prior behavior: sets the local provider,
  marks welcome complete, and opens the dashboard.
- Migrates WelcomeView tests for the removed FeatureRow/InstructionRow
  subcomponents and feature-list copy.

From the Claude Design handoff bundle (AudioWhisper Prototype).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the microphone.circle SF Symbol and its 1Hz red/black blink with
a custom 5-bar waveform glyph that ties back to the Classic visualization.
A MenuBarIconRenderer owns the icon's state (idle / recording / processing
/ success / error) and drives a smooth ~0.4Hz coral pulse while recording
instead of the chunky blink.

The status menu gains a branded header (coral mark + provider + today's
WPM), an inline "Recent" section showing up to 3 transcripts (click to
copy), and visible keyboard shortcuts. DashboardWindowManager keeps a
recent-records cache that the menu reads synchronously and refreshes on
each open.

- Removes the old DispatchSource blink animation from AppDelegate+Hotkeys.
- Migrates AppDelegate tests for the removed recordingAnimationTimer and
  the new menu structure.

From the Claude Design handoff bundle (AudioWhisper Prototype).

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

coderabbitai Bot commented May 15, 2026

Warning

Rate limit exceeded

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

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ 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: ASSERTIVE

Plan: Pro

Run ID: 8b380327-2cf1-4ac2-9d43-1e1e8f9efd13

📥 Commits

Reviewing files that changed from the base of the PR and between 8e4d662 and f579518.

📒 Files selected for processing (16)
  • Sources/App/MenuBarIcon.swift
  • Sources/Views/Components/MenuPopupViews.swift
  • Sources/Views/Components/Waveform/ConstellationWaveformView.swift
  • Sources/Views/Components/Waveform/DialWaveformView.swift
  • Sources/Views/Components/Waveform/HaloWaveformView.swift
  • Sources/Views/Components/Waveform/HeartbeatPulseView.swift
  • Sources/Views/Components/Waveform/NeonWaveformView.swift
  • Sources/Views/Components/Waveform/SpectrumWaveformView.swift
  • Sources/Views/Components/Waveform/StreamWaveformView.swift
  • Sources/Views/Components/Waveform/WaveformContainer.swift
  • Sources/Views/Components/Waveform/WaveformPreviewHelpers.swift
  • Sources/Views/Dashboard/DashboardHome+Sections.swift
  • Sources/Views/Dashboard/DashboardHomeCharts.swift
  • Sources/Views/Dashboard/DashboardHomeView.swift
  • Sources/Views/Dashboard/DashboardView.swift
  • Sources/Views/WelcomeView.swift
📝 Walkthrough

Walkthrough

This PR comprehensively overhauls AudioWhisper's menu bar icon system, waveform visualizations, and dashboard UI. The menu bar icon shifts from timer-driven animations to state-managed rendering; the waveform system gains five new animated styles while removing three legacy ones; the dashboard home receives a complete layout refresh with new metrics presentation and activity timeline; and the welcome flow simplifies to a style-picker onboarding. All supporting infrastructure, tests, and theme tokens are updated accordingly.

Changes

Menu bar icon and status menu

Layer / File(s) Summary
MenuBarIconRenderer component
Sources/App/MenuBarIcon.swift
New MenuBarIconState enum (idle/recording/processing/success/error) and MenuBarIconRenderer class manage icon rendering via state transitions, smooth coral pulse animation for recording, and image helpers for bars/checkmark glyphs.
AppDelegate icon renderer integration
Sources/App/AppDelegate.swift, Sources/App/AppDelegate+Lifecycle.swift, Sources/App/AppDelegate+Hotkeys.swift
Replaces recordingAnimationTimer property with iconRenderer; initializes renderer on launch and refreshes on screen config changes; delegates icon state updates from updateMenuBarIcon.
Dynamic status menu with recent transcripts
Sources/App/AppDelegate+Menu.swift, Sources/Views/Components/MenuPopupViews.swift, Sources/Managers/Windows/DashboardWindowManager.swift
Rebuilds menu as NSMenuDelegate-driven dynamic layout with SwiftUI-hosted header (MenuHeaderView), cached recent transcript rows (RecentTranscriptMenuRow), and clipboard copy actions; updates menu item titles and adds async cache refresh.

Waveform visualization system overhaul

Layer / File(s) Summary
WaveformStyle enum redesign
Sources/Views/Components/Waveform/WaveformStyle.swift
Replaces circular, pulseRings, particles cases with stream, constellation, halo, dial, heartbeat; adds isRadial and isNew properties; adjusts requiresEnhancedAudio logic for the new palette.
New waveform components
Sources/Views/Components/Waveform/StreamWaveformView.swift, ConstellationWaveformView.swift, HaloWaveformView.swift, DialWaveformView.swift, HeartbeatPulseView.swift
Adds five Canvas-based animated waveform visualizations: particle streams, drifting node constellations, rotating halo bars, energy dial with perimeter ticks, and audio-driven heartbeat rings.
Updated waveform components styling
Sources/Views/Components/Waveform/NeonWaveformView.swift, SpectrumWaveformView.swift, ColorTheme.swift
Refactors NeonWaveformView to single coral palette (removes cyan/magenta/yellow cycling); simplifies SpectrumWaveformView with ember-to-cream gradient and removes labels/shadows; adds creamDim and amber color constants.
WaveformContainer integration
Sources/Views/Components/Waveform/WaveformContainer.swift
Refactors to new "Frameless + glow" design with state-driven processing/success/error visuals, recordingStartedAt tracking, ProcessingShimmerView, and dispatches to new waveform components.
Live preview sampling infrastructure
Sources/Views/Components/Waveform/WaveformPreviewHelpers.swift
Adds LivePreviewSampler observable object generating synthetic audio data (~30 FPS) and WaveformStylePreview rendering each style with sampler output.

Dashboard home redesign

Layer / File(s) Summary
Dashboard home data computation
Sources/Views/Dashboard/DashboardHomeView.swift
Refactors from ProviderStat model to tuples; introduces pure helpers for provider stats aggregation, daily activity merging, streak/active-day calculation, and number/duration formatting; tracks last-28-day values and month-over-month delta.
Dashboard home UI layout
Sources/Views/Dashboard/DashboardHomeView.swift
Rebuilds body with hero stats card (words this month + sparkline), activity section (28-day timeline with axis labels and best-day summary), sources with usage bars, and refined recent transcripts list with app icons.
Dashboard home supporting views
Sources/Views/Dashboard/DashboardHomeView.swift
Adds ActivityTimeline histogram and MiniSparkline line chart; updates sectionHeader builder and removes old header/activity/stats extension files.

Dashboard theme and visuals UI

Layer / File(s) Summary
Dashboard theme redesign
Sources/Views/Dashboard/DashboardView.swift
Updates theme to coral accent palette; applies macOS semantic sidebar colors (.labelColor, .secondaryLabelColor); adjusts typography (serif design), spacing, and card radius.
Dashboard view sidebar and layout
Sources/Views/Dashboard/DashboardView.swift
Replaces sidebar gradient with SidebarVisualEffect (NSVisualEffectView with .sidebar material); updates masthead to coral mark + system mic; refines nav button spacing/sizing and stats footer styling.
Dashboard visuals expanded with live preview
Sources/Views/Dashboard/DashboardVisualsView.swift
Transforms from settings picker to expanded dashboard with 2×4 live-preview style picker grid, "NEW" badges, and dedicated PreviewWindow; adds StylePickerCard and PreviewWindow components.

Welcome screen redesign

Layer / File(s) Summary
Welcome view rewrite
Sources/Views/WelcomeView.swift
Replaces multi-section onboarding (privacy, features, model setup) with consolidated layout: waveform hero preview, 4-column style tile grid with "NEW" badges, selected-style readout, and "Get started" CTA that directly updates UserDefaults, posts .welcomeCompleted, and shows dashboard.
Welcome window sizing
Sources/Design/LayoutMetrics.swift, Sources/Managers/Windows/WelcomeWindow.swift
Updates LayoutMetrics.Welcome.windowSize to 600×720 (height from 650); updates WelcomeWindow.showWelcomeDialog to use LayoutMetrics constants instead of hardcoded values.

Test updates

Layer / File(s) Summary
AppDelegate test updates
Tests/AppDelegate/*
Updates tests to assert iconRenderer instead of recordingAnimationTimer; refactors menu tests for new dynamic structure with hardcoded action titles and SwiftUI-hosted header assertions.
Waveform and style test updates
Tests/Views/Components/WaveformViewTests.swift, Tests/Waveform/*, Tests/WaveformStyleTests.swift
Removes CircularSpectrumView, PulseRingsView, ParticleFieldView test coverage; adds smoke tests for new waveform views; updates WaveformStyle enum assertions for 8 cases and new properties (isRadial, isNew).
Dashboard and welcome test updates
Tests/Views/Dashboard/*, Tests/Views/WelcomeViewTests.swift
Updates dashboard home tests for new layout (removes heatmap/week-generation tests); updates welcome tests for new window height and style-grid assertions; removes feature-row coverage.
Other test cleanup
Tests/Design/, Tests/EdgeCases/, Tests/Integration/, Tests/Performance/, Tests/UISnapshotTests+Waveform.swift
Updates layout metrics for welcome height; removes waveform calculation edge-case and performance tests; replaces with encoding performance tests; updates snapshot baselines for new waveform styles.

🎯 4 (Complex) | ⏱️ ~60 minutes

🐰 A waveform dances in shades of coral bright,
Five new shapes bloom where streams of particles took flight,
The dashboard gleams with sparklines clean and new,
While welcome tiles let users choose their hue.
State-driven icons pulse in harmony—
Each frame a rhythm, rendered splendidly!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch design-handoff-prototype

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

Caution

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

⚠️ Outside diff range comments (1)
Sources/Views/Dashboard/DashboardVisualsView.swift (1)

6-166: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add @MainActor to the view components in this file.

DashboardVisualsView, StylePickerCard, and PreviewWindow are UI components but are not annotated with @MainActor.

Proposed fix
-internal struct DashboardVisualsView: View {
+@MainActor
+internal struct DashboardVisualsView: View {
@@
-private struct StylePickerCard: View {
+@MainActor
+private struct StylePickerCard: View {
@@
-private struct PreviewWindow: View {
+@MainActor
+private struct PreviewWindow: View {

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

Also applies to: 170-246, 250-295

🤖 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 6 - 166, The
SwiftUI components DashboardVisualsView, StylePickerCard, and PreviewWindow must
be annotated with `@MainActor` so their UI state and lifecycle run on the main
thread; locate the struct/type declarations for DashboardVisualsView,
StylePickerCard, and PreviewWindow and add the `@MainActor` attribute to each
(e.g., `@MainActor` struct DashboardVisualsView: View), and ensure any related
view models or `@StateObject` initializers used only by these views are also
main-actor-isolated if needed to eliminate non-main-thread UI access warnings.
🤖 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/App/AppDelegate`+Menu.swift:
- Around line 29-82: populateStatusMenu(_:) is too long; split it into smaller
builder helpers to comply with the 40-line rule. Extract logical sections into
private methods such as buildHeaderMenuItems(menu:),
buildPrimaryActionItems(menu:), buildRecentTranscriptsSection(menu:) (using
recentTranscriptsForMenu() and makeRecentItem(record:)), and
buildFooterItems(menu:) (Dashboard/Help/Quit), and then have
populateStatusMenu(_:) simply call these helpers in order while keeping the same
use of makeHeaderItem(), makeActionItem(...), makeSectionLabelItem(_:), and
makeRecentItem(record:). Ensure each new helper is <=40 lines and preserves the
exact selectors and key equivalents.
- Around line 118-133: makeRecentItem sets item.view to a SwiftUI host which
prevents AppKit from invoking copyRecentTranscript(_:); to fix, update the
SwiftUI content used in item.view (the RecentTranscriptMenuRow wrapper) to
handle clicks — e.g., wrap RecentTranscriptMenuRow in a SwiftUI Button whose
action calls NSApp.sendAction(`#selector`(AppDelegate.copyRecentTranscript(_:)),
to: nil, from: item) or otherwise invokes the AppDelegate selector with the menu
item or its representedObject as sender; change the NSHostingView rootView in
makeRecentItem to this Button-wrapped view (and apply a plain button style) so
clicking the custom view triggers copyRecentTranscript(_:).

In `@Sources/App/MenuBarIcon.swift`:
- Around line 155-158: Rename the one-letter locals in the drawing loop to
descriptive names to satisfy the lint gate: change `i` to `index` (or
`barIndex`), `h` to `heightFraction` (or `relativeHeight`), `barH` to
`barHeight`, and `x`/`y` to `barX`/`barY`; update all usages inside the for-loop
in the MenuBarIcon drawing code (the `for (i, h) in heights.enumerated()` block)
so the computed values and subsequent drawing calls use the new names.

In `@Sources/Views/Components/MenuPopupViews.swift`:
- Around line 11-12: Annotate the SwiftUI view structs with `@MainActor` to ensure
main-thread isolation for UI: add the `@MainActor` attribute before the
declaration of MenuHeaderView and the other view structs in this file (the ones
at the referenced locations around lines 98-99 and 125-126) so each struct
declaration (e.g., MenuHeaderView) becomes main-actor isolated; no other
behavioral changes are needed.
- Around line 23-76: The MenuHeaderView.body is too long; split it into smaller
subviews and/or view modifiers: extract the coral logo bauble (the
RoundedRectangle + Canvas overlay + shadow) into a private View or computed
property (e.g., private var logoView or struct LogoBadge) that uses coral and
coralDeep and preserves the Canvas drawing; extract the Title + status VStack
into a private view (e.g., private var titleStatusView) that uses statusLine and
sage; then have MenuHeaderView.body simply compose HStack { logoView;
titleStatusView; Spacer() } with the same .padding calls so layout and styling
remain identical. Ensure the new subviews are private and keep all existing
constants (heights, barW, gap, startX, maxH) inside the extracted logo view.

In `@Sources/Views/Components/Waveform/ConstellationWaveformView.swift`:
- Around line 16-74: The body is over the 40-line limit; extract the node
creation and drawing logic into private helpers to shorten body: add a private
func generateNodes(count:t:size:) -> [CGPoint] that contains the loop computing
homeX/homeY/driftR/driftSpeed/x/y, a private func
drawEdges(context:size:nodes:level:proximity:) that contains the nested loops
computing dx/dy/d and stroking the line with coral and linkAlpha, and a private
func drawNodes(context:nodes:level:) that handles the glow and core fills using
cream and r; then have body simply compute level/proximity, call generateNodes
to get nodes, call drawEdges and drawNodes, and keep the existing
.onAppear/.onDisappear/.onReceive Timer logic unchanged. Ensure helper names
(generateNodes, drawEdges, drawNodes) reference count, t, isActive/audioLevel,
coral and cream so callers compile.
- Line 5: Annotate the SwiftUI view struct with `@MainActor` to enforce
main-thread isolation for UI state mutations: add the `@MainActor` attribute to
the ConstellationWaveformView declaration (the struct ConstellationWaveformView:
View) so its body and any `@State` properties are executed on the main actor;
ensure the attribute is placed immediately before the struct keyword.

In `@Sources/Views/Components/Waveform/DialWaveformView.swift`:
- Line 6: The DialWaveformView SwiftUI component should be annotated with
`@MainActor` to ensure UI work runs on the main actor; update the declaration of
the struct DialWaveformView to add the `@MainActor` attribute (e.g., `@MainActor`
struct DialWaveformView: View) so the view and its async UI-related state are
constrained to the main thread.
- Around line 44-48: The loop in DialWaveformView indexes frequencyBands
directly and can crash when frequencyBands is empty; replace direct usage with a
safe non-empty array before the loop (e.g., let safeBands =
frequencyBands.isEmpty ? [0.0] : frequencyBands), use let bandCount =
safeBands.count and then read values from safeBands[i % bandCount] (or
early-return/skip drawing when empty) to avoid out-of-bounds access in the loop
that computes a, v and brightness.
- Around line 85-88: The timer ticker is driving unnecessary 30fps state updates
by incrementing t in the .onReceive block while t is never read; remove the
Timer.publish(...).autoconnect() .onReceive { _ in guard isViewActive else {
return }; t += 0.033 } block from DialWaveformView and also delete the
associated state/storage for t (and any unused imports) so the view no longer
triggers pointless re-renders; if an animation is needed later, replace with a
SwiftUI animation tied to meaningful state instead of this ticker.

In `@Sources/Views/Components/Waveform/HaloWaveformView.swift`:
- Around line 28-116: The body is too long; extract the hub rendering and the
rotating-bar Canvas into two private view-builder helpers (e.g., private func
hubView(rIn: CGFloat) -> some View and private func barsCanvas(size: CGSize,
rIn: CGFloat, rMax: CGFloat) -> some View or mark them with `@ViewBuilder`) and
replace the inlined hub and Canvas blocks in body with calls to those helpers;
ensure the helpers accept the computed values they need (size, rIn, rMax,
palette, frequencyBands, t, barCount, coral, audioLevel) or capture them as
properties, keep each helper under 40 lines, and preserve the surrounding
GeometryReader, positioning, blur, and lifecycle code
(.onAppear/.onDisappear/.onReceive) in body.
- Line 5: The HaloWaveformView UI component is missing the `@MainActor`
annotation; update the declaration for the struct HaloWaveformView (the type
named HaloWaveformView: View) by adding `@MainActor` before the struct keyword so
all its UI work runs on the main actor, and adjust any related nested types or
initializers in that file to avoid actor-isolation conflicts (remove/modify
conflicting actor annotations or add `@MainActor` to them as needed).

In `@Sources/Views/Components/Waveform/HeartbeatPulseView.swift`:
- Around line 32-86: The body view is too long; split it into small private
builder methods to keep each function <=40 lines. Extract the ForEach rings
block into a private func ringsView(size:center:), extract the soft outer glow
and core orb into coreGlowView(coreR:) and coreOrbView(coreR:), and extract the
idle breath Circle into idleBreathView(size:). Keep the GeometryReader only in
body to compute size, center, and coreR, then call these private builders and
keep the lifecycle and timer modifiers (isViewActive state,
onAppear/onDisappear, onReceive) in body; ensure those helpers reference the
same properties (rings, audioLevel, isActive, idlePhase) and that updateRings()
is still called from the timer.
- Line 6: The HeartbeatPulseView UI component must be annotated with `@MainActor`:
add the `@MainActor` attribute immediately before the struct declaration (i.e.,
change "struct HeartbeatPulseView: View" to "`@MainActor` struct
HeartbeatPulseView: View") so the view's lifecycle and UI-related code run on
the main actor; update any associated previews or extensions (e.g.,
HeartbeatPulseView_Previews or extensions for HeartbeatPulseView) similarly if
they perform UI work.

In `@Sources/Views/Components/Waveform/StreamWaveformView.swift`:
- Around line 20-70: The body view is too long; extract the per-particle math
and drawing into private helper methods to keep body <=40 lines: create a
private func computeLaneParameters(index: Int, t: CGFloat, size: CGSize, level:
CGFloat) -> (x: CGFloat, y: CGFloat, baseSize: CGFloat, color: Color, trailLen:
CGFloat) that computes lane, phaseOffset, rawX/x, y, baseSize, colorPick and
trailLen, and a private func drawParticle(context: inout GraphicsContext, size:
CGSize, params: (x: CGFloat, y: CGFloat, baseSize: CGFloat, color: Color,
trailLen: CGFloat), level: CGFloat) that performs the trail stroke, outer glow
fill and core fill; then simplify the Canvas loop in body to call
computeLaneParameters(...) and drawParticle(...), keeping references to symbols
like isActive, audioLevel, count, t, Canvas, and the color constants (coral,
coralLite, cream).
- Line 6: The StreamWaveformView UI component is missing the `@MainActor`
annotation; update the struct declaration for StreamWaveformView to be annotated
with `@MainActor` (i.e., add `@MainActor` before `struct StreamWaveformView: View`)
so the view and its body run on the main actor; ensure any related nested types
or extensions of StreamWaveformView that manipulate UI are also marked
`@MainActor` or placed in the same annotated scope.

In `@Sources/Views/Components/Waveform/WaveformContainer.swift`:
- Around line 121-126: The code currently clears recordingStartedAt whenever
oldStatus is .recording, which causes it to be nil during the common .recording
→ .processing → .success flow; update the condition so we only clear
recordingStartedAt when leaving .recording into a non-processing, non-success
state. Concretely, in the block handling newStatus/oldStatus (referencing
recordingStartedAt, newStatus, oldStatus, and isSuccess), keep setting
recordingStartedAt = Date() when newStatus is .recording, but change the else-if
so it only clears recordingStartedAt when oldStatus was .recording AND the
newStatus is not .processing and isSuccess is false (i.e., do not clear on a
transition into .processing so SuccessRecapLabel can compute duration).
- Around line 411-517: The four private View structs (ProcessingShimmerView,
TimerLabel, HotkeyHint, SuccessRecapLabel) are missing the required `@MainActor`
annotation; fix by adding `@MainActor` to each struct declaration (e.g., change
"private struct ProcessingShimmerView: View" to "`@MainActor` private struct
ProcessingShimmerView: View" and do the same for TimerLabel, HotkeyHint, and
SuccessRecapLabel) so these UI components run on the main actor per project
guidelines.

In `@Sources/Views/Components/Waveform/WaveformPreviewHelpers.swift`:
- Around line 78-129: Add the `@MainActor` attribute to the WaveformStylePreview
type and refactor its large body into one or more small helpers (e.g., a private
func viewForStyle(_ style: WaveformStyle) -> some View or individual factory
helpers like classicView(), neonView(), etc.). Move the switch from body into
those helpers so body only delegates (e.g., return viewForStyle(style)), keeping
each helper under the 40-line limit and preserving the existing uses of
sampler.audioLevel, sampler.samples, and sampler.bands and view initializers
(ClassicWaveformView, NeonWaveformView, SpectrumWaveformView,
StreamWaveformView, ConstellationWaveformView, HaloWaveformView,
DialWaveformView, HeartbeatPulseView).

In `@Sources/Views/Dashboard/DashboardHomeView.swift`:
- Around line 103-109: The headerSubtitle is claiming "this month" while
calculateActiveDays()/computeActiveDays(from:) and
mergeDailyActivity(base:records:) include historical entries; update the logic
so calculateActiveDays() (or computeActiveDays(from:)) only counts dailyActivity
entries within the current month (use Calendar to compare year+month or filter
by start/end of current month) before computing activeDays and streak, or
alternatively change the subtitle text to not claim "this month" (e.g., "Active
for … total days"); refer to symbols headerSubtitle, calculateActiveDays(),
computeActiveDays(from:), mergeDailyActivity(base:records:), and dailyActivity
to locate where to apply the date-range filter or copy change.
- Line 18: Annotate the UI view types with `@MainActor` to ensure main-thread
isolation: add the `@MainActor` attribute to the declarations of
DashboardHomeView, ActivityTimeline, and MiniSparkline (i.e., before the struct
declarations) so those view types are main-actor-isolated; update each type
declaration in the file(s) where they appear so the attribute precedes the
struct keyword.
- Around line 795-806: The mergeDailyActivity function currently only adds the
first TranscriptionRecord for a day missing from base because of the if
activity[day] == nil || activity[day] == 0 guard; change it to always accumulate
each record's wordCount into the per-day bucket so multiple records on the same
day sum correctly: use activity[day, default: 0] += record.wordCount for every
record (preserving existing base values) and remove the conditional that skips
later records; refer to mergeDailyActivity, activity, records, record.date, and
record.wordCount when updating the logic.
- Around line 685-690: The loop in bestDay() uses a forced unwrap best!.1;
replace this with a safe binding: for each date compute words then use guard let
current = best else { best = (date, words); continue } and then compare words >
current.1 to update best, eliminating the force unwrap on best and making the
intent explicit (referencing the variables best, dailyActivity, calendar and the
surrounding for offset loop).

In `@Sources/Views/Dashboard/DashboardView.swift`:
- Around line 89-92: The four constant declarations (static let xs, sm, md, lg)
in DashboardView.swift violate the `colon` lint rule due to extra space before
the colon; update each line to remove the padded space so the colon is directly
after the identifier (e.g., change "static let xs:  CGFloat = 4" to "static let
xs: CGFloat = 4") and apply the same fix for sm, md, and lg.
- Around line 121-130: Annotate the UI types with `@MainActor` to ensure
main-thread execution: add the `@MainActor` attribute to the SidebarVisualEffect
struct declaration (the NSViewRepresentable) and to the DashboardView struct
declaration (the SwiftUI View) so their lifecycle methods and state updates run
on the main actor; update both declarations to include `@MainActor` before the
struct keyword and keep existing members unchanged.

In `@Sources/Views/Dashboard/DashboardVisualsView.swift`:
- Around line 105-111: The Picker currently created as Picker("", selection:
$visualIntensity) with .labelsHidden() lacks an accessible label for VoiceOver;
update the Picker to provide an explicit accessibility label (for example via
the init Picker("Visual intensity", selection: $visualIntensity) or by adding
.accessibilityLabel("Visual intensity")) so VoiceOver announces the control and
continue to use VisualIntensity and visualIntensity as the selection symbols
when making the change.

In `@Sources/Views/WelcomeView.swift`:
- Around line 21-24: The comma-spacing lint fails on the Color initializer for
the constant coral; update the initializer in the declaration private let coral
= Color(red: 0.85,  green: 0.45,  blue: 0.30) to use single spaces after each
comma (e.g., red: 0.85, green: 0.45, blue: 0.30) so the commas are followed by
exactly one space and the lint passes; verify similar spacing for coralDeep if
needed.
- Around line 259-305: Add accessible name and selection state to the style tile
Button by applying accessibility modifiers to the Button created with
Button(action: onSelect). Use the style's stable identifier/name (e.g.,
style.name or style.id) for .accessibilityLabel, provide a descriptive
.accessibilityHint if helpful, and add selection semantics with
.accessibilityAddTraits(selected ? .isSelected : []); optionally add an
identifier with .accessibilityIdentifier("styleTile_\(style.id)"). Update the
Button declaration (the Button(action: onSelect) wrapping WaveformStylePreview)
to include these modifiers so VoiceOver announces the style name and whether it
is selected.
- Around line 15-18: The SwiftUI view types WelcomeView and StyleTile must be
annotated with `@MainActor` to ensure their state and UI effects run on the main
thread; update the declarations of struct WelcomeView and struct StyleTile to
add the `@MainActor` attribute (and if StyleTile is nested or uses a different
declaration name, add `@MainActor` to that type) so all view-owned
`@State/`@StateObject properties and UI methods are main-actor isolated.

In `@Tests/AppDelegate/AppDelegateErrorTests.swift`:
- Around line 61-64: The test testTerminationDoesNotCrash currently only calls
appDelegate.applicationWillTerminate(notification) with no assertions; before
calling applicationWillTerminate, set up a non-default piece of state on the
AppDelegate (for example initialize a status/menu/icon-related field such as a
statusItem, mainMenu, or a flag like iconVisible), then invoke
appDelegate.applicationWillTerminate(notification) and add an XCTAssert that
verifies a post-termination invariant (e.g. statusItem is nil, menu items were
cleared, or iconVisible is false) so the test fails if cleanup regresses; use
the existing test name testTerminationDoesNotCrash and the method
appDelegate.applicationWillTerminate(...) to locate where to add the setup and
assertion.

In `@Tests/AppDelegate/AppDelegateMenuTests.swift`:
- Around line 28-41: The test testMakeStatusMenuActionItemTitles is
over-constrained by asserting an exact list from appDelegate.makeStatusMenu
(actionItems and expectedTitles) which breaks when dynamic "Recent transcripts"
exist; either reset the recent-transcript cache before calling
appDelegate.makeStatusMenu in the test setup so the menu is deterministic, or
change the assertion to verify only required core actions are present (e.g.,
check that actionItems.map { $0.title } contains "Start Recording", "Transcribe
a File…", "Dashboard", "Help" and that "Quit AudioWhisper" is the final action),
and apply the same fix to the other menu-structure test(s) that currently
hard-code titles.

In `@Tests/Views/Components/WaveformViewTests.swift`:
- Around line 10-53: The current tests (testSpectrumWaveformViewConstructs,
testSpectrumWaveformViewConstructsWithEmptyBands,
testStreamWaveformViewConstructs, testConstellationWaveformViewConstructs,
testHaloWaveformViewConstructs, testDialWaveformViewConstructs,
testHeartbeatPulseViewConstructs) only call XCTAssertNotNil which is a no-op for
SwiftUI Views; replace each with real assertions that exercise view behavior:
host the view in a UIHostingController or use ViewInspector to access its body
and assert on expected subviews/state (e.g., presence/absence of bars for empty
frequencyBands, active vs. inactive appearance, audioLevel-driven
opacity/scale), or add snapshot assertions for each variant (active/inactive,
empty vs populated bands) to verify rendered output instead of just
initialization.

In `@Tests/Views/WelcomeViewTests.swift`:
- Around line 30-34: The test testStyleGridColumns is asserting a local constant
against itself; instead instantiate or access the production value from
WelcomeView (e.g., a public layout metric or exposed property like
WelcomeView.gridColumnCount or WelcomeView.layout.columns) and assert that that
value equals 4 while still asserting WaveformStyle.allCases.count == 8; replace
XCTAssertEqual(columnCount, 4) with an assertion that queries the real source of
truth on WelcomeView (or add a small test-only computed property on WelcomeView
if needed) so the test fails when the view’s column count changes.

---

Outside diff comments:
In `@Sources/Views/Dashboard/DashboardVisualsView.swift`:
- Around line 6-166: The SwiftUI components DashboardVisualsView,
StylePickerCard, and PreviewWindow must be annotated with `@MainActor` so their UI
state and lifecycle run on the main thread; locate the struct/type declarations
for DashboardVisualsView, StylePickerCard, and PreviewWindow and add the
`@MainActor` attribute to each (e.g., `@MainActor` struct DashboardVisualsView:
View), and ensure any related view models or `@StateObject` initializers used only
by these views are also main-actor-isolated if needed to eliminate
non-main-thread UI access warnings.
🪄 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: 9fda4593-6f4d-49f2-98f8-001315a51539

📥 Commits

Reviewing files that changed from the base of the PR and between 7b604f1 and 8e4d662.

⛔ Files ignored due to path filters (1)
  • Sources/Resources/DashboardLogo.jpg is excluded by !**/*.jpg
📒 Files selected for processing (52)
  • Sources/App/AppDelegate+Hotkeys.swift
  • Sources/App/AppDelegate+Lifecycle.swift
  • Sources/App/AppDelegate+Menu.swift
  • Sources/App/AppDelegate.swift
  • Sources/App/MenuBarIcon.swift
  • Sources/Design/LayoutMetrics.swift
  • Sources/Managers/Windows/DashboardWindowManager.swift
  • Sources/Managers/Windows/WelcomeWindow.swift
  • Sources/Views/Components/MenuPopupViews.swift
  • Sources/Views/Components/Waveform/CircularSpectrumView.swift
  • Sources/Views/Components/Waveform/ColorTheme.swift
  • Sources/Views/Components/Waveform/ConstellationWaveformView.swift
  • Sources/Views/Components/Waveform/DialWaveformView.swift
  • Sources/Views/Components/Waveform/HaloWaveformView.swift
  • Sources/Views/Components/Waveform/HeartbeatPulseView.swift
  • Sources/Views/Components/Waveform/NeonWaveformView.swift
  • Sources/Views/Components/Waveform/ParticleFieldView.swift
  • Sources/Views/Components/Waveform/PulseRingsView.swift
  • Sources/Views/Components/Waveform/SpectrumWaveformView.swift
  • Sources/Views/Components/Waveform/StreamWaveformView.swift
  • Sources/Views/Components/Waveform/WaveformContainer.swift
  • Sources/Views/Components/Waveform/WaveformPreviewHelpers.swift
  • Sources/Views/Components/Waveform/WaveformStyle.swift
  • Sources/Views/Dashboard/DashboardHome+Activity.swift
  • Sources/Views/Dashboard/DashboardHome+Header.swift
  • Sources/Views/Dashboard/DashboardHome+Recent.swift
  • Sources/Views/Dashboard/DashboardHome+Sources.swift
  • Sources/Views/Dashboard/DashboardHome+Stats.swift
  • Sources/Views/Dashboard/DashboardHomeView.swift
  • Sources/Views/Dashboard/DashboardView.swift
  • Sources/Views/Dashboard/DashboardVisualsView.swift
  • Sources/Views/WelcomeView.swift
  • Tests/AppDelegate/AppDelegateBaseTests.swift
  • Tests/AppDelegate/AppDelegateErrorTests.swift
  • Tests/AppDelegate/AppDelegateHotkeysTests.swift
  • Tests/AppDelegate/AppDelegateLifecycleTests.swift
  • Tests/AppDelegate/AppDelegateMenuTests.swift
  • Tests/AppDelegateExtensionTests.swift
  • Tests/Design/LayoutMetricsTests.swift
  • Tests/EdgeCases/ServiceEdgeCaseTests.swift
  • Tests/Integration/ConcurrentRecordingTriggerTests.swift
  • Tests/Performance/PerformanceTests.swift
  • Tests/UISnapshotTests+Waveform.swift
  • Tests/Utilities/AppDefaultCoverageTests.swift
  • Tests/Views/Components/WaveformViewTests.swift
  • Tests/Views/Dashboard/DashboardHomeViewTests.swift
  • Tests/Views/Dashboard/DashboardViewTests.swift
  • Tests/Views/Dashboard/DashboardVisualsViewTests.swift
  • Tests/Views/WelcomeViewTests.swift
  • Tests/Waveform/WaveformViewsTests.swift
  • Tests/Waveform/WaveformVisualizationTests.swift
  • Tests/WaveformStyleTests.swift
💤 Files with no reviewable changes (12)
  • Sources/Views/Components/Waveform/CircularSpectrumView.swift
  • Sources/Views/Components/Waveform/ParticleFieldView.swift
  • Sources/Views/Components/Waveform/PulseRingsView.swift
  • Tests/Integration/ConcurrentRecordingTriggerTests.swift
  • Sources/Views/Dashboard/DashboardHome+Recent.swift
  • Sources/Views/Dashboard/DashboardHome+Header.swift
  • Sources/Views/Dashboard/DashboardHome+Stats.swift
  • Sources/Views/Dashboard/DashboardHome+Activity.swift
  • Tests/EdgeCases/ServiceEdgeCaseTests.swift
  • Tests/Views/Dashboard/DashboardHomeViewTests.swift
  • Sources/Views/Dashboard/DashboardHome+Sources.swift
  • Tests/Performance/PerformanceTests.swift

Comment on lines +29 to +82
func populateStatusMenu(_ menu: NSMenu) {
menu.removeAllItems()

// ── Header
menu.addItem(makeHeaderItem())
menu.addItem(.separator())

// ── Primary actions
menu.addItem(makeActionItem(
title: "Start Recording",
selector: #selector(toggleRecordWindow),
keyEquivalent: " ",
modifiers: [.command, .shift]
))
menu.addItem(makeActionItem(
title: "Transcribe a File…",
selector: #selector(transcribeAudioFile),
keyEquivalent: ""
))
return menu

menu.addItem(.separator())

// ── Recent transcripts (inline)
let recent = recentTranscriptsForMenu()
if !recent.isEmpty {
menu.addItem(makeSectionLabelItem("Recent"))
for record in recent.prefix(3) {
menu.addItem(makeRecentItem(record: record))
}
menu.addItem(.separator())
}

// ── Dashboard / Help / Quit
menu.addItem(makeActionItem(
title: "Dashboard",
selector: #selector(showDashboard),
keyEquivalent: ",",
modifiers: [.command]
))
menu.addItem(makeActionItem(
title: "Help",
selector: #selector(showHelp),
keyEquivalent: ""
))

menu.addItem(.separator())

menu.addItem(makeActionItem(
title: "Quit AudioWhisper",
selector: #selector(NSApplication.terminate(_:)),
keyEquivalent: "q",
modifiers: [.command]
))
}
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

Split populateStatusMenu(_:) into smaller builders.

This method exceeds the repository’s 40-line function cap and should be decomposed (e.g., header/actions/recent/footer section helpers).

As per coding guidelines, {Sources/**/*.swift,Sources/**/*.py}: Keep functions to 40 lines or fewer.

🤖 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/App/AppDelegate`+Menu.swift around lines 29 - 82,
populateStatusMenu(_:) is too long; split it into smaller builder helpers to
comply with the 40-line rule. Extract logical sections into private methods such
as buildHeaderMenuItems(menu:), buildPrimaryActionItems(menu:),
buildRecentTranscriptsSection(menu:) (using recentTranscriptsForMenu() and
makeRecentItem(record:)), and buildFooterItems(menu:) (Dashboard/Help/Quit), and
then have populateStatusMenu(_:) simply call these helpers in order while
keeping the same use of makeHeaderItem(), makeActionItem(...),
makeSectionLabelItem(_:), and makeRecentItem(record:). Ensure each new helper is
<=40 lines and preserves the exact selectors and key equivalents.

Comment on lines +118 to +133
private func makeRecentItem(record: TranscriptionRecord) -> NSMenuItem {
let item = NSMenuItem(
title: record.text, // Fallback for VoiceOver
action: #selector(copyRecentTranscript(_:)),
keyEquivalent: ""
)
item.target = self
item.representedObject = record

let host = NSHostingView(rootView: RecentTranscriptMenuRow(
timeString: Self.menuTimeFormatter.string(from: record.date),
text: record.text
))
host.frame = NSRect(x: 0, y: 0, width: menuWidth, height: 26)
item.view = host
return item
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

🌐 Web query:

For macOS AppKit: when NSMenuItem has a custom view, does clicking it automatically invoke the NSMenuItem target/action, or must the custom view handle mouse events / trigger action itself?

💡 Result:

For AppKit NSMenuItem with a custom view (setting NSMenuItem.view), clicking the menu item does NOT automatically invoke the menu item’s target/action in the general case; with a custom view you typically must handle interaction inside the view (e.g., forward mouse events to perform the action) yourself. Evidence: 1) Developers report that when an NSMenuItem has a custom view, the menu item’s target/action callback does not get called on click, and the usual mouse event pathway differs from items without a custom view [1]. 2) Apple’s “Views in Menu Items” documentation states that “A view in a menu item can receive all mouse events as normal” [2]. That implies responsibility shifts to the view for mouse-driven behavior. What to do instead (practical pattern): - In your custom view, override mouse handling (commonly mouseUp:) and explicitly trigger the menu item’s action (e.g., call through the enclosing menu item / menu). For example, there are community-proven implementations that locate the enclosing menu item and call the menu’s action performer when the user releases the mouse [3]. So, answer to your question: - Must the custom view handle mouse events / trigger action itself? Yes—if you need the target/action to run when the user clicks the item and you provided a custom NSView, you should not rely on automatic target/action invocation; implement mouse handling in the view and invoke the menu item’s action explicitly [1][3][2].

Citations:


🏁 Script executed:

# First, locate the RecentTranscriptMenuRow file
fd -t f "RecentTranscriptMenuRow" Sources/

Repository: jtn0123/AudioWhisper

Length of output: 46


🏁 Script executed:

# Search for RecentTranscriptMenuRow definition
rg "RecentTranscriptMenuRow" Sources/

# Also check the full AppDelegate+Menu.swift file to understand structure
wc -l Sources/App/AppDelegate+Menu.swift

Repository: jtn0123/AudioWhisper

Length of output: 381


🏁 Script executed:

# Read MenuPopupViews.swift to find RecentTranscriptMenuRow implementation
cat -n Sources/Views/Components/MenuPopupViews.swift | grep -A 30 "RecentTranscriptMenuRow"

# Also check AppDelegate+Menu.swift around lines 118-133 and see if copyRecentTranscript is defined
cat -n Sources/App/AppDelegate+Menu.swift | grep -A 5 -B 5 "copyRecentTranscript"

Repository: jtn0123/AudioWhisper

Length of output: 2374


🏁 Script executed:

# Check the complete makeRecentItem function and surrounding context
cat -n Sources/App/AppDelegate+Menu.swift | sed -n '115,140p'

Repository: jtn0123/AudioWhisper

Length of output: 1071


Custom view blocks menu item action—the "copy recent transcript" feature will not work on click.

When NSMenuItem.view is set, AppKit bypasses the normal target/action invocation. The RecentTranscriptMenuRow SwiftUI view currently has no interaction handler, so clicks on the menu item will not trigger copyRecentTranscript(_:). Either wrap the view content in a SwiftUI Button or add explicit mouse event handling in the view to invoke the action.

🤖 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/App/AppDelegate`+Menu.swift around lines 118 - 133, makeRecentItem
sets item.view to a SwiftUI host which prevents AppKit from invoking
copyRecentTranscript(_:); to fix, update the SwiftUI content used in item.view
(the RecentTranscriptMenuRow wrapper) to handle clicks — e.g., wrap
RecentTranscriptMenuRow in a SwiftUI Button whose action calls
NSApp.sendAction(`#selector`(AppDelegate.copyRecentTranscript(_:)), to: nil, from:
item) or otherwise invokes the AppDelegate selector with the menu item or its
representedObject as sender; change the NSHostingView rootView in makeRecentItem
to this Button-wrapped view (and apply a plain button style) so clicking the
custom view triggers copyRecentTranscript(_:).

Comment thread Sources/App/MenuBarIcon.swift Outdated
Comment on lines +11 to +12
internal struct MenuHeaderView: View {

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

Annotate these UI view structs with @MainActor.

The new SwiftUI menu components should be explicitly main-actor isolated per repository rule.

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

Also applies to: 98-99, 125-126

🤖 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/MenuPopupViews.swift` around lines 11 - 12, Annotate
the SwiftUI view structs with `@MainActor` to ensure main-thread isolation for UI:
add the `@MainActor` attribute before the declaration of MenuHeaderView and the
other view structs in this file (the ones at the referenced locations around
lines 98-99 and 125-126) so each struct declaration (e.g., MenuHeaderView)
becomes main-actor isolated; no other behavioral changes are needed.

Comment on lines +23 to +76
var body: some View {
HStack(spacing: 10) {
// Coral logo bauble
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(
LinearGradient(
colors: [coral, coralDeep],
startPoint: .topLeading, endPoint: .bottomTrailing
)
)
.frame(width: 26, height: 26)
.overlay(
// Mini bars glyph
Canvas { context, size in
let heights: [CGFloat] = [0.45, 0.85, 1.0, 0.7, 0.35]
let barW: CGFloat = size.width / 11
let gap: CGFloat = size.width / 14
let total = barW * 5 + gap * 4
let startX = (size.width - total) * 0.5
let maxH = size.height * 0.6
for (i, h) in heights.enumerated() {
let barH = maxH * h
let x = startX + CGFloat(i) * (barW + gap)
let y = (size.height - barH) * 0.5
let path = Path(roundedRect: CGRect(x: x, y: y, width: barW, height: barH),
cornerRadius: barW / 2)
context.fill(path, with: .color(.white))
}
}
.frame(width: 14, height: 14)
)
.shadow(color: .black.opacity(0.15), radius: 1, y: 1)

// Title + status
VStack(alignment: .leading, spacing: 1) {
Text("AudioWhisper")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.primary)

HStack(spacing: 5) {
Circle()
.fill(sage)
.frame(width: 6, height: 6)
Text(statusLine)
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
}

Spacer(minLength: 0)
}
.padding(.horizontal, 12)
.padding(.vertical, 9)
}
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

Break MenuHeaderView.body into smaller subviews/modifiers.

This body exceeds the function-length limit and should be split (e.g., logo block, status block, root layout).

As per coding guidelines, {Sources/**/*.swift,Sources/**/*.py}: Keep functions to 40 lines or fewer.

🤖 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/MenuPopupViews.swift` around lines 23 - 76, The
MenuHeaderView.body is too long; split it into smaller subviews and/or view
modifiers: extract the coral logo bauble (the RoundedRectangle + Canvas overlay
+ shadow) into a private View or computed property (e.g., private var logoView
or struct LogoBadge) that uses coral and coralDeep and preserves the Canvas
drawing; extract the Title + status VStack into a private view (e.g., private
var titleStatusView) that uses statusLine and sage; then have
MenuHeaderView.body simply compose HStack { logoView; titleStatusView; Spacer()
} with the same .padding calls so layout and styling remain identical. Ensure
the new subviews are private and keep all existing constants (heights, barW,
gap, startX, maxH) inside the extracted logo view.

Comment on lines +259 to 305
Button(action: onSelect) {
ZStack {
Color.clear
.frame(width: 28, height: 28)

Image(systemName: icon)
.font(.title3.weight(.medium))
.foregroundStyle(Color.accentColor)
.symbolRenderingMode(.monochrome)
}

VStack(alignment: .leading, spacing: 3) {
Text(title)
.font(.subheadline.weight(.semibold))
.fixedSize(horizontal: false, vertical: true)
Text(description)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Color(red: 0.04, green: 0.04, blue: 0.04)
WaveformStylePreview(style: style, sampler: sampler)
.frame(
width: style.isRadial ? 64 : 108,
height: style.isRadial ? 64 : 60
)

if style.isNew {
VStack {
HStack {
Spacer()
Text("NEW")
.font(.system(size: 7, weight: .bold))
.tracking(0.4)
.foregroundStyle(coral)
.padding(.horizontal, 4)
.padding(.vertical, 1)
.background(
RoundedRectangle(cornerRadius: 2)
.fill(Color(red: 0.04, green: 0.04, blue: 0.04).opacity(0.55))
.overlay(
RoundedRectangle(cornerRadius: 2)
.stroke(coral.opacity(0.4), lineWidth: 0.5)
)
)
}
Spacer()
}
.padding(3)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 70)
.clipShape(RoundedRectangle(cornerRadius: 7, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 7, style: .continuous)
.stroke(selected ? coral : Color(red: 0.235, green: 0.176, blue: 0.118).opacity(0.14),
lineWidth: 1)
)
.shadow(
color: selected ? coral.opacity(0.25) : .clear,
radius: selected ? 3 : 0
)
}
.frame(maxWidth: .infinity, alignment: .leading)
.frame(minHeight: 50) // Ensure consistent height
.buttonStyle(.plain)
}
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

Add an accessible name and selection state to each style tile.

These buttons are preview-only visuals today, so VoiceOver users do not get a stable style name or selected-state announcement and cannot reliably choose a waveform.

Suggested change
         }
         .buttonStyle(.plain)
+        .accessibilityLabel(Text(style.rawValue))
+        .accessibilityValue(Text(selected ? "Selected" : "Not selected"))
+        .accessibilityHint(Text("Select this waveform style"))
     }
 }
🤖 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/WelcomeView.swift` around lines 259 - 305, Add accessible name
and selection state to the style tile Button by applying accessibility modifiers
to the Button created with Button(action: onSelect). Use the style's stable
identifier/name (e.g., style.name or style.id) for .accessibilityLabel, provide
a descriptive .accessibilityHint if helpful, and add selection semantics with
.accessibilityAddTraits(selected ? .isSelected : []); optionally add an
identifier with .accessibilityIdentifier("styleTile_\(style.id)"). Update the
Button declaration (the Button(action: onSelect) wrapping WaveformStylePreview)
to include these modifiers so VoiceOver announces the style name and whether it
is selected.

Comment on lines +61 to 64
func testTerminationDoesNotCrash() {
let notification = Notification(name: NSApplication.willTerminateNotification)
appDelegate.applicationWillTerminate(notification)
XCTAssertNil(appDelegate.recordingAnimationTimer)
}

func testAnimationTimerCancellationOnTerminate() {
let timer = DispatchSource.makeTimerSource()
timer.schedule(deadline: .now() + 100, repeating: .seconds(1))
timer.resume()
appDelegate.recordingAnimationTimer = timer

let notification = Notification(name: NSApplication.willTerminateNotification)
appDelegate.applicationWillTerminate(notification)

XCTAssertNil(appDelegate.recordingAnimationTimer)
}
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

Strengthen termination coverage beyond a no-op call

This test currently passes even if termination cleanup regresses, because it has no observable assertions. Please assert at least one post-termination invariant after creating non-default state (for example, initialized menu/icon-related state), so cleanup behavior is actually verified.

🤖 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 `@Tests/AppDelegate/AppDelegateErrorTests.swift` around lines 61 - 64, The test
testTerminationDoesNotCrash currently only calls
appDelegate.applicationWillTerminate(notification) with no assertions; before
calling applicationWillTerminate, set up a non-default piece of state on the
AppDelegate (for example initialize a status/menu/icon-related field such as a
statusItem, mainMenu, or a flag like iconVisible), then invoke
appDelegate.applicationWillTerminate(notification) and add an XCTAssert that
verifies a post-termination invariant (e.g. statusItem is nil, menu items were
cleared, or iconVisible is false) so the test fails if cleanup regresses; use
the existing test name testTerminationDoesNotCrash and the method
appDelegate.applicationWillTerminate(...) to locate where to add the setup and
assertion.

Comment on lines +28 to 41
func testMakeStatusMenuActionItemTitles() {
let menu = appDelegate.makeStatusMenu()

// Count menu items (including separators)
XCTAssertEqual(menu.items.count, 8, "Menu should have 8 items including separators")

// Verify non-separator items
let nonSeparatorItems = menu.items.filter { !$0.isSeparatorItem }
XCTAssertEqual(nonSeparatorItems.count, 6, "Menu should have 6 non-separator items")
}

func testMakeStatusMenuItemTitles() {
let menu = appDelegate.makeStatusMenu()
let nonSeparatorItems = menu.items.filter { !$0.isSeparatorItem }
let actionItems = menu.items.filter { !$0.isSeparatorItem && $0.action != nil }

let expectedTitles = [
LocalizedStrings.Menu.record,
"Transcribe Audio File...",
"Dashboard...",
LocalizedStrings.Menu.history,
"Start Recording",
"Transcribe a File…",
"Dashboard",
"Help",
LocalizedStrings.Menu.quit
"Quit AudioWhisper"
]

for (index, expectedTitle) in expectedTitles.enumerated() {
XCTAssertEqual(nonSeparatorItems[index].title, expectedTitle,
"Menu item at index \(index) should have title '\(expectedTitle)'")
}
XCTAssertEqual(actionItems.map { $0.title }, expectedTitles)
}
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

Menu structure assertions are over-constrained for a dynamic menu

These tests hard-code exact action-title lists and fixed item/separator counts. With dynamic “Recent transcripts” entries, this can fail nondeterministically based on cached state rather than real regressions. Prefer asserting required core actions are present (and critical ordering like Quit-last), or explicitly reset the recent-transcript cache in setup before asserting exact counts.

Also applies to: 52-65

🤖 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 `@Tests/AppDelegate/AppDelegateMenuTests.swift` around lines 28 - 41, The test
testMakeStatusMenuActionItemTitles is over-constrained by asserting an exact
list from appDelegate.makeStatusMenu (actionItems and expectedTitles) which
breaks when dynamic "Recent transcripts" exist; either reset the
recent-transcript cache before calling appDelegate.makeStatusMenu in the test
setup so the menu is deterministic, or change the assertion to verify only
required core actions are present (e.g., check that actionItems.map { $0.title }
contains "Start Recording", "Transcribe a File…", "Dashboard", "Help" and that
"Quit AudioWhisper" is the final action), and apply the same fix to the other
menu-structure test(s) that currently hard-code titles.

Comment on lines +10 to +53
func testSpectrumWaveformViewConstructs() {
let view = SpectrumWaveformView(
frequencyBands: [0.8, 0.6, 0.5, 0.4, 0.3, 0.25, 0.2, 0.15],
isActive: true
)
XCTAssertNotNil(view)
}

func testSpectrumSmoothedLevelInstantAttack() {
// When target > current, should jump immediately to target
let current: Float = 0.2
let target: Float = 0.8
let result = SpectrumWaveformView.testableSmoothedLevel(current: current, target: target)

XCTAssertEqual(result, target)
func testSpectrumWaveformViewConstructsWithEmptyBands() {
let view = SpectrumWaveformView(frequencyBands: [], isActive: false)
XCTAssertNotNil(view)
}

func testSpectrumSmoothedLevelGradualDecay() {
// When target < current, should decay gradually (25% towards target)
let current: Float = 0.8
let target: Float = 0.2
let result = SpectrumWaveformView.testableSmoothedLevel(current: current, target: target)

// Specifically: 0.8 * 0.75 + 0.2 * 0.25 = 0.6 + 0.05 = 0.65
XCTAssertEqual(result, 0.65, accuracy: 0.001)
func testStreamWaveformViewConstructs() {
let view = StreamWaveformView(audioLevel: 0.5, isActive: true)
XCTAssertNotNil(view)
}

func testSpectrumPeakDecayNewPeak() {
// When level exceeds current peak, new peak is set
let currentPeak: Float = 0.5
let newLevel: Float = 0.8
let result = SpectrumWaveformView.testablePeakDecay(current: currentPeak, level: newLevel)

XCTAssertEqual(result, newLevel)
func testConstellationWaveformViewConstructs() {
let view = ConstellationWaveformView(audioLevel: 0.5, isActive: true)
XCTAssertNotNil(view)
}

func testSpectrumPeakDecaySlowFade() {
// When level is below peak, peak slowly decays
let currentPeak: Float = 0.5
let level: Float = 0.3
let result = SpectrumWaveformView.testablePeakDecay(current: currentPeak, level: level)

// Should be slightly below current peak
XCTAssertEqual(result, 0.49, accuracy: 0.001)
func testHaloWaveformViewConstructs() {
let view = HaloWaveformView(
frequencyBands: [0.7, 0.5, 0.4, 0.6, 0.3, 0.45, 0.25, 0.35],
audioLevel: 0.5,
isActive: true
)
XCTAssertNotNil(view)
}

func testSpectrumPeakDecayNeverNegative() {
// Peak should never go below zero
let currentPeak: Float = 0.005
let level: Float = 0.0
let result = SpectrumWaveformView.testablePeakDecay(current: currentPeak, level: level)

XCTAssertEqual(result, 0.0)
func testDialWaveformViewConstructs() {
let view = DialWaveformView(
frequencyBands: [0.8, 0.6, 0.5, 0.4, 0.3, 0.25, 0.2, 0.15],
audioLevel: 0.5,
isActive: true
)
XCTAssertNotNil(view)
}

// MARK: - Consistency Tests

func testCircularMirroringIsSymmetric() {
// Bars 0 and 15 should map to same band
XCTAssertEqual(
CircularSpectrumView.testableBandIndex(for: 0),
CircularSpectrumView.testableBandIndex(for: 15)
)
// Bars 1 and 14 should map to same band
XCTAssertEqual(
CircularSpectrumView.testableBandIndex(for: 1),
CircularSpectrumView.testableBandIndex(for: 14)
)
// Bars 7 and 8 should map to same band
XCTAssertEqual(
CircularSpectrumView.testableBandIndex(for: 7),
CircularSpectrumView.testableBandIndex(for: 8)
)
func testHeartbeatPulseViewConstructs() {
let view = HeartbeatPulseView(audioLevel: 0.5, isActive: true)
XCTAssertNotNil(view)
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

XCTAssertNotNil on SwiftUI view values is effectively a no-op.

These tests will pass as long as the type initializes, so they don’t meaningfully validate rendering/state behavior (especially empty-band and idle/active variants). Please replace with assertions that exercise behavior/output (e.g., inspected body state, hosted rendering expectations, or snapshot assertions for each state).

🤖 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 `@Tests/Views/Components/WaveformViewTests.swift` around lines 10 - 53, The
current tests (testSpectrumWaveformViewConstructs,
testSpectrumWaveformViewConstructsWithEmptyBands,
testStreamWaveformViewConstructs, testConstellationWaveformViewConstructs,
testHaloWaveformViewConstructs, testDialWaveformViewConstructs,
testHeartbeatPulseViewConstructs) only call XCTAssertNotNil which is a no-op for
SwiftUI Views; replace each with real assertions that exercise view behavior:
host the view in a UIHostingController or use ViewInspector to access its body
and assert on expected subviews/state (e.g., presence/absence of bars for empty
frequencyBands, active vs. inactive appearance, audioLevel-driven
opacity/scale), or add snapshot assertions for each variant (active/inactive,
empty vs populated bands) to verify rendered output instead of just
initialization.

Comment on lines +30 to +34
func testStyleGridColumns() {
// The consolidated welcome shows the 8 waveform styles in a 4-column grid.
let columnCount = 4
XCTAssertEqual(columnCount, 4)
XCTAssertEqual(WaveformStyle.allCases.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 | 🟡 Minor | ⚡ Quick win

testStyleGridColumns is tautological and misses the real behavior check.

Line 33 asserts a local constant against itself, so this passes even if the view no longer uses a 4-column grid. Please assert against a production source of truth (e.g., a layout metric or test hook exposed by WelcomeView) instead.

🤖 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 `@Tests/Views/WelcomeViewTests.swift` around lines 30 - 34, The test
testStyleGridColumns is asserting a local constant against itself; instead
instantiate or access the production value from WelcomeView (e.g., a public
layout metric or exposed property like WelcomeView.gridColumnCount or
WelcomeView.layout.columns) and assert that that value equals 4 while still
asserting WaveformStyle.allCases.count == 8; replace XCTAssertEqual(columnCount,
4) with an assertion that queries the real source of truth on WelcomeView (or
add a small test-only computed property on WelcomeView if needed) so the test
fails when the view’s column count changes.

Clears the 65 swiftlint --strict violations the redesign introduced:

- Renames single-character graphics/loop identifiers to descriptive names.
- Replaces the 3-member providerStats tuple with a ProviderStat struct.
- Splits DashboardHomeView (869 lines) into DashboardHomeView,
  DashboardHome+Sections, and DashboardHomeCharts to clear file_length.
- Moves WaveformContainer's state-helper computed properties into an
  extension to clear type_body_length.

No rules suppressed; build and tests unchanged.

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

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

@jtn0123 jtn0123 merged commit f1db9f8 into master May 15, 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