conao3-sa
Local git diff & repo reviewer. Browse a working tree, walk the commit graph, and review diffs (file-tree, viewed state, inline comments, vim-flavoured keybindings) — all from your own machine, no upload.
Crate name: conao3-sa. Executable: sa.
Status
Early development. The codebase is intentionally small and the API surfaces are not stable.
Install & run
conao3-sa is published on crates.io
(crates.io/crates/conao3-sa).
Prerequisites
- Rust toolchain (1.95+) — install via rustup or
the Nix devShell (
nix develop). - A
gitbinary onPATH(the backend shells out to it). - For the Tauri shell on Linux:
webkit2gtk-4.1,libsoup-3,gtk3(see the Tauri docs for your distro).
Option A — install from crates.io
One executable lands in ~/.cargo/bin:
| Command | What it does |
|---|---|
sa |
Tauri desktop shell — spawns the axum backend in-process and opens a WebView. |
sa --serve |
Headless axum backend at 127.0.0.1:4000 (GraphQL + REST + SSE), no UI. |
The published crate bundles a pre-built SPA, so sa works out of
the box without a Node/pnpm toolchain on the host.
Option B — install from source
make dist runs pnpm build and copies frontend/.output/public into
src-tauri/dist, which is the path Tauri reads as frontendDist.
Producing a platform bundle
Bundling needs the usual platform tooling (linuxdeploy/fpm on Linux,
Xcode on macOS, MSVC + WiX on Windows); cargo-tauri provisions most of
it on first run.
Launching
# Desktop UI. The shell starts the backend on 127.0.0.1:4000 in a side
# thread, then opens the WebView pointed at the embedded SPA.
# Or headless backend + browser (useful for development):
&
# then run the frontend dev server in another shell:
# make -C frontend dev # vite via portless at https://sa.localhost
Note:
sastarts the UI at/. In-app navigation uses TanStack Router (client-side), so links work; a manual reload of a deep URL like/browsemay bounce back to/because Tauri's static asset resolver does not implement a SPA fallback. Stick to in-app navigation and reloads are rarely needed.
The first time you launch, you'll be on / — point it at any local
repo via the folder picker and you'll land in /browse. From there
graph / diff are linked in the top bar.
Preferences
Theme is persisted to ~/.config/sa/config.toml. Everything else
(layout, density, pane widths, recents, comments, viewed state) lives
in browser localStorage.
Stack
- Backend (
src-tauri/) — Rust, axum, async-graphql, Tauri 2. Shells out togitfor diff / log / show / ls-tree / for-each-ref; serves GraphQL at/api/graphqland SSE at/api/events. Per-repo file watcher via notify-debouncer-mini, filtered through theignorecrate so nested.gitignoreand global excludes are honored. Preferences persist to~/.config/sa/config.toml. - Frontend (
frontend/) — TanStack Start (React 19) on Vite + Rolldown. React Compiler, TanStack Router / Form / Hotkeys, Apollo Client 4, react-aria-components, Tailwind v4. Diff rendering via@pierre/diffs, file tree via@pierre/trees, syntax highlighting via Shiki. - Tooling — oxlint, oxfmt, knip, tsc (via
make lint); cargo-watch for backend auto-reload; treefmt + nixfmt / rustfmt / prettier; flake devShell.
Layout
src-tauri/ Rust backend (axum + GraphQL + git CLI)
src/main.rs Tauri shell (executable: sa)
src/server.rs axum + GraphQL backend (also reachable via `sa --serve`)
frontend/ TanStack Start frontend
src/routes/ __root, /, /browse, /compare/$, /graph, /preference, /design, /health
src/components, src/lib
Makefile Orchestrates src-tauri + frontend
flake.nix Nix devShell (rust, node, pnpm, cargo-tauri, cargo-watch)
Routes
/— landing. Repo input + folder picker (fuzzy filter; click theGITbadge on any row to open it directly); recents list with per-row trash./browse?repo=<abs>&path=<rel>&rev=<ref>— repo browser. Closed tree on first paint; clicking a file fetches its content from/api/bloband renders it with Shiki. Blob & highlight are cached by URL/path so revisits are instant.revdefaults toHEAD./compare/$spec?repo=<abs>&w=1— diff reviewer.specaccepts any git rev (HEAD,main,v1.0,feature/foo), a two-dot range (main..feature), or three-dot (HEAD~3...HEAD). The pseudosworkingandstagingresolve togit diff HEADandgit diff --cached HEAD, and can also participate in ranges (<commit>..working,<commit>..staging,staging..working, …). Merge commits show first-parent diff.?w=1adds-wto ignore whitespace. Toggle layout (unified/split) and whitespace from the gear menu in the top bar./graph?repo=<abs>— commit log + range picker.- Sticky
COMMITSheader, infinite scroll for older history. WORKING/STAGINGpseudo-rows pinned at the top, branches and tags as collapsible sections (with a fuzzy filter when there are more than a handful).- click sets base, Ctrl/Cmd + click sets head,
drag across rows picks
base..headin one gesture (with the intermediate commits tinted), double-click opens that commit's diff in/compare.
- Sticky
/preference— settings. Theme (light/dark) is persisted to~/.config/sa/config.toml; display options (layout, density, pane widths) live in localStorage./design— design tokens & palette reference.
Backend API
POST /api/graphql health, preferences, listDir, commits(limit, skip, repo),
files(rev, repo, w), branches(repo), tags(repo),
tree(repo, rev), setPreferences(theme)
GET /api/diff ?rev=&path=&repo=[&w=1] text/x-diff, gzip
GET /api/blob ?rev=&path=&repo= text/plain, gzip
GET /api/events ?repo= SSE; per-repo, gitignore-aware
?repo=<absolute-path> is required on every URL that touches a
repository. There is no implicit default.
Development
For working on rust-sa itself (auto-rebuild backend, vite HMR for frontend). Requires Nix with flakes (provides rust, node 24, pnpm 10, cargo-tauri, cargo-watch). Otherwise install those tools manually.
# enter devShell
# install frontend deps
&&
# run backend (axum at :4000, auto-restart on .rs edits)
# run frontend (vite dev via portless proxy at https://sa.localhost)
devo run wires both processes as a tmux session named rust-sa.
Lint / format
Notable design choices
/browsecaches blob fetches and Shiki highlight output by URL and(path, theme, content prefix), so flipping between files is instant after the first visit. Theloading…indicator is deferred 200 ms — cache hits never get to show it, only cold fetches do.ignorecrate is used in the watcher so SSE only fires for paths the target repo actually cares about (nested.gitignore,info/exclude, global excludes viacore.excludesFile). Directory-level notify events are skipped to suppress spurious refreshes when generated files inside ignored directories churn.- Watcher events are debounced (3 s) and the frontend additionally
debounces SSE (1.5 s) plus compares the file-list signature before
flipping any
liveUI, so background dev servers (e.g. Next.js rebuilding.next/) never flicker the diff view. - React Compiler handles memoisation;
useMemo/useCallbackare avoided in application code. - Comments live in
localStorage, keyed by rev; the model carriesstartLineNumber/endLineNumberso multi-line ranges round-trip. - Theme is the only preference that goes to disk (
config.toml); all ephemeral display state (mode, density, pane widths, section open/closed) stays in localStorage to avoid filesystem chatter.
License
MIT