feat(meeting): opt-in process-list reminder when a meeting app starts

Default off. When on, the layout polls detect_meeting_processes every 15s
with the user's app-name patterns. On a fresh match (edge-triggered — no
re-toast until the app goes away and comes back) we fire a reminder toast
that tells the user which meeting app appeared and their global hotkey. We
never start recording on this signal; the ideology rule says the user
decides. The signal is a single channel: process list match only — no mic
activity heuristic, no calendar.

Backend adds kon_core::process_watch::{list_running_process_names,
match_meeting_patterns} over sysinfo, exposed to the frontend as the
detect_meeting_processes Tauri command.

Settings ships two new fields — meetingAutoCapture (bool) and
meetingAutoCaptureApps (string[]) — with a comma-separated input in the
Output section. Default app list is ["zoom", "teams"], user-editable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 07:54:55 +01:00
parent 63e00c15b1
commit ba0d59f563
9 changed files with 164 additions and 0 deletions

View File

@@ -287,12 +287,44 @@
}
})
.catch(() => { /* update check failure must not affect the app */ });
// 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<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;
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;
}