Skip to content

fix(updater): stop the update prompt from blocking the main actor - #837

Open
nishantkumar1292 wants to merge 1 commit into
altic-dev:mainfrom
nishantkumar1292:fix/update-alert-blocks-main-thread
Open

fix(updater): stop the update prompt from blocking the main actor#837
nishantkumar1292 wants to merge 1 commit into
altic-dev:mainfrom
nishantkumar1292:fix/update-alert-blocks-main-thread

Conversation

@nishantkumar1292

@nishantkumar1292 nishantkumar1292 commented Aug 11, 2026

Copy link
Copy Markdown

Description

The "Update Available" prompt is an NSAlert.runModal() on the main actor. runModal()
does not return until the alert is dismissed, and it runs inside a main actor job, so every
other @MainActor job queues behind it — including the dictation callbacks
GlobalHotkeyManager.triggerDictationMode() posts from its CGEventTap thread
(GlobalHotkeyManager.swift:1843). FluidVoice is a menu bar app that is almost never
frontmost, so the alert also comes up unactivated behind other windows.

Both halves together mean that when a release ships, dictation silently stops working and
stays dead until the user stumbles on the hidden dialog. The event tap keeps running on its
own thread, so the app still logs every hotkey press — it just can never act on one.

This PR presents the same offer from a floating, non-activating NSPanel styled like the
install status panel in SimpleUpdater.showUpdateInstallStatus (SimpleUpdater.swift:471).
The main actor stays free, and .floating + orderFrontRegardless() +
[.canJoinAllSpaces, .fullScreenAuxiliary] puts the prompt above the frontmost app without
stealing focus — so it is visible without interrupting an in-flight dictation (#745).

Install Now / Later semantics are unchanged: Install Now clears the snooze and runs the
existing manual update path, Later snoozes the version for 24 hours. Two small related
changes: a re-entrancy guard, because the hourly check can now actually run while the prompt
is on screen and would otherwise stack panels; and NSApp.activate(ignoringOtherApps:)
before the manual install call, because that path still reports failures through a modal
alert which must not end up hidden behind other windows.

Deadlock evidence

macOS 26.6 (25G70), FluidVoice 1.6.7 (build 18), Apple Silicon, prompt for v1.6.8.

sample of the wedged process — 1744 of 1744 samples on the main thread:

1744 Thread_9557897   DispatchQueue_1: com.apple.main-thread  (serial)
+ 1744 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*)  (in libswift_Concurrency.dylib)
+   1744 closure #1 in AppDelegate.checkForUpdatesAutomatically()  (in FluidVoice)
+     1744 AppDelegate.showUpdateNotification(version:)  (in FluidVoice)
+       1744 -[NSAlert runModal]  (in AppKit)
+         1744 -[NSApplication runModalForWindow:]  (in AppKit)
+           1744 -[NSApplication _doModalLoop:peek:]  (in AppKit)

The main thread is inside a Swift concurrency job that never returns, which is why the modal
run loop keeps pumping AppKit events while every queued @MainActor job starves.

Matching cutover in ~/Library/Logs/Fluid/Fluid.log:

[10:50:11.370] [INFO] [AppDelegate] ✅ Update available: v1.6.8
[10:50:11.370] [INFO] [AppDelegate] Showing update notification for version v1.6.8
  • Before that line: hotkey presses served normally, 6 completed ASR sessions.
  • After it, until the app was force-restarted 89 minutes later: 30 hotkey presses
    logged by the event tap, 30 matching "Transcription release stop deferred until recording
    starts", and zero log lines from ASRService or ContentView
    — no main-actor work of any
    kind ran.

Every dead press looks like this, with nothing following it:

[12:15:33.759] [INFO] [GlobalHotkeyManager] Transcription modifier held (hold mode) - starting
[12:15:33.857] [INFO] [GlobalHotkeyManager] Transcription modifier released (hold mode) - stopping
[12:15:33.857] [DEBUG] [GlobalHotkeyManager] Transcription release stop deferred until recording starts

Reproduction

  1. Run FluidVoice as a menu bar app with automatic update checks enabled, while a release
    newer than the running build exists.
  2. Keep another app frontmost and let the automatic check fire.
  3. Press the dictation hotkey. Nothing happens; the log records the press and nothing else.
  4. sample FluidVoice shows the main thread parked in showUpdateNotificationrunModal.
  5. Activating FluidVoice reveals the hidden "Update Available" dialog; dismissing it restores
    dictation immediately.

Type of Change

  • 🐞 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 🧹 Chore
  • 📝 Documentation update

Related Issue or Discussion

Relates to #564 ("Update-check popup blocks app/hotkey access") and #745 ("Automatic update
prompt interrupts active dictation and can discard transcript"). Both were auto-closed as
stale rather than fixed; the root cause is still present on main. Happy to open a
Discussion first if maintainers prefer that route — filing this with the thread sample and
logs attached since the cause is pinned to a specific line.

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS version: 26.6
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml Sources
  • Ran formatter locally: swiftformat --config .swiftformat Sources
  • Ran tests locally:

