Architecture Overview
A practical map of how Reikon is put together — written for a human reading it before their first PR, not as a redirect toCLAUDE.md (which is internal guidance for Claude
Code sessions specifically, and assumes that context).
Stack
Electron 32, electron-vite, React 18, Tailwind 3, Zustand for state, Phosphor Icons, TypeScript throughout (noany), Vitest for tests.
The three processes
Like any Electron app, Reikon runs across multiple processes with different responsibilities:- Main process (
src/main/) — owns the window, the IPC handlers, the file watcher, and orchestrates analysis. Talks to disk and runs git commands; never touches the UI directly. - Renderer process (
src/renderer/) — the React UI. Has no direct filesystem or git access — everything it needs comes through IPC. - Preload script (
src/preload/) — the bridge between the two. Exposes a typedwindow.analyzerAPI via Electron’scontextBridge; the renderer never talks toipcRendererdirectly.
src/main/worker.ts), spawned by the main process, so a large repo’s
analysis never blocks the window from responding.
Code shared between main and renderer
Most code is environment-specific by necessity — the renderer can’t touch the filesystem, the main process has no DOM. But pure calculation logic with no environment-specific dependencies can and should live insrc/shared/, importable
from both sides (it’s included in both tsconfig.node.json and tsconfig.web.json,
the same way src/types/ already was).
The health score formula is the canonical example: it used to be three independently
hand-written copies (the desktop app, persisted snapshots, and the CLI), which had
already drifted apart in ways nobody caught until an audit found them. It’s now one
implementation in src/shared/health-score.ts, imported by all three. If you’re adding
logic that more than one of needs to compute identically, this is
where it goes — not copied into each consumer “to keep in sync by hand.”
How an analysis actually runs
- Renderer calls
window.analyzer.run(rootPath) - Preload forwards it over IPC to the main process
- Main process spawns (or reuses) the utility process worker
- The worker runs
runAllAnalyzers()— git stats, line counts, complexity, duplicates, imports, dead code, dependency audit, coverage parsing, all in one pass - Results come back as one
AnalysisResultobject, cached on disk keyed by repo path- current commit hash + a hash of the project config
- The renderer stores it in a Zustand store; every tab derives what it needs from
that one object via a single hook (
useAnalysis()) — no tab fetches its own data independently
AnalysisResult, get added to the relevant analyzer in src/main/analyzers/, and get
exposed through useAnalysis() — not fetched ad hoc from a new IPC call inside a
specific tab, unless it’s something that genuinely needs to be computed on demand
(see the next section).
When something isn’t part of the cached analysis
Not everything goes through the cachedAnalysisResult pipeline. Some signals are
computed live, on demand, via a dedicated IPC call instead — typically because they
involve comparing two arbitrary git states (e.g. a staged diff vs. HEAD, or two
arbitrary commits) rather than “the current state of the repo,” which is what the
cached pipeline is built around. The diff health score and post-commit complexity
toast in the Review tab are both built this way: a small main-process handler runs
git show/git diff against the specific refs needed, instead of waiting for or
triggering a full re-analysis.
IPC conventions
Every IPC handler:- Is wired in three places:
main/index.ts(the handler itself),preload/index.ts(the exposedwindow.analyzer.*method), andtypes/ipc.ts(theAnalyzerAPIinterface). Missing any one makes the method unreachable from the renderer with no compile error to catch it. - Never throws. Returns
{ __error: string }on failure; the preload layer checks for that shape and re-throws as a realErroron the renderer side, so renderer code can use normaltry/catch.
The renderer’s data flow
useAnalysis() is the single hook every tab uses to read analysis data — no tab reads
the Zustand store’s raw data field directly, and no tab fetches its own copy of
anything already in AnalysisResult. This keeps every tab’s view of the data
consistent and means a schema change to AnalysisResult only needs updating in one
place to propagate everywhere it’s consumed.
MCP server
A separate, smaller process (src/main/mcp-server.ts), started with a --mcp-server
flag, speaking the Model Context Protocol over
stdio. It reads a lightweight mcp-context.json snapshot written after each analysis
— not a live connection into the running desktop app — so an AI agent’s queries never
block or interfere with whatever you’re doing in the UI. See the
MCP tool reference for what it exposes.
CLI
packages/cli is a separate npm package (rei) that imports the same analyzer code
directly from src/main/analyzers/ — there’s exactly one implementation of every
analyzer, used by both the desktop app and the CLI. See the
CLI reference.
Where to look next
- Design system for UI conventions if you’re touching the renderer
- Lessons learned for non-obvious gotchas worth knowing before you hit them yourself
CONTRIBUTING.mdat the repo root for dev setup and the PR process