`GET /api/events` is a server-sent-event stream the viewer holds open for the life of the page. Two signals, two things the browser could not know: changed source files touched on disk, before any sync — the drift banner index the graph moved, naming what the sync re-indexed — the live refresh The server WATCHES and never syncs: the project tree through the engine's own FileWatcher with a notify-only syncFn, the index through one non-recursive fs.watch on the data directory settled at 400 ms. Both start with the first subscriber and stop with the last, so a viewer nobody has open costs no watch descriptors. Nothing polls, on either side. Drift is now parity with codegraph_node (#1474) rather than an absence. `/api/source?ondrift=current` serves a drifted file's CURRENT bytes flagged `showing: 'current'`, and the three screens that can say so switch off everything anchored to the old line numbering — gutter ports, call-site links, call arcs, the callee rail's anchoring — while keeping the source. The banner is paper-2 with a hairline rule, never amber: amber belongs to the untested badge. Also fixes a stale read this exposed. A long-lived reader holds an LRU of nodes by id that only its own writes invalidate, so `/api/node/<id>` kept answering with a symbol another process's sync had deleted while `/api/search` beside it said it was gone. GraphSession now drops the read caches when the database (or its WAL) has been written, and the Symbol view follows a symbol whose id changed because an edit above it moved its start line, carrying the trail across. Measured on a live viewer: banner 360 ms after a save, toast 440 ms after `codegraph sync` returns, 0 requests in 4 idle seconds, and the client gives up reconnecting after ~90 s with "Not live" rather than hammering a dead port.
98 lines
4.7 KiB
Markdown
98 lines
4.7 KiB
Markdown
# 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/<id>, #/file/<path>, #/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/<id>?hl=<line>&t=<trail>` | symbol view |
|
|
| `#/file/<path>?hl=<line>` | file view — outline in source order |
|
|
| `#/file/<path>?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=<trail>` | 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.
|