fix(plugins): ignore stale plugin panel close notices

The overlay's modal-closed notice now carries the panel id, and the
runtime only finishes the active panel when the ids match. A late notice
for a replaced panel no longer aborts the newer one.
This commit is contained in:
sudacode committed 2026-10-26 22:08:35 -07:00
1 parent 0f53b8beca
commit 7b92511f23
9 files changed
+46 -21

No files matched your search

+5 -3
View File
@@ -575,8 +575,8 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
const openedModals: unknown[] = []; const openedModals: unknown[] = [];
registerIpcHandlers( registerIpcHandlers(
{ {
onOverlayModalClosed: (modal) => { onOverlayModalClosed: (modal, _senderWindow, instanceId) => {
closedModals.push(modal); closedModals.push(instanceId === undefined ? modal : [modal, instanceId]);
}, },
onOverlayModalOpened: (modal) => { onOverlayModalOpened: (modal) => {
openedModals.push(modal); openedModals.push(modal);
@@ -637,7 +637,9 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal'); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal');
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync'); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync');
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'kiku'); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'kiku');
assert.deepEqual(closedModals, ['subsync', 'kiku']); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'plugin-panel', 4);
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'plugin-panel', 'bad');
assert.deepEqual(closedModals, ['subsync', 'kiku', ['plugin-panel', 4], 'plugin-panel']);
handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'bad'); handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'bad');
handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'subsync'); handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'subsync');
+16 -7
View File
@@ -50,6 +50,7 @@ export interface IpcServiceDeps {
onOverlayModalClosed: ( onOverlayModalClosed: (
modal: OverlayHostedModal, modal: OverlayHostedModal,
senderWindow: ElectronBrowserWindow | null, senderWindow: ElectronBrowserWindow | null,
instanceId?: number,
) => void; ) => void;
onOverlayModalOpened?: ( onOverlayModalOpened?: (
modal: OverlayHostedModal, modal: OverlayHostedModal,
@@ -317,6 +318,7 @@ export interface IpcDepsRuntimeOptions {
onOverlayModalClosed: ( onOverlayModalClosed: (
modal: OverlayHostedModal, modal: OverlayHostedModal,
senderWindow: ElectronBrowserWindow | null, senderWindow: ElectronBrowserWindow | null,
instanceId?: number,
) => void; ) => void;
onOverlayModalOpened?: ( onOverlayModalOpened?: (
modal: OverlayHostedModal, modal: OverlayHostedModal,
@@ -527,13 +529,20 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
}, },
); );
ipc.on(IPC_CHANNELS.command.overlayModalClosed, (event: unknown, modal: unknown) => { ipc.on(
const parsedModal = parseOverlayHostedModal(modal); IPC_CHANNELS.command.overlayModalClosed,
if (!parsedModal) return; (event: unknown, modal: unknown, instanceId: unknown) => {
const senderWindow = const parsedModal = parseOverlayHostedModal(modal);
electron.BrowserWindow?.fromWebContents((event as IpcMainEvent).sender) ?? null; if (!parsedModal) return;
deps.onOverlayModalClosed(parsedModal, senderWindow); const senderWindow =
}); electron.BrowserWindow?.fromWebContents((event as IpcMainEvent).sender) ?? null;
deps.onOverlayModalClosed(
parsedModal,
senderWindow,
Number.isInteger(instanceId) ? (instanceId as number) : undefined,
);
},
);
ipc.on(IPC_CHANNELS.command.overlayModalOpened, (event: unknown, modal: unknown) => { ipc.on(IPC_CHANNELS.command.overlayModalOpened, (event: unknown, modal: unknown) => {
const parsedModal = parseOverlayHostedModal(modal); const parsedModal = parseOverlayHostedModal(modal);
if (!parsedModal) return; if (!parsedModal) return;
+3 -3
View File
@@ -6024,12 +6024,12 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
return false; return false;
} }
}, },
onOverlayModalClosed: (modal, senderWindow) => { onOverlayModalClosed: (modal, senderWindow, instanceId) => {
if (modal === 'media-timing-review') { if (modal === 'media-timing-review') {
void mediaTimingReviewRuntime.dispose(); void mediaTimingReviewRuntime.dispose();
} }
if (modal === 'plugin-panel') { if (modal === 'plugin-panel' && instanceId !== undefined) {
pluginPanelRuntime.handleModalClosed(); pluginPanelRuntime.handleModalClosed(instanceId);
} }
if (modal === 'subtitle-sidebar' && senderWindow === overlayManager.getMainWindow()) { if (modal === 'subtitle-sidebar' && senderWindow === overlayManager.getMainWindow()) {
subtitleSidebarRequestedOpen = false; subtitleSidebarRequestedOpen = false;
+1 -1
View File
@@ -303,7 +303,7 @@ test('subtitle sidebar open state is restored for replacement visible overlay wi
/onOverlayModalOpened:\s*\(modal,\s*senderWindow\)\s*=>\s*\{(?<body>[\s\S]*?)\n \},/, /onOverlayModalOpened:\s*\(modal,\s*senderWindow\)\s*=>\s*\{(?<body>[\s\S]*?)\n \},/,
)?.groups?.body; )?.groups?.body;
const closedBlock = source.match( const closedBlock = source.match(
/onOverlayModalClosed:\s*\(modal,\s*senderWindow\)\s*=>\s*\{(?<body>[\s\S]*?)\n \},/, /onOverlayModalClosed:\s*\(modal,\s*senderWindow(?:,\s*instanceId)?\)\s*=>\s*\{(?<body>[\s\S]*?)\n \},/,
)?.groups?.body; )?.groups?.body;
const depsBlock = source.match(/getSubtitleSidebarOpen:\s*\(\)\s*=>\s*(?<body>[^\n,]+)/)?.groups const depsBlock = source.match(/getSubtitleSidebarOpen:\s*\(\)\s*=>\s*(?<body>[^\n,]+)/)?.groups
?.body; ?.body;
+12 -1
View File
@@ -44,7 +44,7 @@ test('closing from the overlay aborts the handle and ignores later updates', asy
await new Promise((resolve) => setImmediate(resolve)); await new Promise((resolve) => setImmediate(resolve));
const sentBefore = h.sent.length; const sentBefore = h.sent.length;
h.runtime.handleModalClosed(); h.runtime.handleModalClosed(1);
panel.update({ markdown: 'too late' }); panel.update({ markdown: 'too late' });
assert.equal(panel.signal.aborted, true); assert.equal(panel.signal.aborted, true);
@@ -52,6 +52,17 @@ test('closing from the overlay aborts the handle and ignores later updates', asy
assert.equal(h.sent.length, sentBefore); assert.equal(h.sent.length, sentBefore);
}); });
test('a late close acknowledgement for a replaced panel leaves the new panel open', () => {
const h = createHarness(Promise.resolve(true));
const first = h.runtime.open({ title: 'One', markdown: '' });
first.close();
const second = h.runtime.open({ title: 'Two', markdown: '' });
h.runtime.handleModalClosed(1);
assert.equal(second.signal.aborted, false);
});
test('opening a new panel aborts the previous one', () => { test('opening a new panel aborts the previous one', () => {
const h = createHarness(Promise.resolve(true)); const h = createHarness(Promise.resolve(true));
const first = h.runtime.open({ title: 'One', markdown: '' }); const first = h.runtime.open({ title: 'One', markdown: '' });
+3 -3
View File
@@ -103,9 +103,9 @@ export function createPluginPanelRuntime(deps: {
}; };
}, },
/** The user closed the panel in the overlay. */ /** The overlay closed `panelId`; a late notice for an already replaced panel is ignored. */
handleModalClosed(): void { handleModalClosed(panelId: number): void {
if (active) finish(active); if (active?.panelId === panelId) finish(active);
}, },
}; };
} }
+2 -2
View File
@@ -532,8 +532,8 @@ const electronAPI: ElectronAPI = {
mediaId, mediaId,
direction, direction,
), ),
notifyOverlayModalClosed: (modal) => { notifyOverlayModalClosed: (modal, instanceId) => {
ipcRenderer.send(IPC_CHANNELS.command.overlayModalClosed, modal); ipcRenderer.send(IPC_CHANNELS.command.overlayModalClosed, modal, instanceId);
}, },
notifyOverlayModalOpened: (modal) => { notifyOverlayModalOpened: (modal) => {
ipcRenderer.send(IPC_CHANNELS.command.overlayModalOpened, modal); ipcRenderer.send(IPC_CHANNELS.command.overlayModalOpened, modal);
+2 -1
View File
@@ -60,12 +60,13 @@ export function createPluginPanelModal(
function closePluginPanelModal(): void { function closePluginPanelModal(): void {
if (!ctx.state.pluginPanelModalOpen) return; if (!ctx.state.pluginPanelModalOpen) return;
const closedPanelId = panelId ?? undefined;
ctx.state.pluginPanelModalOpen = false; ctx.state.pluginPanelModalOpen = false;
panelId = null; panelId = null;
options.syncSettingsModalSubtitleSuppression(); options.syncSettingsModalSubtitleSuppression();
ctx.dom.pluginPanelModal.classList.add('hidden'); ctx.dom.pluginPanelModal.classList.add('hidden');
ctx.dom.pluginPanelModal.setAttribute('aria-hidden', 'true'); ctx.dom.pluginPanelModal.setAttribute('aria-hidden', 'true');
window.electronAPI.notifyOverlayModalClosed('plugin-panel'); window.electronAPI.notifyOverlayModalClosed('plugin-panel', closedPanelId);
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) { if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
ctx.dom.overlay.classList.remove('interactive'); ctx.dom.overlay.classList.remove('interactive');
} }
+2
View File
@@ -625,6 +625,8 @@ export interface ElectronAPI {
| 'character-dictionary' | 'character-dictionary'
| 'changelog' | 'changelog'
| 'plugin-panel', | 'plugin-panel',
/** Which instance closed, for modals that swap content in place (the plugin panel's id). */
instanceId?: number,
) => void; ) => void;
notifyOverlayModalOpened: ( notifyOverlayModalOpened: (
modal: modal: