跳转至

Dictation

Hold the configured shortcut, speak, and release. Hex transcribes locally and inserts the result at the current focus. New captures do not cancel accepted work. Only Toggle mode has a cancel: its cancel shortcut, Escape by default, discards the recording (#786, #1022); every other intentional dictation finishes and pastes.

This map covers the macOS release path. Linux shares the basic journey, not every timing, input, or recovery guarantee; see the platform map. This is a map, not a verified end-to-end driver. Linked checks are locators; no tests or previews were run for this rewrite.

How To Get To It

Settings shortcut capture persists the candidate before showing Saved. If the write fails, capture remains active with an error; retry or cancel it explicitly. The GPUI regression that injected this failure went with GPUI; no Rune test injects a failed shortcut save yet.

Shortcut: Option by default, after macOS permissions and the selected local model are ready. Commands and OpenCode are not required. General > Dictation > Shortcut accepts modifier-only, modifier-plus-key, standalone Fn/Globe, and standalone function-key bindings. On Windows and Linux/X11 it also accepts mouse buttons, and on macOS with Labs › Mouse shortcuts on (off by default; issue 209; see labs.md), alone or with modifiers: click Record, then press the desired mouse button. Mouse 1/2/3 are left/right/middle; Mouse 4 and higher are auxiliary buttons. Mouse 1 (left click) needs a modifier: alone it would swallow every click, Settings' own included, so the recorder answers "Add a modifier", the CLI refuses mouse1, and a saved bare left-click shortcut goes back to its default when settings load (issue 411; checks a_saved_bare_left_click_shortcut_returns_to_the_default_on_load and a_bare_left_click_is_refused_but_other_mouse_buttons_are_not, constructed settings only, no physical mouse run). Escape cancels recording a binding. Mouse bindings use the same hold, toggle, and double-tap rules and conflict checks as keyboard bindings. A consumed mouse press, drag and release do not reach the foreground app; ordinary clicks retain their existing behavior. Capturing a replacement suspends global matching. The CLI accepts the same bindings, for example mouse4 or option+mouse4. Windows supports the five mouse buttons exposed by its low-level mouse hook. Linux/X11 maps Mouse 4/5 to native buttons 8/9, skipping wheel events; the recorder temporarily grabs the pointer while its window has focus and releases it on save, cancellation, focus loss or window close. Windows waits for the click that opened the recorder to be released before accepting a mouse button.

Wayland's XDG shortcut syntax has no mouse-button trigger. Use the desktop's mouse bindings to run hex dictation start/stop (hold) or hex dictation toggle (toggle), or hex paste-last. Existing mouse bindings from X11 appear ungranted on Wayland; they do not prevent keyboard shortcuts from registering. Hex does not use raw input devices or XWayland to bypass the compositor. See the XDG shortcut specification.

Fixed November 1, 2026 (issue 846): the portal path refused navigation and editing keys ("unsupported shortcut key: home"), though GNOME's custom shortcuts took them. Both now share one named-key table (gnome_shortcuts::named_keysym): Home, End, Page Up/Down, the arrows, Insert, Delete, and Print reach the portal as Home, End, Page_Up, … , alone or with modifiers. Escape stays refused by the portal, which never binds it (#786). Check: navigation_keys_have_portal_triggers in linux/input.rs (constructed bindings; no desktop portal run). X11 grabs don't resolve these keys yet.

Linux with Labs › Special keys alone on (labs.standalone_special_keys, off by default; issue 767): Menu, Pause, and Scroll Lock may be any shortcut's key, alone like a function key or with modifiers, for example Caps Lock remapped to Menu with GNOME's keyboard options and hex settings set dictation_hotkey menu (also pause, scrolllock). They reach X11 grabs as keysyms 0xff67/0xff13/0xff14 and the portal as Menu, Pause, Scroll_Lock. Off, hex settings refuses them, the window's recorder refuses them, and a saved shortcut on one goes back to its default when settings load or the toggle turns off. On, the window's recorder takes them too (#1187). Details and checks in labs.md.

Linux with Labs › Section key alone on (labs.section_key_alone, off by default; issue 1050): the ISO § key, left of 1, may be a shortcut with no modifier. The window's recorder takes § pressed alone and shows §, and hex settings set dictation_hotkey § (or section) saves it. It reaches X11 grabs as keysym 0xa7 and the portal as section. Off, the recorder answers "Add a modifier" as before, hex settings refuses it, and a saved § shortcut without a modifier goes back to its default when settings load or the toggle turns off. While bound, § is Hex's in every app. Details and checks in labs.md.

Checks: mouse_buttons_record_with_or_without_modifiers, mouse_bindings_persist_and_conflict_only_with_the_same_trigger, mouse_hold_and_release_use_the_dictation_state_machine, mouse_double_tap_locks_and_a_fresh_press_finishes, mouse_toggle_uses_the_same_capture_controls, mouse_held_during_stale_keyboard_recovery_is_not_forgotten, mouse_release_stays_balanced_after_its_binding_is_removed, and mouse_suppression_balances_release_and_drag_even_after_modifiers_change. These reducer/recorder checks passed on October 25, 2026, with the full macOS scripts/check suite. The locally signed macOS build was installed and the user confirmed it worked. Linux input-module tests and Xvfb button injection checks (x11_mouse_grab_delivers_hold_and_release and x11_mouse_recorder_captures_auxiliary_button_and_releases_its_grab) passed in an isolated Linux container. The full Linux app passed cargo check with the transcription Vulkan backend disabled for the container's older shader compiler. Windows input code and tests were cross-checked for x86_64-pc-windows-gnu in an isolated input-module harness; no full Windows app build or physical Windows mouse test was run. Xvfb does not establish physical Linux desktop behavior.

The shortcut's keys sit in a recessed tray as tall as the recorder, between the side picker (macOS and Windows) and the recorder; with no shortcut the tray reads Off. On macOS and Windows the recorder shows the modifiers held so far as pressed keys (⌥ ⌘ …); a key records them at once, and releasing them all records the most held. Saved shows for a second, then the recorder returns to Change shortcut. The recorder keeps one width in every state, so it, the tray, and the side picker never move while it records; with the picker showing, keycaps leave the side to it, and otherwise they name it (Left ⌥). Checks: a_recorder_shows_the_modifiers_held_so_far in rune_ui/tests.rs, the_shortcut_row_holds_still_through_every_face_and_side, a_new_shortcut_s_keys_stay_inside_their_tray, and esc_stays_on_the_recorder_however_many_keys_are_held in rune_ui/tests/panes.rs, the shortcut-row* scenes, and the ChordCapture tests in rune_ui/macos_host.rs (no native end-to-end run).

Fixed October 29, 2026 (issue 400): macOS marks every function, arrow, and navigation key press with the Fn flag, and the recorder leaves it out of the binding, so a recorded F14 (or ⌘F14 for Voice Action) never matched and never started. Matching now ignores that flag only for a binding on one of those keys that does not ask for Fn; Fn stays significant everywhere else. Check: function_keys_match_despite_the_fn_flag_macos_adds in suppression/tests.rs (constructed events; no physical-keyboard run).

Fixed October 30, 2026 (issue 462): the Globe key can also send its own key event (key code 179) beside the Fn modifier change, seen with Karabiner-Elements running. A Fn shortcut read that event as another key pressed with it and discarded most presses as a chord 60–130 ms after they started. For a shortcut that holds Fn, key code 179 is now neither a chord key nor HID chord evidence; any other key still is. Check: globe_key_events_are_not_a_chord_with_the_function_shortcut in suppression/tests.rs (constructed events; no physical-keyboard run).

Chord Shortcuts

Added October 29, 2026 (issue 394): every shortcut (dictation, Voice Action, Paste Last, Annotate) is a chord: the set of keys held together, in any order and nothing else. A modifier key names its side (left, right, or any); at most one other key or mouse button joins them. Both Options together is [⌥ left, ⌥ right], so it no longer reads as plain Option.

Press the chord's keys -> Down on the edge that completes the set  // dictate.chord
  ├── One key (⌥, Right ⌥, F5, Mouse 4) -> Down at once, no added wait
  ├── Both sides of one modifier (⌥⌥)
  │   ├── Second side within 80 ms of the first, either order -> Down
  │   └── One held, the other added later -> Nothing until both lift
  ├── A side alone (Right ⌥) with the other side also down -> Nothing
  ├── Option (any side) -> One Option or both, as before chords
  └── Modifiers then a key (⌥ Space) -> Down on the key, as before
Lift any of the chord's keys -> Up
Hold, double-tap lock, Toggle, the 300 ms hold threshold, the provisional
start tone, extra-key discards, stale recovery, and the release tail run on
Down and Up unchanged, on the same event timestamps.

Karabiner-Elements' simultaneous keys default to 50 ms against typing rollover; two thumbs on two modifiers do not roll during typing, so the window is 80 ms (BOTH_SIDES_WINDOW). The recorder keeps a lone modifier's side (the Left · Either · Right picker widens it, and shows only for one-modifier chords), keeps both sides of a modifier, and records every other modifier as either side, so ⌥⌘ works from either hand. On macOS, Labs › Sided modifier chords (labs.sided_modifier_chords, off by default, issue 710; see labs.md) makes the recorder keep each modifier's side in a modifier-only chord instead, so Right ⌘ + Right ⌥ records as R⌥ R⌘ and the left keys don't press it; matching was already sided. While recording, every held key shows with its side (L⌥ R⌥ ⌘ on the Mac, Left Alt elsewhere), so an accidental extra key is visible before release. hex settings takes left-option+right-option. Settings store {"keys": [...]}; files with the old modifiers/key shape load as the same chord and are written back once. Windows matches sides through its low-level hook and accepts one modifier alone (Right Alt or Right Ctrl, #508) as well as both Alts:

Windows modifier-only shortcut                           // dictate.windows-lone-modifier
  ├── Press alone -> Down; release -> Up (hold, lock, Toggle as above)
  ├── Any key or click joins while held -> Chord: Cancel if recording, else nothing
  │   (Escape never: it reaches the app and the capture goes on, #786)
  │   (the key reaches the app; locked and toggle captures keep recording)
  ├── Alt or Windows released -> Swallowed, VK 0xE8 tapped, release replayed
  │   -> No menu bar or Start menu; Share shortcut skips the mask
  └── Hex's own injected keys (paste, mask) -> Skipped by the hook (HEX_INJECTED)

On layouts where Right Alt is AltGr, Windows adds a Left Ctrl press, so Right Alt alone never matches there; bind Right Ctrl instead. Checks: one_modifier_alone_is_a_shortcut_and_alt_or_windows_mask_their_release, a_key_joining_a_held_modifier_ends_it_quietly_unless_it_records in windows/input.rs, cross-compiled only; the menu mask and AltGr behavior have not run on a Windows PC.

Fixed November 4, 2026 (issue 1254): a Windows key chord holding Alt or Windows (Alt+0, say) swallows its key, so the app saw Alt pressed and released alone. Chrome took that as a menu tap and moved focus to its menu button, and the paste landed there instead of the page. When Hex swallows the key of a chord (dictation, Dictate without processing, Paste Last, Annotate) that holds Alt or Windows, that modifier's release is now masked like a modifier-only shortcut's: swallowed, VK 0xE8 tapped, release replayed (not with Share shortcut). Check: the holds_menu_modifier assertions in one_modifier_alone_is_a_shortcut_and_alt_or_windows_mask_their_release (cross-compiled only; not run in Chrome on a Windows PC). Linux binds what the portal and X11 grabs take, a key with modifiers and no side: releasing modifiers pressed alone in the recorder says Linux needs a key (or can't bind both sides) and suggests adding one, such as Space.

Checks: the both_options_* tests and one_sided_options_never_start_with_both_held_but_either_side_does in suppression/tests.rs, shortcuts_store_as_chords_and_old_files_convert_once and both_sides_conflict_only_with_either_side_and_match_only_together in app_settings.rs, both_options_record_as_one_chord_of_two_keys in rune_ui/macos_host.rs, both_sides_must_go_down_together in windows/input.rs, and a_modifier_alone_says_linux_needs_a_key_and_suggests_one in rune_ui/tests.rs. Constructed events only; no physical keyboard run on any OS.

CLI: hex listen uses saved settings; --device is authoritative. This is the CLI listener, not Settings. Do not run it beside an existing listener.

Separate targets: Host-owned API capture has its own ownership protocol and does not prove the physical dictation shortcut; see the feature index.

Sub-features

Dictate Without Processing

A second, optional dictation shortcut (issue 451, suggested by @ksajan). It records exactly like the dictation shortcut and pastes the transcript as heard, skipping the Output pane's steps: replacements, corrections, transformations, and OpenCode rewriting.

Output > Without processing > Dictate without processing   // raw.shortcut
├── Unbound by default -> nothing registered, grabbed, or reserved
│   └── Record a shortcut (the same recorder) -> registered on every OS
│       ├── macOS: any binding the dictation shortcut takes; its own
│       │   shortcut machine beside dictation's (recognition::Hotkeys)
│       ├── Windows: a key chord (the hook's raw trigger)
│       └── Linux: an X11 grab, or a portal entry "Dictate without processing",
│           only while bound
├── Hold / double-tap lock / toggle -> as the dictation shortcut
│   ├── The shortcut that started a capture owns it: the other one neither
│   │   finishes it nor counts toward its double tap
│   └── Queueing and ordered output -> shared with dictation
├── Finish -> transcript pasted as heard (trimmed)          // raw.paste
│   ├── History -> kind Raw ("raw" in `hex history --json`)
│   └── Paste Last -> pastes it (it is the last dictation)
├── Clear (trash) -> unbound again
└── hex dictation start|toggle --raw -> the same, without a shortcut
    └── A printing stop (`stop --print`) of it prints the transcript as heard

Checks: raw_dictation_owns_the_capture_it_starts (Linux reducer), every_command_round_trips and the check_flags cases in control.rs. Proof limits: no physical shortcut run on any OS; the macOS machine pairing was not compiled or run here (Linux check, Windows cross-clippy only).

Copy Recent Dictation (Labs, macOS)

Paste Last Dictation reaches only the newest dictation. With Labs › Copy recent dictations on (labs.recent_dictations, off by default, issue 877):

Menu bar > Copy Recent Dictation (under Paste Last Dictation)  // dictate.copy-recent
├── Menu opens -> History read once: five newest entries within retention,
│   newest first, no printed or empty ones (as Paste Last skips them)
│   └── One line each: whitespace collapsed, cut to 48 characters with "…"
├── Click one -> Its full final text on the clipboard; nothing is pasted
└── Toggle off, History Off, or nothing to list -> Item hidden, History unread

Details and checks in labs.md.

Skip Long Pauses (Labs, Linux and Windows)

A dictation transcribed as a whole clip (no stream on this model or device, or the fallback after a failed or late stream) sends every second of silence through the model. With Labs › Skip long pauses on (labs.skip_pauses, off by default, issue 899):

Whole-clip run starts -> labs::skip_pauses()        // dictate.skip-pauses
├── Off -> The clip, untouched (as before)
├── On -> Each pause at the noise floor keeps its first 2.5 s and last 0.3 s,
│   a trailing pause its first 2.5 s; shorter pauses pass whole (the stream's PauseCap)
├── Nothing heard above the floor -> The clip, untouched (#841)
└── Job deadline, History duration, silence discard -> The full clip

Details and checks in labs.md.

Fixed November 3, 2026 (issue 1194): a clip longer than the model's window (Cohere's 35 s) is transcribed in chunks, and a chunk that was only silence (a locked dictation left waiting before the words) went to the model anyway; Cohere invented long repeating text for it, pasted before the real sentence. Each chunk of such a clip now goes through the speech detector first and is skipped when it hears no speech; without the detector every chunk still runs. Clips that fit the window are unchanged. Check: a_long_clip_skips_only_chunks_the_detector_hears_no_speech_in in gguf_session.rs (detector verdict overridden; not run with Cohere on real audio).

Vocabulary Quality Checks

hex benchmark-transcription accepts a scoring vocabulary and expected terms, and reports recall, excess vocabulary occurrences and WER by corpus group, plus overall latency. Its companion scorer also reports per-group latency. The fixture guide describes the synthetic set, public guard download, and ignored private manifest path. This is offline evaluation coverage, not a shipped vocabulary feature or proof of recognition quality on a microphone. Metric checks live in transcription::benchmark::tests; inference results belong in the research report.

Hold And Short Taps

Ordinary hold                            // lock activation is mapped below
  ├── Hold -> Speak -> Release -> Finish          // dictate.hold
  ├── Release before 300 ms -> Discard            // dictate.short-tap
  ├── Early unrelated shortcut/click -> Discard   // not intentional dictation
  │   └── Includes another app's global shortcut  // dictate.hidden-chord
  │       (Spaceman's Option-1) -> Seen at HID -> Discard, no double-tap lock
  ├── Modifier-only hold + any key, at any time -> Discard  // dictate.held-chord
  │   (key reaches the app; locked and toggle captures keep recording)
  └── Modifier-only hold past 300 ms -> One unassigned key press  // dictate.chord-breaker
      -> Other apps' modifier-tap hotkeys see a chord, not a tap

Ordinary shortcut -> No recording mute/pause or idle-sleep prevention
Intentional recording -> No automatic duration limit

Source: handle_hotkey_action in recognition.rs. Checks in suppression/tests.rs: callback_timestamps_preserve_nanoseconds_and_short_tap_discard, callback_press_reaches_intentional_hold_on_the_audio_clock. These cover constructed callback/reducer timing, not physical event delivery.

A chord discard logs its cause to process.log (dictation press discarded as a chord, or dictation press dropped when HID evidence overtook the press) with the key code, the milliseconds after the press, and for HID evidence the raw timestamp, how it was read, and its arrival delay. The discarded event in live.ndjson carries the same cause as reason: key, hid-key, mouse, or extra-modifier. Short taps have no reason.

Fixed October 22, 2026: a global shortcut registered with Carbon (Spaceman's Option-1 to Option-0 workspace keys, for example) consumes its key before the session taps, so Hex saw only the Option press and release. Holding Option past 300 ms while switching workspaces recorded and pasted whatever the microphone heard, and two quick switches could double-tap lock. A listen-only HID tap now reports key presses as chord evidence only; session events still own key state, and Escape and Hex's own shortcuts are excluded. Evidence that overtakes its press still prevents the capture. Command-Tab likewise swallows Tab's release, which caused the routine resynchronized stale input tracking warnings, now logged with key_codes. Until October 26 an Option press within 100 ms of Command-Tab was silently ignored; the press now resynchronizes itself (recover.shortcut-press in recovery). Checks: hid_evidence_discards_a_chord_hidden_by_a_global_shortcut, hidden_chords_in_quick_succession_never_double_tap_lock, hid_evidence_that_overtakes_its_press_prevents_the_capture. Observed with listen-only session and HID taps on Kit's Studio: Spaceman's Option-1/2/3 reached HID but never the session. The installed fix has no physical-keyboard run yet.

Fixed October 30, 2026 (issue 485): on GNOME Wayland, releasing Ctrl before Space (or both at once) can leave the GlobalShortcuts portal without a Deactivated, so a Hold capture kept recording until the shortcut was pressed and released again. Linux now treats a press that arrives while the shortcut is still held as a key repeat only within 1 s of the previous press; a later press is new and finishes the Hold capture whose release was lost, and that press's own repeats and release start nothing. Hex never infers a release from silence or a timeout. Checks: a_hold_whose_release_never_arrives_finishes_on_the_next_new_press and a_balanced_release_finishes_a_repeating_hold in linux/input.rs (constructed inputs; not run on GNOME).

Fixed October 31, 2026 (issue 733): an X11 paste waited, with no bound, until the X server reported no modifiers held. XWayland's modifier state goes stale while a Wayland window has focus and only a later key event clears it, so a finished dictation sat in Transcribing for about a minute. The paste now waits for held modifiers only while one of Hex's shortcuts is down (a newer capture owns them); otherwise it waits at most 2 s, logs that modifiers still read as held, and pastes. Checks: stale_modifiers_without_a_held_shortcut_stop_holding_the_paste and accepted_output_waits_through_another_capture in linux/paste.rs (constructed state; not run on a desktop).

Added November 3, 2026 (issue 1111): on GNOME Wayland the previous clipboard never came back after a paste, because Mutter offers neither ext-data-control nor wlr-data-control. Labs › Restore clipboard on GNOME (gnome-clipboard-restore, off by default, Linux) now snapshots, checks, and restores it through XWayland's CLIPBOARD selection, which Mutter bridges to Wayland apps both ways. It applies only on a Wayland session without data-control and only while this user's Xwayland process already runs with DISPLAY set, so Hex never starts XWayland; the generation and still-holds-the-paste rules are unchanged. While it is off there, Settings › Output shows a note pointing to it. Checks: xwayland_restores_only_by_opt_in_without_data_control, xwayland_is_looked_for_only_when_it_could_be_used, and xwayland_counts_only_this_users_process in linux/clipboard.rs; gnome_desktops_point_to_the_clipboard_lab_until_it_is_on (constructed state; not run on GNOME).

Fixed November 3, 2026 (issue 1158): on Linux with PipeWire, Hex's microphone stream (held open while the microphone is kept ready) was named by the ALSA plugin after the binary, with spaces and varying by packaging, so audio idle inhibitors couldn't be told to ignore it and the screen never locked. Every ALSA stream Hex opens now carries application.name and node.name hex (PIPEWIRE_ALSA, set at startup beside state.restore-target). Check: streams_carry_a_stable_name_without_spaces in linux/input_route.rs (property string only; not run against PipeWire or an inhibitor).

Fixed November 4, 2026 (issue 1309): on Linux (PipeWire), about a second of speech or background audio from before the press was transcribed. The press was placed at the end of the newest microphone chunk, and ALSA's default period can be most of a second, so the 450 ms pre-roll counted from there. Hex now asks for a 20 ms period (falling back to the device default), places presses and releases on the stream clock (StreamClock in linux/dictation.rs), and keeps exactly the pre-roll before the press, dropping earlier audio from chunks still to arrive (DictationCapture::start_at_press). The hold threshold counts from the press, and a replaced or reopened stream starts its clock over; a press before its first chunk records from that chunk. Checks: a_press_inside_a_large_chunk_keeps_exactly_the_pre_roll and a_press_ahead_of_large_chunks_measures_the_hold_from_the_press in dictation.rs, a_press_between_large_chunks_is_placed_on_the_stream_clock and a_reopened_stream_carries_no_old_clock in linux/dictation.rs (constructed chunks; not run against PipeWire or a physical microphone).

Changed November 4, 2026 (issue 1309 follow-up): with "While dictating" set to Mute or Pause media, the pre-roll came from before the mute took effect and picked up whatever was playing. On every OS, those hotkey captures now keep no pre-roll (dictation::hotkey_pre_roll); Duck and Do nothing keep 450 ms. There is no separate setting. Check: muting_or_pausing_while_dictating_keeps_no_pre_roll in dictation.rs (constructed audio; not run on a device).

Fixed November 2, 2026 (issue 978): a Windows paste refused to run when the clipboard held only a format Hex can't snapshot (an app's private format), failing every dictation with "the current clipboard format cannot be preserved safely". The transcript now pastes anyway; that clipboard just isn't restored afterwards, and process.log says why. Check: an_unreadable_clipboard_still_pastes_and_restores_nothing in windows/paste.rs (constructed state; not run on a Windows PC).

Changed October 22, 2026: Kit holds Option a little past 300 ms before pressing the digit, which 2.3.0 and the Swift app both ignored after the threshold. Any non-modifier key pressed while a modifier-only shortcut is still held now discards the capture, with no transcription or paste. Keyed shortcuts keep the 300 ms rule. Check: held_modifier_only_shortcut_plus_any_key_discards_after_the_hold_threshold.

Changed October 27, 2026 (chord breaker): a bare-modifier shortcut is not consumed, so other apps saw an Option dictation as an Option tap on release. A Raycast hotkey on an Option tap activated Slack on every dictation, switching Kit out of a fullscreen Space before the paste. Tap detectors (Raycast, Karabiner's to_if_alone, AutoHotkey's Start-menu mask) cancel a tap when any key goes down while the modifier is held. Once Hex owns a modifier-only press it now posts one press of key code 0x44 under the modifiers held at that moment, once per press and only while exactly the shortcut's modifiers are still physically down (any other modifier already breaks the tap):

Hold past 300 ms (Hold mode, or a held second tap) -> Chord breaker
Toggle start press, 40 ms after the press          -> Chord breaker
  (the start tone's chord grace: an extra modifier pressed with it,
   as for ⌥⇧S, discards the press first; a toggle tap is too short
   to wait out the 300 ms hold)
Press that finishes a lock                         -> Chord breaker at once
Short tap, chord discard, double-tap lock taps     -> Nothing (still a tap)
Press during voice-delimited dictation             -> Nothing (starts nothing)
Keyed shortcuts (consumed), Globe/Fn (system tap)  -> Nothing

The breaker never reaches the focused app:

Posted at HID with Hex's marker
  ├── HID taps, session taps            -> See a key: tap cancelled
  ├── NSEvent global monitors           -> See a key: tap cancelled
  ├── Hex's HID tap and its key tap     -> Marker: not a key, chord, or HID evidence
  └── Hex's annotated-session key tap   -> Dropped
      -> The focused app never gets it, so it never beeps (a non-text
         view, or any text view with ⌘ held, beeps at an unhandled key)

0x44 is a hole in the virtual key codes (between keypad * and +): no keyboard sends it, it has no system action, and of the 264 layouts macOS ships only Unicode Hex Input maps it (to U+0000, never with Option alone). Source: DictationHotkey::break_chord and event_callback in suppression.rs, post_chord_breaker in keyboard.rs, called from the listener loop once no input is pending. Checks: a_held_modifier_only_shortcut_breaks_the_chord_once_after_the_hold_threshold, short_taps_chords_and_other_bindings_never_break_the_chord, locked_and_toggle_captures_break_the_chord_only_for_presses_hex_owns, marked_chord_breaker_never_reaches_the_shortcut_machines_or_the_app.

Observed October 27, 2026 on a macOS 27.0.1 VM (macvm), with a stand-in for Hex (the same marked HID post, dropped by a head-inserted annotated-session default tap) rather than Hex itself: an Option hold reached a test app as an unhandled keyDown that beeped in a plain view when the breaker passed through; when dropped, the app received nothing, while an Option-tap detector on a HID tap, a session tap, and an NSEvent global monitor each saw the key and cancelled its tap. Without the breaker all three fired. Unproven: Raycast itself (its binary uses both event taps and a global monitor), a detector on an annotated-session tap placed after Hex's (it never sees the key), and a physical keyboard. If Hex's key tap is briefly disabled by a timeout, the breaker reaches the app.

Shortcut Mode

General > Shortcut mode (under Shortcut)               // dictate.shortcut-mode
  -> Names the choice; opens the Shortcut mode modal
  Hold or double-tap (default) -> Hold, plus double-tap to lock
  Hold                         -> Hold only
  Tap                          -> Toggle, below
  Double-tap                   -> Double-tap only; needs a key binding
Tap
  Press -> Start (pre-roll as usual) -> Release keeps recording
  -> Fresh shortcut press -> Finish (with the release tail)
  -> Cancel shortcut (Escape by default) -> Cancelled, see Toggle Cancel below
  // Shortcut chords during the first 300 ms still discard
  macOS and Linux, Labs › Tap or hold on (labs.tap_or_hold, off by default, #711, #1144):
  ├── Release 300 ms or more after the press -> Finish (with the release tail)
  ├── Sooner -> Keeps recording, as above
  └── Linux: key repeats keep the first press's time; the dictation shortcut
      and Dictate without processing alike; hex dictation commands unchanged
  Linux: release never arrives (Hyprland Lua bind, Alt up before Z)
  ├── Press 1 s or more after the last press -> Finish
  └── Sooner -> Ignored as a key repeat (KWin repeats held shortcuts)

The one choice is stored in the three settings it replaced (shortcut_mode, double_tap_lock, double_tap_only); of their eight combinations only four behave differently, and Trigger::of in shortcut_mode.rs folds the rest onto them (redundant_and_invalid_combinations_fold_away). hex settings still sets each one. Linux with only hex commands shows no row: the commands ignore it.

Tap (toggle) reuses the lock state on every platform: each press behaves as the second tap of a double-tap. Persisted as shortcut_mode (hold/toggle) in each platform's settings; files without it load as Hold. Checks: toggle_mode_starts_on_a_press_and_finishes_on_the_next and toggle_mode_escape_leaves_a_locked_capture_recording in suppression.rs (macOS reducer), the same names in windows/input.rs, and toggle_mode_starts_on_a_tap_and_finishes_on_the_next, a_capture_that_would_lock_finishes_when_the_desktop_drops_its_release, and a_repeating_held_press_never_finishes_the_capture in linux/input.rs, whose one state machine serves X11 grabs, Wayland portal shortcuts, and the hex dictation commands alike. The dropped release was observed on 2026-10-28 on Hyprland 0.56.2 with xdg-desktop-portal-hyprland 1.4.1: uinput Alt+Z sent no Deactivated when Alt came up first (the fix itself has not been run there yet). Only the Windows build was compiled and tested locally (2026-10-19); macOS relies on CI. The Linux state machine and X11 grab tests ran on 2026-10-22 (the grabs on an isolated Xwayland display). No physical keyboard check yet.

General > Shortcut mode                               // dictate.shortcut-explainer
  -> Modal: the four choices, each its name and one line beside the
     shortcut's keycaps (one per key, pressing together) and two lanes (key
     above, recording below) on a chart-recorder tape: a still pen at the
     right, the tape scrolling left without end and fading out at its left
     edge; opening it ends a shortcut recording and closes the model picker
  ├── The current choice has a focus-coloured border
  ├── Double-tap with a lone modifier -> Dimmed, takes no focus or press,
  │   says "Needs a shortcut with a key, like ⌥Space"
  ├── Long chord -> The panel widens (up to the window) so each option's
  │   lanes keep at least 64 pt beside the keycaps
  ├── Pick an option (click, Space) -> Saves the choice
  ├── Tab -> each available option, Done; focus stays inside
  ├── Done, Return, Escape, or a press outside -> Closes; focus returns to
  │   the row
  ├── Change pane, menu bar, or `hex window show` -> Closes
  └── Reduce Motion -> One whole take, still, no wakes; turned off while
      open, the tape starts again when the window regains focus

The lanes are Rune shapes drawn at their laid-out width and redrawn from the window's poll clock while the modal is open (src/rune_ui/explainer.rs). Checks: the_shortcut_mode_row_names_the_choice, shortcut_mode_opens_from_its_row_and_keeps_focus_inside, each_option_picks_its_trigger, the_explainer_loops_on_the_poll_clock, reduce_motion_shows_the_finished_explainer_without_waking, the_explainer_restarts_when_reduce_motion_turns_off, a_long_chord_widens_shortcut_mode (five keys), shortcut_mode_draws_a_keycap_per_key_and_lines_up_the_lanes, and the_explainer_picker_and_shortcut_recording_never_share_the_window in tests/panes.rs, the tape's own nothing_draws_past_the_pen, the_tape_scrolls_without_steps, the_still_frame_shows_a_whole_take, narrow_lanes_draw_nothing and the_past_leaves_the_tape in explainer.rs, plus the shortcut-mode (Option, Double-tap unavailable), shortcut-mode-key (Option-Space, Double-tap chosen), and shortcut-mode-chord (Ctrl+Alt+Space) goldens (frozen at 1.8 s into the tape). The animation itself has been seen only in headless frames, not on a real window.

On Linux, hex dictation start and stop bypass every Shortcut mode: start records unless something already records, stop finishes, and toggle does whichever applies. Bound to a key's press and release they give hold-to-dictate; a shortcut press still finishes a capture a command started.

The short-tap threshold belongs to ordinary hold activation. Voice-delimited and explicit programmatic capture have different intentional-start rules.

Lock, With Or Without Hold

Shortcut mode = Hold or double-tap or Double-tap     // dictate.lock
  Hold -> Ordinary hold path remains available
  Tap -> Tap
    ├── Second press AND release <300 ms after first release -> LOCKED
    └── Slow second release -> No lock

Shortcut mode = Double-tap                           // dictate.double-tap-only
  Requires a key binding AND locking enabled
  Single press/hold -> No capture                     // disables hold, not a toggle
  Same two qualifying complete taps -> LOCKED         // same lock, not a third mode

LOCKED -> Speak hands-free -> Fresh shortcut press -> Finish
       -> Escape -> Nothing: it reaches the app (#786)
       -> Missed key release -> Neutral keyboard repairs tracking -> Still LOCKED

Checks in suppression/tests.rs: callback_double_tap_locks_for_modifier_and_key_bindings, callback_hold_and_double_tap_boundaries_are_milliseconds, second_tap_finishes_as_press_and_hold_when_double_tap_cannot_lock, double_tap_only_waits_for_two_complete_key_chord_taps, locked_dictation_finishes_after_a_missing_key_release. These cover gesture decisions, not hardware keyboard-layout or remapping equivalence.

Warm And Cold Capture

Keep ready -> Press shortcut                          // dictate.boundaries
  -> Include up to 450 ms of available pre-roll
     └── "While dictating" mutes or pauses -> No pre-roll (it predates the mute)
  -> Linux: press and release placed on the stream clock (newest chunk's end
     plus the time since it arrived), so large chunks add no pre-press audio
  -> Delayed handling still uses original event boundaries
  -> Release excludes audio captured after physical release + 200 ms

Release or locked finishing press                     // dictate.release-tail
  -> Hold threshold judged on the physical release alone
  -> Wait for audio through release + 200 ms (POST_RELEASE_TAIL)
     ├── Arrives -> Clip ends exactly at release + 200 ms
     └── Stream stalls/fails -> Finish within tail + 100 ms with what arrived
  -> Not short taps, voice controls, programmatic stop, or meetings
  -> Linux and Windows wait the same tail; Linux ends at the release it observes
     (an X11 key release, a portal Deactivated, or `hex dictation stop`)
  -> A press inside the tail starts after the old clip finishes, with its own pre-roll

Release that will transcribe                          // dictate.stop-tone
  -> Stop tone and mute/pause/sleep restore at the release, while the tail is
     still captured; a muted output (macOS, Linux) unmutes first, then the tone
     ├── Short tap or chord -> No stop tone; the start tone is retracted
     ├── Clip later near-silent or without speech -> Tone already played;
     │   discard quietly, nothing pasted, no error tone or note; the log has
     │   the measured level and peak against the thresholds
     └── Escape after the release -> Nothing; the clip still transcribes
  -> macOS: the audio owner announces it before draining the tail; HUD
     Transcribing and submission still follow the tail
  -> Paused media resumes without delaying the tone
  -> Voice controls (no tail) -> Tone when the clip is submitted, as before
  -> The silence gate ignores the tail, so a tone the microphone hears there
     never makes a silent clip transcribe

Release when idle -> Press shortcut                   // dictate.cold-microphone
  -> Open microphone asynchronously, no pre-roll
     ├── Release before readiness -> Discard
     ├── Stream fails before the hold threshold -> Reopen for the same press
     └── Ready while held or locked -> Capture
  -> Capture idle -> Microphone can close             // accepted jobs do not keep it open

Source: DictationCapture in dictation.rs; checks: delayed_press_reconstructs_audio_from_the_original_boundary, delayed_release_removes_audio_captured_after_the_physical_release, capture_continues_past_sixty_seconds_until_explicitly_finished, release_tail_keeps_audio_through_the_tail_and_no_further, short_tap_discards_on_its_physical_release_despite_the_tail, resampled_clips_keep_the_last_word_through_a_delayed_release (44.1/48 kHz, streamed and not, with the tail). These cover sample-buffer/timestamp logic, not a microphone recording. The physical release stays exact; the 200 ms tail is an explicit extension of the trim boundary. The opt-in release_boundary_harness (dictation/tail_tests.rs; HEX_TAIL_MODEL, HEX_TAIL_WAVS) and evaluate_dictation_corpus (dictation/corpus_tests.rs; scripts/dictation-corpus.sh, HEX_DICTATION_CORPUS_DIR) replay recordings through the capture timeline at release offsets (-300 ms to +200 ms) and onset offsets (-300 ms to +100 ms). On 2026-10-15, before the tail, across 102 clips (70 real voice recordings, 20 macOS say clips, and 12 ElevenLabs eleven_v4 clips) with Unified English, clips matched the release to the sample; releasing at 0 ms..+200 ms kept the final word on 10/10 real clips (-50 ms: 9/10; -100 ms: 7/10; -150 ms: 4/10; -200 ms: 2/10). Warm 450 ms pre-roll kept the initial word even when speech began -300 ms before press, whereas cold 0 ms pre-roll lost initial syllables when speech preceded readiness by 150–300 ms. The since-removed pause chunking on or off, and 1 versus default decoder threads, gave identical tail results. The harnesses now apply the tail; they have not been re-run with it. The tail also keeps any key-up or click sound made at release; no filtering is applied. The owner in dictation/audio.rs has cold_capture_restart_rejects_late_open_results_and_closes_after_finish and pending_open_uses_physical_press_for_intentional_threshold: controlled opening and ownership transitions, not physical device startup latency. Tail checks there drive the owner with a fake stream: release_waits_for_its_tail_and_keeps_no_later_audio, a_press_inside_the_tail_starts_its_own_capture_after_the_old_one_finishes, a_stalled_or_failed_stream_bounds_the_tail_wait, and escape_and_short_taps_finish_without_waiting_for_a_tail. The macOS coordinator blocks on the tail, so HUD Transcribing and submission follow the release by up to 200 ms (300 ms on a stalled stream). The stop tone no longer does: the audio owner's Finish ends the recording environment through end_recording_environment_then and plays the tone from the environment worker once a muted output is restored (at once with paused media), then drains the tail; finish_dictation plays the tone itself only for tail-free finishes. This macOS change compiles and passes Clippy on hailmary (2026-10-22); physical tone timing and the unmute-then-tone order on a Mac audio device remain unverified. Linux and Windows play the stop tone at the release too. On Linux this used to follow the tail too: on 2026-10-22, an isolated service on apollo (ALSA Loopback microphone and speaker, hex dictation stop) measured the stop tone requested 203 ms after the release was handled before the fix and within the same loop pass after it (about 40 ms after the CLI mark, mostly process start), with the tail still collected for 203 ms afterwards; the output was already open, so no device reopen was involved. Checks: a_release_finishes_its_stream_with_the_complete_clip (a release that transcribes is announced before its tail), escape_and_short_taps_cancel_the_stream_and_submit_nothing (a short tap is silent), restoring_without_a_mute_runs_its_callback_first, and the_stop_tone_in_the_tail_does_not_make_silence_transcribe. The mute ordering (unmute, then tone) is checked by reading the code, not on a muted device, and a physical F12 release has not been recorded.

Growing tail, November 3, 2026 (issue 1101): stopping while still saying the last word cut it partway, and the model guessed another word. The 200 ms tail is now the minimum: when sustained speech (100 ms of 20 ms frames 12 dB over the noise floor, syllable gaps up to 40 ms) runs into its end, capture continues until 160 ms of quiet or 600 ms after the release (DictationCapture::release_tail, MAX_RELEASE_TAIL). A key click is too short to count. The stop tone may be: speech then also has to be heard once the current set's stop sound (feedback::stop_tone_length) plus 60 ms has passed, so a release followed only by the tone waits until then and keeps 200 ms. The release event stays exact. The macOS audio owner and the Linux and Windows loops (ReleaseWait) wait at most the base tail plus 100 ms, or 700 ms once speech grows it; a lost microphone finishes with what arrived. Escape, short taps, voice controls, programmatic stops, and meetings still get no tail. Checks in dictation.rs: a_word_said_through_the_release_keeps_the_tail_open_until_it_ends, a_release_after_the_speech_keeps_the_base_tail_without_waiting_longer, speech_that_carries_on_stops_the_tail_at_its_cap, and the_stop_tone_or_a_key_click_never_grows_the_tail (synthetic tones through the capture timeline at 16 kHz). Not measured on a microphone or a real speaker playing the tone.

Streaming Finish

Model advertises streaming, GPU device             // dictate.streaming
  // Metal on macOS, Vulkan on Linux and x64 Windows
  Load or switch model -> Probe a one-second silent stream (also prewarms the stream graph)
       ├── Succeeds -> Stream shortcut captures
       └── Fails, CPU device, Apple Speech, or HEX_STREAMING=0 -> Whole-clip path only
  Shortcut capture passes the hold threshold (Toggle on Windows and Linux: on press)
    -> Open one live stream; feed the 16 kHz backlog, then every 160 ms
       // same audio as the final clip, pre-roll included; never blocks capture
  Release -> Collect the release tail (200 ms, up to 600 ms while speech goes on)
    -> Finish with the complete clip
    -> Feed any unfed remainder
    -> macOS Unified English: feed 400 ms of silence, as the whole clip gets
    -> Finalize -> Final text
       ├── Stream failed, produced no text, or was fed > 250 ms past the clip
       │   -> Transcribe the whole clip offline (source=fallback)
       ├── ≥ 300 ms of speech the stream heard lies > 400 ms past its text's end
       │   -> Transcribe the whole clip offline (source=fallback)   // #1101
       ├── No final text within 1.5 s -> Abort the stream -> Whole clip
       └── Model switched during the capture -> Stream closed -> Whole clip
  Short tap, chord discard, microphone failure -> Drop the stream
  Voice-delimited and programmatic captures (macOS) -> Whole clip after release
  Nothing is pasted or reaches the focused app until the final text exists
  Labs + labs.live_text -> After each feed the stream publishes its text   // dictate.live-text
  ├── Stable (committed) and tentative text, per capture; a new capture starts empty
  ├── One click-through line just past the HUD, on the HUD's screen
  │   ├── macOS: beside the plate the HUD placed (under the menu bar, over the Dock,
  │   │   in the menu bar, out of the notch), on the pointer's screen
  │   ├── Linux, Windows: past the plate `rune_hud` draws, on the HUD's display
  │   ├── HUD hidden -> In the HUD's place at the top
  │   ├── Appears with the first words, already drawn; prewarmed, so macOS never
  │   │   reveals the last dictation's words for a frame (Linux, Windows: unverified)
  │   ├── Grows with the words to 552 pt; the newest stay in view, "…" cutting the start
  │   └── Tentative words, when the model has any, are dimmed after the stable ones
  ├── Release, short tap, or a lost microphone -> The recording ends -> Hidden
  ├── Provisional: never pasted, copied, or kept (the final text replaces it)
  ├── A failed or interrupted stream clears the text while the capture goes on
  └── process.log: `partials` on "finished live stream"; per change at debug (no words)
  Picker -> "Streams" tag on catalog models whose artifact streams   // setup.model-card
  Picker -> The saved language's Recommended/Fastest/Most accurate cards first  // setup.model-routing
  ├── el: Cohere; tr, ms, fil, fa, mk, ar, ja, fi, sv, da: Whisper turbo first
  ├── auto, yue, ko: no SenseVoice; mt: Parakeet v3 only (Whisper fails Maltese)
  ├── en: Underdog Bark 3.0 Pro and Clean (Parakeet v3 fine-tunes; Clean
  │   drops filler words) follow without a badge
  ├── ar: QwenCleo-ASR 1.7B (Qwen3-ASR 1.7B fine-tune for Egyptian Arabic and
  │   Arabic/English code-switching; Arabic only, 2.2 GB; its 19.85% WER is
  │   the author's own test) follows without a badge
  ├── he: ivrit.ai Whisper large-v3-turbo Recommended (Hebrew only, always with
  │   the `he` hint; 18.45% WER on FLEURS Hebrew test against stock turbo's
  │   29.69%, Hex's run), then Whisper turbo for Vocabulary (#752)
  ├── ru: GigaAM v3 Recommended (ai-sage's e2e RNN-T, Russian only, 274 MB, no
  │   hint; 5.37% WER on FLEURS Russian test against Parakeet v3's 6.52% and
  │   Whisper turbo's 5.36%, Hex's run), then Whisper turbo for Vocabulary (#952)
  ├── no: NB-Whisper Medium Recommended (National Library of Norway, Norwegian
  │   only, always with the `no` hint, 539 MB; 6.68% WER on FLEURS Norwegian
  │   test against Whisper turbo's 8.94%, Hex's run), then Whisper turbo for
  │   Vocabulary (#1150)
  ├── en, fr, de, es, pt, it, ja: Granite Speech 5.0 (IBM's TurboCTC, 506 MB,
  │   no hint) follows without a badge: on FLEURS test it trailed Parakeet
  │   Unified/v3 in every European language and tied Whisper turbo in Japanese
  │   (5.04% CER against 5.02%), Hex's run (#1252)
  └── Every other available model follows without a badge, selectable and
      deletable; never auto-switched or auto-downloaded (setup.language-select)

Source: transcription/engine.rs (one thread owns the model on every OS; streams, finishes, offline runs, and macOS model loads and switches share its FIFO because a live stream holds the model's compute lease), probe_stream and with_stream in transcription/gguf_session.rs, the stream entry points in transcription/local_transcriber.rs and parakeet.rs, and begin_stream in dictation.rs: the capture owns its stream, feeds it as audio arrives, cancels it with the recording, and hands it to the finished clip. macOS streams hotkey captures from the audio owner in dictation/audio.rs; DictationWorker::transcribe hands the clip to the engine when it is submitted, so its stream finishes before a later capture opens one, and the job then waits for its text in order. Linux (linux/dictation.rs) and Windows (windows/dictation.rs) drive the same engine from their capture loops. Parakeet buffers with its largest context menu (Unified English: 5.6 s left, 1.04 s chunk, 1.04 s right), its most accurate and least compute-intensive setting; HEX_STREAM_CHUNK_MS overrides the chunk for benchmarks and HEX_STREAMING=1 streams on any device, including a CPU (diagnostics and the Linux CI fixture). Each dictation logs source=stream|offline|fallback and finish_ms. The macOS "Transcribe during pauses" setting no longer affects dictation.

Checks: recorded_audio_is_a_prefix_of_the_finished_clip (16, 44.1, and 48 kHz), resampled_clips_keep_the_last_word_through_a_delayed_release (streamed and not), a_stream_starts_with_the_backlog_and_feeds_in_blocks, live_text_follows_the_recording_and_never_outlives_it, and short_taps_cancellation_and_unstreamable_models_end_the_stream in dictation.rs; hotkey_captures_stream_once_intentional_and_others_do_not in dictation/audio.rs; hold_streams_after_the_threshold_and_toggle_streams_from_the_press, a_release_finishes_its_stream_with_the_complete_clip, escape_and_short_taps_cancel_the_stream_and_submit_nothing, and a_model_that_cannot_stream_transcribes_the_whole_clip_after_release in linux/dictation.rs; source_labels_distinguish_fallbacks, streams_send_their_audio_in_order_and_cancel_when_dropped, and (macOS, a real engine thread without a model) a_finish_without_a_stream_transcribes_the_clip_offline, an_unusable_stream_falls_back_to_the_complete_clip, work_runs_in_order_and_failures_reach_the_caller, and dropping_the_engine_stops_despite_an_open_stream in transcription/engine.rs. These use synthetic audio and recorded engine commands. The Linux CI fixture scripts/test-linux-dictation.sh replays the same speech through a virtual microphone twice with the real model: once on the CPU (whole clip, source=offline) and once with HEX_STREAMING=1 (source=stream, or fallback when the CPU stream misses its deadline). The hidden Windows model stream-bench <wav> [--speed N] command and the opt-in macOS metal_stream_tracks_offline_transcription (parakeet.rs; HEX_STREAM_MODEL, HEX_STREAM_WAVS, HEX_STREAM_SPEED) replay a WAV in real time and compare both paths.

Observed October 19, 2026, RTX 5070 Ti, Vulkan, Unified English Q8, Windows TTS clips replayed at 4× (131 s also at 1×): release-to-text 55/55/93 ms (10/27/131 s) streaming versus 82/185/873 ms whole-clip; GPU busy 3.1–3.6% of the dictation; process peak working set 214 MB and about 3% of one core during a real-time 131 s stream. Against the script, both paths transcribed the 10 s and 27 s clips perfectly; on 131 s the stream added one homophone error ("cache" → "cash") to formatting differences both paths share. Smaller chunks (560 or 160 ms) added more errors and up to 20% GPU without a faster release. After the engine became shared (same machine and clips at 4×): 56/91 ms streaming versus 92/865 ms whole-clip for 10/131 s, with 0/28 and 1/408 word differences, matching the earlier build.

Observed October 19, 2026, MacBook Air M4, Metal, Unified English Q8, the same clips at 4× through metal_stream_tracks_offline_transcription: Metal qualified for streaming and both clips finished from the stream: 140/253 ms release-to-text (10/131 s) versus 156/2,838 ms whole-clip, with 0/28 and 1/408 word differences. This drives the production engine and Parakeet adapter, not the shortcut, microphone, or paste. Not measured: physical microphone corpora, noisy rooms, or Linux on a real GPU.

Observed October 28, 2026, Apple Silicon Mac (macOS 27), Metal, Unified English Q8, a 13.1 s say clip (40 words) at 1× through metal_stream_tracks_offline_transcription with live text: the text changed 12 times, once per 1.04 s chunk; the first words arrived 2.24 s into the audio, and at 12.6 s 35 of 40 words showed (about 1–2 s behind speech). The stable text was never rewritten and the tentative text was always empty, so with Parakeet's buffered settings live text grows in whole-chunk steps. The final text came from the stream 154 ms after release, with 0/40 word differences from the whole clip. Synthetic speech; not a physical microphone or the HUD.

Observed November 1, 2026, apollo (Radeon 8060S, Vulkan), Nemotron 3.5 ASR Q8, FLEURS clips replayed at 1× into the production engine with live text: the stream qualified and every clip finished from it in 34–40 ms after the release tail (163 ms for the first, cold), against 168–889 ms whole-clip for 10.6–54.6 s. German (17.9 s and 54.6 s, de) and Auto matched the whole clip word for word; live text showed its first words 2.4–5.0 s into the audio, updated once per 1.12 s chunk, and never rewrote stable text. A 43.7 s English clip of five sentences joined by 0.5 s of digital silence lost 13 of 96 words against the whole clip, which itself lost a sentence; Parakeet v3 lost more of the same clip, so treat long joined FLEURS clips as a weak test, not a streaming defect. A clip alternating English and German sentences with Auto kept each sentence in its language (1 word different from the whole clip), though both paths dropped one English sentence after a gap. Not a microphone, the HUD, macOS, or Windows.

Live text observed October 28, 2026, Apple M3 Pro, Metal, Unified English Q8, Hex Test.app at 84f594a with live-text, a physical microphone: the line appeared under the HUD and grew with the words; three dictations in a row reappeared with no trace of the previous text; a dictation released before its first words never showed the line and one released just after hid it; with the HUD at the Bottom the line sat above it, and with the HUD hidden it took the HUD's place at the top. Not observed: a second display, Dot scope placements, Linux, Windows.

Known defect, observed October 28, 2026 (same Mac and model): the stream can drop the first words spoken after a pause, and the paste carries the loss because a stream with text is never retranscribed. In real dictation the words that resumed after 6 s and 13 s pauses were lost or garbled. Replaying say speech around 3, 8, and 13 s gaps (digital silence and a −50 dBFS noise floor) through metal_stream_tracks_offline_transcription lost "I" after the 8 s gap of digital silence only, and the whole clip kept it: the loss depends on where speech resumes against the stream's chunks. Live text does not cause it: HEX_STREAM_LIVE_TEXT=0 gives the same text.

Mitigation, October 29, 2026 (#338's diagnosis, by ElliottH): the engine's PauseCap now feeds a dictation stream only the first 2.5 s and last 0.3 s of each pause (20 ms frames the meeting pause detector calls not speech); the clip and its offline fallback keep every sample, and "finished live stream" logs pauses_dropped_ms. /streams is unchanged. Check: a_dictation_stream_hears_only_the_ends_of_long_pauses (synthetic audio, a fake stream). Not re-measured on a real model here; ElliottH's Metal replays went from 18/36 to 0/36 lost words for 6–14 s gaps of silence or steady noise. Limit: noise that rises mid-dictation counts as speech for up to about 10 s, so that pause is not cut.

Fixed October 30, 2026 (issue 458): over steady background noise, speech only about 8 dB louder than the room missed the meeting detector's 12 dB speech margin, so the whole dictation counted as one pause and the stream heard only its first 2.5 s ("This in a test on the" for a 17 s clip over −35 dBFS noise). PauseCap now cuts only frames within 4 dB of the noise floor (or under −70 dBFS since #841) (FrameLevel::quiet); anything louder is kept. Noisier rooms keep more pause audio rather than lose speech. Check: quiet_speech_over_steady_noise_is_never_cut_as_a_pause (synthetic noise bursts, a fake stream; no real-model or microphone run).

Fixed November 1, 2026 (issue 841): a quiet built-in microphone (Linux, Wayland) carried whole dictations under −55 dBFS, so every frame counted as quiet and the stream again kept only the first 2.5 s, which pasted as a success. The absolute quiet bound is now −70 dBFS, and a stream whose cap heard no frame above the noise floor yet cut audio as a pause is unusable: the whole clip is transcribed offline (source=fallback). Checks: quiet_speech_over_steady_noise_is_never_cut_as_a_pause (now also −62 dBFS speech over −72 dBFS noise) and a_stream_that_heard_only_quiet_audio_falls_back_to_the_whole_clip (synthetic noise, a fake stream; no microphone run).

Fixed November 1, 2026 (issue 841, follow-up): on 3.1.2 the same microphone (a Ryzen built-in digital microphone) still kept exactly 2.5 s in the stream and relied on the fallback. Its level was measured with the DC offset included, which the dictation silence gate already removes; a steady offset louder than the speech pins every frame to the noise floor. SpeechFrames now removes each 20 ms frame's mean before measuring, for the pause cap and the meeting pause detector alike. Check: quiet_speech_over_steady_noise_is_never_cut_as_a_pause (now also −50 dBFS speech over −58 dBFS noise on a −26 dBFS DC offset; synthetic, a fake stream). The DC offset is inferred from the reporter's logs (clips that pass the −50 dBFS silence gate yet read as quiet), not measured on the device.

Fixed November 3, 2026 (issue 1101): a short last phrase after a pause of about 1 s could be missing from the paste while the clip held it: the stream finalized without it, and a stream with text was never retranscribed. PauseCap now records where each speech frame (the meeting detector's 12 dB margin) the stream heard ends, and after finalizing compares that with the end of the stream's last token. At least 300 ms of speech more than 400 ms past the text makes the stream unusable, so the whole clip is transcribed offline (source=fallback, reason "the stream's text ends at … ms but … ms of speech follows"). A key click or stop tone in the 200 ms tail is shorter than that. Checks: a_stream_that_dropped_a_phrase_after_a_pause_falls_back_to_the_whole_clip and a_short_sound_after_the_text_does_not_discard_the_stream (synthetic tones, a fake stream). Observed November 3, 2026, apollo (Radeon 8060S, Vulkan), Unified English Q8, an ad hoc probe through PauseCap and the real stream: a FLEURS clip cut to speech, 1 s of digital silence, and a 1.2 s phrase from the same speaker lost the phrase in the stream (with or without the macOS 400 ms trailing silence) while the whole clip kept it ("…channels 25 to 30 years"); the check flagged it. Seven ordinary clips (JFK, FLEURS, product names, 1.7–15 s) finished from the stream; an 84 s speech ending in applause fell back. The stream's own loss is not fixed upstream.

Nemotron 3.5 ASR, observed November 3, 2026, apollo (Vulkan, Q8, Auto): ten clips through PauseCap and the real cache-aware stream (JFK, FLEURS English and German, product names, and speech, a 1–1.2 s pause, and a short phrase from the same or another speaker) matched the whole clip word for word (one clip differed on a single product name), and none lost the late phrase, so the drop did not reproduce with this model. Its tokens carry real timings, but it puts the closing full stop on the clip's last frame, which would have hidden a dropped phrase; the text's end is now the last token with letters or digits (spoken_text_end_ms), and no clip fell back. Check: a_closing_full_stop_at_the_clips_end_does_not_cover_a_dropped_phrase. Not macOS or a microphone.

Ongoing Jobs And Output

Completed long recording                  // dictate.model-windows
├── Cohere -> Runs of at most 35 seconds, including the final remainder
└── Other GGUF models -> Existing backend-advertised input limit
    -> Transcribe each chunk -> Join text in original order

Chunking bounds inference, not capture duration. No silence is removed.

The shared max_audio_chunk_samples policy in transcription/models.rs is used by parakeet.rs and transcription/local_transcriber.rs. Cohere's pinned backend reports a 400-second encoder capacity, while its reference processor uses a 35-second transcription window. Treating the encoder capacity as a useful inference window caused #73's lost passages. A stricter backend bound still wins; other model policies are unchanged.

cohere_chunks_at_reference_window_not_encoder_capacity checks the policy. The opt-in cohere_long_form_keeps_all_sections_with_and_without_pauses runs the real pinned model on synthetic speech. Reproduce it on macOS with scripts/test-cohere-long-form.sh and an explicit model path. The script uses Samantha speech synthesis, never microphone capture, and retains fixtures on failure (HEX_KEEP_COHERE_FIXTURES=1 keeps successes).

Observed September 4, 2026: the same 57-second fixture went from 44 word errors to zero after chunking; the 73-second paused fixture went from 71 errors to one. Short references transcribed correctly before and after. The retained native regression also passed a roughly 235-second repeated fixture, retaining all three sections in each repetition. This is Metal inference evidence on an M2 Max, not physical capture/paste or Linux runtime proof. Chunk boundaries can still affect individual words; this is not a claim of perfect recognition or silence handling.

The macOS Transcriber owns backend input padding for ordinary transcription. short_unified_input_is_minimum_padded_before_trailing_context in transcription.rs checks minimum-duration padding followed by Unified English's additional trailing silence. This is a pure sample policy check, not native inference-quality proof.

Fixed October 31, 2026 (issue 657): with Unified English on macOS, dictated sentences and questions pasted without their closing full stop or question mark. Streamed dictations (the Metal path) finalized with no trailing silence at all, and whole clips got only 200 ms. Both now get 400 ms of silence after the clip (UNIFIED_ENGLISH_TRAILING_SILENCE_SAMPLES; the stream hears it through PauseCap just before finalizing). Check: a_dictation_stream_hears_trailing_silence_before_it_finalizes in transcription/engine.rs (a fake stream). Not measured on the real model or a microphone; Linux and Windows streams are unchanged. Padding is now included in inference_ms; prepare_ms covers clip conversion and diagnostic retention.

Listening idle                            // dictate.model-memory
├── Keep the selected model weights warm // avoids loading on each dictation
└── Completed offline inference
    -> Release input-sized GGML/Metal compute scratch
    -> Keep only reusable model/session state

The selected GGUF remains resident by design; Parakeet v3's pinned Q8 artifact is 739,508,576 bytes before runtime metadata and decoder state. Input-sized scheduler scratch must not remain at its high-water mark after a dictation. The pinned transcribe-cpp 0.1.3 runtime owns that scratch for the session lifetime, so Hex rotates the lightweight session after every offline run while retaining the loaded model. A later dictation recreates its session without reloading the weights.

Observed September 18, 2026, isolated Metal probe on M2 Max: with the similarly sized Parakeet Unified English model, transcribe-cpp 0.1.3 grew from a 792 MB loaded footprint to 1,195 MB after a 45-second input and retained 1,195 MB after another short run. The same 0.1.3 runtime with Hex's session rotation measured 792 MB loaded, 801 MB after the long input, and 802 MB after the following short run. The probe used generated silence and no live microphone, UI, or paste. This establishes the backend scratch cause and reclamation on that model and machine, not Parakeet v3's exact whole-app footprint on the reporter's Mac. Activity Monitor can include additional app memory.

Accepted job on a GPU device             // dictate.inference-threads
├── Inference thread -> macOS user-interactive QoS
└── GGUF session -> One CPU decoder thread, no ggml worker pool
    // CPU-only devices keep the backend's default thread count

Source: session_options in transcription/gguf_session.rs and prioritize_inference_thread in parakeet.rs; check gpu_sessions_bound_decoder_threads_and_cpu_sessions_keep_library_default. Parakeet's per-token TDT decoder runs on CPU even with Metal encoding. ggml's default pool (up to 8 spin-waiting threads) is created with plain pthread_create, so its workers start at default QoS rather than inheriting the inference thread's.

Observed October 14, 2026, standalone transcribe-cpp 0.2.4 bench on a Mac Studio (18 cores) already busy with other work, Unified English Q8, Metal: under 36 extra yes processes, user-interactive QoS medians for 3/30/90-second clips were 170/703/1,819 ms with one thread, 136/1,907/4,940 ms with two, and 178/4,304/13,519 ms with four. Without the extra load, one thread measured 83/364/1,631 ms and two 64/705/2,889 ms. This is backend timing, not physical capture/paste on the reporter's MacBook Pro, and not Linux runtime proof.

Finish
  ├── Near-silent capture -> Discard, no inference    // dictate.silence-gate
  │       // loudest 30 ms window (DC removed) below -50 dBFS; no paste,
  │       // History, or last-result update; process.log records the level
  ├── Louder capture, no detected speech -> Discard, no inference
  │       // the Silero speech detector, once installed, finds no speech of
  │       // at least 160 ms before the release; process.log records
  │       // audio_ms and vad_ms (at debug level when speech passes)
  ├── Capacity available -> Accepted job -> Local transcription -> Mode processing
  │    -> Paste successful nonempty output once, at current focus
  │       // the clipboard write carries nspasteboard.org Transient and
  │       // AutoGenerated markers, so conforming clipboard managers skip it
  │       // a Chromium 155/156 app in front (read from its bundle's
  │       // framework version) gets the text typed instead (#212)
  │       // synthetic ⌘V resolves on any active layout: it uses the layout's
  │       // Command layer ("Dvorak – QWERTY ⌘"), and a non-Latin layout
  │       // falls back to the ASCII-capable layout like AppKit itself
  └── Queue full -> Explicit rejection                // bounded admission

A pending -> Start capture B                          // dictate.overlap
  -> B records independently; A is not cancelled
  -> Accepted outputs retain submission order
  -> Completing/pasting A must not end B

Source: finish_dictation in recognition.rs. The silence gate stops models from hallucinating closing phrases such as "thank you" on an empty hold (#108). It covers hotkey and voice-delimited captures on macOS, and the Linux hotkey loop. The local API's programmatic dictation and host-audio transcription are deliberately left ungated: they skip both silence checks, and only output that repeats the vocabulary prompt (Whisper, Qwen3-ASR) is dropped there. The threshold is conservative, so speech is not dropped; a noisy room, the start tone, or a key click can lift a capture above it. Those captures then go to the Silero speech detector (transcription/speech_detector.rs), which discards them when it finds no speech. The detector (1.2 MB, pinned revision and checksum in transcription/models.rs) downloads in the background whenever a transcription model is downloaded or activated, and at the first dictation, so an installed model still activates at once offline; a failed download is retried at the next dictation. Model preparation (macOS, Windows) and the Linux listener load it when present and never download it. It runs on the CPU with a speech threshold of 0.4 instead of Silero's 0.5, which misses a whisper at -43 dBFS. While it is missing or fails to load, captures above the threshold still reach the model, as before. Output that only repeats the vocabulary prompt (Whisper, Qwen3-ASR) is dropped as well (models). Other hallucinations after real speech are not addressed. silence_gate_discards_only_near_silent_captures and speech_detector_discards_loud_captures_without_speech in dictation.rs check the policy on synthetic samples with the detector's verdict fixed; real_detector_rejects_silence_and_noise, real_detector_hears_speech, and real_detector_hears_quiet_whispers (ignored; HEX_VAD_MODEL, HEX_VAD_SPEECH_WAV, HEX_VAD_WHISPER_WAV) run the real detector, and an_installed_model_does_not_wait_for_the_speech_detector checks the background download. Key resolution: keyboard.rs; non-Latin layout fallback details live in shortcut recovery. Checks in parakeet.rs: parallel_processing_results_are_released_in_submission_order, ordered_waiting_outputs_remain_bounded_after_channel_drain. These cover output bookkeeping/admission, not sustained end-to-end inference/paste. transcript_write_is_marked_for_clipboard_managers in paste.rs checks the markers on a private pasteboard; whether a given clipboard manager (Maccy, Raycast) honors them is not verified here. The restored previous clipboard is written back unmarked, exactly as captured. detects_the_chromium_major_version_from_the_bundle_framework and chromium_versions_need_four_numeric_components in paste.rs check version detection on fake bundles; typing_splits_text_into_twenty_unit_events_without_splitting_surrogate_pairs and typing_sends_each_line_break_as_one_shift_return in keyboard.rs check the typed events. Neither proves insertion into a real Chrome page.

Chromium 155 and 156: these versions on macOS can lose a ⌘V that races Chromium's own pasteboard change notification (#136, #212; regression Chromium CL 8217390, fixed by CL 8503629 in 157). The race depends on how busy Chrome's main thread is, so the earlier fixed 150 ms pre-paste delay still missed about 1 in 4 dictations and was removed; no app waits before ⌘V. Instead, when the frontmost app's bundle contains Contents/Frameworks/*Framework.framework/Versions/<a.b.c.d>/ with major version 155 or 156 (any version present counts, so a staged update is covered), Hex types the text as synthesized Unicode key events: at most 20 UTF-16 units per event, never splitting a surrogate pair, and every line break as Shift-Return so chat boxes don't submit. Typing leaves the clipboard untouched, so there is nothing to restore; ordering, continuation joins, Send's trailing Return, and Paste Last behave as for a paste. Chromium 157 and later, older versions, Electron apps (Versions/A), and every other app keep clipboard paste. Limits: the framework version is Chromium's for Chrome, Helium, Brave, Edge, and Chromium; a fork that names that folder after its own version (Opera, possibly Vivaldi) is classified by that number. Installed web-app shims carry no framework and keep paste. An active input method may treat typed events differently from a paste. Remove the typing path once Chrome 157 is the floor.

Windows paste (issue 477): Windows puts the transcript on the clipboard, waits for Shift, Alt, and Win to come up, sends Ctrl+V, and restores the previous clipboard 500 ms later unless a newer paste or another app changed it. Labs › Terminal paste (paste_with_shift, off by default; on Linux it is a General › Advanced row) sends Ctrl+Shift+V instead, for apps that ignore a synthetic Ctrl+V (the herdr desktop app). Shift goes down after Ctrl and up before it; a Ctrl the user still holds is never pressed or released by Hex. A saved change applies to the next paste without restarting the listener. Source: control_chord in windows/paste.rs. Check: paste_chords_press_shift_inside_control_and_leave_a_held_control_alone (the synthesized key sequence only; no paste into a real Windows app).

Toggle Cancel

Settings › General, Shortcut mode = Tap (Toggle)     // dictate.toggle-cancel
  -> Cancel row: the shortcut (`cancel_hotkey`), Escape by default
     ├── Recorder -> any chord the OS's rules allow (Escape ends the recorder)
     ├── Restore -> back to Escape
     └── Wayland -> no recorder: bind `hex dictation cancel` in the desktop
  Hold modes -> no row, no cancel
Toggled recording (held or locked) -> Cancel shortcut
  -> Recording discarded: nothing transcribed, pasted, or kept in History
  -> Cancel tone; the HUD shows it cancelled
  -> The key is kept from the app (press, repeats, release)
  -> Jobs already transcribing -> finish and paste in order
Idle, or any Hold mode -> Cancel shortcut -> Nothing: it reaches the app
hex dictation cancel -> Discards whatever records, any mode; idle -> unchanged
Annotate drawing -> Escape stops drawing first
Programmatic dictation (local API) -> Its owner cancels through /cancel

Until #786, Escape cancelled the active capture in every mode, then the newest unfinished job, and an accidental Escape wiped a 5-minute dictation. #795 removed cancel; the people who asked for it back use Toggle, so it returned there only, and never reaches past the recording. Hex takes the key only while a toggled recording runs: macOS's session tap suppresses it only while the listener arms it, Windows' hook only while its capture records, and X11 grabs it only for the recording. A shortcuts portal would hold the key from every app for as long as Hex runs, so Wayland binds nothing for it. Checks: the_cancel_shortcut_discards_only_a_toggled_recording and the_cancel_key_is_suppressed_only_while_armed in suppression.rs (macOS, not compiled on Linux), cancel_discards_a_toggled_recording, cancel_is_ignored_in_hold_mode, and cancel_is_not_taken_while_idle in linux/input.rs, cancel_is_wanted_only_while_a_toggled_capture_records in windows/input.rs, the_cancel_shortcut_shows_in_toggle_and_restores_escape and the toggle-cancel goldens in tests/panes.rs, and cancel_parses_and_takes_no_flags in control.rs. The X11 grab and the hooks' suppression have no automated end-to-end check.

Feedback

Record -> Release -> Transcribing/processing/paste -> Finished // dictate.feedback
  HUD + tones distinguish capture from pending work          // never take focus
  Pending processing/paste -> Still unfinished, not recording
  Shortcut press -> Start tone ~50 ms later                  // dictate.start-tone
     -> Chord within 40 ms (e.g. Option-Command-I) -> Silent
     -> Chord or short tap before 300 ms -> Tone fades out over 20 ms
     -> Hold reaches 300 ms -> Tone already played; mute/pause/sleep begin now
     -> Release microphone while idle -> Output warms on press; tone once
        the microphone is open and the hold counts
  Dictation pasted >= 2 s after the stop tone -> Done tone      // dictate.done-tone
     -> Pasted sooner (the usual case) -> Stop tone only; `DONE_MIN_AFTER_STOP`
     -> Empty, failed, cancelled, Paste Last, programmatic -> No done tone
  Failure -> Error tone; wake/sleep keep macOS Pop/Tink      // dictate.error-tone
     -> Classic has none: macOS Basso, Signature's error on Windows and Linux
     -> A lost microphone or input -> No tone; the capture ends
  Tone requested -> Playback worker opens the default output, then plays
     -> Output stays open five minutes past the last tone, then releases
     -> Released output holds no macOS idle-sleep assertion
  Feedback volume = 0 -> No tones
     -> Playback worker releases output on its next 250 ms observation
     -> Volume enabled again -> Reopen for the next tone, never capture/UI
  Cold audio stack exceeds preload admission -> Warn once; recognition continues
     -> Loader keeps running; later tones play once the output opens

Fixed November 1, 2026 (issue 873): on macOS the HUD remembered its last placement and skipped sending it to Rune again when it matched, so a HUD Rune had put in the wrong spot on a second display (offset by the display's origin, with a different backing scale) stayed there on every later dictation. Each capture's start and each hide now forget the placement, so the next show sends the display and rectangle again, as Linux and Windows already do. Not run on a two-display Mac; the misplacement inside Rune itself is not addressed here.

Settings > Sounds (next to Sound volume)                    // dictate.sound-set
├── Default / older settings -> Classic
├── Row shows the saved set -> Opens the Sounds sheet
│   ├── Stage -> Hex's own HUD, where this OS puts it, does what it does
│   │   │          at each sound of a short dictation
│   │   ├── Where -> Indicator position: top / bottom; Off shows none
│   │   ├── Start (0 s) -> Recording, with a demo voice's level
│   │   ├── Stop (1.4 s) -> Transcribing
│   │   └── Done (2.1 s) -> Done look, then it leaves
│   ├── Lone Error (macOS) -> Failed look 0.6 s (Dot scope coral), then leaves
│   ├── Chips -> Start / Stop / Done / Error (macOS) play one sound
│   │   └── Set has no such sound (Classic's or Thock's done) -> Chip faint and inert
│   ├── Each set -> Name, what it sounds like, play button
│   │   ├── Play -> Plays its dictation without choosing it; again stops it
│   │   └── Press the set -> Save and apply live; play its dictation
│   ├── Any playback -> Stops the one before it (20 ms fade)
│   ├── Sound volume -> Plays at it; Off plays nothing, the HUD still shows
│   └── Close / Escape / scrim / leaving the pane -> Stops playback
├── hex settings set sound_theme <signature|classic|click|soft|wood|analog|thock>
└── Every set -> Same loudness per sound at a given volume

The sheet is a modal like the shortcut explainer, opened from a sheet_button because its choices need room to be heard, not a menu. Checks: auditions_play_each_sound_at_its_moment, stopping_an_audition_fades_all_of_it, each_moment_shows_from_its_sound_to_the_next, the_hud_shows_a_dictation_and_then_leaves, a_lone_sound_shows_its_moment_and_leaves, dot_scope_turns_coral_on_failure, the_sound_sets_sheet_plays_and_chooses_sets (Linux), and the sound-sets, and sound-sets-playing goldens. The stage replays Hex's HUD on the sheet's clock: dictation/indicator's model, drawn by the window's compositor with rune_hud's painter and rune_hud.wgsl, as the real overlay draws it. It is a preview, not the overlay: Dot scope's in-bar and notch placements show below the menu bar, the voice is HUD Lab's demo, not the microphone, and it ignores Reduce Motion.

Hex signature (2026-10-27) is one kalimba-like FM voice playing one motif, D-B-A-D (a sixth, since "hex" is six): start D5 to B5 (116 ms), stop B5 to A5 (230 ms), done D6 over A5 (330 ms), error D4 then a minor D4+Bb4 (280 ms), and onboarding success, the whole motif landing on a D sixth chord (1.1 s), played when the first run reaches Hex is ready. Click is mechanical key clicks, Soft tone warm sine blips in A major, Wood a soft-mallet marimba with hollow wood blocks for error, and Warm synth two detuned saws through a closing low-pass filter, and Thock one mechanical keyboard key, from the team's vote on the start sound (its winner, an ElevenLabs take kept in scripts/sounds/thock/): start is the press, stop the same key's upstroke, error two keys at once, twice, and success a run of keys up to a full press; it has no done sound because the stop already says it. Classic is the sounds Hex played before the signature set (start clicks, stop swish), restored from git; it has no done tone, errors play macOS Basso as they did, and onboarding success plays its stop sound (the other sets have their own). The done sound of every set plays only when the paste lands at least DONE_MIN_AFTER_STOP (2 s) after the stop sound, as after a slow rewrite; the Sounds sheet's demo still plays it. Checked by done_plays_only_after_a_slow_paste.

Each set is a folder in resources/audio/sounds/ made by scripts/sounds/synth.py (uv run scripts/sounds/synth.py rewrites them; signature regenerates byte-identical). Synthesized sounds are 48 kHz mono WAVs starting on their first sample; Classic keeps its 44.1 kHz stereo, trimmed to start at its first audible sample. Every sound is normalized to one loudness per kind (start -25.5 LUFS max momentary, stop and done -27, error -26.5, success -25), so a volume sounds the same in every set. Classic's start and stop come up 2.5 and 3 dB (a soft limiter touches 0.1% of the start's samples). There is a cancel sound for Toggle's cancel shortcut. Every OS plays the error sound for a failed dictation or paste (fail in linux/dictation.rs and windows/dictation.rs), including an empty transcript, which macOS treats as nothing pasted. Checks: bundled_sounds_are_short_and_sound_immediately (each sound under its length budget; every start, and every synthesized sound, audible within 1 ms), every_set_but_classic_has_every_sound, sound_set_offers_every_set. The sounds were checked by waveform, spectrum and loudness only, never played aloud on a Mac.

Feedback admission is advisory on every platform: a cold audio stack can exceed the two-second preload timeout, so the recognition worker logs a warning and continues rather than stopping. The loader thread keeps running and registers the player when the default output finally opens, restoring tones for the rest of the session. Linux already logged and continued; the macOS worker now matches that behavior instead of stopping desktop recognition.

The playback worker never opens the output at startup. It opens the default output for the first tone, keeps it through a five-minute idle grace so bursts of dictation share a warm device, and releases it after that grace or on the next observation after sounds turn Off. macOS idle-sleep timers count from the last user input, and a tone implies recent input, so the grace never delays sleep by itself. A device open already in progress must return before the worker can observe that change. Output-open failures retry at most once per two seconds, and a released device reopens without that backoff. Bundled samples remain decoded in memory. A tone that arrives after the grace pays a cold device open first; the tone is delayed, not clipped or dropped. Overlapping tones extend the playing window rather than shortening it.

Fixed after 2.3.0: the start tone used to wait for the 300 ms intentional hold, then pay rodio's default 50 ms output buffer, so it sounded about 345 ms after the press (about 375 ms from a cold device) where the Swift app rang on key-down. The press now requests a provisional tone. The worker opens the output at once, waits out a 40 ms chord grace (on Kit's Mac about 40% of discarded presses were modifier chords landing within ~25 ms), then plays through a 512-frame (~10 ms) buffer: about 50 ms warm or cold on the built-in speakers. A chord or short tap retracts the tone, dropping it inside the grace or fading it over 20 ms. Recording mute, media pause, and idle-sleep prevention still begin only at the 300 ms threshold. Checks: provisional_start_sounds_after_the_chord_grace_unless_retracted, retracted_tone_fades_out_without_a_click_or_channel_skew, and the ignored native_start_tone_path_latency (open-to-presentation timing, with HEX_TONE_PATH=legacy for the old path; route output to a silent device or set HEX_TONE_MUTED). The chord-to-retraction path is checked by reading the code, not by a physical keyboard run.

Fixed in 2.1.21: through 2.1.20 the worker opened the output at startup and kept it open for as long as sounds were On, so coreaudiod held a PreventUserIdleSystemSleep assertion for Hex's PID and an idle Mac never slept (#95). 2.1.18 released the stream only when sounds were Off. Startup admission therefore no longer reports could not open the audio output; a device failure surfaces as a warning when the first tone plays. output_opens_for_a_tone_and_releases_after_the_idle_grace and overlapping_tones_extend_playback_instead_of_truncating_it check ownership, grace, and drop with a controlled sink; failed_output_initialization_retries_with_a_bounded_backoff checks failure-to-success recovery without an audio device. The opt-in native_output_release_clears_the_idle_sleep_assertion opens the real default output and reads pmset -g assertions; on September 22, 2026 it observed the assertion appear for the test PID and clear within a second of release on the built-in speakers. That is not an installed-app or USB/Bluetooth measurement. timed_out_admission_still_publishes_a_usable_player checks the production publication path after the admission receiver has gone away. These checks do not measure the reporter's CoreAudio CPU usage or establish audible playback.

Contributor-reported September 10, 2026 (PR #84): a locally built /Applications/Hex.app (approximating 2.1.16 plus this change) was started right after a restart where the stock build had stopped its desktop recognition worker with timed out preloading feedback audio. The patched build started on the first attempt: the worker reached dictation listener started and resolved paste key … paste_key_code=9, and held-Option dictations pasted into the focused app. The late-registration path (loader still running when the admission wait times out) is implemented, but its end-to-end tone recovery has not yet been observed on hardware; it is expected behavior, not executed evidence.

Linux now uses the same recording sounds with its own persisted Sound volume control; see Linux feedback for its entry points, timing, defaults, and verification limits.

Check: in a debug app, hex dev hud recording|transcribing|processing|reset or the HUD Lab pane drives the real HUD with deterministic state, so fullscreen and Space transitions can be checked. (The dictation-hud preview that looped the sequence went with GPUI.) This does not prove that actual capture caused that state.

The macOS HUD is a click-through, nonactivating NSPanel with CanJoinAllSpaces, CanJoinAllApplications, and FullScreenAuxiliary collection behaviors. The cross-application flag explicitly allows the overlay to join another application's fullscreen Space (#75); panel style flags on an ordinary NSWindow were not sufficient.

Each time the HUD shows, Rune clears and resets those collection behaviors before ordering the panel in, and orders it out first if it is still shown on another Space (#1084). Changing the activation policy (the Dock icon follows Settings) could leave the panel tied to one Space while AppKit still reported the old mask, and re-setting an unchanged mask does nothing. Not yet observed on hardware; only the macOS compile is checked.

At the top position the HUD sits 6 pt below the menu bar (rune_hud::hud_placement::TOP_OFFSET), or below the notch (NSScreen.safeAreaInsets) where a fullscreen Space hides the menu bar and visibleFrame reaches the top of a notched screen. Screens without a notch and normal Spaces keep their position. Check: scripts/check dictation::indicator::tests::top_capsule covers the math only; placement over a fullscreen app on a notched Mac is not yet observed.

Dot scope

The HUD is Dot scope on every OS (rune_hud/dot_scope.rs, dot_scope_fragment in rune_hud.wgsl, drawn by a Rune overlay). It was the dot-scope experiment (#264) until 2026-10-29, when it replaced the glossy capsule, which Omarchy users on X called a "static red thingy"; the capsule's shader and HUD Lab styles are deleted. It is the t422 prototype's look A with its values. Linux and Windows draw it in a 304×80 pt window at the HUD's edge (no menu-bar or notch placement), on the display under the pointer when the capture starts, as macOS does (#486; rune_hud::display). Windows and X11 read the pointer and pick its monitor; Windows keeps it inside that monitor's work area and draws it at that monitor's scale. A layer-shell surface pins no output, so niri, Sway and Hyprland put it on the focused output. GNOME and KDE on Wayland keep the primary display; on X11 they follow the pointer like any other X11 desktop (#1200; check only_gnome_and_kde_on_wayland_keep_the_primary_display, not run on a two-monitor GNOME X11 session). On Windows and X11, while the HUD shows it checks the pointer's display every 250 ms (Hud::follow_pointer, Rune's refollow_pointer), and the HUD and live text move there, without animating or taking focus, once two checks in a row find the pointer on the same other display (250–500 ms), so a pointer on a display's edge doesn't flip it between them (#807). A position change or the next capture places it again (tested by reading the code only, not on multi-monitor hardware). On Windows the window is a topmost, click-through tool window with no redirection bitmap, drawn through DirectComposition. Rune shows and hides it with Win32 calls alone and re-asserts its styles on each show; winit never sees its visibility. The click-through style makes it a layered window, which Windows composes only once it has layered attributes; Rune gives it opaque ones (5778a3a3). Only a DX12 DirectComposition swapchain composites alpha on Windows: a Vulkan surface is a swapchain on the HWND, which this window shows black (#499) or not at all (#510, a hybrid laptop where the integrated GPU's Vulkan driver claimed premultiplied alpha). Rune draws transparent Windows surfaces only through DX12, on the lowest-power adapter that drives a display, and fails the overlay when none can (93c7a04b; from reading the code and Windows' rules, not yet observed on Windows). If the window, its GPU surface or the show fails (including a cloaked window), Rune reports why (OverlayHandle::failure). Hex logs the HUD's surface (backend, adapter, alpha mode) once, and treats a dictation start whose HUD presents no frame within 1.5 s as a failure too. Each failure is logged as an error in process.log once per reason, and Settings shows it as the listener's warning (HudSender::failure). Every OS takes dark or light and Reduce Motion from Rune, and loads dot-scope.json at launch.

Dot scope                                    // dot_scope
├── Recording -> A scanner unfolds a resting braid; the voice's level widens it
│   ├── Auto level (default) -> 24 dB below the recent peak (3 s release),
│   │   at least 16 dB above the room and the −60 dB noise floor   // AutoLevel
│   ├── Auto level off -> Fixed RMS window, −40 to −10 dB
│   └── Warm pearl; history travels out from the centre; phosphor tail 0.6 s
├── Locked / hands-free -> Same as recording; the HUD draws no lock
├── Transcribing / Processing -> Processing look, at least 200 ms so a fast
│   transcription still shows it (PROCESSING_MIN_DURATION):
│   ├── Flow (default) -> The braid keeps turning; bands of light travel right
│   ├── Pulse -> Rings swell in at the middle and travel out
│   └── Sweep -> Braid irons flat; a cool beam sweeps 76 pt (round 1)
├── Completed -> Every look clears from the middle out (0.3 s; Sweep's
│   beam fades where it was and its dark field shows only the two fronts);
│   100 ms in, the HUD (or island) collapses, so the two end together
├── Failed -> Coral for 0.6 s, then fades
├── Cancelled / discarded -> Fades at once
├── Reduce Motion -> A still braid; opacity fades only
├── Dark / light -> Read from AppleInterfaceStyle at each capture start
└── Queued jobs -> Not drawn                // gap

The simulation steps at a fixed 240 Hz, so any frame rate draws the same picture. Placement (rune_hud/hud_placement.rs, pure geometry the capture capsule can reuse) puts it on the screen with the pointer:

Placement                                     // rune_hud::hud_placement::place
├── Below the menu bar (default) -> 6 pt under the menu bar, or above the Dock
├── In the menu bar -> A plate fitted to the bar's height, centred on a flat
│   screen, just right of the notch on a notched one
├── From the notch -> A black island grows out of the notch, 28 pt wider
│   each side (Island past the notch, 0–60 pt), and shrinks back into it;
│   the field fills it at the same dot spacing (more columns), inset 7 pt
│   all round with corners that follow the island's, snapped to whole
│   columns so the dots sit as far from the sides as from the top and
│   bottom; In the menu bar on a flat screen
├── Pretend notch -> Draws the island from a 185 pt notch on a flat screen
└── Over the menu bar -> Window level NSStatusWindowLevel + 1, click-through

The window is 320×128 pt so the widest plate, its shadow, and the widest island fit. Notch metrics come from safeAreaInsets.top and auxiliaryTopLeftArea/auxiliaryTopRightArea; the bar's height from NSStatusBar.systemStatusBar.thickness. HUD Lab tunes it live in debug builds, and in release builds while the playground experiment is on (Settings › Labs › HUD Lab):

HUD Lab
├── Open -> The HUD shows a demo: Recording follows a synthetic voice
├── Move a slider -> Applies live to whatever the HUD shows, a real
│   dictation included; the demo doesn't restart
├── Recording / Transcribing / Processing -> Restarts the demo in that state
├── Processing, Placement, Pretend notch, Auto level, Island past the notch
│   -> Apply live
├── Save -> Writes dot-scope.json in Application Support; the HUD loads it at launch
├── Copy values -> The same JSON; Reset -> the prototype's values
└── Leave the pane or close the window -> The demo hides; unsaved values
    stay on the HUD until Hex restarts

The playground (⇧⌘D, experiment playground) lists HUD stories (hud_stories.rs), so every combination plays without HUD Lab:

Prototypes › HUD                             // R plays, P next look, A auto level, S stop
├── Notch, Pretend notch, Menu bar, Below the menu bar -> A whole dictation
│   from that placement; variants <look>-<ending>: flow, pulse, or sweep
│   with done, cancel (while recording), or fail (while processing)
├── Processing -> Records, then holds processing 12 s
├── Your voice -> Waits for your shortcut and meters your microphone
│   └── Both: variants hud-lab, notch, pretend-notch, menu-bar, below
└── Links name the story and variant:
    hex://story/prototypes/hud/notch?variant=pulse-fail&play=1

The HUD is its own Rune overlay, not a view in the window, so a story drives the real HUD at the top of the screen and its stage only says what it shows. Playing a story puts its placement and look into the one tuning it shares with HUD Lab (rune_hud::dot_scope::lab).

Hex meters no spectrum, so the prototype's band emphasis is the same for every component.

Check: scripts/check rune_hud::dot_scope (defaults, knobs, frame-rate independence, Reduce Motion, the stages, auto level, each processing look settling dark), scripts/check dictation::indicator (phases, the processing minimum, failure and cancel, a hot microphone still moving), scripts/check rune_hud::hud_placement (each placement on flat and notched screens), and rune_hud::tests::rune_hud_frames (ignored; renders a fixed dictation through the real shaders offscreen into PNG frames, with HEX_HUD_TUNING for the look and placement over a pretend 14-inch MacBook Pro's menu bar). On 2026-10-28 its Dot scope and notch frames matched the retired Metal HUD's render of the same inputs on hailmary to within 3/255 per channel; on 2026-10-29 a headless capture on apollo showed recording, processing, and done after Dot scope became the default. Not yet observed on a real notched display, over a fullscreen app, or with a hidden menu bar.

Hex itself never activates on paste. Another app's modifier-tap hotkey once switched Spaces before the paste; the chord breaker keeps a held bare-modifier dictation from reading as a tap.

Observed September 14, 2026: the unmodified HUD was not found on screen over a native fullscreen TextEdit fixture. Changing only the window class to NSPanel did not produce a visible HUD in the initial check. The candidate with CanJoinAllApplications and the repeating fixture visibly showed the red HUD over fullscreen TextEdit, captured through Cua Driver. This was a debug preview with no microphone capture, not a signed installed-app or multi-display test. Kit independently confirmed the repeating candidate was visible in fullscreen.

Combined verification September 14, 2026: 468 Rust tests passed with ten native/opt-in checks ignored, plus all twelve keyboard-layout child scenarios in debug and release. Strict all-target/all-feature Clippy, 46 command-SDK tests, and the command-SDK build passed. The native preview also built successfully. These checks preceded the app release.

2.1.18 candidate verification September 14, 2026: the optimized suite passed all 468 Rust tests and twelve keyboard-layout child scenarios; ten opt-in checks were skipped. Strict release all-target/all-feature Clippy and app-identity fixtures passed. The signed, notarized bundle's isolated HUD preview was visibly present over native fullscreen TextEdit. This exercises the packaged renderer, not microphone capture or an installed-app update. The preview and disposable TextEdit process were stopped afterward.

Gaps And Constraints

  • Physical input: Fn/Globe, assistive input, remapped modifier sides, and cross-tap delivery ordering need their own native evidence. A passing Option fixture does not cover every binding. See shortcut recovery.
  • Full user path: source connections and individual checks do not prove the complete path on a user's Mac. Retain action-to-result evidence for hold, lock, Escape passing through, and a second capture during older processing in a controlled target app. Observe insertion and input responsiveness, not just the HUD.
  • Microphone modes: verify onset and idle closure separately for Keep ready and Release when idle on an identified device. The macOS microphone indicator while Keep ready is idle is expected, not proof of recording.
  • Streaming: dictate.streaming has benchmark, synthetic, and Linux CI (CPU-forced) evidence only. Real microphones, room noise, Bluetooth inputs, and a physical shortcut on a Mac or a Linux GPU have not been observed with a live stream.
  • Paste consumption: current paste policy waits a fixed interval rather than acknowledging target consumption. Internal ordering does not establish correctness for an arbitrarily slow target application.
  • Native checks require deliberate setup: permissions, model, keyboard, microphone, output device, and a disposable text target must be recorded. This map does not authorize operating the installed app or injecting global input.

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 hex19/hex