From Panels to a Reader

Rebuilding the UI as a document-first Svelte SPA on an event stream, and turning the app into a real desktop application.

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 BroadcastChannel and 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.

AI Insights: The Interface Reveals the Model
Source Code: MPL-2.0 · Content: CC BY-SA 4.0 · Credits & AI Disclosure