fix: keep macOS overlay interactive while mpv remains active

- Overlay no longer hides or becomes click-through during tracker refreshes when mpv is the focused window
- Preserve already-visible overlay when tracker is temporarily not ready but mpv target signal is active
- Add regression tests for active-mpv tracker refresh and transient tracker-not-ready paths
This commit is contained in:
2026-05-11 02:35:05 -07:00
parent 47499eccff
commit eff33e2027
4 changed files with 193 additions and 6 deletions
@@ -0,0 +1,74 @@
---
id: TASK-344
title: Fix macOS overlay tracker hiding while mpv remains active
status: Done
assignee:
- codex
created_date: '2026-05-11 08:27'
updated_date: '2026-05-11 08:41'
labels:
- bug
- macos
- overlay
dependencies: []
references:
- src/main/runtime/overlay-visibility-runtime.ts
- src/window-trackers
modified_files:
- src/core/services/overlay-visibility.ts
- src/core/services/overlay-visibility.test.ts
- changes/344-macos-overlay-active-mpv.md
priority: high
ordinal: 182500
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
macOS playback overlay should match Windows behavior: the tracker may only hide or alter overlay layering when mpv is no longer the active playback window. When mpv remains topmost or fullscreen, the visible overlay must stay present and interactive unless the user manually hides it or minimizes mpv.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 With mpv active on macOS, window-tracker updates do not hide the visible overlay or make it click-through.
- [x] #2 With mpv fullscreen on macOS, tracker geometry/layering refreshes preserve overlay interactivity.
- [x] #3 Overlay visibility still changes when mpv is no longer active, and manual hide/minimize behavior remains intact.
- [x] #4 A regression test covers the macOS active-mpv path that previously produced an overlay loading OSD and non-interactive overlay.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused regression in `src/core/services/overlay-visibility.test.ts` for macOS with mpv tracked/focused and retained geometry: visibility refresh must not hide the overlay, must not emit loading OSD, and must leave the overlay interactive (`setIgnoreMouseEvents(false)`) unless forced passthrough is active.
2. Update `src/core/services/overlay-visibility.ts` so macOS tracker refreshes preserve the visible overlay while mpv is active/fullscreen, and only hide/re-layer for explicit manual hide, minimized/untracked target, or non-active mpv cases.
3. Run the focused overlay visibility tests, then a broader fast gate if the focused fix is green.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented in the shared overlay visibility service. macOS now keeps tracked/focused mpv overlays interactive instead of defaulting to mouse passthrough, and preserves an already visible active-mpv overlay during temporary tracker-not-ready refreshes without showing the loading OSD. Forced passthrough, modal hide, manual hide, Windows minimized handling, and initial macOS tracker-not-ready startup behavior remain covered by tests.
Verification: `bun test src/core/services/overlay-visibility.test.ts` passed; affected overlay/mouse/runtime group passed; `bun run typecheck`, `bun run changelog:lint`, `bun run build`, `bun run test:env`, and `bun run test:smoke:dist` passed. `bun run test:fast` is blocked by existing cross-file test pollution: `src/core/services/subsync.test.ts` passes alone, but fails when run after `src/renderer/handlers/keyboard.test.ts` because `window.electronAPI` is undefined in a lingering keyboard handler; Bun then reports nested node:test errors for later files. `bun run format:check:src` is blocked by pre-existing formatting drift in `src/core/services/stats-window.ts`; touched files pass direct Prettier check.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Updated macOS overlay visibility logic so a tracked/focused mpv window keeps the visible overlay interactive instead of click-through.
- Preserved an already visible active-mpv overlay during temporary macOS tracker-not-ready refreshes, avoiding the loading OSD/hide path for that active playback case.
- Added regression coverage for active mpv tracker refreshes and transient tracker-not-ready refreshes, plus updated old macOS expectations to the new active-mpv contract.
- Added a changelog fragment for the user-visible overlay fix.
Verification:
- Passed: `bun test src/core/services/overlay-visibility.test.ts`
- Passed: `bun test src/core/services/overlay-visibility.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/overlay-shortcut-handler.test.ts src/renderer/handlers/mouse.test.ts`
- Passed: `bun run typecheck`
- Passed: `bun run changelog:lint`
- Passed: `bun run build`
- Passed: `bun run test:env`
- Passed: `bun run test:smoke:dist`
- Blocked: `bun run test:fast` by existing keyboard/subsync cross-file global pollution; isolated `bun test src/core/services/subsync.test.ts` passes.
- Blocked: `bun run format:check:src` by pre-existing formatting drift in `src/core/services/stats-window.ts`; touched files pass direct Prettier check.
<!-- SECTION:FINAL_SUMMARY:END -->
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Overlay: Kept the macOS overlay visible and interactive while mpv remains the active tracked window, including transient tracker refreshes.
+105 -3
View File
@@ -883,7 +883,7 @@ test('visible overlay stays hidden while a modal window is active', () => {
assert.ok(!calls.includes('update-bounds'));
});
test('macOS tracked visible overlay stays click-through without passively stealing focus', () => {
test('macOS tracked visible overlay stays interactive without passively stealing focus', () => {
const { window, calls } = createMainWindowRecorder();
const tracker: WindowTrackerStub = {
isTracking: () => true,
@@ -915,11 +915,113 @@ test('macOS tracked visible overlay stays click-through without passively steali
isWindowsPlatform: false,
} as never);
assert.ok(calls.includes('mouse-ignore:true:forward'));
assert.ok(calls.includes('mouse-ignore:false:plain'));
assert.ok(calls.includes('show'));
assert.ok(!calls.includes('focus'));
});
test('macOS keeps active mpv overlay visible and interactive during tracker refresh', () => {
const { window, calls } = createMainWindowRecorder();
const osdMessages: string[] = [];
const tracker: WindowTrackerStub = {
isTracking: () => true,
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
isTargetWindowFocused: () => true,
};
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
mainWindow: window as never,
windowTracker: tracker as never,
trackerNotReadyWarningShown: false,
setTrackerNotReadyWarningShown: () => {
calls.push('tracker-warning');
},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
isWindowsPlatform: false,
showOverlayLoadingOsd: (message: string) => {
osdMessages.push(message);
},
} as never);
assert.ok(calls.includes('update-bounds'));
assert.ok(calls.includes('sync-layer'));
assert.ok(calls.includes('mouse-ignore:false:plain'));
assert.ok(calls.includes('ensure-level'));
assert.ok(calls.includes('enforce-order'));
assert.ok(calls.includes('sync-shortcuts'));
assert.ok(!calls.includes('hide'));
assert.deepEqual(osdMessages, []);
});
test('macOS preserves an already visible active mpv overlay while tracker is temporarily not ready', () => {
const { window, calls } = createMainWindowRecorder();
const osdMessages: string[] = [];
let trackerWarning = false;
const tracker: WindowTrackerStub = {
isTracking: () => false,
getGeometry: () => null,
isTargetWindowFocused: () => true,
};
window.show();
calls.length = 0;
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
mainWindow: window as never,
windowTracker: tracker as never,
trackerNotReadyWarningShown: trackerWarning,
setTrackerNotReadyWarningShown: (shown: boolean) => {
trackerWarning = shown;
calls.push(`tracker-warning:${shown}`);
},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
isWindowsPlatform: false,
showOverlayLoadingOsd: (message: string) => {
osdMessages.push(message);
},
} as never);
assert.equal(trackerWarning, false);
assert.ok(calls.includes('sync-layer'));
assert.ok(calls.includes('mouse-ignore:false:plain'));
assert.ok(calls.includes('ensure-level'));
assert.ok(calls.includes('sync-shortcuts'));
assert.ok(!calls.includes('hide'));
assert.deepEqual(osdMessages, []);
});
test('forced mouse passthrough keeps macOS tracked overlay passive while visible', () => {
const { window, calls } = createMainWindowRecorder();
const tracker: WindowTrackerStub = {
@@ -1243,7 +1345,7 @@ test('macOS preserves visible overlay during transient tracker loss with retaine
assert.deepEqual(osdMessages, []);
assert.ok(calls.includes('update-bounds'));
assert.ok(calls.includes('sync-layer'));
assert.ok(calls.includes('mouse-ignore:true:forward'));
assert.ok(calls.includes('mouse-ignore:false:plain'));
assert.ok(calls.includes('ensure-level'));
assert.ok(calls.includes('enforce-order'));
assert.ok(calls.includes('sync-shortcuts'));
+10 -3
View File
@@ -92,10 +92,14 @@ export function updateVisibleOverlayVisibility(args: {
const showPassiveVisibleOverlay = (): void => {
const forceMousePassthrough = args.forceMousePassthrough === true;
const wasVisible = mainWindow.isVisible();
const shouldDefaultToPassthrough =
args.isMacOSPlatform || args.isWindowsPlatform || forceMousePassthrough;
const isVisibleOverlayFocused =
typeof mainWindow.isFocused === 'function' && mainWindow.isFocused();
const shouldDefaultToPassthrough =
args.isWindowsPlatform ||
forceMousePassthrough ||
(args.isMacOSPlatform &&
!isVisibleOverlayFocused &&
!(args.windowTracker?.isTargetWindowFocused?.() ?? true));
const windowsForegroundProcessName =
args.lastKnownWindowsForegroundProcessName?.trim().toLowerCase() ?? null;
const windowsOverlayProcessName = args.windowsOverlayProcessName?.trim().toLowerCase() ?? null;
@@ -261,8 +265,11 @@ export function updateVisibleOverlayVisibility(args: {
}
const hasRetainedTrackedGeometry = args.windowTracker.getGeometry() !== null;
const hasActiveMacOSTargetSignal =
args.isMacOSPlatform && (args.windowTracker.isTargetWindowFocused?.() ?? false);
const shouldPreserveTransientTrackedOverlay =
(args.isMacOSPlatform && hasRetainedTrackedGeometry) ||
(args.isMacOSPlatform &&
(hasRetainedTrackedGeometry || (mainWindow.isVisible() && hasActiveMacOSTargetSignal))) ||
(args.isWindowsPlatform &&
typeof args.windowTracker.isTargetWindowMinimized === 'function' &&
!args.windowTracker.isTargetWindowMinimized());