{
  "title": "Voice Wake & Push-to-Talk",
  "content": "* **Wake-word mode** (default): always-on Speech recognizer waits for trigger tokens (`swabbleTriggerWords`). On match it starts capture, shows the overlay with partial text, and auto-sends after silence.\n* **Push-to-talk (Right Option hold)**: hold the right Option key to capture immediately—no trigger needed. The overlay appears while held; releasing finalizes and forwards after a short delay so you can tweak text.\n\n## Runtime behavior (wake-word)\n\n* Speech recognizer lives in `VoiceWakeRuntime`.\n* Trigger only fires when there’s a **meaningful pause** between the wake word and the next word (\\~0.55s gap). The overlay/chime can start on the pause even before the command begins.\n* Silence windows: 2.0s when speech is flowing, 5.0s if only the trigger was heard.\n* Hard stop: 120s to prevent runaway sessions.\n* Debounce between sessions: 350ms.\n* Overlay is driven via `VoiceWakeOverlayController` with committed/volatile coloring.\n* After send, recognizer restarts cleanly to listen for the next trigger.\n\n## Lifecycle invariants\n\n* If Voice Wake is enabled and permissions are granted, the wake-word recognizer should be listening (except during an explicit push-to-talk capture).\n* Overlay visibility (including manual dismiss via the X button) must never prevent the recognizer from resuming.\n\n## Sticky overlay failure mode (previous)\n\nPreviously, if the overlay got stuck visible and you manually closed it, Voice Wake could appear “dead” because the runtime’s restart attempt could be blocked by overlay visibility and no subsequent restart was scheduled.\n\n* Wake runtime restart is no longer blocked by overlay visibility.\n* Overlay dismiss completion triggers a `VoiceWakeRuntime.refresh(...)` via `VoiceSessionCoordinator`, so manual X-dismiss always resumes listening.\n\n## Push-to-talk specifics\n\n* Hotkey detection uses a global `.flagsChanged` monitor for **right Option** (`keyCode 61` + `.option`). We only observe events (no swallowing).\n* Capture pipeline lives in `VoicePushToTalk`: starts Speech immediately, streams partials to the overlay, and calls `VoiceWakeForwarder` on release.\n* When push-to-talk starts we pause the wake-word runtime to avoid dueling audio taps; it restarts automatically after release.\n* Permissions: requires Microphone + Speech; seeing events needs Accessibility/Input Monitoring approval.\n* External keyboards: some may not expose right Option as expected—offer a fallback shortcut if users report misses.\n\n## User-facing settings\n\n* **Voice Wake** toggle: enables wake-word runtime.\n* **Hold Cmd+Fn to talk**: enables the push-to-talk monitor. Disabled on macOS \\< 26.\n* Language & mic pickers, live level meter, trigger-word table, tester (local-only; does not forward).\n* Mic picker preserves the last selection if a device disconnects, shows a disconnected hint, and temporarily falls back to the system default until it returns.\n* **Sounds**: chimes on trigger detect and on send; defaults to the macOS “Glass” system sound. You can pick any `NSSound`-loadable file (e.g. MP3/WAV/AIFF) for each event or choose **No Sound**.\n\n## Forwarding behavior\n\n* When Voice Wake is enabled, transcripts are forwarded to the active gateway/agent (the same local vs remote mode used by the rest of the mac app).\n* Replies are delivered to the **last-used main provider** (WhatsApp/Telegram/Discord/WebChat). If delivery fails, the error is logged and the run is still visible via WebChat/session logs.\n\n## Forwarding payload\n\n* `VoiceWakeForwarder.prefixedTranscript(_:)` prepends the machine hint before sending. Shared between wake-word and push-to-talk paths.\n\n## Quick verification\n\n* Toggle push-to-talk on, hold Cmd+Fn, speak, release: overlay should show partials then send.\n* While holding, menu-bar ears should stay enlarged (uses `triggerVoiceEars(ttl:nil)`); they drop after release.",
  "code_samples": [],
  "headings": [
    {
      "level": "h2",
      "text": "Modes",
      "id": "modes"
    },
    {
      "level": "h2",
      "text": "Runtime behavior (wake-word)",
      "id": "runtime-behavior-(wake-word)"
    },
    {
      "level": "h2",
      "text": "Lifecycle invariants",
      "id": "lifecycle-invariants"
    },
    {
      "level": "h2",
      "text": "Sticky overlay failure mode (previous)",
      "id": "sticky-overlay-failure-mode-(previous)"
    },
    {
      "level": "h2",
      "text": "Push-to-talk specifics",
      "id": "push-to-talk-specifics"
    },
    {
      "level": "h2",
      "text": "User-facing settings",
      "id": "user-facing-settings"
    },
    {
      "level": "h2",
      "text": "Forwarding behavior",
      "id": "forwarding-behavior"
    },
    {
      "level": "h2",
      "text": "Forwarding payload",
      "id": "forwarding-payload"
    },
    {
      "level": "h2",
      "text": "Quick verification",
      "id": "quick-verification"
    }
  ],
  "url": "llms-txt#voice-wake-&-push-to-talk",
  "links": []
}