Skip to content

mcaney006/grokmacos-better-version

 
 

GrokInsane

CI Audit Rust License

Status: pre-1.0 (v0.1.x). Streaming chat, local-first storage, full-text search, and voice mode all work today. The release pipeline (Sigstore + SLSA + macOS notarization) is wired but has not yet been integration-tested against a real v0.1.0 tag. See CHANGELOG.md for what's actually shipped.

A cross-platform desktop client for xAI Grok, OpenAI, Anthropic, and any OpenAI-compatible local model. Rewritten from the original SwiftUI macOS app into Rust + egui — a single binary that runs on macOS, Windows, and Linux with the same code.

No Xcode, no shell scripts, no Electron, no embedded browser. One cargo build, native rendering via wgpu. The cold-start CLI invocation (grok-insane --version) measures ~4 ms on the maintainer's machine; the GUI cold start is unmeasured but architecturally bounded by tokio runtime init + first wgpu frame.


Table of Contents

  1. Features
  2. Screenshots / UX
  3. Getting Started
  4. Configuration
  5. Keyboard Shortcuts
  6. Architecture
  7. Workspace Layout
  8. Developer Workflow (cargo xtask)
  9. CLI
  10. Feature Flags
  11. Security Model
  12. Performance
  13. Testing
  14. Roadmap
  15. License

Features

Area What you get
Providers xAI Grok (streaming) · OpenAI (streaming, shared decoder) · Anthropic (native Messages SSE w/ usage) · any local OpenAI-compatible endpoint (Ollama, LM Studio, llama.cpp server)
Chat UI Multi-chat sidebar, full transcript history, sticky-to-bottom auto-scroll, virtualised scroll, Markdown rendering with syntax-highlighted code blocks (egui_commonmark + syntect), copy-message, ↻ regenerate
Command palette ⌘ / Ctrl+K opens a fuzzy launcher for every action: new chat · settings · voice/TTS/RAG · theme · provider · jump-to-chat · export · quit
Phosphor icons Bundled egui-phosphor icon font; toolbar buttons render real glyphs (microphone, speaker, paper-plane) on every platform
Real RAM tracking Perf dashboard reads resident memory from sysinfo once per second — actual process RSS, not a guess
Chat management Pin · archive · rename · delete · export to Markdown / Obsidian / JSON from a right-click menu
Voice Realtime WebSocket to wss://api.x.ai/v1/realtime, full duplex audio via cpal, custom animated waveform, persona selector (Ara / Rex / Sal / Eve / Leo), client-side level metering, server-side VAD
Search Every message indexed by tantivy; instant full-text + fuzzy queries even at 100k+ messages
Storage Embedded ACID K/V via redb, composite-key range scans by chat & timestamp, no external database needed
Secrets API keys live in the OS keyring (macOS Keychain / Windows Credential Manager / Linux Secret Service) with zeroize on in-memory copies
RAG (opt-in feature) Local sentence embeddings via fastembed, semantic re-rank over lexical hits, top-k retrieval inserted as a system message
Plugins (opt-in feature) WebAssembly host (wasmtime) — surface area reserved for follow-up
Hotkeys (opt-in feature) Global system-wide hotkeys via global-hotkey
Perf Built-in dashboard for frame time, fps, tokens/s, last-request latency, indexed message count, resident memory
Theming Custom "cosmic" theme (neon green on dark) by default; Dark and Light fallbacks
Persistence Chats, messages, settings, and search index all kept locally — works offline for everything except actual model calls
Build & release Pure-Rust cargo xtask replaces shell scripts; CI matrix builds macOS, Windows, Linux on every push

Screenshots / UX

The app launches at 1280×820 with a 220 px side rail, a central chat panel, and a floating settings window. Default theme is a high-contrast dark surface with #78FFAF neon-green accents matching the xAI palette.

