Skip to content

Add Notch Indicator recording overlay style + instant-feedback fix#277

Open
MufeezQadri wants to merge 3 commits into
zachlatta:mainfrom
MufeezQadri:feat/notch-indicator-overlay-style
Open

Add Notch Indicator recording overlay style + instant-feedback fix#277
MufeezQadri wants to merge 3 commits into
zachlatta:mainfrom
MufeezQadri:feat/notch-indicator-overlay-style

Conversation

@MufeezQadri

@MufeezQadri MufeezQadri commented Jul 18, 2026

Copy link
Copy Markdown

Summary

  • Notch Indicator overlay style — a new third option alongside Minimalist and Pill. Stays a thin idle hairline until dictation starts, then opens into a small floating capsule (frontmost-app icon + live waveform), Superwhisper-style. Unlike Minimalist/Pill, it's device-independent — same slim shape whether or not the Mac has a physical camera notch.
    • OverlayStyle enum replaces the old use_compact_overlay Bool, with a one-time migration so existing users' preference carries over.
    • Added to both the Setup wizard's and Settings' overlay-style pickers, each with a live mini preview.
    • AppDelegate wires the idle hairline's on/off state at launch and whenever the style is changed.
  • Instant recording feedback — separate fix, included here since it's small: the overlay previously 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. Now the overlay (in whichever style you use) shows immediately when recording starts; the waveform just sits flat for a brief moment until real audio arrives, instead of the whole overlay being delayed.

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 && make builds cleanly with only these changes on top of current main.
  • Verified in Setup and Settings: all three overlay styles selectable, each with correct live preview.
  • Verified the Notch Indicator idle hairline appears/disappears correctly when toggling the style and across app relaunch (migration from use_compact_overlay).
  • Verified the recording overlay now appears immediately on hotkey press across all three styles, with the waveform catching up a moment later.
  • Maintainer review of interaction with notched-Mac geometry handling (notchWidth/notchOverlap, already present on main).

Summary by CodeRabbit

  • New Features

    • Added three recording overlay styles: Minimalist menu-bar overlay, Drop-down pill, and Notch Indicator.
    • Notch Indicator now shows the frontmost app icon, waveform, status, and controls.
    • Recording overlays now appear immediately when recording starts.
    • Overlay preference is persisted as a typed setting and migrated automatically from the prior option.
  • Bug Fixes

    • Improved Notch Indicator transitions to return smoothly to idle after errors/feedback and during setup/restore flows.
    • Optimized selection snapshot handling to avoid unnecessary cross-process capture when editing/command mode is inactive.

MufeezQadri and others added 2 commits July 18, 2026 17:55
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>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3149b25e-615c-4f0e-8b32-b2899bd74fa3

📥 Commits

Reviewing files that changed from the base of the PR and between dbb5577 and f14cddd.

📒 Files selected for processing (2)
  • Sources/AppContextService.swift
  • Sources/AppState.swift

📝 Walkthrough

Walkthrough

The 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.

Changes

Overlay style and recording indicator

Layer / File(s) Summary
Overlay style preference and selection
Sources/RecordingOverlay.swift, Sources/AppDelegate.swift, Sources/SettingsView.swift, Sources/SetupView.swift
The boolean preference is replaced by OverlayStyle, migrated at launch, and exposed through three setup and settings options with style-specific previews.
Notch manager layout and sizing
Sources/RecordingOverlay.swift, Sources/AppDelegate.swift
Notch idle behavior, active positioning, sizing, shadow handling, and startup synchronization are added.
Notch session lifecycle and views
Sources/RecordingOverlay.swift
Recording sessions capture the frontmost app icon, switch notch display modes, return to the idle hairline after dismissal or errors, and render icon, waveform, status, stop, and failure content.
Recording startup and selection snapshots
Sources/AppState.swift, Sources/AppContextService.swift
The recording overlay appears immediately, readiness updates its status and clears pending dismissal state, and selection snapshots are collected only when command mode is enabled.

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
Loading

Possibly related PRs

Suggested reviewers: marcbodea

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: a new Notch Indicator overlay style and faster recording feedback.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1de2c2f and dbb5577.

📒 Files selected for processing (5)
  • Sources/AppDelegate.swift
  • Sources/AppState.swift
  • Sources/RecordingOverlay.swift
  • Sources/SettingsView.swift
  • Sources/SetupView.swift

Comment thread Sources/AppState.swift
Comment on lines 2197 to 2203
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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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.

Comment thread Sources/SetupView.swift
Comment on lines +1608 to +1627
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant