Lifecycle
Events tell you a surface did something. Lifecycle lets your surface act before it closes — most importantly, prevent the close. The canonical case: a file editor that refuses to close while the file is dirty and shows its own Save / Don't Save / Cancel dialog instead.
muxy.lifecycle is available on tab, panel, popover, and webview modal pages. There is no manifest field and no permission to declare: registering a handler is the opt-in.
Intercepting a close
Register a handler from the surface's own page. Before Muxy closes that surface, it calls your handler and waits for the verdict.
muxy.lifecycle.onBeforeClose(async () => {
if (!isDirty) return false; // allow the close
const choice = await muxy.dialog.confirm({
title: 'Unsaved changes',
message: 'Save before closing?',
buttons: ['Save', "Don't Save", 'Cancel'],
});
if (choice === 'Cancel') return true; // PREVENT the close
if (choice === 'Save') await save();
return false; // allow — the close proceeds
});
- Return (or resolve)
true— or{ prevent: true }— to prevent the close. - Return anything else (
false,undefined, …) to allow it. - The handler may be sync, return a Promise, or be
async(so you canawaityour own dialog or a save). onBeforeClosereturns an unsubscribe function. Registering again replaces the handler — there is one per surface.
The handler receives a small context: { surface: 'tab' | 'panel' | 'popover' | 'modalWebview', instanceID }.
Closing yourself
When your handler has decided the close should happen (the user picked "Save" or "Don't Save"), finish it with:
muxy.lifecycle.close();
This closes this surface and bypasses the veto — it will not ask onBeforeClose again, so there's no loop. Use it instead of returning false when you want to drive the close yourself after your own UI.
Guarantees
- Fail-open. If you register no handler, your handler throws, or the page never responds, the close proceeds. A surface can never wedge the close button. A page that has a handler is given a few seconds to acknowledge the request; once it does, it may take as long as it needs (e.g. while a human reads a Save / Don't Save dialog) — the close waits for the verdict.
- Scoped to closes that include your surface. A handler runs only when its own surface is being closed. Closing a top-level tab also closes every split-child tab it owns, so Muxy asks the parent and child surfaces together; a veto from any of them cancels the entire hierarchy close and leaves every sibling open. Direct closes of unrelated tabs, panels, popovers, and modals are unaffected.
- Bulk closes ask in parallel. "Close Other Tabs" (and similar) ask every affected surface at once — you get one round of prompts, not a queue of blocking dialogs.
Limits
- Quitting the app and closing a window skip the veto (the app's own quit confirmation governs there) — they only emit the observation events. Don't rely on
onBeforeCloseto guard against quit; persist on a timer or ontab.focused/blur instead. - Toggling a panel or popover closed is a show/hide, not a close — it bypasses the veto.
onBeforeClosefires for genuine close intents: the surface's close button, the programmaticmuxy.panels.close()/muxy.popover.close()/muxy.panes.close(), and closing a tab. A topbar/command toggle that hides the surface does not ask, andmuxy.lifecycle.close()deliberately bypasses it. - Switching projects force-closes extension panels for the project you leave (no veto) and recreates that project's saved set when you return. Observation events still fire (
panel.closed/panel.opened). See Panels — Per-project session. - A popover dismissed by clicking outside it cannot be vetoed — macOS has already torn it down. You still get
popover.closed.
Observing closes
To merely react after a close (not prevent it), subscribe to the observation events — tab.closed, panel.closed, popover.closed, and the open counterparts — see Events. Those fire after the surface is gone; a prevented close emits nothing.