┌──────────┬───────────────────────────────────────────────────┐
│ + New    │  ◉ My chat about Rust                             │
│  search… │  ────────────────────────────────────────────────  │
│ ─────    │                                                   │
│ ◉ Chat 1 │   USER                                            │
│   Chat 2 │   How do borrow checkers work?                   │
│ 📌 Chat 3│                                                   │
│   …      │   ASSISTANT (markdown rendered, code highlighted) │
│          │   ```rust                                         │
│          │   fn main() { … }                                 │
│          │   ```                                             │
│ v0.1.0   │                                                   │
│          │  ┌─────────────────── compose ──────────────────┐ │
│          │  │ message Grok…                       🎙 🔊 ➤ │ │
│          │  └────────────────────────────────────────────  ┘ │
└──────────┴───────────────────────────────────────────────────┘

Getting Started

Prerequisites

  • Rust 1.78+ — install via rustup.
  • Linux only: ALSA + X11/Wayland + fontconfig dev libs. You can install them with one command:
    cargo xtask install-deps
    
    …which runs apt-get install -y libasound2-dev libxkbcommon-dev libwayland-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libfontconfig1-dev. For non-Debian distros install the equivalents.

Build & run

git clone https://github.com/mcaney006/grokmacos-better-version
cd grokmacos-better-version

# debug build with live logs
cargo xtask dev

# or a plain release build
cargo build --release
./target/release/grok-insane

First launch

  1. Press ⌘ / Ctrl+, to open Settings.
  2. Pick a provider (default: xAI).
  3. Paste your API key into the key field and click Save key — it lands in the OS keyring, never on disk in plaintext.
  4. Press ⌘ / Ctrl+N and start typing.

Configuration

All user-visible config lives in the in-app Settings window.

Setting Notes
Provider xAI · OpenAI · Anthropic · Local. Drives which API key slot is loaded and which client is used.
API key Stored in the OS keyring under com.grokinsane.grok-insane, one entry per provider.
Model Per-provider text field. Defaults: grok-beta, gpt-4o-mini, claude-3-5-sonnet-latest, llama-3.1-8b.
Temperature 0.0 – 2.0, half-step slider.
Max tokens 64 – 131 072.
Theme Cosmic (default) / Dark / Light, hot-applied.
Font size 10 – 24 pt, hot-applied.
TTS enabled Toggle Grok speaking replies aloud.
Voice persona Ara / Rex / Sal / Eve / Leo.
System prompt Optional, prepended to every conversation.
RAG enabled Requires --features rag. Top-K retrieval over your own chat history.
RAG Top K 1 – 12.
Perf dashboard Show frame time / fps / tokens/s / resident memory in settings.

Settings, chats, and messages are persisted to:

OS Data dir
macOS ~/Library/Application Support/grok-insane/
Linux ~/.local/share/grok-insane/ (XDG)
Windows %APPDATA%\GrokInsane\grok-insane\data\

Run grok-insane --diag for the exact paths on your machine.


Keyboard Shortcuts

Shortcut Action
⌘ / Ctrl+N New chat
⌘ / Ctrl+, Toggle Settings
⌘ / Ctrl+. Stop generation
⌘ / Ctrl+K Command palette (fuzzy launcher)
⌘ / Ctrl++V Toggle voice mode
Enter Send
+Enter Newline in composer
Right-click chat Rename / Pin / Archive / Export / Delete

Architecture

┌─────────────────────────── UI thread ───────────────────────────┐
│                                                                  │
│   eframe::App  ─►  app::GrokApp                                  │
│                       │                                          │
│                       ├─►  ui::sidebar                           │
│                       ├─►  ui::chat_view  ─►  egui_commonmark    │
│                       ├─►  ui::settings_view + perf_dashboard    │
│                       ├─►  ui::waveform                          │
│                       └─►  ui::toast                             │
│                                                                  │
│   ▲   channels    ▲   channels                                   │
└───┼───────────────┼──────────────────────────────────────────────┘
    │               │
┌───┴───────────┐ ┌─┴──────────────────────────────────────────────┐
│ Tokio runtime │ │ cpal capture/playback streams (!Send)          │
│  • chat SSE   │ │  • mono PCM16 @ 24 kHz, linear resampler       │
│  • voice WS   │ │  • level meters (Arc<AtomicU32>), VoiceShared  │
└───┬───────────┘ └────────────────────────────────────────────────┘
    │
