Developer guide
How the codebase is organized, why each boundary exists, and where to change things safely. New here? Read the user-facing “how it works” first — this page explains the machinery behind it.
Mental model
Kivo is one Tauri process with four pre-created webview surfaces. React owns presentation and transient UI state — nothing else. Rust owns shortcuts, window placement, speech sessions, selected text, replacements, settings, credentials, Gemini requests, and tray lifecycle.
This split is deliberate: the webview is convenient for UI but untrusted for secrets. The Rust core (AppCore in src-tauri/src/commands/, wired in lib.rs) is the only thing allowed to touch credentials, the microphone path, or another app’s text.
Repo map
Kivo/
├── src/ # React UI — presentation only
│ ├── App.tsx # surface router (flow-bar | writing-tools | settings | onboarding)
│ ├── components/ # Button, Icon, Switch, ShortcutRecorder, …
│ ├── features/
│ │ ├── dictation/ # FlowBar + reducer state machine
│ │ ├── writing-tools/ # popup, actions.ts, SafeMarkdown, reducer
│ │ ├── settings/ # SettingsWindow (5 sections)
│ │ └── onboarding/ # first-run permission + setup flow
│ ├── hooks/ platform/ # nativeBridge, useNativeEvent, prefs
│ ├── theme/ # kivo.ts (Astryx theme) + built/ artefacts
│ └── styles/ # app.css (document-level rules only)
├── src-tauri/src/
│ ├── lib.rs main.rs # composition root, tray, shortcuts, surfaces
│ ├── commands/ # Tauri invoke handlers + AppCore
│ ├── speech/ # session management over platform engines
│ ├── text/ # selection capture + replacement
│ ├── ai/ # Gemini client (mod.rs), link_summary.rs
│ ├── platform/ # windows.rs, linux.rs, adapters.rs, mod.rs
│ ├── config/ # settings.json repository
│ ├── security/ # SecretString + redaction helpers
│ └── shell.rs # windows, tray, shortcuts, placement
└── website/ # this site (static, no build step)
The four surfaces
| Surface | Role |
|---|---|
flow-bar | Non-activating, always-on-top dictation pill. Tightly sized; never steals focus. |
writing-tools | Compact selection-aware command and result popup with native placement. |
settings | Native-window preferences. Closing hides the window — it never quits the app. |
onboarding | Short first-run permission and setup flow. |
Windows are pre-created and shown/hidden rather than spawned per use — that’s what keeps the Flow Bar and popup instant. Placement logic lives in shell.rs (multi-monitor aware; verify on both OSes before release). UI reducers (features/dictation/state.ts, features/writing-tools/state.ts) model the popup lifecycle, including generation counters that discard late responses when a popup closes.
Platform boundary
All OS code is isolated under src-tauri/src/platform/ behind the adapter traits in adapters.rs (PlatformSpeechEngine, PlatformTextService, PlatformCredentialStore). The rest of Rust never touches an OS API directly.
| Concern | Windows 11 24H2+ | Linux test bench |
|---|---|---|
| Text access | UI Automation and guarded clipboard fallback | Simulated text buffer |
| Speech | Desktop SAPI and native local transcription | Simulated system speech |
| Secrets | Credential Manager | Development-only file vault |
| Distribution | Per-user NSIS installer | No supported release |
The boundary reserves application-specific and clipboard-fallback replacement strategies — but V1 fails safely when native Accessibility / UI Automation can’t preserve the selection, rather than risk mangling user text.
Dictation pipeline
speech/ manages sessions; platform/* feeds audio to the OS engine and streams levels back for the waveform (dictation-level events) and status snapshots (dictation-state: idle → listening → processing → success/error). The React side is a thin reducer over those events — see FlowBar.tsx.
On release, the transcript goes through optional AI cleanup, then text/ inserts it at the saved cursor. esc cancels; closing paths bump a generation counter so late engine callbacks are ignored.
Selection & replace pipeline
Invoking Writing Tools captures a SelectionContext (app name, bounds, initial text, canReplace) and emits writing-context. The popup runs its reducer (OPEN → RUN → RESULT / REPLACED / FAIL), calls runWritingAction, and on Replace asks Rust to write back through the platform text service. Summarize and Key Points preview results by default. Text summaries can offer explicit replacement when the captured selection is replaceable; link summaries cannot replace a selection.
AI client
Curated in src-tauri/src/ai/mod.rs (SUPPORTED_GEMINI_MODELS, mirrored in src/ai/models.ts):
DEFAULT_GEMINI_MODEL = "gemini-3.8-flash" // low thinking, latency-sensitive
endpoint = .../v1beta/interactions
store = false // always — retention off
Prompts are built in writing_prompt() behind a guard: source text is treated as untrusted content, never instructions (system prefix in mod.rs), text summaries over 200,000 chars are rejected, and link summaries go through link_summary.rs (URL-context retrieval for pages, video input for YouTube) with a 90-second deadline vs. 20 seconds for ordinary writing calls. Successful URL-retrieval evidence is required before a webpage summary is shown.
SUPPORTED_GEMINI_MODELS and BLOCKED_MODEL_SUBSTRINGS in src-tauri/src/ai/mod.rs, run bun run generate:models to update src/ai/models.ts, and update fixtures at the same time. Validation is allow-all: any well-formed model id works and only blocked non-text families are rejected. Requests try the ordered model queue until one succeeds; key or balance problems stop failover. Provider routing and pricing live in src-tauri/src/ai/providers.rs.Credentials & settings
Keys are saved via PlatformCredentialStore, tested against the Interactions API, and their status surfaced as configured / untested / testing / connected / invalid / rate-limited / offline. The key value itself is a SecretString that never serializes to the frontend. Plain preferences live in settings.json via config/SettingsRepository; FrontendSettings is the sanitized projection the UI may see.
Why it’s built this way
Native AI routing. Gemini, OpenCode, and custom endpoints share an ordered model queue in the Rust process. Provider keys remain in the operating-system credential store.
A direct Windows download. Kivo uses desktop SAPI speech with installed language packs and ships as a per-user .exe. Downloads need no account; unsigned builds may show a SmartScreen warning.
Audio never touches Gemini. The OS engine is good and already privileged; routing voice to a cloud model would add latency, cost, and a privacy liability for no quality win.
Short, vague user errors; rich-in-kind internal codes. User-facing strings stay concise; diagnostics record operation names, error categories, and non-sensitive OS codes only.
Vite-only harness for UI. bun dev renders all four surfaces with mock bridges (append ?state=listening|processing|success|error for Flow Bar states) so UI work never needs OS integration or a real key.
Conventions
| Rule | Detail |
|---|---|
| Logging | Never log selections, transcripts, responses, clipboard, or credentials. |
| Frontend | Reducers + nativeBridge; generation counters to drop stale async results. |
| Rust | OS calls stay in platform/; everything else goes through adapters. |
| Markdown | Render AI output only through SafeMarkdown. |
| UI | Astryx components + StyleX in src/features and src/components; theme source in src/theme/kivo.ts, built artefacts committed. |
| Theming | Astryx tokens via <Theme>; data-theme / data-platform on root. |
Dev workflow
bun install
bun tauri dev # full app (VS Build Tools on Windows + OS perms)
bun dev # Vite-only UI harness, no OS integration
bun typecheck && bun lint && bun run test # tsc, Oxlint, bun test
bun run format:check # Biome; `bun run format` applies
bun run knip && bun run theme:check # dead-code + Astryx theme drift
bun test:ui # Playwright; isolated port: KIVO_UI_TEST_PORT=1437 bun run test:ui
bun check:rust && cargo test --manifest-path src-tauri/Cargo.toml
Physical testing on Windows is required before a release: Windows-key suppression, speech model availability, multi-monitor placement, accessibility behavior in third-party apps, and signing.
Releases & updates
Pushing an app-v* tag runs .github/workflows/release.yml (Windows artifacts, drafted GitHub Release). bun run prepare:release generates the ignored release-only Tauri config and injects the updater public key — normal local builds have no trusted updater key on purpose. Configure TAURI_UPDATER_PUBKEY, TAURI_SIGNING_PRIVATE_KEY (+_PASSWORD), WINDOWS_CERTIFICATE_BASE64 / _PASSWORD before tagging. Never commit updater private keys or certificates.