Add Notch Indicator recording overlay style + instant-feedback fix#277
Add Notch Indicator recording overlay style + instant-feedback fix#277MufeezQadri wants to merge 3 commits into
Conversation
New third overlay style alongside minimalist and pill: a thin idle hairline that expands into a small floating capsule (app icon + waveform) when dictation starts. Device-independent — looks the same on notched and non-notched Macs, unlike the other two styles which key off the physical notch geometry. - OverlayStyle enum replaces the old use_compact_overlay Bool, with a one-time migration for existing users. - Adds the Notch Indicator option to Setup and Settings' overlay style pickers, with a live preview. - AppDelegate wires up the idle hairline's on/off state at launch and whenever the style changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… buffer Previously the overlay only appeared once the mic engine confirmed a real, non-silent audio buffer (or after a 0.2s fallback timer), so every dictation had a visible 100-300ms gap between pressing the hotkey and seeing any feedback. Show the overlay right when recording starts instead; the waveform now just sits flat for a brief moment until real audio arrives, rather than delaying the whole overlay. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe overlay preference now supports minimalist, pill, and notch styles with legacy migration. Notch mode adds idle and active rendering, frontmost app icon capture, waveform content, style-aware sizing, and immediate recording-overlay startup. Selection snapshots are skipped when command mode is disabled. ChangesOverlay style and recording indicator
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant SettingsView
participant AppDelegate
participant AppState
participant RecordingOverlayManager
participant NotchIndicatorView
User->>SettingsView: choose Notch Indicator
SettingsView->>RecordingOverlayManager: enable notch idle
AppDelegate->>RecordingOverlayManager: restore notch setting at launch
User->>AppState: start recording
AppState->>RecordingOverlayManager: show overlay immediately
RecordingOverlayManager->>NotchIndicatorView: render icon and waveform
AppState->>RecordingOverlayManager: recording-ready status update
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/AppState.swift`:
- Around line 2197-2203: Update the audioRecorder.onRecordingReady callback in
AppState so the queued main-thread handler verifies that recording is still
active before changing statusText, clearing the pending overlay dismissal token,
or playing the alert sound. Reuse the existing recording or cancellation state
check, and return immediately for stale callbacks.
In `@Sources/RecordingOverlay.swift`:
- Line 1057: Update the NotchWaveformView call in the recording overlay to
disable the activity pulse, so the waveform uses only the real state.audioLevel
and remains flat while no audio is present.
In `@Sources/SetupView.swift`:
- Around line 1608-1627: Update the .notch branch in the shared preview to
mirror NotchIndicatorView’s two-chip layout: render a separate circular icon
badge and waveform capsule with the same spacing and styling, rather than
combining them inside one Capsule. Preserve the existing preview positioning and
sizing context while matching the actual indicator structure.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9caf67a7-a0bc-4da4-9465-58164f186471
📒 Files selected for processing (5)
Sources/AppDelegate.swiftSources/AppState.swiftSources/RecordingOverlay.swiftSources/SettingsView.swiftSources/SetupView.swift
| audioRecorder.onRecordingReady = { [weak self] in | ||
| DispatchQueue.main.async { | ||
| guard let self else { return } | ||
| self.cancelRecordingInitializationTimer() | ||
| os_log(.info, log: recordingLog, "first real audio — transitioning to waveform") | ||
| os_log(.info, log: recordingLog, "first real audio") | ||
| self.statusText = "Recording..." | ||
| self.clearPendingOverlayDismissToken() | ||
| if overlayShown { | ||
| self.overlayManager.transitionToRecording( | ||
| mode: self.activeRecordingTriggerMode ?? triggerMode, | ||
| isCommandMode: self.currentSessionIntent.isCommandMode | ||
| ) | ||
| } else { | ||
| self.overlayManager.showRecording( | ||
| mode: self.activeRecordingTriggerMode ?? triggerMode, | ||
| isCommandMode: self.currentSessionIntent.isCommandMode | ||
| ) | ||
| } | ||
| overlayShown = true | ||
| self.playAlertSound(named: "Tink") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Ignore stale readiness callbacks after recording stops.
A readiness callback already queued on the main thread can run after stop or cancellation, changing the status back to “Recording…”, playing “Tink”, and clearing dismissal state.
Proposed fix
audioRecorder.onRecordingReady = { [weak self] in
DispatchQueue.main.async {
- guard let self else { return }
+ guard let self,
+ self.isRecording,
+ self.activeRecordingTriggerMode != nil else { return }
os_log(.info, log: recordingLog, "first real audio")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| audioRecorder.onRecordingReady = { [weak self] in | |
| DispatchQueue.main.async { | |
| guard let self else { return } | |
| self.cancelRecordingInitializationTimer() | |
| os_log(.info, log: recordingLog, "first real audio — transitioning to waveform") | |
| os_log(.info, log: recordingLog, "first real audio") | |
| self.statusText = "Recording..." | |
| self.clearPendingOverlayDismissToken() | |
| if overlayShown { | |
| self.overlayManager.transitionToRecording( | |
| mode: self.activeRecordingTriggerMode ?? triggerMode, | |
| isCommandMode: self.currentSessionIntent.isCommandMode | |
| ) | |
| } else { | |
| self.overlayManager.showRecording( | |
| mode: self.activeRecordingTriggerMode ?? triggerMode, | |
| isCommandMode: self.currentSessionIntent.isCommandMode | |
| ) | |
| } | |
| overlayShown = true | |
| self.playAlertSound(named: "Tink") | |
| audioRecorder.onRecordingReady = { [weak self] in | |
| DispatchQueue.main.async { | |
| guard let self, | |
| self.isRecording, | |
| self.activeRecordingTriggerMode != nil else { return } | |
| os_log(.info, log: recordingLog, "first real audio") | |
| self.statusText = "Recording..." | |
| self.clearPendingOverlayDismissToken() | |
| self.playAlertSound(named: "Tink") |
🤖 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/AppState.swift` around lines 2197 - 2203, Update the
audioRecorder.onRecordingReady callback in AppState so the queued main-thread
handler verifies that recording is still active before changing statusText,
clearing the pending overlay dismissal token, or playing the alert sound. Reuse
the existing recording or cancellation state check, and return immediately for
stale callbacks.
| .font(.system(size: 10, weight: .semibold)) | ||
| .foregroundStyle(.white.opacity(0.92)) | ||
| } | ||
| NotchWaveformView(audioLevel: state.audioLevel, showsActivityPulse: true) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep the waveform flat until real audio arrives.
showsActivityPulse: true animates the bars even when audioLevel == 0, because amplitude adds quietPulse. Use the real audio level alone during recording.
Proposed fix
- NotchWaveformView(audioLevel: state.audioLevel, showsActivityPulse: true)
+ NotchWaveformView(audioLevel: state.audioLevel)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| NotchWaveformView(audioLevel: state.audioLevel, showsActivityPulse: true) | |
| NotchWaveformView(audioLevel: state.audioLevel) |
🤖 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/RecordingOverlay.swift` at line 1057, Update the NotchWaveformView
call in the recording overlay to disable the activity pulse, so the waveform
uses only the real state.audioLevel and remains flat while no audio is present.
| case .notch: | ||
| // Small floating capsule with icon + waveform dots, sitting | ||
| // just below the menu bar — distinct from both other styles, | ||
| // which attach directly to the notch cutout. | ||
| Capsule() | ||
| .fill(Color.black) | ||
| .overlay( | ||
| HStack(spacing: 3) { | ||
| Circle() | ||
| .fill(Color.white.opacity(0.85)) | ||
| .frame(width: 5, height: 5) | ||
| ForEach(0..<3, id: \.self) { _ in | ||
| Capsule() | ||
| .fill(Color.white.opacity(0.85)) | ||
| .frame(width: 1.5, height: 6) | ||
| } | ||
| } | ||
| ) | ||
| .frame(width: 34, height: 12) | ||
| .padding(.top, menuBarHeight + 3) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the Notch preview match the actual two-chip layout.
The preview shows one capsule, while NotchIndicatorView renders a separate circular icon badge and waveform pill with a gap. The shared Setup/Settings preview currently misrepresents the selected style.
Proposed rendering
case .notch:
- Capsule()
- .fill(Color.black)
- .overlay(
- HStack(spacing: 3) {
- Circle()
- .fill(Color.white.opacity(0.85))
- .frame(width: 5, height: 5)
+ HStack(spacing: 3) {
+ Circle()
+ .fill(Color.black)
+ .overlay(
+ Circle()
+ .fill(Color.white.opacity(0.85))
+ .frame(width: 5, height: 5)
+ )
+ .frame(width: 12, height: 12)
+
+ Capsule()
+ .fill(Color.black)
+ .overlay(
+ HStack(spacing: 3) {
ForEach(0..<3, id: \.self) { _ in
Capsule()
.fill(Color.white.opacity(0.85))
.frame(width: 1.5, height: 6)
}
- }
- )
- .frame(width: 34, height: 12)
- .padding(.top, menuBarHeight + 3)
+ }
+ )
+ .frame(width: 24, height: 12)
+ }
+ .padding(.top, menuBarHeight + 3)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case .notch: | |
| // Small floating capsule with icon + waveform dots, sitting | |
| // just below the menu bar — distinct from both other styles, | |
| // which attach directly to the notch cutout. | |
| Capsule() | |
| .fill(Color.black) | |
| .overlay( | |
| HStack(spacing: 3) { | |
| Circle() | |
| .fill(Color.white.opacity(0.85)) | |
| .frame(width: 5, height: 5) | |
| ForEach(0..<3, id: \.self) { _ in | |
| Capsule() | |
| .fill(Color.white.opacity(0.85)) | |
| .frame(width: 1.5, height: 6) | |
| } | |
| } | |
| ) | |
| .frame(width: 34, height: 12) | |
| .padding(.top, menuBarHeight + 3) | |
| case .notch: | |
| // Small floating capsule with icon + waveform dots, sitting | |
| // just below the menu bar — distinct from both other styles, | |
| // which attach directly to the notch cutout. | |
| HStack(spacing: 3) { | |
| Circle() | |
| .fill(Color.black) | |
| .overlay( | |
| Circle() | |
| .fill(Color.white.opacity(0.85)) | |
| .frame(width: 5, height: 5) | |
| ) | |
| .frame(width: 12, height: 12) | |
| Capsule() | |
| .fill(Color.black) | |
| .overlay( | |
| HStack(spacing: 3) { | |
| ForEach(0..<3, id: \.self) { _ in | |
| Capsule() | |
| .fill(Color.white.opacity(0.85)) | |
| .frame(width: 1.5, height: 6) | |
| } | |
| } | |
| ) | |
| .frame(width: 24, height: 12) | |
| } | |
| .padding(.top, menuBarHeight + 3) |
🤖 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/SetupView.swift` around lines 1608 - 1627, Update the .notch branch
in the shared preview to mirror NotchIndicatorView’s two-chip layout: render a
separate circular icon badge and waveform capsule with the same spacing and
styling, rather than combining them inside one Capsule. Preserve the existing
preview positioning and sizing context while matching the actual indicator
structure.
collectSelectionSnapshot() runs two synchronous, cross-process AXUIElementCopyAttributeValue calls on every hotkey press, before the recording overlay can even show. resolveSessionIntent ignores the result entirely when Edit Mode is off, so for the common case (Edit Mode disabled by default) this was pure overhead sitting in the hotkey's critical path -- worse on apps with slower accessibility trees (Electron/Chromium in particular). Skip the query and use an empty snapshot when Edit Mode is off. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
OverlayStyleenum replaces the olduse_compact_overlayBool, with a one-time migration so existing users' preference carries over.AppDelegatewires the idle hairline's on/off state at launch and whenever the style is changed.Motivation
Built and used this daily; wanted a slimmer, more unobtrusive indicator than the existing two styles that still shows which app I'm dictating into, and noticed the hotkey felt laggy because the overlay waited on mic startup before appearing at all.
Test plan
make clean && makebuilds cleanly with only these changes on top of currentmain.use_compact_overlay).notchWidth/notchOverlap, already present onmain).Summary by CodeRabbit
New Features
Bug Fixes