Verification performed:

  • xcodebuild -project Fluid.xcodeproj -scheme Fluid -configuration Debug build on
    macOS 26.6 / Xcode 26.6 (Apple Silicon): BUILD SUCCEEDED, no new warnings.
  • SwiftLint 0.63.2 --strict via the same container image CI uses: 0 violations in 150 files.
  • SwiftFormat 0.62.1 --lint: no changes required.
  • The bug itself was reproduced and diagnosed on macOS 26.6 with v1.6.7 (sample + logs above).
  • Ran the patched Debug build on macOS 26.6 (Apple Silicon): the panel presents at floating
    level without activating the app (screenshots above), and Install Now correctly invokes the
    existing manual update path.

Screenshots / Video

Captured from the patched Debug build on macOS 26.6 (Apple Silicon), 2x retina:

Light Dark
Update panel — light mode Update panel — dark mode

In context — floating above the frontmost window without activating the app:

Update panel floating over another window

Notes

  • Scope is deliberately limited to the automatic update-prompt path. showUpdateAlert(...)
    (manual "No Updates" / "Update Check Failed") still uses runModal(); it is user-initiated
    and the app is active at that point, and installOfferedUpdate() now activates the app
    before invoking that path so those alerts cannot end up hidden. Converting the remaining
    alerts is a reasonable follow-up.
  • The panel does not activate the app, by design: a dictation app should not steal focus
    mid-typing. Visibility comes from the floating window level instead. UpdatePromptButton
    overrides acceptsFirstMouse so a single click works while another app is frontmost. The
    flip side is that a stray first click over the panel acts immediately; if that trade-off is
    unwanted, dropping the override makes the first click focus the panel instead.
  • The informative text dropped "Would you like to install it now?" (redundant beside the
    buttons, and the blank line made the panel unnecessarily tall). Easy to restore if
    preferred.

🤖 Generated with Claude Code

The "Update Available" prompt used NSAlert.runModal(), which does not return
until the alert is dismissed. It runs inside a main actor job, so every other
@mainactor job queues behind it - including the dictation callbacks
GlobalHotkeyManager posts from its event tap. FluidVoice is a menu bar app that
is rarely frontmost, so the alert also came up unactivated behind other
windows. The result is that dictation stops working the moment a release ships
and stays dead until the user finds the hidden dialog.

Present the offer from a floating, non-activating panel styled like the install
status panel in SimpleUpdater. The main actor stays free, the prompt floats
above the frontmost app without stealing focus, and the Install Now / Later
behaviour is unchanged. Guard against duplicate panels, since the hourly check
can now actually run while the prompt is on screen.

Refs altic-dev#564, altic-dev#745

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the needs screenshots Pull request needs screenshot or video evidence. label Aug 11, 2026
@github-actions

Copy link
Copy Markdown

The PR Policy check is blocking this PR because required template information is missing.

Please update the PR description with:

  • Screenshots / Video

Screenshots or video are required for UI, UX, settings, onboarding, overlay, menu bar, or visual behavior changes. If this PR has no visual changes, check the no-visual-change box in the template.

If this remains incomplete for 48 hours after opening, the PR may be closed.

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown

Greptile Summary

The PR replaces the blocking automatic-update alert with a guarded, nonactivating floating panel so main-actor dictation callbacks remain responsive.

  • Adds a single-panel re-entrancy guard and custom first-click button handling.
  • Preserves Install Now and Later behavior while activating the app before the existing manual installation flow.
  • Adds explicit panel dismissal and ownership cleanup.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete blocking or non-blocking defects identified in the changed update-prompt flow.

The new panel keeps the main actor available, prevents duplicate prompts, explicitly clears its retained window when either action is selected, and continues through the updater’s existing serialized installation path.

Reviews (1): Last reviewed commit: "fix(updater): stop the update prompt fro..." | Re-trigger Greptile

nishantkumar1292 added a commit to nishantkumar1292/FluidVoice that referenced this pull request Aug 11, 2026
nishantkumar1292 added a commit to nishantkumar1292/FluidVoice that referenced this pull request Aug 11, 2026
nishantkumar1292 added a commit to nishantkumar1292/FluidVoice that referenced this pull request Aug 11, 2026
@github-actions github-actions Bot removed the needs screenshots Pull request needs screenshot or video evidence. label Aug 11, 2026
@nishantkumar1292

Copy link
Copy Markdown
Author

Friendly bump on this one. The reason I'd love a maintainer's eyes: this bug silently kills dictation for everyone on the current release whenever a new version ships — the update alert opens invisibly behind other windows and blocks the main actor, so the app looks dead until you stumble on the hidden dialog. It bit me twice in a single day, and the two earlier reports of it (#564, #745) were auto-closed as stale without a fix.

The diff is small (+158/−14, one file) and reuses the existing SimpleUpdater panel pattern; lint, PR Policy, and Greptile are all green. Happy to rework the approach if you'd prefer something different — just let me know.

@bookernath

Copy link
Copy Markdown

@altic-dev please review, this is the main cause of crashes for me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants