Skip to content

fix(macos): handle capture commands while the main window is closed - #15

Merged
PCcoding666 merged 4 commits into
masterfrom
fix/macos-capture-commands-window-closed
Oct 11, 2026
Merged

PCcoding666 merged 4 commits into
masterfrom
fix/macos-capture-commands-window-closed

Conversation

@PCcoding666

@PCcoding666 PCcoding666 commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

With the main window closed, Start/Stop Capture from the Window menu, the status bar item, and the ⌃⌥⌘S hotkey did nothing. Each of them only posts .toggleCapture, and the only observer was MainView. After the window closed (the app keeps running in the menu bar), nothing was listening. Reproduced on 2026-10-10: no start() called line appeared until the window was reopened.

Closing the window also destroyed MainView's @StateObject TranscriptionViewModel. Reopening started a fresh, idle session next to an overlay that could still be bound to the old one.

Changes

  • New AppCoordinator (macos/LCTMac/App/AppCoordinator.swift), owned by AppDelegate so it lives for the whole run:
    • owns the single TranscriptionViewModel, created lazily after onboarding so it still loads the settings onboarding saved, plus the OverlayWindowController;
    • is the one observer of .toggleCapture, .togglePause, .toggleOverlay and .settingsDidChange;
    • for a capture command while the main window is closed or minimized, it activates the app and reopens the window, then delivers the command through the unchanged start() / stop() / toggleCapture() state machine;
    • when the window is already on screen, nothing steals focus. The hotkey still works from other apps, as before;
    • during onboarding, a capture command only brings the window (the Welcome screen) forward.
  • WindowGroup → single-instance Window("LCT", id: "main"). Closing a Window only hides it. SwiftUI's openWindow(id:) reopens the same window, is idempotent when it's already open, and restores it when minimized. Before this, openWindow on a WindowGroup always spawned a new window, and ⌘N could create several MainViews that each started their own capture. MainWindowCommands captures openWindow from the Commands environment so AppKit callers (status bar, hotkey) can use it.
  • MainView takes the view model as @ObservedObject and handles only window-scoped commands (Copy Translation, Show History).
  • Model prewarm (prepareModelOnLaunch) runs once when the coordinator creates the view model, not on every window appearance. Reopening the window mid-capture no longer re-warms the model.
  • The status bar's Show Main Window now uses the coordinator. It used to pick NSApp.windows.first, which after a close isn't the main window.

Pause, overlay and settings-change moved along with start/stop because they target the same view model, which now outlives the window. Without the move, the ⌃⌥⌘P / ⌃⌥⌘O hotkeys would also do nothing with the window closed. Settings saved from the ⌘, window while the main window was closed would also be lost, because the old code picked them up by rebuilding the view model on reopen.

Test plan

  • New AppCoordinatorTests (10 tests):
    • a .toggleCapture notification with no window present is delivered;
    • order is show window, then start / stop / toggle;
    • no window activation when the window is visible;
    • onboarding is respected;
    • the view model survives close and reopen;
    • pause and settings changes reach the view model with the window closed.
  • Mutation check: with the coordinator's .toggleCapture subscription removed, the two notification tests fail.
  • cd macos && ./Scripts/run_tests.sh after merging current master (feat(macos): capture system audio with a Core Audio tap — no screen recording permission #16): debug, release and -warnings-as-errors builds pass, and swift test passes all 454 tests with 0 failures.
  • Manual check (signed build from dev-run.sh, 2026-10-10), log lines from ~/Library/Logs/LCTMac.log:
    • Window closed → Window ▸ Start/Stop Capture: [AppCoordinator] Capture command toggle — main window not visible, showing it, then ▶️ start() called; the window came back and captions appeared.
    • Window closed during capture: capture kept running and new lines were recognized and translated while the window was closed.
    • Window closed → Window ▸ Start/Stop Capture again: window came back, capture stopped, and every earlier line was still there (the view model survived the close/reopen).
    • File ▸ Export as Markdown… with the window closed: window came back, the save panel opened, and the file was written.
    • The File menu no longer offers New Window.

Merged with master (P2, #17–#20)

  • Resolved the one conflict in MainView: the coordinator keeps the start/stop, pause and settings-change observers; master's refreshLanguageAvailability() stays in MainView's .task (the model prewarm already runs once in AppCoordinator.makeAppViewModel).
  • Follow-up fix: P2's File ▸ Export observer also lived in MainView, so ⌘E did nothing after the window was closed, even though the session now outlives the window. Moved it to the coordinator. Like the capture commands, it brings the window back first so the result notice is visible. Added 3 tests to AppCoordinatorTests for this.
  • ./Scripts/run_tests.sh after the merge: debug, release and -warnings-as-errors builds pass, and swift test passes all 522 tests with 0 failures. No new warnings, including the build without DEVELOPER_DIR.

🤖 Generated with Claude Code

Start/Stop Capture from the Window menu, the status bar item and the
⌃⌥⌘S hotkey only posted .toggleCapture, whose sole observer was
MainView. After the user closed the window (the app keeps running in
the menu bar) nothing observed it and the command was silently dropped.
Closing the window also destroyed MainView's @StateObject view model,
so reopening started a fresh session next to any overlay still bound
to the old one.

Move ownership to an app-lifetime AppCoordinator on the app delegate:
it creates the single TranscriptionViewModel (lazily, after
onboarding) and the overlay controller, and observes the capture,
pause, overlay and settings-change notifications. A capture command
that arrives while the main window is closed or minimized first
activates the app and reopens the window, then goes through the
unchanged start()/stop()/toggleCapture() state machine; with the
window on screen nothing steals focus, as before.

The main scene becomes a single-instance Window so closing only hides
it and SwiftUI's openWindow (captured from a Commands environment)
reopens the same window from AppKit callers. Model prewarm now runs
once per view model instead of on every window appearance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 09:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Resolve the AppDelegate conflict: keep master's removal of the startup
screen-recording preflight and route the launch-time window activation
through AppCoordinator.showMainWindow().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PCcoding666
PCcoding666 force-pushed the fix/macos-capture-commands-window-closed branch from 5f94b3e to 25ce079 Compare October 10, 2026 10:37
PCcoding666 and others added 2 commits October 11, 2026 11:52
Resolve the MainView conflict with P2: keep the coordinator owning the
start/stop, pause and settings-change observers, and keep master's
refreshLanguageAvailability() in MainView's .task (the model prewarm
already runs once in AppCoordinator.makeAppViewModel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session now outlives the main window (AppCoordinator owns the view
model), but the .exportSession observer still lived in MainView, so
File ▸ Export (⌘E) did nothing after the window was closed. Move it to
the coordinator alongside the other app-level commands: it brings the
window back so the result notice is visible, then exports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PCcoding666
PCcoding666 merged commit 0effe2d into master Oct 11, 2026
2 checks passed
@PCcoding666
PCcoding666 deleted the fix/macos-capture-commands-window-closed branch October 11, 2026 03:07
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