Hold a key → speak → LLM-polished text lands in whatever input you were using, in under a second. Local and cloud transcription, batch and realtime, minimal UI.
Framework (user-mandated): Native SDK (native-sdk.dev, vercel-labs/native v0.4.1, Apache-2.0) — UI in declarative .native markup, logic in Zig, compiled to a real native macOS binary. No Electron, no webview. All research below verified against live docs on 2026-07-10; items marked
How this plan is used: §0–§2 are shared context pasted to every coding agent. §3 contains one self-contained work order per phase — each is spawned as an Opus agent with max effort. §4 is the orchestration playbook (waves, gates, merge rules).
idle ──key-down──► recording ──key-up / ✓──► transcribing ──► formatting ──► injecting ──► done
▲ │ Esc or ✕ = cancel │
│ ▼ ├─ focused editable field → text inserted at cursor
└────────────── HUD hides ◄── success flash / error state ◄────────────────────┴─ no valid target → clipboard + fallback card (Copy)
- Two engines classes: Local (on-device whisper.cpp; later sherpa-onnx/Parakeet) and Cloud (BYO API key: Groq, Deepgram, OpenAI, Mistral, ElevenLabs).
- Two delivery modes: Batch (default; paste once after speech) and Realtime (streaming partials; only finalized segments are typed into the field — never rewrite text already committed to the user's app). Realtime with no focused editable field at key-down → auto-degrades to batch for that utterance.
- Formatting step: fast cloud LLM fixes grammar/punctuation/casing, strips fillers and self-corrections ("no wait, I mean"), applies dictated structure (lists, "new paragraph"). Never adds content. Output = text only. Skipped for ≤3-word utterances; hard 1.2 s timeout → paste raw transcript (speed is never held hostage). User-switchable model + Off.
- Keybinds (rebindable, recorder UI): dictation key default Fn/Globe — hold = push-to-talk (release ends), quick tap (<300 ms) = toggle;
Esccancel; paste-last-transcript default⌥⇧V. Fallback preset⌥Spaceoffered in onboarding. - Fallback card (matches screenshot): when injection has no target (no field focused, secure field, or AX failure) — dark card above the HUD pill: hint "Select a text field, then dictate", the transcript, a Copy button, ✕. Text is already on the clipboard when the card appears.
| Path | Target p50 key-up→inserted | Stage budget |
|---|---|---|
| Cloud batch (Groq turbo), 10 s utterance | ≤ 900 ms | finalize 30 → upload+STT 450–650 → LLM 250–350 → inject 80 |
| Local batch (whisper turbo-q5, M-series) | ≤ 1.2 s | progressive chunk transcription during speech; only tail remains at key-up |
| Realtime partial lag | ≤ 500 ms behind speech | provider interim + render |
- Main window — "Welcome back, {name}"; day-grouped history list (hover: copy / delete); right stat rail: total words · avg WPM · day streak (serif numerals); gear → settings.
- Settings — Transcription (Local|Cloud segmented; model dropdown; per-provider API-key fields → Keychain + Test button; local model manager with download progress), Formatting (provider+model, On/Off), Keybinds, Behavior (realtime toggle, launch at login, history on/off + clear), Language hint.
- HUD pill — bottom-center of the active screen, dark rounded pill
✕ | live waveform | ✓; states: recording / processing (spinner) / ✓ flash / ⚠ error. Must never steal focus. - Fallback card — above the pill, as described.
- Tray — menu-bar app (Dock-hidden accessory): Start dictation, Copy last transcript, Settings, Quit.
- Onboarding — 3 permission steps (Microphone → Accessibility → Input Monitoring) with live status + System Settings deep links, then a playground text field.
- Scaffold:
npm i -g @native-sdk/cli && native init shriflow· dev loopnative dev(hot reload) ·native check(ms-fast view validation) ·native test(headless UI tests) ·native build·native package --target macos. - Agent skills (mandated):
npx skills add vercel-labs/native, plus pipenative skills get core --full | native-ui | automationinto.claude/skills/…/SKILL.md. Every app embeds an automation server:native automate snapshot | widget-click | screenshot | wait— our agent-driven E2E loop. - App model:
Modelstruct +Msgtagged union +update(); effects via.update_fx/.init_fx— "subprocesses, HTTP, files, timers flow through the effects channel and back intoupdateas ordinary messages". Background threads/modules reach the loop via the bridge dispatcher (RuntimeOptions.bridge) /runtime.dispatchEvent⚠️ exact idiom fromcore --fullskill. - Runtime API:
createWindow(.{label,title,default_frame: RectF}) / listWindows / focusWindow / closeWindow(max 16),createTray/updateTrayMenu/removeTray(NSStatusItem; menu clicks →CommandEventsource=.tray),read/writeClipboard,set/get/deleteCredential(Keychain),showNotification, dialogs,openExternalUrl. app.zon:id, name, display_name, version, icons, platforms, permissions, capabilities (gpu_surfaces, native_views, menus, shortcuts…), commands(≤256), shortcuts(≤64, in-window only), menus, url_schemes, security; windows via.windowsor.shellview trees; titlebarstandard|hidden_inset|chromeless. Validate:native validate app.zon,native doctor --strict.- Custom drawing:
kind=.gpu_surface(Metal),present_mode=.timerfor animated content (waveform),GpuSurfaceFrameEventtiming,runtime.supports(.gpu_surfaces). - Extensibility:
extensions.Module(info+start/command/stophooks) in aModuleRegistry; custom-build examples shipbuild.zig→ we link C libs/frameworks freely (Zig compiles C and Obj-C.mfiles natively). - Signing:
native package --target macos --signing identity --identity "Developer ID Application: …" [--entitlements file] [--team-id];adhocmode for local; templateassets/native-sdk.entitlements; notarize viaxcrun notarytool … && xcrun stapler staple;zig build dmg.⚠️ Info.plist extra keys (NSMicrophoneUsageDescription) undocumented → post-process bundle with PlistBuddy in a build step ifapp.zoncan't express it. - SDK gaps we fill in Zig (pure C APIs unless noted): global hotkeys w/ down+up (
CGEventTapCreate), focused-element probe + text injection (AXUIElement*), mic capture (AudioUnit HAL), non-activating floating panel (NSPanel via small Obj-C shim — Risk 1), local STT (link whisper.cpp), accessory activation policy (NSApp.setActivationPolicy(.accessory)via shim).
| Provider | Model | Mode | Price | Notes |
|---|---|---|---|---|
| Groq ⭐batch default | whisper-large-v3-turbo |
batch | $0.04/hr, 10 s min bill | 228× realtime; OpenAI-compatible POST https://api.groq.com/openai/v1/audio/transcriptions; flac/mp3/m4a/ogg/wav/webm; verbose_json word timestamps; 25 MB free-tier file cap; multilingual; no streaming |
| Deepgram ⭐realtime default | nova-3 (+ flux-general-en end-of-turn model) |
WS streaming + batch | ~$0.0077/min stream |
wss://api.deepgram.com/v1/listen, interim results ~300 ms, endpointing/utterance-end events; raw PCM16 accepted |
| ElevenLabs | scribe_v1; Scribe v2 Realtime |
batch + WS | $0.22/hr; $0.28–0.39/hr rt | ~150 ms claimed rt latency (p95 ~250); 90+ langs |
| Mistral | Voxtral Mini Transcribe 2 (v26.02); Voxtral Realtime (open-weights 4B, sub-200 ms) |
batch (+rt later) | diarization, context biasing, ≤3 hr files | |
| OpenAI | gpt-4o-transcribe / gpt-4o-mini-transcribe / whisper-1 |
batch + rt WS | the OpenAI-compat reference shape | |
| Custom | any OpenAI-compatible base URL | batch | — | free coverage of Fireworks etc. |
| Provider | Model | $/1M in/out | Speed | Role |
|---|---|---|---|---|
| Groq ⭐default | llama-3.1-8b-instant |
0.05/0.08 | 840 tok/s | ~150-tok cleanup ≈ 250–400 ms; same key as Groq STT |
| Groq | gpt-oss-20b |
0.075/0.30 | 1,000 tok/s | alt on same key |
| Cerebras | gpt-oss-120b |
free tier | ~3,000 tok/s | quality+speed monster |
gemini-2.5-flash-lite |
0.10/0.40 | fast, free tier | budget alt (gemini-3.1-flash-lite = 0.25/1.50) |
|
| — | Off | — | 0 ms | raw passthrough |
- v1 engine: whisper.cpp v1.9.1 (MIT; C API
whisper.h; Metal first-class; built-in Silero VAD--vad). Models (ggml, Hugging Faceggml-org/whisper.cpp):large-v3-turbo-q5_0≈574 MB (default "Quality"),small-q5_1≈190 MB ("Balanced"),base.en-q5_1≈60 MB ("Light"). Progressive chunked transcription during speech → only the tail transcribes at key-up. - v1.5: sherpa-onnx (Apache-2.0, C API, prebuilt macOS arm64): Parakeet
tdt-0.6b-v2int8 offline + streaming Zipformer (true local realtime) + Moonshine + Silero VAD. Same abstraction, new backends. - Prior art validating stack: VoiceInk (GPL-3, whisper.cpp + Parakeet, KeyboardShortcuts lib, SelectedTextKit).
- Hotkeys: one session
CGEventTapforkeyDown/keyUp/flagsChanged. Fn/Globe =flagsChanged+kCGEventFlagMaskSecondaryFn; active tap consumes the event to suppress the system Globe action⚠️ (if macOS still triggers input-source switcher, onboarding instructs Keyboard → "Press 🌐 key to" = Do Nothing — the standard peer-app workaround). Needs Accessibility + Input Monitoring TCC. - Focus probe:
AXUIElementCreateSystemWide→kAXFocusedUIElementAttribute; editable = role ∈ {AXTextField, AXTextArea, AXComboBox, web text roles} ∧AXValuesettable; secure =kAXSecureTextFieldsubrole → never inject. - Injection router: ① paste-restore (default): save pasteboard → write text +
org.nspasteboard.ConcealedType→ synthetic ⌘VCGEventPost→ restore old contents after ~300 ms; ②CGEventKeyboardSetUnicodeStringtyping in ~20-char chunks (terminals / paste-hostile apps; per-app override map); ③ AXkAXSelectedTextAttributeinsert where reliable. Focus re-probed immediately before injecting. - Mic: AudioUnit HAL, pre-warmed at launch (instant start on key-down), 16 kHz mono f32→i16 ring buffer, RMS levels ~30 Hz to HUD.
- Permissions:
AXIsProcessTrustedWithOptions,IOHIDCheckAccess/IOHIDRequestAccess, mic via AVFoundation request; deep linksx-apple.systempreferences:com.apple.preference.security?Privacy_{Accessibility,ListenEvent,Microphone}. Dev builds must sign with a stable identity (adhoc re-signs reset TCC grants).
§2. Architecture, contracts & conventions (frozen by P0; agents MUST NOT edit contracts.zig — propose changes to the orchestrating session instead)
shriflow/
PLAN.md app.zon build.zig assets/…
.claude/skills/{native-core,native-ui,native-automation}/SKILL.md
vendor/whisper.cpp/ # pinned submodule, Metal ON
src/
main.zig # app root: Model, update(), update_fx wiring
contracts.zig # ← single source of truth: types + Msg + module command API
session.zig # dictation state machine (P3)
app.native # markup: main window, settings, onboarding
hud.zig # HUD pill + fallback card views (P4)
platform/ module.zig hotkeys.zig focus.zig inject.zig mic.zig panel.zig panel_shim.m permissions.zig
engines/ stt.zig stt_openai_compat.zig stt_deepgram.zig stt_elevenlabs.zig stt_whispercpp.zig models.zig format.zig format_openai_compat.zig
store/ settings.zig history.zig keys.zig
telemetry.zig
tests/ # native test suites per phase
pub const AudioChunk = struct { samples: []const i16, sample_rate: u32 = 16_000, ts_ms: u64 };
pub const FocusInfo = struct { editable: bool, secure: bool, app_bundle_id: [:0]const u8, role: [:0]const u8 };
pub const InjectMethod = enum { paste_restore, type_unicode, ax_insert };
pub const InjectOutcome= enum { injected, no_target, secure_field, failed };
pub const EngineId = enum { groq, openai, mistral, elevenlabs, deepgram, custom_compat, whispercpp };
pub const Transcript = struct { text: []const u8, duration_ms: u32, engine: EngineId };
pub const Partial = struct { text: []const u8, final: bool }; // streaming
pub const PermKind = enum { microphone, accessibility, input_monitoring };
pub const PermState = enum { unknown, denied, granted };
pub const Msg = union(enum) {
// platform → app
hotkey: struct { action: enum { down, up, tap, cancel }, },
audio_level: f32, // RMS 0..1 @ ~30 Hz
inject_done: InjectOutcome,
permission: struct { kind: PermKind, state: PermState },
// engines → app
stt_partial: Partial,
stt_done: union(enum) { ok: Transcript, err: EngineError },
format_done: union(enum) { ok: []const u8, timeout_raw: []const u8, err: EngineError },
model_download: struct { model: []const u8, pct: u8, done: bool, err: ?[]const u8 },
// ui/store
settings_changed: SettingsPatch, history_loaded: …, ui: UiMsg,
};
// Platform module command surface (dispatched via extensions dispatchCommand)
pub const PlatformCmd = union(enum) {
start_mic, stop_mic,
probe_focus, // → replies with FocusInfo msg
inject: struct { text: []const u8, method: ?InjectMethod }, // → inject_done
set_binding: Binding, // dictation key / paste-last
hud: union(enum) { show_recording, show_processing, show_success, show_error: []const u8, show_card: []const u8, hide },
check_permissions, request_permission: PermKind, open_settings_pane: PermKind,
paste_last,
};
// Engine interfaces (vtable structs)
pub const SttEngine = struct { /* caps():SttCaps; transcribe(audio,opts,post:*PostFn) void; openStream(opts,post) ?*StreamSession */ };
pub const StreamSession = struct { /* pushAudio(AudioChunk); finish(); cancel(); */ };
pub const Formatter = struct { /* format(text, ctx: FormatCtx, post) void — owns skip-heuristic + 1.2 s timeout → .timeout_raw */ };- Zig 0.14+ style per repo
zig fmt; errors as typed error unions — no panics on user paths; every fallible boundary returns aMsgerror variant that the UI can render. - All engine/network work off the UI thread; results only enter the app through the bridge dispatcher as
Msgs (idiom documented by P0 incontracts.zigheader after readingcore --full). - Allocation: arena per request/session; no leaks under
native test(leak-check on). - Secrets only via
runtime.credentials; never in settings JSON or logs. Audio buffers never touch disk (except opt-in debug). - Telemetry: every stage wraps
telemetry.span(stage); spans land on the history record. - Each phase adds
native testcoverage + (where UI-visible) anative automatesmoke script undertests/;native checkmust pass before an agent reports done. - Commit style:
P<phase>: <what>— one phase per branch/worktree (see §4 merge rules).
Objective: compilable skeleton + frozen contracts so waves can run in parallel. Tasks:
git init; commit this plan asPLAN.md.npm i -g @native-sdk/cli;native init shriflow(flatten into repo root); verifynative devrenders.- Install skills:
npx skills add vercel-labs/native;mkdir -p .claude/skills/…;native skills get core --full > .claude/skills/native-core/SKILL.md; same fornative-ui,automation. Readcore --fulland document incontracts.zigheader: (a) the exact background-thread →Msgdispatch idiom, (b) window creation/show-without-focus behavior, (c) module registration boilerplate. app.zon: idcom.shriflow.app, display_nameShriflow, icons placeholder, permissions/capabilities (clipboard, credentials, notifications, gpu_surfaces, native_views, menus), commands for tray,url_schemes = shriflow.- Land
src/contracts.zig(per §2, adjusted to real SDK idioms),src/main.zigwith empty Model/Msg/update compiling, stub files for every module in §2's tree (compile-clean,TODO(P<n>)markers),build.zigwith whisper.cpp submodule wired but behind a-Dlocal-sttflag (so wave-1 agents aren't blocked by its build). - Tray with placeholder menu; accessory activation policy call (
panel_shim.m:[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]) proving the Obj-C shim compiles. tests/smoke.zig+ automation script: app boots headless, snapshot returns the main window. Acceptance:native check+native test+native devall green; skills present; contracts committed; every stub compiles; README with dev commands. Out of scope: any real feature logic.
Objective: all macOS system integration behind PlatformCmd, emitting contract Msgs.
Tasks:
hotkeys.zig: CGEventTap (keyDown/keyUp/flagsChanged); binding model {Fn} | {modifiers+key}; Fn viakCGEventFlagMaskSecondaryFnwith event consumption; hold-vs-tap (<300 ms) disambiguation;Esc-while-recording →.hotkey .cancel; tap auto-re-enable onkCGEventTapDisabledByTimeout; binding-capture mode for the settings recorder (report next chord pressed).mic.zig: AudioUnit HAL input, pre-warmed engine; 16 kHz mono i16 ring buffer;start_mic/stop_mic;audio_levelRMS @30 Hz; AudioChunk stream to registered consumer (STT); device hot-swap (default-device-changed listener).focus.zig: system-wide AX probe →FocusInfo(editable/secure/bundle-id/role); cache per-app quirks table (terminals → prefer typing).inject.zig: the three methods + router + verification (re-probe before inject;no_target/secure_fieldoutcomes); pasteboard save/restore with ConcealedType;paste_lastreplay.permissions.zig: check/request all three + deep links; emit.permissionon app-activate re-checks.panel.zig+panel_shim.m: HUD host — Path A: SDK chromeless window shown unfocused, then mungeNSWindow.level=.statusBar,collectionBehavior=[canJoinAllSpaces,fullScreenAuxiliary,stationary],preventsActivationwhere possible; Path B (fallback, required if A steals focus even once in testing): realNSPanel(.nonactivatingPanel)hosting a CALayer-drawn pill (waveform bars fromaudio_level, ✕/✓ hit zones →PlatformCmdevents). Bottom-center of the screen owning the frontmost window; multi-display correct. Decide A vs B in the first day and record the decision in PLAN.md.- Debug harness window (dev-only) exercising every command with logs.
Acceptance: manual QA checklist (documented in
tests/platform-qa.md): Fn hold/tap/Esc all fire correct Msgs; recording starts <50 ms after key-down (log timestamps); paste lands in TextEdit/Chrome/Slack/Notes/VS Code; Terminal via typing; password field →secure_field; no-focus →no_target; clipboard restored byte-identical; HUD visible over fullscreen apps and never steals key status (verified: type in Chrome while pill shows).native testfor pure-logic parts (binding disambiguation state machine, ring buffer). Out of scope: session logic, UI polish, engines.
Objective: all cloud STT + formatter clients behind SttEngine/Formatter.
Tasks:
- Shared HTTP: SDK effects HTTP (or std.http fallback) with keep-alive + connection pre-warm on record-start; multipart builder; WAV encoder (16 kHz mono PCM16).
stt_openai_compat.zig: Groq/OpenAI/Mistral/custom base URL;verbose_json; error taxonomy (auth/rate/net/format) →EngineError.stt_deepgram.zig: WS streaming client (nova-3; flux flag) — raw PCM16 frames, interim →.stt_partial{final:false},speech_final/utterance-end →final:true; also batch REST path. WS client: minimal RFC6455 impl or vetted Zig lib — document choice.stt_elevenlabs.zig: scribe batch REST + Scribe-v2-realtime WS.format.zig+format_openai_compat.zig: system prompt (contract in §0; temperature 0.2; max_tokens 2×input); skip-heuristic; 1.2 s deadline →.timeout_raw; provider matrix Groq/Cerebras/Gemini-compat/custom.- Key handling via
store/keys.ziginterface (stub until P2c; compile-time interface already in contracts). "Test key" command per provider (1 s micro-request). - Mock-server-based
native testsuite: happy path, 401, 429, timeout, malformed JSON, WS interim ordering. Record real-latency fixture script (dev tool hitting real APIs when keys present, printing stage timings vs §0 budgets). Acceptance: tests green without network; with a real Groq key, fixture shows 10 s clip → transcript+cleanup within budget; Deepgram stream yields ordered partials with correct final flags. Out of scope: UI, session wiring, local engine.
Objective: whisper.cpp fully integrated; models downloadable in-app. Tasks:
build.zig: compile vendored whisper.cpp with Metal (+Accelerate),-Dlocal-sttdefault ON from this phase; pin submodule @v1.9.1.stt_whispercpp.zig: context lifecycle (load once, keep warm); Silero VAD chunker; progressive mode: transcribe VAD-closed chunks during speech, concatenate, tail-only at finish; language hint; greedy decode params tuned for dictation; memory cap by model tier.models.zig: registry {turbo-q5_0 574 MB "Quality", small-q5_1 190 MB "Balanced", base.en-q5_1 60 MB "Light"}; HF resumable download (Range), SHA256 verify, progressMsgs, delete; storage~/Library/Application Support/Shriflow/models/.- Benchmark harness (dev cmd): RTF per model on this machine → stored, shown in settings.
Acceptance:
native testwith a bundled tiny model fixture: JFK sample transcribes correctly; progressive mode: simulated 20 s stream → final text identical to one-shot (± whitespace); key-up→text for 10 s audio ≤ 1.2 s with small-q5 on dev machine; downloads resumable after kill. Out of scope: sherpa-onnx (v1.5), UI.
Objective: settings, secrets, history/stats, latency spans. Tasks:
settings.zig: versioned JSON at~/Library/Application Support/Shriflow/settings.json; schema: engine selections, per-provider model, keybinds, mode, language, behavior flags; atomic writes; migrations.keys.zig: Keychain viaruntime.credentials(servicecom.shriflow.app, account = provider id).history.zig: append-only JSONL (history.jsonl); record {ts, text, raw_text, app_bundle_id, duration_ms, words, engine, spans, outcome}; derived stats: total words, avg WPM (words/speech-minutes), day streak; retention toggle + clear-all; paste-last source.telemetry.zig: span API + per-session rollup; debug overlay data feed. Acceptance:native test: settings roundtrip + migration from v0; history 10k rows loads <50 ms; stats match hand-computed fixtures; keychain mocked interface conformance. Out of scope: UI rendering.
Objective: session.zig state machine wiring everything per §0's flow.
Tasks:
- States idle/recording{batch|realtime}/transcribing/formatting/injecting/card/error; single active session; cancellation at every stage (Esc, ✕, hotkey re-press).
- Batch: key-down → HUD show + mic + (cloud: connection pre-warm); key-up → finalize → engine.transcribe → formatter → probe+inject → outcome (success flash | card) → history append (with spans).
- Realtime: probe at key-down — editable target? open stream: partials → HUD ghost text; finals → inject via router (typing method for increments); no target → silently run batch semantics; key-up → flush finals (+ optional whole-utterance cleanup OFF by default).
- Fallbacks: engine error → if cloud failed and a local model is installed + "offline fallback" enabled → retry locally once; formatter timeout → raw; every error → HUD error state with human message.
- Paste-last keybind → platform
paste_lastwith history head. - Debug overlay (dev flag): live stage timings vs budget.
Acceptance:
native automateE2E: scripted fake-engine run walks all states (snapshot assertions per state); real-run checklist: Groq batch into Chrome ≤ 900 ms p50 over 10 tries (spans logged); realtime into TextEdit streams finals only; no-focus run ends with card + clipboard set; cancel leaves no HUD residue, no stray audio. Out of scope: visual polish.
Objective: all §0 surfaces in .native markup + Zig views, matching the screenshots' minimal aesthetic.
Tasks:
- Design tokens: cream
#FAF9F6canvas, ink#1A1A1A, muted#8A8781, serif stat numerals, 8-pt spacing grid, pill dark#161618@ 92% opacity, radius 999. - Main window: header, day-grouped virtualized history list (copy/delete hover actions), stat rail, empty state.
- Settings views per §0 (segmented sections; provider key fields masked with Test button state; model manager rows with progress bars; keybind recorder using P1 capture mode; behavior toggles).
- HUD pill + fallback card contents (waveform bars ←
audio_level, processing spinner, success/error states, card with Copy) rendered into P1's host (A or B — coordinate on the chosen path's drawing API: SDK gpu_surface vs CALayer spec handed to P1). - Onboarding flow with live permission states + deep-link buttons + playground field.
- Tray menu final items.
Acceptance:
native automatesnapshot suite per surface (all states, incl. HUD states via debug commands);native checkclean; visual review screenshots attached to PR description. Out of scope: business logic (drive everything through Msgs/fixtures).
Objective: shippable, signed, honest-to-budget build. Tasks:
- Latency pass on real hardware vs §0 table; optimize (connection reuse, model warm-up, chunk sizes); publish measured table in README.
- Injection QA matrix: Chrome, Safari, Arc, Slack, Discord, Notes, Mail, VS Code, Cursor, Terminal, iTerm, secure fields, IME (at least one CJK layout) — record per-app method matrix into
focus.zigquirks table. - Permissions lifecycle: fresh-machine onboarding run-through; signature-stable dev cert documented; Sequoia/Tahoe permission-nag behavior checked.
- Packaging: icons,
native package --signing identity+ entitlements (mic), PlistBuddy step for usage strings if needed, notarization + staple + DMG; auto-update wiring per SDK/updates. - Full
native test+ automation suite green in one command (./scripts/verify.sh); crash/edge sweep (mic unplug mid-dictation, wifi drop mid-upload, 5-min dictation, rapid key spam). Acceptance: signed notarized DMG installs on a second machine; all QA checklists green; budgets met or regressions documented with cause.
Waves (all coding agents = Opus, max effort, worktree-isolated):
- Wave 0: P0 (one agent, serial). Gate: repo builds + contracts frozen.
- Wave 1 (5 parallel): P1, P2a, P2b, P2c, P4. Each gets: §0–§2 + its own §3 order + instruction to load the three native-sdk skills first. P1 must report its Path A/B panel decision immediately (P4 depends on the drawing API).
- Wave 2: P3 (one agent) once P1 + P2a merge; P4 integration PRs land alongside.
- Wave 3: P5 after M2.
Gates: M1 = Fn-hold → Groq → cleaned text in Chrome ≤ 1 s (post-P3). M2 = local whisper + fallback card + settings complete. M3 = realtime mode. M4 = signed DMG.
Merge rules: one worktree/branch per phase; only the orchestrating session merges; contracts.zig changes require orchestrator sign-off; every merge runs ./scripts/verify.sh (native check + test + automation smoke).
Verification loop for agents: build → native automate snapshot/widget-click/screenshot against the running app → assert → iterate. No agent reports done with red checks or unmet acceptance criteria; each ends with an adversarial self-review pass of its own diff.
- Name/bundle: Shriflow /
com.shriflow.app. - Hotkey: Fn hold/tap (fallback preset
⌥Space); paste-last⌥⇧V. - v1 providers: Groq (STT+LLM defaults), Deepgram (realtime), OpenAI, Mistral, ElevenLabs, Cerebras, Gemini, custom OpenAI-compat endpoint.
- v1 local: whisper.cpp (turbo-q5 default, small/base tiers); sherpa-onnx + Parakeet + local streaming in v1.5; local formatter LLM deferred (cloud or Off in v1).
- Realtime commits only finalized segments; per-segment cleanup off by default.
- History on by default, 100% local, toggle + clear.
- Signing assumes an Apple Developer ID exists (needed for stable TCC + distribution) — confirm.
- HUD focus-steal — dual path planned (P1 task 6), decided day one. Highest technical risk; everything else is well-trodden.
- Fn suppression quirks per macOS version → documented workaround + alternate binding.
- Info.plist usage strings via app.zon unknown → PlistBuddy post-process fallback (P0 spike, P5 owns).
- native-sdk v0.4 churn → pin CLI+SDK versions in repo; isolate SDK touchpoints behind thin wrappers.
- Effects/threading idiom partially undocumented → P0 resolves from
core --fullskill before Wave 1; contracts encode the answer. - Spend limit → §4 prerequisite.
native-sdk.dev: /windows /capabilities /extensions /tray /skills /packaging /packaging/signing /app-zon /state /runtime /native-surfaces /app-model /keyboard-shortcuts · github.com/vercel-labs/native · console.groq.com/docs/speech-to-text · groq.com/pricing · developers.deepgram.com (models overview) · elevenlabs.io/realtime-speech-to-text, /pricing/api · docs.mistral.ai (audio) · inference-docs.cerebras.ai · ai.google.dev/gemini-api/docs/pricing · github.com/ggml-org/whisper.cpp v1.9.1 · github.com/k2-fsa/sherpa-onnx · github.com/Beingpax/VoiceInk · Apple developer docs (CGEventFlags, AX attributes).
-
P0 (done, 2026-07-10): scaffold ejected (
native eject) so build.zig can link the Obj-C shim + ApplicationServices/CoreGraphics/AudioToolbox/IOKit and, behind-Dlocal-stt, whisper.cpp. SDK pinned via committed symlinkvendor/native-sdk-cli(absolute-target → resolves from any worktree).contracts.zigfrozen; theMsgunion lives inmain.zig(markup dispatch needs flat tags there) with the contract-required variants already present as no-op arms. Verified:zig build,zig build test,native check, and a live automation smoke (boot → tray installed → view asserts → clean stop) all green. -
Wave-1 file ownership: only P4 edits
src/main.zig+src/app.native; P1/P2a/P2b/P2c stay inside their own files/dirs and report any main.zig needs in their final summary. -
P1 — HUD Path A/B decision (2026-07-10): PATH B. The HUD is a real non-activating
NSPanelcreated and drawn by the shim (panel_shim.m), not the SDK-declaredhudwindow of Path A. Rationale: (1) focus-steal is the #1 risk (§6) and anNSPanelwithNSWindowStyleMaskNonactivatingPanel+canBecomeKeyWindow == NO+.statusBarlevel + collectionBehavior {canJoinAllSpaces, fullScreenAuxiliary, stationary, ignoresCycle} cannot become key/main by construction — no reliance on munging an SDK-owned window; (2) Path A needsOptions.windows_fn/window_viewwired in src/main.zig, which P1 does not own (frozen against P1 edits) — Path B is entirely inside P1's files (panel.zig + panel_shim.m), host AND drawing, matching the P1 work order and needing zero coordination with P4/P3; (3) the pill (✕ | waveform | ✓), spinner, success/error, and fallback card are one AppKitdrawRectfed from theme.zig tokens (via exported Zig adapters — single source of truth) and the tested placement math in panel.zig. Cost: the HUD is invisible tonative automatesnapshots/screenshots (those cover SDK gpu_surface views only), so HUD visual + focus-steal QA is manual (tests/platform-qa.md). P4's planned "draw HUD contents into P1's host" is therefore unnecessary — P1 owns HUD drawing end to end; P4/P3 talk onlyPlatformCmd.hud. -
Wave-1 launch BLOCKED (2026-07-10): account monthly spend limit is hit — every subagent (research agents earlier, then the P1 worktree agent) dies instantly with "You've hit your monthly spend limit." The 5-agent parallel fleet cannot run until the limit is raised at claude.ai/settings/usage. Main session is unaffected, so phases that verify fully headlessly (P2c store, most of P2a, P4 UI, P2b logic) are being built directly in the main worktree meanwhile; P1 (needs real TCC grants + mic + event-tap hardware testing) waits for the fleet + a human at the keyboard.
-
Toolchain note (from the dead P1 agent's partial output): bare
native build --releaseerrors (optimize-mode.anyhard-errors in the native_sdk dependency's own build.zig). Usezig build(what we use),native build, ornative build --release=safe|fast. The app source compiles clean. -
P2c (done, 2026-07-10): store & telemetry built + verified in the main session (fleet still blocked). settings JSON with defensive parsing + migration hook; Keychain wrapper with redaction; history JSONL + streaming stats (words/WPM/streak); telemetry spans + budget overlay. 45/45 tests green.
-
Status after this session: P0 ✅, P2c ✅ (both verified, committed). Remaining P1/P2a/P2b/P4 are the parallel-fleet work — blocked on raising the monthly spend limit (claude.ai/settings/usage). P1 additionally needs the user's Mac for TCC grants + event-tap/mic testing.
-
Wave-1 integration COMPLETE (2026-07-10): all five implementation phases merged to
mainand verified together — P4 UI, P2a cloud engines, P2b local whisper (+Metal), P2c store/telemetry, P1 platform. Merged tree: 153/154 tests (cloud-only) / 155/157 (-Dlocal-stt),native checkclean, app boots into onboarding correctly. Environment note: a mid-session OS filesystem-lock/EPERM incident killed the first fleet run; recovered on its own; the resumed/relaunched agents finished cleanly (concurrent builds ≤3 were fine). -
HUD = Path B (P1's call): a real non-activating
NSPaneldrawn bypanel_shim.m, never-key by construction. Consequence: the HUD is invisible tonative automate(manual QA only), and P4 does NOT draw HUD content — P1 owns it end to end; everyone else talksPlatformCmd.hud. -
Resolved integration seams:
stt.zigwhispercpp test assertion gated on-Dlocal-stt; formatter honors the frozen (no-*Effects) vtable via a worker-thread watchdog (P3 may instead drivefx.fetchusing the exportedbuildChatBody/SseAccumulator); mic/CoreAudio reached viadlopen(no build.zig change).DownloadSinklives inmodels.zig(mirrorsSttSink). -
P3 wiring contract (from P1's report): hold a
platform.Platform;start()on boot (main thread);registerDeliverfor the Bridge fast path; repurpose thepoll_ticktimer to repeatingcontracts.poll_interval_mscallingdrain(); route everyPlatformEventthrough the session state machine; write transcript to clipboard BEFOREPlatformCmd.hud{.card}; replace P4's fakeopen_perm/recheck_perms/quit_app/test_keystubs with real platform/store/engine calls. -
P3 (done, 2026-07-11): the session orchestrator is wired — the app is now feature-complete.
session.zigholds the testable core (thread-safe engine bus + STT/formatter sinks, per-utteranceSessionwith bounded audio accumulation + spans + settings snapshot + seq-guard, pure pipeline decisions);main.zigholds the runtime singletons + boot + the repeatingpoll_tickdrain loop + the batch pipeline (hotkey → mic → STT viaengineFor+ cached key → formatter/skip → inject → outcome→success|clipboard+card → stats) + offline local fallback + cancel/paste-last, and makes the tray/permission/key stubs real. Boots clean (dispatch_errors=0), 159/160 tests,native checkgreen. M1 is code-complete but needs the user's Mac + a Groq key to verify live (real hotkey/mic/inject + TCC grants). -
P3 → P5 follow-ons (documented, non-blocking): (1) the 15ms drain poll rebuilds the idle view ~66×/s — replace with P1's dispatch_async fast-path deliver (needs a
*Runtime/window handle captured inmain) + a slow fallback poll; this is the top perf item. (2) history JSONL persistence viafx.writeFile+ settings load-at-boot + Keychain key load (only in-memory key cache today). (3) realtime finals-injection (batch is complete; streaming partials are ignored). (4)native automate assertraces the rebuild churn → verify.sh reads snapshot.txt directly. -
Changes 1–4 (done, 2026-07-11): owner-requested post-M1 changes, planned by Fable, built by 2 parallel Opus agents, merged + verified. (1) Cleanup formatter default → Vercel AI Gateway (
openai/gpt-oss-20bathttps://ai-gateway.vercel.sh/v1; Groqllama-3.1-8b-instantone-key alt; Gemini ids refreshed). (2) All realtime/streaming removed — DeliveryMode/Partial/StreamSession/SttCaps/openStream + Deepgram/ElevenLabs engines + net_ws.zig/local_vad.zig deleted; cloud STT = Groq/OpenAI/Mistral + local whisper; batch push-to-talk only; settings schema stays v1 (defensive reader). (3) HUD redesigned — 210×48 dark pill, ~10 thick rounded equalizer bars with rolling-max ripple, gray ✕ circle / white ✓ circle (panel_shim.m draw + pure tested math in panel.zig). (4) All dummy/seed UI removed — 0/0/0 stats, empty "No dictations yet" history, honest not-installed downloads + boot install-scan + real P2b downloader wired (models.download on a detached thread → dlSink seq 0;.downloadcase hoisted above the stale-seq guard). Merged main green: cloud 135/137,-Dlocal-stt137/140,native checkclean, automation smoke confirms empty-state + Gateway option + no realtime + dispatch_errors=0. HUD visual + real download + live gateway latency need the owner's Mac. -
Changes 1–4 Fable review (2026-07-11): reviewed the C1–C4 diff at xhigh; confirmed C2 surgery clean (no dangling removed-symbols, vtable consistent, freeOwned correct), C3 HUD math clean, C1 correct (gpt-oss-20b fits Buf(64), 401→raw never blocks paste). FIXED: (1 MAJOR) the download
dlSinkshared round-robin stamp slot 0 with every 64th dictation → dropped transcript + stuck session + data race — now a dedicatedEngineBus.dl_stamp; (3 MAJOR)startModelDownloadhad no in-flight guard → double-click spawned two workers corrupting the.part— addedif dl_state==.downloading return; (6 MINOR) keyless install paid a doomed ~1.2 s formatter round-trip per dictation —kickoffFormatnow injects raw when the key is empty. All green after fixes. -
Deferred hardening from the review (top follow-ons): (5, most important — CORE)
net_http.Transportignorestimeout_ms→ a hung cloud STT upload sticks the session at.transcribinguntil Esc (audio lost); (2) same root cause makes a network-dropped model download hang at.downloadingforever (recovery: relaunch →.partresumes) — proper fix is real read/connect deadlines innet_http(+ optionally a cancel button and cancel-on-quit). (4) failed download discardsde.err(reverts to available; no user-facing reason). (nits) validate a 206Content-Rangevs requested offset; boot install-scan runs 3 stats on the loop thread; stale comments (local_whisper"progressive",contracts"WS",models.verifyFileunused param). -
Persistence + history fix + notch HUD (done, 2026-07-11): three owner-reported issues from live testing, planned via explore/plan agents + live-web notch research, built in-session. (1) Persistence wired — the tested-but-unwired P2c stores now run: settings.json loads synchronously in
boot()(drives route/bindings/placement pushes) + saves viafx.writeFileon every settings mutation; history.jsonl's byte mirror (g_hist_bytes, 512 KB cap, whole-line eviction) is the source of truth — the visible ring + stats derive from it on every change (boot/dictation/delete/clear), so display always matches disk; API keys live in the macOS Keychain (owner's choice) via SecItem helpers in the shim + a duck-typedkeychain.Runtimeplugged into the existingkeys.Store;hydrateKeychainrestoresg_keys+ the "•••• last4 saved" UI at boot. (2) History display fixed — root cause was a zero-capacityhistory: [0]ui.HistEntry+ a stats-onlyrecordHistory(stats counted words; rows could never exist). Now[200]ui.HistEntryOwnedwith owned text, newest-first render, day-group/time labels computed at render fromts_msvialocaltime_r(Today rolls to Yesterday honestly). (3) Notch HUD — new default recording overlay: pure-black wings (opaque #000, square top / 14 pt bottom corners, no shadow, level mainMenu+3) extending the physical housing on the built-in display, 5-bar equalizer in the left wing, spinner/✓ there, card/error as a black dropdown below the notch (18 pt bottom corners, Copy/✕ live); no chips — Esc cancels, release confirms, all other clicks pass through. Geometry is pure tested Zig (hasNotch/notchRect/notchLayout/resolveMode, notch APIs are @available(12.0)-gated, built-in found viaCGDisplayIsBuiltin); mode resolved per show; screen-change observer re-anchors (250 ms debounce). Settings → Behavior → "Recording indicator" (Auto | Notch | Bottom) persisted + pushed via newPlatformCmd.set_hud_placement; Bottom keeps the old pill byte-identical (owner: external displays stay bottom-pill). Smokes are hermetic now (scratch$HOME) since the app persists real state. Verified: full verify.sh green, live two-boot round-trip (onboarding_done + hud_placement survive relaunch), history-from-disk renders Today/Yesterday groups + stats, dispatch_errors=0. Notch visuals need the owner's notched Mac (tests/platform-qa.md updated). -
ce1a4ae Fable review (2026-07-11, max scrutiny): no criticals; byte-mirror memory ops, lifetimes, notch geometry/unpacking, markup enum dispatch, bottom-pill regression, and hermetic smokes all verified correct. FIXED (this commit): (M1 MAJOR) stats used a fixed 1024-row buffer → total_words silently froze once history outgrew it — now STREAMS StatsBuilder over every line; (M2 MAJOR) ring eviction memmoved ~234 KB per line past 200 on the loop thread at paste time — now a modulo slice-ring that full-parses only the newest 200; (M5) a crash-torn history.jsonl tail got appended onto, merging records — loadHistory truncates the mirror to the last complete line; (M3) a save racing an in-flight same-key write was rejected + only logged → latest state could miss disk —
.rejectednow sets a dirty flag the 15 ms drive tick re-issues; (M7) save_key claimed "saved" even when the Keychain write failed — persists first, only marks saved on success (failure shows unsaved + failed test chip); (M6) row Copy served the ring's 1024-byte display copy — now re-parses the full transcript from the JSONL line; (M4) settings.zig header falsely claimed tmp+rename atomic writes — corrected (atomic writes = follow-on); deadnewestTextremoved. RIDING (documented): non-atomic fx.writeFile (M4 proper), boot Keychain reads can prompt on ad-hoc re-signs (M8, in platform-qa.md), delete-by-ts picks the first duplicate (M9, unreachable live), >512 KB external history file loads empty then is overwritten (M10), UTC-vs-local streak day seam, per-frame label recompute (P5 view-rebuild item).