Implement Claude Design handoff: waveform, dashboard, welcome & menu bar redesign#17
Conversation
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>
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (16)
📝 WalkthroughWalkthroughThis 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. ChangesMenu bar icon and status menu
Waveform visualization system overhaul
Dashboard home redesign
Dashboard theme and visuals UI
Welcome screen redesign
Test updates
🎯 4 (Complex) | ⏱️ ~60 minutes
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 winAdd
@MainActorto the view components in this file.
DashboardVisualsView,StylePickerCard, andPreviewWindoware 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@MainActorannotation 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
⛔ Files ignored due to path filters (1)
Sources/Resources/DashboardLogo.jpgis excluded by!**/*.jpg
📒 Files selected for processing (52)
Sources/App/AppDelegate+Hotkeys.swiftSources/App/AppDelegate+Lifecycle.swiftSources/App/AppDelegate+Menu.swiftSources/App/AppDelegate.swiftSources/App/MenuBarIcon.swiftSources/Design/LayoutMetrics.swiftSources/Managers/Windows/DashboardWindowManager.swiftSources/Managers/Windows/WelcomeWindow.swiftSources/Views/Components/MenuPopupViews.swiftSources/Views/Components/Waveform/CircularSpectrumView.swiftSources/Views/Components/Waveform/ColorTheme.swiftSources/Views/Components/Waveform/ConstellationWaveformView.swiftSources/Views/Components/Waveform/DialWaveformView.swiftSources/Views/Components/Waveform/HaloWaveformView.swiftSources/Views/Components/Waveform/HeartbeatPulseView.swiftSources/Views/Components/Waveform/NeonWaveformView.swiftSources/Views/Components/Waveform/ParticleFieldView.swiftSources/Views/Components/Waveform/PulseRingsView.swiftSources/Views/Components/Waveform/SpectrumWaveformView.swiftSources/Views/Components/Waveform/StreamWaveformView.swiftSources/Views/Components/Waveform/WaveformContainer.swiftSources/Views/Components/Waveform/WaveformPreviewHelpers.swiftSources/Views/Components/Waveform/WaveformStyle.swiftSources/Views/Dashboard/DashboardHome+Activity.swiftSources/Views/Dashboard/DashboardHome+Header.swiftSources/Views/Dashboard/DashboardHome+Recent.swiftSources/Views/Dashboard/DashboardHome+Sources.swiftSources/Views/Dashboard/DashboardHome+Stats.swiftSources/Views/Dashboard/DashboardHomeView.swiftSources/Views/Dashboard/DashboardView.swiftSources/Views/Dashboard/DashboardVisualsView.swiftSources/Views/WelcomeView.swiftTests/AppDelegate/AppDelegateBaseTests.swiftTests/AppDelegate/AppDelegateErrorTests.swiftTests/AppDelegate/AppDelegateHotkeysTests.swiftTests/AppDelegate/AppDelegateLifecycleTests.swiftTests/AppDelegate/AppDelegateMenuTests.swiftTests/AppDelegateExtensionTests.swiftTests/Design/LayoutMetricsTests.swiftTests/EdgeCases/ServiceEdgeCaseTests.swiftTests/Integration/ConcurrentRecordingTriggerTests.swiftTests/Performance/PerformanceTests.swiftTests/UISnapshotTests+Waveform.swiftTests/Utilities/AppDefaultCoverageTests.swiftTests/Views/Components/WaveformViewTests.swiftTests/Views/Dashboard/DashboardHomeViewTests.swiftTests/Views/Dashboard/DashboardViewTests.swiftTests/Views/Dashboard/DashboardVisualsViewTests.swiftTests/Views/WelcomeViewTests.swiftTests/Waveform/WaveformViewsTests.swiftTests/Waveform/WaveformVisualizationTests.swiftTests/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
| 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] | ||
| )) | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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 |
There was a problem hiding this comment.
🧩 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:
- 1: https://stackoverflow.com/questions/44527792/nsmenuitem-with-custom-view-doesnt-receive-mouse-events
- 2: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MenuList/Articles/ViewsInMenuItems.html
- 3: https://stackoverflow.com/questions/1395556/custom-nsview-in-nsmenuitem-not-receiving-mouse-events
🏁 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.swiftRepository: 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(_:).
| internal struct MenuHeaderView: View { | ||
|
|
There was a problem hiding this comment.
🛠️ 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
🛠️ 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.
| 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) |
There was a problem hiding this comment.
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>
|


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.WaveformContaineradopts 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.NSVisualEffectViewsidebar (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.DashboardHomeViewconsolidated from 5 extension files into one.LivePreviewSampler.Drops
CircularSpectrumView,PulseRingsView,ParticleFieldView, the blink animation, andDashboardLogo.jpg. All affected tests were migrated.Test plan
swift build— Sources compile cleanswift build --build-tests— test target compiles clean🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
UI/Design Updates
Refactor