diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 75ea081..cf26d2b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -340,43 +340,52 @@ invoke("prewarm_default_model_cmd").catch(() => {}); } - // Meeting auto-capture: poll the process list and toast when a match - // appears (edge-triggered — no repeat toasts until the app goes away - // and comes back). We never start recording from this signal; the - // user decides whether to hit the hotkey. - if (tauriRuntimeAvailable) { - let previous: Set = new Set(); - meetingCapturePoller = window.setInterval(async () => { - if (!settings.meetingAutoCapture) { previous = new Set(); return; } - const patterns = settings.meetingAutoCaptureApps; - if (!Array.isArray(patterns) || patterns.length === 0) return; - try { - const matches: string[] = await invoke("detect_meeting_processes", { patterns }); - const current = new Set(matches); - for (const match of matches) { - if (!previous.has(match)) { - toasts.info( - `${match[0].toUpperCase()}${match.slice(1)} detected`, - `Press ${settings.globalHotkey} to start recording.`, - ); - } - } - previous = current; - } catch { /* ignore — backend may be mid-restart */ } - }, 15000); - } + // Meeting auto-capture is wired up below as a `$effect` so the + // 15-second poller only exists while the setting is enabled — + // toggling off in Settings stops the wakeups, not just the work. }); - let meetingCapturePoller: number | null = null; + // Meeting auto-capture poller. Edge-triggered: the first time a matching + // process appears we surface a non-modal toast; subsequent ticks where + // the same match is still present stay quiet until the app exits and + // returns. We never start recording from this signal — the user decides. + // + // The effect's only tracked dependencies are `tauriRuntimeAvailable` + // (constant after mount) and `settings.meetingAutoCapture`. Reads of + // `settings.meetingAutoCaptureApps` and `settings.globalHotkey` happen + // inside the interval callback, which runs after the synchronous setup + // and therefore is not tracked — editing the apps list doesn't tear + // down the poller. + $effect(() => { + if (!tauriRuntimeAvailable) return; + if (!settings.meetingAutoCapture) return; + + let previous: Set = new Set(); + const id = window.setInterval(async () => { + const patterns = settings.meetingAutoCaptureApps; + if (!Array.isArray(patterns) || patterns.length === 0) return; + try { + const matches: string[] = await invoke("detect_meeting_processes", { patterns }); + const current = new Set(matches); + for (const match of matches) { + if (!previous.has(match)) { + toasts.info( + `${match[0].toUpperCase()}${match.slice(1)} detected`, + `Press ${settings.globalHotkey} to start recording.`, + ); + } + } + previous = current; + } catch { /* ignore — backend may be mid-restart */ } + }, 15000); + + return () => { window.clearInterval(id); }; + }); onDestroy(() => { window.removeEventListener("resize", handleResize); if (onWindowError) window.removeEventListener("error", onWindowError); if (onUnhandledRejection) window.removeEventListener("unhandledrejection", onUnhandledRejection); - if (meetingCapturePoller !== null) { - window.clearInterval(meetingCapturePoller); - meetingCapturePoller = null; - } if (!tauriRuntimeAvailable) { return; }