This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
Turn an OmniGraph graph database into a read-and-act dashboard you describe in one YAML file — rendered identically in a terminal and a browser.
Normally you inspect a graph by writing queries and reading JSON, or by building a bespoke UI. A notebook is the layer between: a YAML file that declares what slices of the graph to show and what actions to allow, not code. Each data cell is a typed lens (Table/Path/Subgraph/ActionList/Timeline/Card/Quote/Text) fed by query.ref to a server-owned .gq catalog query, or a control (Select/Toggle/Button) that filters state or dispatches actions. See examples/company-server.notebook.yaml: a clause review list with inline Approve/Reject buttons, a decisions table, and a Signal → Decision path — no UI code anywhere.
Two bets make it work:
- Typed lenses, not a generic graph viewer — you name the view you want; the system renders it.
- Write once, render anywhere — the same YAML drives the Ink terminal UI and the React web UI, against a live omnigraph-server (a local cluster in dev, a remote server in prod). It's bidirectional: lenses read the graph, controls and actions write back to it.
This is a pnpm workspace (pnpm 10.30.3, Node ≥20). All scripts run from the repo root unless noted.
pnpm install # install workspace deps
pnpm -r build # tsc-build every package — REQUIRED before tui/web run
pnpm -r typecheck # tsc --noEmit across all packages
pnpm -r test # vitest run across all packages
pnpm --filter @modernrelay/notebook-<pkg> build # rebuild one package
pnpm --filter @modernrelay/notebook-<pkg> test # vitest run for one package
pnpm --filter @modernrelay/notebook-<pkg> test -- <pattern> # single test file/name
pnpm tui examples/company-server.notebook.yaml # Ink TUI, server mode — server URL + graph id
# come from the notebook (run server-demo.sh first)
pnpm --filter @modernrelay/notebook-web dev # Vite dev server at 127.0.0.1:5173
# add ?server=/og&graph=company (same-origin proxy)
pnpm --filter @modernrelay/notebook-web build # tsc + vite production build
scripts/server-demo.sh # build omnigraph v0.7.0 CLI/server, boot a local
# filesystem cluster (graph "company") on :8080The TUI consumes built dist/ from sibling workspace packages — always run pnpm -r build after editing a non-TUI/non-web package before running pnpm tui. Web's Vite bundles via TS sources directly, but tsc --noEmit (pnpm -r typecheck) is what enforces cross-package types.
One catalog, two renderers, one server-backed runtime. A notebook is YAML; each cell renders as a typed lens (Table/Path/Subgraph/ActionList) or a control (Button/Toggle/Select). Both the Ink TUI and the React Web app share the same catalog of component definitions and the same runtime; only the leaf component implementations and the host shell differ.
| Package | Role |
|---|---|
@modernrelay/notebook-core |
The engine — start here. One package, three internal modules: spec (Zod schemas + YAML parser, query model — ref/rawGq — mutation specs), catalog (component+action definitions shared by both renderers; assembleLensSpec / assembleControlSpec produce json-render specs), runtime (capability-aware execution, state mirror, dependency invalidation, action dispatch, mutation lifecycle, optimistic reconciliation). The @json-render/core analog. |
@modernrelay/notebook-client |
The only data source. ServerSource + a Client facade over the @modernrelay/omnigraph SDK (/queries/{name} + /query escape hatch + /mutate, graph-scoped). translateMutation exists only for the interim set_field write path. |
@modernrelay/notebook-tui |
Ink renderer + CLI entry (bin/omnigraph-tui.js); host shell for terminal. |
@modernrelay/notebook-web |
Vite + React + Tailwind renderer; host shell for browser. |
@modernrelay/notebook (packages/cli) |
The published front-door CLI. Bundles every @modernrelay/notebook-* lib (tsup, noExternal) and ships the built web SPA in web-dist/. Subcommands: view (browser — static server + /og BFF proxy with server-side token injection, reusing web/src/config.ts's URL-param contract), tui (calls @modernrelay/notebook-tui main), validate/render/catalog/schema (agent-DX, JSON out; schema via Zod 4 z.toJSONSchema). The workspace root is the private notebook-workspace; @modernrelay/notebook is the CLI, not the root. |
YAML ─parseNotebook→ Notebook ─createNotebookRuntime→ RuntimeSnapshot ─Renderer→ UI
│ │
▼ ▼
Source.capabilities/read/mutate assembleLensSpec()
(ServerSource) → json-render Spec
@modernrelay/notebook-core'sspecmodule parses+validates YAML against frozen v1 Zod schemas. Defines the cell query model (query.ref→ a server-owned catalog query, orquery.rawGqraw.gqescape hatch) and theMutationSpecdiscriminated union (currently onlyset_field). The v1 schema is strict.@modernrelay/notebook-core'screateNotebookRuntimevalidates notebook compatibility againstSource.capabilities(), resolves{ $state: "/ptr" }expressions for data reads, invalidates only cells whose query dependencies changed, callsSource.read(), and hands results toassembleLensSpec(core'scatalogmodule). Control cells skip reads and pass props through toassembleControlSpec. Per-cell errors are captured onCellExecution.error; runtime-level compatibility failures surface onRuntimeSnapshot.error.- core's
catalogmodule exportslensComponents(Zod prop schemas + descriptions) andlensActions(setState,mutate). Author-time props are validated here; the renderer'sdefineCatalogconsumes the same schemas. - The renderer (
packages/tuiorpackages/web) callsdefineRegistryagainst its UI library, supplying concrete Ink or React+Tailwind component implementations under the same component IDs (Table,Path, ...). The App subscribes to the runtime snapshot and passes each cell'sLensSpecto<Renderer />.
Defined in @modernrelay/notebook-core (its runtime module) as a capability-aware contract: capabilities(), read(request, context), and mutate(command, context). There is one implementation; unsupported features fail during runtime compatibility validation or with explicit source errors:
ServerSource(@modernrelay/notebook-client): the only source. Invokes server-owned catalog queries by name via the SDK'sog.queries.invoke(query.ref, the default path), or sends raw.gqad-hoc viaog.query(query.rawGqescape hatch).mutatecompiles the interimset_fieldto.gq. omnigraph-server 0.7.0+ serves these under/graphs/{graph}/….
Connection: cli/src/source.ts and tui/src/index.tsx resolve via the shared Node-only @modernrelay/notebook-client/node operator-config resolver (~/.omnigraph/config.yaml + credentials: named servers, profiles, keyed-token chain). Flags (--server NAME|URL/--graph/--token/--branch/--profile) and the notebook's server/graph layer in. web/src/config.ts stays on URL params + the view proxy — the browser can't read operator files.
Both Apps instantiate a NotebookRuntime and subscribe to RuntimeSnapshot. JSONUIProvider.onStateChange forwards JSON-pointer patches to runtime.applyStateChanges(). The runtime mirrors state, extracts $state query dependencies, and re-runs only affected data cells.
Mutations are runtime-owned. Renderer handlers call runtime.dispatch("mutate", { params }); the runtime builds mutation context with the originating cell, read target, write target, current state, and optimistic patch metadata. Branch reads write back to that branch. Snapshot reads write to the runtime default branch when one is configured, otherwise the source default applies. Renderers never read from a global mutation source.
Two kinds of cells:
- Data cells (
Table/Path/Subgraph/ActionList) — require aquery. - Control cells (
Button/Toggle/Select) — must NOT have aquery; bind to state via$bindStateor fire actions viaon.press.
A data cell may additionally declare inline controls: [...] — control descriptors that filter or act on that cell's data view. They render above the lens output as separate json-render specs (CellExecution.controlSpecs) and use the same registry as the main lens.
Strict mode + noUncheckedIndexedAccess. All packages extend tsconfig.base.json and emit dist/ with declaration files; consumers import from @modernrelay/notebook-<pkg> (resolves to dist/index.js).
omnigraph-server 0.7.0+ is cluster-only (RFC-011): every read/write is served under /graphs/{graph_id}/…, so server-mode notebooks must carry a graph: id (overridable via --graph/?graph= or the operator-config default_graph). The SDK pins to a matching server line — @modernrelay/omnigraph@^0.7.0 talks to a 0.7.x server only.
scripts/server-demo.sh needs an omnigraph v0.7.0+ checkout on disk — the sibling ../omnigraph by default, or set OMNIGRAPH_REPO. It cargo builds omnigraph-cli + omnigraph-server (release), then materializes a local filesystem-backed cluster under .server-demo/cluster (graph company, schema examples/server/company.pg, seed examples/server/company.jsonl) via cluster import/apply + load, and boots omnigraph-server --cluster … --unauthenticated on :8080 (PID/log under .server-demo/, gitignored). No RustFS/S3 required. Re-running reuses the cluster (mutations persist); delete .server-demo to reset. The demo runs unauthenticated, so bearer tokens are ignored; the web app reaches it same-origin through the Vite /og proxy (the 0.7.0 server sets no CORS headers).