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
+42 -17

No files matched your search

+5 -3
View File
@@ -575,8 +575,8 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
const openedModals: unknown[] = [];
registerIpcHandlers(
{
onOverlayModalClosed: (modal) => {
closedModals.push(modal);
onOverlayModalClosed: (modal, _senderWindow, instanceId) => {
closedModals.push(instanceId === undefined ? modal : [modal, instanceId]);
},
onOverlayModalOpened: (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)!({}, 'subsync');
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)!({}, 'subsync');
+12 -3
View File
@@ -50,6 +50,7 @@ export interface IpcServiceDeps {
onOverlayModalClosed: (
modal: OverlayHostedModal,
senderWindow: ElectronBrowserWindow | null,
instanceId?: number,
) => void;
onOverlayModalOpened?: (
modal: OverlayHostedModal,
@@ -317,6 +318,7 @@ export interface IpcDepsRuntimeOptions {
onOverlayModalClosed: (
modal: OverlayHostedModal,
senderWindow: ElectronBrowserWindow | null,
instanceId?: number,
) => void;
onOverlayModalOpened?: (
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(
IPC_CHANNELS.command.overlayModalClosed,
(event: unknown, modal: unknown, instanceId: unknown) => {
const parsedModal = parseOverlayHostedModal(modal);
if (!parsedModal) return;
const senderWindow =
electron.BrowserWindow?.fromWebContents((event as IpcMainEvent).sender) ?? null;
deps.onOverlayModalClosed(parsedModal, senderWindow);
});
deps.onOverlayModalClosed(
parsedModal,
senderWindow,
Number.isInteger(instanceId) ? (instanceId as number) : undefined,
);
},
);
ipc.on(IPC_CHANNELS.command.overlayModalOpened, (event: unknown, modal: unknown) => {
const parsedModal = parseOverlayHostedModal(modal);
if (!parsedModal) return;
+3 -3
View File
@@ -6024,12 +6024,12 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
return false;
}
},
onOverlayModalClosed: (modal, senderWindow) => {
onOverlayModalClosed: (modal, senderWindow, instanceId) => {
if (modal === 'media-timing-review') {
void mediaTimingReviewRuntime.dispose();
}
if (modal === 'plugin-panel') {
pluginPanelRuntime.handleModalClosed();
if (modal === 'plugin-panel' && instanceId !== undefined) {
pluginPanelRuntime.handleModalClosed(instanceId);
}
if (modal === 'subtitle-sidebar' && senderWindow === overlayManager.getMainWindow()) {
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 \},/,
)?.groups?.body;
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;
const depsBlock = source.match(/getSubtitleSidebarOpen:\s*\(\)\s*=>\s*(?<body>[^\n,]+)/)?.groups
?.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));
const sentBefore = h.sent.length;
h.runtime.handleModalClosed();
h.runtime.handleModalClosed(1);
panel.update({ markdown: 'too late' });
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);
});
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', () => {
const h = createHarness(Promise.resolve(true));
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. */
handleModalClosed(): void {
if (active) finish(active);
/** The overlay closed `panelId`; a late notice for an already replaced panel is ignored. */
handleModalClosed(panelId: number): void {
if (active?.panelId === panelId) finish(active);
},
};
}
+2 -2
View File
@@ -532,8 +532,8 @@ const electronAPI: ElectronAPI = {
mediaId,
direction,
),
notifyOverlayModalClosed: (modal) => {
ipcRenderer.send(IPC_CHANNELS.command.overlayModalClosed, modal);
notifyOverlayModalClosed: (modal, instanceId) => {
ipcRenderer.send(IPC_CHANNELS.command.overlayModalClosed, modal, instanceId);
},
notifyOverlayModalOpened: (modal) => {
ipcRenderer.send(IPC_CHANNELS.command.overlayModalOpened, modal);
+2 -1
View File
@@ -60,12 +60,13 @@ export function createPluginPanelModal(
function closePluginPanelModal(): void {
if (!ctx.state.pluginPanelModalOpen) return;
const closedPanelId = panelId ?? undefined;
ctx.state.pluginPanelModalOpen = false;
panelId = null;
options.syncSettingsModalSubtitleSuppression();
ctx.dom.pluginPanelModal.classList.add('hidden');
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()) {
ctx.dom.overlay.classList.remove('interactive');
}
+2
View File
@@ -625,6 +625,8 @@ export interface ElectronAPI {
| 'character-dictionary'
| 'changelog'
| 'plugin-panel',
/** Which instance closed, for modals that swap content in place (the plugin panel's id). */
instanceId?: number,
) => void;
notifyOverlayModalOpened: (
modal: