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:
@@ -1518,6 +1518,28 @@
|
||||
label="Auto-paste into focused window"
|
||||
description={pasteBackendsDescription}
|
||||
/>
|
||||
<Toggle
|
||||
bind:checked={settings.meetingAutoCapture}
|
||||
label="Remind me when a meeting starts"
|
||||
description="Toasts when a matching app appears in the process list. You still hit the hotkey — Kon never records on its own."
|
||||
/>
|
||||
{#if settings.meetingAutoCapture}
|
||||
<div class="ml-[50px] mt-2 mb-1 animate-fade-in">
|
||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-1.5">Apps to watch (comma-separated)</p>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full bg-bg-input border border-border-subtle rounded-lg px-3 py-1.5 text-[12px] text-text"
|
||||
value={settings.meetingAutoCaptureApps.join(", ")}
|
||||
oninput={(event) => {
|
||||
const raw = event.currentTarget.value;
|
||||
settings.meetingAutoCaptureApps = raw
|
||||
.split(",")
|
||||
.map((entry) => entry.trim().toLowerCase())
|
||||
.filter((entry) => entry.length > 0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Toggle bind:checked={settings.includeTimestamps} label="Include timestamps in exports" />
|
||||
<Toggle
|
||||
bind:checked={settings.saveAudio}
|
||||
|
||||
@@ -47,6 +47,8 @@ const defaults: SettingsState = {
|
||||
britishEnglish: true,
|
||||
autoCopy: true,
|
||||
autoPaste: false,
|
||||
meetingAutoCapture: false,
|
||||
meetingAutoCaptureApps: ["zoom", "teams"],
|
||||
includeTimestamps: true,
|
||||
theme: "Dark",
|
||||
fontSize: 14,
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface SettingsState {
|
||||
britishEnglish: boolean;
|
||||
autoCopy: boolean;
|
||||
autoPaste: boolean;
|
||||
meetingAutoCapture: boolean;
|
||||
meetingAutoCaptureApps: string[];
|
||||
includeTimestamps: boolean;
|
||||
theme: "Dark" | "Light" | "System";
|
||||
fontSize: number;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user