Skip to content

Add meeting app exclusions and a Settings window - #16

Merged
TerrifiedBug merged 1 commit into
mainfrom
feat/meeting-exclusions-settings-window
Aug 31, 2026
Merged

Add meeting app exclusions and a Settings window#16
TerrifiedBug merged 1 commit into
mainfrom
feat/meeting-exclusions-settings-window

Conversation

@TerrifiedBug

Copy link
Copy Markdown
Owner

What

Meeting detection prompts for any external app on the mic. That default stays — an allowlist silently misses meeting apps nobody has heard of — but there is now a way to say "not this one, ever":

  • "Ignore " on the meeting prompt, "Ignore" on the auto-record toast. Clicking it adds the app to meeting_excluded_apps, ends any recording that toast just started, and yap says nothing about that app again.
  • Meetings → Ignored apps in a new Settings window: each app by icon and name, a minus button to remove one, and "Add App…" to exclude one ahead of time.

Excluded apps are invisible to every part of meeting logic — the check runs first in onMeetingStart, before the accessibility title lookup and before the back-to-back-session stop — not merely unprompted.

The list stores bundle identifiers, not names or paths: stable across renames and localization, and resolvable back to an icon and a name through NSWorkspace. A capture process with no .app around it has no identity to store, so it gets no Ignore button; Dismiss still covers it.

Settings window

"Edit config…" becomes "Settings…", a lazily-created window covering every config key, with "Open Config File" preserved at the bottom. config.json stays the single store: the window is a GUI over that exact file, and the existing ConfigWatcher turns each change into a live reload — no Apply button, no restart.

Config grows the write path this needs: update(_:) plus serialized(_:), which emits the file in template key order and round-trips keys yap has never heard of, at both levels. A malformed file is left untouched rather than clobbered — somebody may be mid-edit.

The dropdown gets a header card (mark, state line, model id) in place of two disabled text lines, plus SF Symbols and Title Case on the actions.

The one rule

No new dependencies (SwiftUI is a system framework), no new processes, nothing new running while idle — the window is not built until the menu item is first clicked, and the exclusion check runs inside onMeetingStart, which is event-driven, not the once-a-second detector poll. None of this touches AudioCapture, TextInjector or the placement path, so the latency table is untouched; an excluded app now costs strictly less than one that prompts, because it returns before the AX title lookup.

Verified

Local CI gate (swift test, swift build -c release --arch arm64, the three smoke subcommands, embedded __info_plist) green, then a signed binary run in the foreground on an M4:

  • Backfill — an old config gains "meeting_excluded_apps": [] on start, values untouched.
  • Settings round-trip — toggling "Tap to toggle" writes "tap_to_toggle": true in template key order with unrelated keys preserved, stderr logs config reloaded, and the menu header flips to idle · tap fn to dictate.
  • Ignore from the prompt — Chrome holding the mic gives Dismiss · Ignore Google Chrome · Record; clicking Ignore writes ["com.google.Chrome"] and logs ◇ Google Chrome added to the ignore list. The next capture logs ◇ Google Chrome is excluded — ignoring and shows no prompt.
  • Settings list — the row renders with Chrome's icon and name; the minus button empties the list and the next capture prompts again.
  • Auto-record toast IgnoreStop · Ignore; Ignore adds the exclusion and stops the session it just started (○ stopped · 0:01).
  • Menu — header card renders yap / ● recording · 0:02 / model id live, Stop Recording · 0:02 swaps record.circle for stop.circle, Settings… opens in front from an .accessory app.
  • Settings window — opens with Dictation, Recording, Meetings and Open Config File all on screen without scrolling, and is resizable.

New unit tests cover Config.serialized(_:): template round-trip with template key order, and unknown keys surviving at both levels.

Meeting detection prompts for any external app on the mic, which is the
right default — an allowlist misses meeting apps nobody has heard of. But
there was no way to tell it "not this one, ever". Now there is: an
"Ignore <App>" button on the meeting prompt, an "Ignore" on the
auto-record toast, and a manageable list of them under Meetings in a new
Settings window. Excluded apps are invisible to every part of meeting
logic, not merely unprompted.

The list stores bundle identifiers rather than names or paths: stable
across renames and localization, and resolvable back to an icon and a
name through NSWorkspace. A capture process with no .app around it has no
identity to store, so it gets no Ignore button — Dismiss still covers it.

"Edit config…" becomes "Settings…", a lazily-created window covering
every config key, with "Open Config File" preserved at the bottom.
config.json stays the single store; the window is a GUI over it and the
existing watcher turns each change into a live reload. Config grows a
write path for this — update() plus a serializer that emits the file in
template key order and round-trips keys yap has never heard of.

The dropdown itself gets a header card (mark, state line, model) in place
of two disabled text lines, and SF Symbols on the actions.

Measured: none of this touches AudioCapture, TextInjector or the
placement path. The exclusion check runs inside onMeetingStart, which is
event-driven, not the once-a-second detector poll, and it runs before the
accessibility title lookup — so an excluded app costs strictly less than
one that prompts.
@TerrifiedBug
TerrifiedBug merged commit c0dad12 into main Aug 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant