perf(meeting): only run process poller while auto-capture is enabled

Previously the 15-second meeting-detection setInterval was started in
onMount unconditionally (when Tauri runtime was available). When
`settings.meetingAutoCapture` was disabled the callback still fired every
15 s, just to early-return — burning a wakeup that did no useful work
and confusing "is this firing? did the toggle take effect?" debugging.

Move the timer into a `$effect` whose only tracked dependency is
`settings.meetingAutoCapture`. Toggling off now clears the interval; toggling
on creates a fresh one. Reads of `meetingAutoCaptureApps` and `globalHotkey`
happen inside the interval callback (post-setup) so they don't trigger the
effect to tear down on every keystroke in the apps editor.

The `meetingCapturePoller` variable and its `onDestroy` cleanup are gone —
the effect's own cleanup return takes care of it on unmount.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
This commit is contained in:
Claude
2026-04-25 09:37:05 +00:00
parent 41be27b410
commit fd48f55edb

View File

@@ -340,43 +340,52 @@
invoke("prewarm_default_model_cmd").catch(() => {}); invoke("prewarm_default_model_cmd").catch(() => {});
} }
// Meeting auto-capture: poll the process list and toast when a match // Meeting auto-capture is wired up below as a `$effect` so the
// appears (edge-triggered — no repeat toasts until the app goes away // 15-second poller only exists while the setting is enabled —
// and comes back). We never start recording from this signal; the // toggling off in Settings stops the wakeups, not just the work.
// user decides whether to hit the hotkey.
if (tauriRuntimeAvailable) {
let previous: Set<string> = 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);
}
}); });
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<string> = 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(() => { onDestroy(() => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
if (onWindowError) window.removeEventListener("error", onWindowError); if (onWindowError) window.removeEventListener("error", onWindowError);
if (onUnhandledRejection) window.removeEventListener("unhandledrejection", onUnhandledRejection); if (onUnhandledRejection) window.removeEventListener("unhandledrejection", onUnhandledRejection);
if (meetingCapturePoller !== null) {
window.clearInterval(meetingCapturePoller);
meetingCapturePoller = null;
}
if (!tauriRuntimeAvailable) { if (!tauriRuntimeAvailable) {
return; return;
} }