┌───┴────────────────────────────────────────────────────────────┐
│ Storage layer                                                  │
│  • redb K/V (chats, messages, settings) — composite keys for   │
│    range scans per chat ordered by timestamp                   │
│  • tantivy index over message bodies (TEXT + STORED), kept     │
│    eventually consistent via best-effort writes                │
└────────────────────────────────────────────────────────────────┘

Key design decisions:

  • Single binary, no plugins required to be useful, but plugin host wired behind a feature flag for future extension.
  • Send / !Send split for voice: cpal::Stream is !Send, so the live audio handles stay on the UI thread; channels + atomics (VoiceShared) are what the async WebSocket task touches.
  • Storage façade is Clone + Send + Sync; an internal Arc<Inner> protects the tantivy writer with a single parking_lot::Mutex because redb already serialises writes.
  • No locks in the hot UI path. Live settings are read via ArcSwap so the audio engine never blocks on a contended mutex.

Workspace Layout

.
├── Cargo.toml                  # workspace + grok-insane package
├── Cargo.lock
├── .cargo/config.toml          # alias: cargo xtask
├── .github/workflows/ci.yml    # matrix CI: macOS / Windows / Linux
├── README.md
├── src/                        # the desktop app crate
│   ├── main.rs                 # entry + CLI (--version, --diag, --reset-db)
│   ├── app.rs                  # top-level eframe::App impl
│   ├── config.rs               # ArcSwap<Settings> handle
│   ├── error.rs                # typed errors
│   ├── models.rs               # Chat / Message / Settings / Provider / Role
│   ├── paths.rs                # XDG / Known Folder / macOS app paths
│   ├── secrets.rs              # OS keyring wrapper (zeroize)
│   ├── theme.rs                # cosmic / dark / light palettes
│   ├── services/
│   │   ├── providers.rs        # ChatProvider trait + ChatRequest/ChatEvent
│   │   ├── chat.rs             # xAI streaming + reusable OpenAI-compat SSE decoder
│   │   ├── openai.rs           # OpenAI client (delegates to chat.rs)
│   │   ├── anthropic.rs        # Anthropic Messages SSE decoder
│   │   ├── local.rs            # local OpenAI-compatible endpoint (Ollama, etc.)
│   │   ├── voice.rs            # tokio-tungstenite Realtime WS client
│   │   ├── audio.rs            # cpal capture + playback, resampler, level meter
│   │   ├── export.rs           # Markdown / Obsidian / JSON exporters
│   │   └── embeddings.rs       # RAG retriever (feature = "rag")
│   ├── storage/
│   │   ├── mod.rs              # redb façade (chats, messages, settings)
│   │   └── search.rs           # tantivy index over message bodies
│   └── ui/
│       ├── chat_view.rs        # transcript + composer + markdown
│       ├── sidebar.rs          # chat list + context menu (rename/pin/archive/export/delete)
│       ├── settings_view.rs    # modal settings window
│       ├── perf_dashboard.rs   # in-app stats
│       ├── waveform.rs         # custom animated bar widget
│       └── toast.rs            # bottom-right toast queue
└── xtask/                      # Rust build/release helpers (no shell scripts)
    ├── Cargo.toml
    └── src/main.rs             # check / fmt / lint / test / dev / dist / bundle / reset / install-deps / ci / clean

Developer Workflow (cargo xtask)

There are no shell scripts in this repository. Build/release/dev helpers live in the xtask/ crate and are invoked with cargo xtask <command>.

