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:
@@ -5,6 +5,7 @@
|
||||
import SearchPalette from './SearchPalette.svelte';
|
||||
import type { PaletteItem } from '../lib/search-model';
|
||||
import { walkTo } from '../lib/walk';
|
||||
import { live } from '../lib/live.svelte';
|
||||
|
||||
interface Props {
|
||||
/** Indexed project name, e.g. "codegraph/". Null until stats load. */
|
||||
@@ -111,6 +112,31 @@
|
||||
}
|
||||
|
||||
let searchBox: HTMLDivElement | null = $state(null);
|
||||
|
||||
/**
|
||||
* Why this page has stopped updating itself, when it has.
|
||||
*
|
||||
* The whole point of the live channel is that the screen keeps up with the
|
||||
* project; a screen that has silently stopped keeping up is worse than one
|
||||
* that never claimed to. So both ways it can end say so, in the one place
|
||||
* that is on every view.
|
||||
*/
|
||||
let liveNote = $derived.by(() => {
|
||||
if (live.degraded !== null) {
|
||||
return {
|
||||
text: 'Live updates off',
|
||||
title: `${live.degraded} This page no longer refreshes itself — reload it after a sync.`,
|
||||
};
|
||||
}
|
||||
if (live.stopped) {
|
||||
return {
|
||||
text: 'Not live',
|
||||
title:
|
||||
'Lost the connection to codegraph ui and stopped retrying. Focus this tab to try again, or reload the page.',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window {onpointerdown} />
|
||||
@@ -152,6 +178,7 @@
|
||||
</div>
|
||||
|
||||
<div class="project" title="Indexed project">
|
||||
{#if liveNote}<span class="offline" title={liveNote.title}>{liveNote.text}</span>{/if}
|
||||
{#if project}<span class="mono">{project}</span>{/if}
|
||||
{#if stats}<span class="dim">{stats}</span>{/if}
|
||||
</div>
|
||||
@@ -246,9 +273,20 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Below ~1000px the stats are the first thing worth losing. */
|
||||
.offline {
|
||||
padding: 2px 6px;
|
||||
margin-right: 8px;
|
||||
border: 1px solid var(--rule-soft);
|
||||
background: var(--paper-2);
|
||||
color: var(--ink-3);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
/* Below ~1000px the stats are the first thing worth losing — but not the
|
||||
note that the page has stopped updating itself. */
|
||||
@media (max-width: 1000px) {
|
||||
.project {
|
||||
.project .mono,
|
||||
.project .dim {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user