From Panels to a Reader
The first UI was a collection of panels that showed off the layers: a list of sources, a list of atoms, a list of pages. It was honest and it was wrong. I do not want to browse atoms; atoms are machinery. I want to read, and I want the wiki to feel like a document, not a database.
So I rewrote the interface, and the shape of the app changed with it.
Document-First, Library-Centered
The new center is the Library: a collapsible sidebar of curated, hierarchical topics (domain → subtopic), and one rendered Markdown document at a time. Everything else is secondary:
- Quick-find (⌘K) opens a page through FTS5 search instead of navigating a tree.
- The document footer shows the page’s sources (opened inline in an overlay), its related pages, and its backlinks.
- Sources are add-by-path, drag-and-drop, or a URL. They are secondary to the reading experience, but they carry their metadata, their topics and tags, and the claims they produced—atoms visible as provenance detail, never as a navigation target.
The styling is Notion-flavored, on the studio’s palette: calm, generous whitespace, no chrome competing with the text. The rule is that the app is a reader, not an authoring IDE.
One Event Stream Instead of a Timer
The UI is event-driven. A single SSE connection (GET /api/v1/events) pushes run, source, wiki, findings,
and urls events, and the UI patches only the relevant data containers—so a background refresh never steals focus from
a form field. There is no polling.
Runs started by a button are also resolved by the client, not only by the stream. That redundancy is deliberate: a lost SSE frame must never leave a spinner turning forever. And the progress bar reserves its space whether or not a run is active, so the layout does not jump when an ingest starts.
The Front End Is Just the Front End
The SPA is Svelte 5 + Vite + TypeScript in web/, built into internal/web/dist, and embedded with go:embed. Same
origin as the API—no CORS, no token. The build is part of task build, so there is one artifact and no separate
front-end deployment to reason about.
A Desktop App Without a WebView
With the UI in place, the last piece was making it feel installed rather than visited.
- The app does not launch a browser and does not embed one. On start it logs its address and hands it to the operating system’s URL handler; the installed PWA (standalone manifest, service worker, PNG icons) provides the window.
- The single-window rule is owned by the UI: a duplicate window detects the primary over a
BroadcastChanneland focuses it instead of living on as a second window. - The single-instance rule is owned by the app: a second launch cannot take the vault lock, so it pings the running
instance, opens its UI, and exits
0. - The tray offers Open, Pause watcher, and Quit, on top of the same pure-Go stack.
The interface was rebuilt around the document, and the chapter treats that as a claim about what the tool is. Panels that displayed the implementation—lists of atoms, lists of sources—gave way to a reader where provenance is a detail opened on demand. The event stream replaced polling, and the desktop shell became a tray around a local service rather than a second UI. A screen reveals what its author believes the system is; the earlier screens revealed the pipeline.