Command What it does
cargo xtask check fmt --check + clippy --all-targets -- -D warnings + cargo test --workspace. The exact pipeline CI runs.
cargo xtask fmt cargo fmt --all.
cargo xtask lint cargo clippy --all-targets --workspace -- -D warnings.
cargo xtask test cargo test --workspace.
cargo xtask dev [args] Debug build with RUST_LOG=grok_insane=debug,info. Forwards any extra args to the binary.
cargo xtask dist Release build + copies the binary to dist/<target>/grok-insane[.exe].
cargo xtask bundle Release build + per-OS packaging: a real .app on macOS (incl. Info.plist with NSMicrophoneUsageDescription), grok-insane.exe on Windows, staged tar source on Linux.
cargo xtask reset Calls grok-insane --reset-db --yes.
cargo xtask install-deps sudo apt-get install … for the Linux dev libs (no-op on macOS / Windows).
cargo xtask ci install-deps + check + dist.
cargo xtask clean cargo clean + remove dist/.
cargo xtask help Print the above.

Command palette (⌘/Ctrl+K)

Press ⌘ / Ctrl+K from anywhere in the app to open the command palette. Fuzzy-matches against every action — new chat, settings, voice/TTS/RAG toggles, theme + provider switches, "go to chat" by title, export, quit. Arrow keys + Enter to run, Esc to close.

Add a new helper by editing xtask/src/main.rs; each helper is a regular Rust function calling std::process::Command.


CLI

grok-insane itself ships a small CLI surface so the binary is usable headless (smoke tests, support sessions, CI):

grok-insane                  # launch the desktop app
grok-insane --version        # print version
grok-insane --help           # usage
grok-insane --diag           # self-test: paths, redb open, chat/message counts, keyring presence
grok-insane --reset-db --yes # wipe local DB + search index

Example --diag output:

data dir:    /root/.local/share/grok-insane
config dir:  /root/.config/grok-insane
cache dir:   /root/.cache/grok-insane
db path:     /root/.local/share/grok-insane/grok-insane.redb
index path:  /root/.local/share/grok-insane/search-index

chats:       0
messages:    0

api key [xai      ] missing
api key [openai   ] missing
api key [anthropic] missing
api key [local    ] missing

Feature Flags

All are off by default — the base build stays slim.

Flag Adds
rag fastembed for local sentence embeddings; the retriever re-ranks lexical hits by cosine similarity to the query. Model weights download on first use.
hotkeys global-hotkey for system-wide shortcuts (e.g. ⌘⇧V to toggle voice from any window).
plugins wasmtime host (Cranelift + async). Plugin surface area is reserved; ready to expand.
cargo run --features rag
cargo run --features rag,hotkeys
cargo build --release --features plugins

Releases (macOS DMG with Apple signing + notarization)

A tag push (v0.1.0 and up) drives .github/workflows/release.yml, which:

  1. Builds release binaries on macOS arm64, macOS x86_64, Linux x86_64, and Windows x86_64.
  2. On macOS, imports your Developer ID certificate into an ephemeral keychain and runs cargo xtask dmg — which calls codesign --options runtime --timestamp --entitlements packaging/Entitlements.plist, submits to xcrun notarytool submit --wait, staples with xcrun stapler staple, and builds the DMG with hdiutil. Both the .app and the .dmg itself are signed and stapled.
  3. Uploads grok-insane-<version>-<triple>.dmg / .tar.gz / .zip to a GitHub Release using softprops/action-gh-release.

Required repository secrets

Add these in Settings → Secrets and variables → Actions before tagging:

Secret What it is
APPLE_CERTIFICATE_P12_BASE64 Your Developer ID Application cert exported as .p12, then base64 -i cert.p12 | pbcopy.
APPLE_CERTIFICATE_PASSWORD The password you set when exporting the .p12.
APPLE_DEVELOPER_ID_APPLICATION The full identity string, e.g. Developer ID Application: Your Name (TEAMID12345). Get it with security find-identity -v -p codesigning.
APPLE_ID Your Apple ID email.
APPLE_TEAM_ID The 10-char Team ID from your Apple Developer account.
APPLE_APP_SPECIFIC_PASSWORD Generate at https://appleid.apple.com → Sign-in & Security → App-Specific Passwords.
KEYCHAIN_PASSWORD Any long random string. Used only to lock the temporary keychain on the runner.

What Gatekeeper, XProtect, and SentinelOne will see

State First-launch behaviour
Signed and notarized (all secrets present) DMG opens cleanly. The .app opens with no warnings. Gatekeeper + XProtect verify the stapled ticket offline; SentinelOne sees a trusted Apple-notarized binary with a known Team ID.
Signed only (no notary creds) Gatekeeper shows "X cannot be opened because Apple cannot check it for malicious software." User can right-click → Open as the escape hatch.
Ad-hoc signed (no Apple secrets) Gatekeeper blocks the app outright on first launch; same right-click escape hatch. SentinelOne will frequently flag and quarantine.

Note that no client-side change can guarantee SentinelOne won't flag the binary — S1 runs behavioural rules set by your IT admin. A notarized binary from a known Team ID is the strongest signal you can ship; if S1 still flags it, the only fix is for an admin to add an exclusion.

To cut a release:

git tag v0.1.0
git push origin v0.1.0
# CI takes a few minutes; the release appears under
# https://github.com/<you>/<repo>/releases

You can also kick off the workflow manually under Actions → Release → Run workflow (it'll build but won't publish a release unless triggered by a tag).

Verifying a release

Every artifact under the Releases tab is:

  1. Signed via Sigstore keyless signing. The signing identity is the GitHub Actions workflow that produced it; the signature + public cert are recorded in the Rekor transparency log. No GPG keys.
  2. Attested with a SLSA v1.0 build-provenance attestation proving the artifact came from this repository and this workflow.
  3. Documented by a CycloneDX 1.5 SBOM listing every dependency, version, license, and source.

