feat(ui): live refresh and drift banners — the viewer keeps up with the project (CG-53)

`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.
This commit is contained in:
Colby McHenry
2026-08-27 04:28:56 -05:00
parent bd99c5e99a
commit ecd6e1cd15
28 changed files with 2236 additions and 142 deletions
+11 -1
View File
@@ -31,7 +31,17 @@ The viewer never presents a guess as a fact:
- Edges CodeGraph resolved by name alone, below its confidence threshold, fold into an "uncertain" line rather than sitting among the resolved ones. Nothing is silently dropped — the count is always there.
- A symbol that no test reaches within three caller hops wears a badge saying exactly that.
- Calls into symbols that aren't in the index are counted and marked, not omitted.
- A file that changed on disk since it was indexed shows a banner instead of source that may no longer line up.
- A file that changed on disk since it was indexed wears a banner and switches to the file's **current** source, with everything the graph anchors to a line number — the gutter markers, the call arcs, the right-hand list — switched off. The bytes on disk are right by construction; the line numbers the index recorded are the part that stopped being true.
## It keeps up with your project
The viewer follows the project while it is open, and it does it by watching, never by asking on a timer.
- **Save a file and the banner appears** — about a third of a second later, before any sync has run. That is the honest state: the file on disk and the index have parted company, and the screen says so rather than showing you a body sliced at the wrong lines.
- **When something re-indexes** — your agent's background sync, `codegraph sync`, a git hook — whatever is on screen refetches itself and a small "Index updated · reloaded" note appears at the bottom. The symbol, the file, the map and the flow are all answers about the graph as a whole, so all of them re-read it.
- **A symbol that moved is followed, not lost.** Adding two lines above a function changes its identity in the graph; the viewer finds it again in its file and carries your trail across, rather than telling you the thing you were reading no longer exists.
If the viewer ever loses touch with the server, it retries a handful of times with a growing delay and then stops and says **"Not live"** in the top bar — it never falls back to polling. Focus the tab to reconnect.
## Getting around