# ui/ — the `codegraph ui` viewer The browser reader for an indexed project: Svelte 5 + Vite, built as static files and served by the CLI over loopback. An npm workspace of the engine, so `npm ci` at the repo root installs its toolchain; nothing here is a runtime dependency of the engine and nothing here is published to npm on its own. Design spec (every token, size and measurement): `../docs/design/codegraph-ui-design-spec.md`. ## Build ```bash npm run build # from the repo root: tsc -> copy-assets -> this app npm run build:ui # just this app, plus the dist assertion npm run dev -w ui # Vite dev server on 127.0.0.1:5174 npm run check -w ui # svelte-check ``` `npm run build` emits **`dist/viewer/`** (`index.html` + hashed assets). `scripts/check-ui-build.mjs` then asserts the tree is complete, so a broken UI build fails the release instead of shipping a CLI that serves a 404. The same check runs again in `scripts/build-bundle.sh` (after the bundle stage copies `dist`) and in `scripts/pack-npm.sh` (after each archive is unpacked). ### Why `dist/viewer` and not `dist/ui` `src/ui/` is the engine's **terminal** UI (shimmer progress and its worker) and tsc compiles it to `dist/ui/`. Pointing Vite there deletes those modules — the CLI then dies at startup with `Cannot find module '../ui/shimmer-progress'` — and would also leave the static server handing out compiled engine internals. `check-ui-build.mjs` re-asserts the compiled engine is intact after every UI build so that mistake cannot land twice. ## Layout ``` src/ main.ts fonts + tokens, mounts App into index.html's #app app.css design tokens (light/dark), reset, shell grid App.svelte top bar / trail bar / main, global keys lib/router.svelte.ts hash router: #/s/, #/file/, #/map, #/flow lib/trail.svelte.ts the walked path; mirrored into the `t` query param lib/kinds.ts kind glyph letters lib/map-model.ts the Map's deterministic layered layout (pure) lib/flow-model.ts the Flow strip's card/link geometry — a DAG (pure) lib/filecode-model.ts the whole-file view: fixed line height, arcs, paging (pure) lib/live.svelte.ts /api/events: two counters every screen refreshes from lib/toast.svelte.ts the one transient note ("Index updated · reloaded") components/ TopBar, TrailBar, KindGlyph, DriftBanner, Toast, map/, flow/, symbol/, file/ views/ one component per route ``` Fonts (Archivo Variable, IBM Plex Mono) are vendored through `@fontsource*` and emitted into `dist/viewer/assets`: a local reader must work offline and must not announce the project to a font CDN. ## Routes | hash | view | |---|---| | `#/` | nothing selected | | `#/s/?hl=&t=` | symbol view | | `#/file/?hl=` | file view — outline in source order | | `#/file/?src=1` | file view — the whole file's source, with ports and call arcs | | `#/map?root=&depth=&tests=1` | module map | | `#/flow?from=&to=` | flow strip — the call path between two symbols | | `#/flow?symbols=a,b,c` | flow strip — `codegraph_explore`'s own question | | `#/flow?t=` | flow strip — the trail you walked, read as a flow | ## Live updates The viewer never polls. `lib/live.svelte.ts` holds one `EventSource` on `/api/events` for the life of the page and exposes two counters: - **`indexTick`** — the graph moved (somebody synced). Every screen refetches: a rail is an answer about the whole graph, and a symbol gains a caller when some *other* file is edited, so filtering by the focused file would leave the rails quietly wrong. One request per sync. - **`diskTick`** — source files changed on disk and the index has not caught up. Only the screen showing one of those files reacts, and what it does is draw a drift banner. `liveRefresh(file, refresh)` is the three lines of bookkeeping that turns a counter into a single call; the Map and the Flow strip instead read `live.indexTick` straight inside the effect that already fetches them. Reconnection is ours, not `EventSource`'s: each failure closes the stream and schedules ONE retry on a backoff that ends after eight attempts (~90 s), at which point the top bar says "Not live" and nothing more is requested until the tab is focused again. A `degraded` event — the server's watcher gave up — is shown the same way and never answered with a poll. Node ids and file paths are encoded per slash-separated segment, so `#/file/src/mcp/tools.ts` stays readable and still round-trips a segment containing a reserved character. Build hashes with `symbolHref()` / `fileHref()` / `mapHref()` / `flowHref()` rather than by hand.