To verify any artifact (one-time cosign install required — https://docs.sigstore.dev/cosign/installation):

# 1. Cryptographic signature + transparency-log entry
cosign verify-blob \
  --bundle GrokInsane-0.1.0.dmg.cosign-bundle \
  --certificate-identity-regexp 'https://github.com/mcaney006/grokmacos-better-version/.github/workflows/release\.yml@.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  GrokInsane-0.1.0.dmg

# 2. SLSA build provenance (requires `gh` CLI logged in)
gh attestation verify GrokInsane-0.1.0.dmg --owner mcaney006

# 3. SBOM — scan with Grype for known CVEs
grype sbom:grok-insane-0.1.0.sbom.json

If any of those three steps fails, do not run the binary. Either the file was tampered with after release, or it didn't come from this repository at all.

Building a DMG locally on macOS

# ad-hoc signed (warns at launch but works for local testing):
cargo xtask dmg

# fully signed + notarized:
export APPLE_DEVELOPER_ID_APPLICATION="Developer ID Application: Your Name (TEAMID12345)"
export APPLE_ID="you@example.com"
export APPLE_TEAM_ID="TEAMID12345"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
cargo xtask dmg

The output lands at dist/<host-triple>/GrokInsane-<version>.dmg.

Security Model

  • Secrets never touch disk in plaintext. API keys live in the OS keyring (com.grokinsane.grok-insane, one entry per provider). In-memory copies are wrapped in zeroize::Zeroizing so the buffer is overwritten on drop.
  • No telemetry, no analytics, no third-party SDKs.
  • TLS everywhere via rustls (no OpenSSL dependency); WebPKI roots are bundled at build time.
  • WS auth rides as a Bearer header — same key as the REST API.
  • PCM audio stays on your machine until you toggle voice mode; once enabled it is streamed to the configured xAI Realtime endpoint only.
  • Database is a single redb file in your local data dir. It can be copied, backed up, or wiped with grok-insane --reset-db --yes.

Performance

Targets the rewrite is built around (measured on an M2 Air with 50 k cached messages):

Metric Target Why
Cold startup < 150 ms redb open is lazy; eframe boots without glow.
Frame time < 8 ms (120 fps) Immediate-mode + GPU compositor + cached markdown.
Resident memory < 80 MB idle No GC, no JS, no embedded browser.
Search latency < 5 ms / 100 k msgs Tantivy MMAP segments.
Stream-to-screen latency bounded by network UI drains its channel each frame; no extra hop.

Toggle the perf dashboard in Settings → Show performance dashboard to watch live numbers.


Testing

cargo xtask check                  # fmt + clippy + tests
cargo nextest run --workspace      # parallel test runner
cargo test --workspace             # if you don't have nextest installed

105 tests passing across the workspace (default + --no-default-features

  • --features hq-resample configurations). Coverage spans:
  • Streaming decoders — both providers (xAI/OpenAI-compatible + Anthropic) have property tests (proptest, 256 cases × 4 KiB inputs, with counterexamples persisted to proptest-regressions/) AND an in-tree 1000-seed LCG fuzz. Adversarial regression tests cover post-[DONE] silence, oversize lines, mid-feed terminators, zero-byte chunks, malformed JSON, provider error events, and EOF-before-terminator.
  • Tool-use streaminginput_json_delta accumulator tested for multi-fragment assembly across chunk sizes 1/7/16/64, empty-input calls, malformed-JSON error surfacing, and fence-injection sanitisation.
  • WebSocket health — connect-timeout against a hung handshake, keepalive-failure surfaces an Error event, receive-watchdog deadline.
  • Storage — chat/message round-trip, range-scan ordering, delete-chat / delete-message cleanup across redb + tantivy, settings decode-failure fallback, individually-corrupt entry skip, and the new startup reconciliation when redb and tantivy diverge.
  • Cancellationconsume_chat_stream honours cancel-before-loop and cancel-mid-stream, surfaces provider errors without trailing Done, and synthesises a Done on silent EOF.
  • Rate-limit retry — mock HTTP server confirms Retry-After is honoured, retry budget is bounded, and non-429 errors short-circuit.
  • Audio + resampler — identity rate, ~0.5 downsample ratio, empty input, queue overflow drops cleanly without panic.

See TESTING.md for the full inventory and the docs/THREAT_MODEL.md for the security risk → control mapping.

A cargo-fuzz workspace lives in fuzz/ with libFuzzer harnesses for both SSE decoders, seeded from in-tree golden fixtures. Run on-demand with cargo +nightly fuzz run sse_decoder -- -max_total_time=60. Not run continuously by CI today.

  • services::chat::tests::sse_decoder_extracts_deltas_and_done — OpenAI- compatible SSE state machine.
  • services::anthropic::tests::anthropic_decoder_parses_text_delta_and_stop — Anthropic event stream + usage tracking.
  • services::export::tests::markdown_export_includes_messages_and_title
  • services::export::tests::obsidian_export_has_front_matter_and_quotes_title_with_colon
  • services::export::tests::json_export_round_trips

Roadmap

Already shipped this iteration: see CHANGELOG.md.

Not yet implemented (treat these as experimental until they ship)

  • Streaming TTS playback — play audio chunks as they arrive instead of waiting on audio.done. The WebSocket plumbing exists; the audio sink hookup does not.
  • Interruptible TTS — barge-in via a key while speaking.
  • Image input for vision-capable models. Provider clients don't yet serialise image content blocks.
  • File / tool attachments in the composer. The Anthropic decoder surfaces tool_use events end-to-end; there is NO local tool runner. The plugins feature gate is reserved but contains no implementation — wasmtime was intentionally NOT pulled in because of outstanding RUSTSEC advisories at the time of writing. Will reinstate when wasmtime >= 38 is compatible with our other deps.
  • Continuous fuzzing in CIfuzz/ harnesses run on-demand; scheduling them weekly requires a nightly job, deferred.
  • Apple Developer ID signing in CI — workflow exists, secrets aren't configured. First DMG will be ad-hoc signed; macOS users will see a Gatekeeper prompt until real Developer ID credentials land.

Shipped (was on the original roadmap, completed)

  • macOS notarization — implemented in xtask::mac::{sign, notarize, dmg}, exercised by the release workflow.
  • Sigstore keyless signing — every release artifact gets a cosign bundle via the release workflow.
  • SLSA build provenance — via actions/attest-build-provenance.
  • CycloneDX SBOM — generated per release via cargo-cyclonedx.

License

Dual-licensed under either of:

at your option.

About

Native Grok app for macOS

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Rust 100.0%