CodeNib Web UI¶
For one local repository, use the packaged two-command path in the Quickstart:
This guide covers the advanced multi-repository development deployment. It has two halves sharing one backend:
- Wiki — an auto-generated page tree per repo with source-grounded prose, a per-page interactive code graph (the page rendered as a view over the symbol graph), and a launchable full-screen dependency explorer.
- Ask — the agent QA flow: ask a question about a repo, get an answer whose inline code spans become clickable citation chips that scroll and highlight the code panel.
The stack is a FastAPI backend (codenib.web.app, package codenib/web/)
plus a React/Vite frontend (web/). Release wheels embed the compiled SPA;
Wiki content is generated by codenib/wiki/.
Interactive graph
Earlier versions of this demo embedded Mermaid diagrams. The wiki now
leads with an interactive Cytoscape graph, and the page strips any
LLM-generated Mermaid before rendering with stripGeneratedDiagrams().
Diagrams are no longer the graph surface — the live, clickable Cytoscape
graph is.
Prerequisites¶
- A CodeNib source checkout with development dependencies (
make dev). - Node.js
^20.19.0or>=22.12.0(only for frontend source development, matching the checked-in Vite toolchain). Install deps once withmake web-deps. - An LLM provider reachable through LiteLLM, for Ask answers and generated wiki pages:
OPENAI_API_KEYfor the defaultgpt-4o(a non-thinking model; setmodelinqa_config.yamlorCODENIB_DEMO_MODELto change it —openai/gpt-4o-miniis only the narrator's last-resort fallback when no model is configured anywhere), or- gcloud Application Default Credentials for
vertex_ai/gemini-2.5-flash(gcloud auth application-default login). The demo reads the project from the ADCquota_project_id; no extra env vars are required.
The site still loads repository lists, source views, graphs, and deterministic wiki pages without a usable model provider. Model-backed Ask requests fail, and generated wiki prose degrades to deterministic, source-grounded text.
1. Build (or reuse) the index¶
The server reads a registry written by the build script:
This selects instances from the CodeNib Base dataset
(fishmingyu/codeminer-base-dataset on the Hub), checks out each repo at
its base_commit, builds indexes under data_dir (default .codenib_qa/),
and writes qa_registry.json.
Reusing pre-built indexes. If a read-only tree of per-instance artifacts
already exists (layout <dir>/<instance_id>/{repo,l0,l2}/...), point
prebuilt_dir at it to skip cloning and embedding — only BM25 is built locally.
Set it in qa_config.yaml or via env:
The path is fully configurable; nothing is hardcoded to a specific machine.
2. Launch the backend¶
From the repository root (so the relative data_dir resolves):
# autoreload during dev:
uvicorn codenib.web.app:app --host 127.0.0.1 --port 8000 --reload
# or the console script:
codenib-web --reload # honors CODENIB_DEMO_HOST/PORT
The codenib-web console script maps to codenib.web.app:main, which reads
CODENIB_DEMO_HOST (default 127.0.0.1) and CODENIB_DEMO_PORT (default
8000). Index loading takes ~20s. Check it is up:
Configuration and env overrides¶
Config lives in qa_config.yaml (override path with CODENIB_DEMO_CONFIG);
environment variables beat the YAML (load_config() in
codenib/web/config.py):
| Config key | Env override | Purpose |
|---|---|---|
model |
CODENIB_DEMO_MODEL |
LiteLLM model for the Ask agent (default gpt-4o) |
wiki_model |
CODENIB_DEMO_WIKI_MODEL |
Separate model for wiki prose and edge labels; unset → uses model |
model_api_base / model_api_key |
CODENIB_DEMO_API_BASE / CODENIB_DEMO_API_KEY |
OpenAI-compatible endpoint for the Ask model; provider-native models (Vertex, Anthropic) leave these unset |
wiki_api_base / wiki_api_key |
CODENIB_DEMO_WIKI_API_BASE / CODENIB_DEMO_WIKI_API_KEY |
Optional endpoint and credential dedicated to Wiki generation |
model_options |
CODENIB_DEMO_MODEL_OPTIONS |
Provider-specific LiteLLM options for Ask; the environment value is a JSON object and overrides matching YAML fields |
wiki_model_options |
CODENIB_DEMO_WIKI_MODEL_OPTIONS |
Nested overrides applied only to Wiki, narration, and edge-label calls |
data_dir |
CODENIB_DEMO_DATA_DIR |
Where checked-out repos, indexes, and the registry live (default .codenib_qa/) |
prebuilt_dir |
CODENIB_DEMO_PREBUILT_DIR |
Read-only tree of pre-built per-instance artifacts (see above) |
embedding_provider |
CODENIB_EMBEDDING_PROVIDER |
huggingface (in-process, default) or openai (OpenAI-compatible endpoint) |
embedding_model / embedding_base_url / embedding_api_key |
CODENIB_EMBEDDING_MODEL / CODENIB_EMBEDDING_BASE_URL / CODENIB_EMBEDDING_API_KEY |
Embedding model plus the endpoint and credential for the remote provider |
edge_labels |
CODENIB_EDGE_LABELS |
Opt-in LLM-written edge phrases in the graph view (off by default; each first-seen edge costs one small LLM call, then cached) |
edge_label_model |
CODENIB_EDGE_MODEL |
Optional cheaper model for the short edge-label calls |
When wiki_model and model use the same LiteLLM provider prefix, Wiki may
reuse model_api_base and model_api_key. A different provider never inherits
the Ask endpoint or credential: configure wiki_api_base / wiki_api_key, or
leave them unset for provider-native routing and credentials. For example, a
vertex_ai/... Wiki model will not be sent to an openai/... Ask server.
LiteLLM provider selection comes from the model prefix, for example
anthropic/..., vertex_ai/..., ollama/..., or openrouter/.... CodeNib
does not infer request parameters from a model name. Put non-secret
provider-specific parameters in the option mappings:
model: vertex_ai/gemini-2.5-flash
model_options:
vertex_project: my-project
vertex_location: us-central1
wiki_model_options:
timeout: 60
For an OpenAI-compatible Qwen endpoint whose chat template supports the setting:
model: openai/qwen3
model_api_base: http://127.0.0.1:8080/v1
model_options:
extra_body:
chat_template_kwargs:
enable_thinking: false
The codenib wiki and codenib doctor commands accept the repeatable CLI
equivalent
--model-option extra_body.chat_template_kwargs.enable_thinking=false.
codenib-web itself has no --model-option flag; use the YAML or environment
settings above. CodeNib rejects options that attempt to replace managed fields
such as model, messages, tools, or api_key. Keep credentials in the
provider's standard environment variables or the dedicated API-key setting.
Managed local dev servers¶
For day-to-day development, prefer the repo-local manager instead of launching
long-lived uvicorn and vite processes by hand:
make web-start # start backend :8000 and frontend :3000
make web-status # show PIDs, cwd, port owners, health, and log paths
make web-logs # print recent backend/frontend logs
make web-stop # stop managed processes
make web-restart # restart managed processes
make web-reclaim # reclaim current-user listeners on :3000/:8000, then restart
PID files and logs live under .codenib_demo/server/:
.codenib_demo/server/backend.log.codenib_demo/server/frontend.log
If localhost:3000 returns Internal Server Error after switching branches or
deleting a worktree, run make web-status. A stale Vite process from an old
cwd is usually visible there. Use make web-reclaim to stop current-user
listeners on the demo ports and restart both services from the current checkout.
The backend exposes (all under /api):
| Endpoint | Purpose |
|---|---|
GET /api/health |
Liveness + repo count |
GET /api/repos |
Repo list (each carries capabilities, e.g. codemap, plus incremental cost stats for repos with a commit window) |
GET /api/repos/{id}/wiki |
Wiki page tree |
GET /api/repos/{id}/wiki/{page} |
A wiki page (markdown + citations) |
GET /api/repos/{id}/wiki/{page}/graph |
The page's subsystem graph (view over the symbol graph) |
GET /api/repos/{id}/commits |
Commit window for the graph view, newest first (available: false without a prebuilt window) |
GET /api/repos/{id}/codemap |
Dependency subgraph around a symbol (symbol, direction, depth, max_nodes, commit) |
GET /api/repos/{id}/source |
Source lines for the code/peek panels |
POST /api/repos/{id}/edge-label |
Short LLM phrase for how a graph edge's source uses its target (opt-in via edge_labels; returns disabled when off) |
POST /api/chat |
Ask: answer + citations |
The /commits endpoint and the commit parameter are served from per-commit
graph snapshots — see Per-commit graph snapshots
below.
3. Launch the frontend¶
Open http://localhost:3000. With the default or a
loopback configuration, browser requests stay same-origin on :3000: the
Vite server proxies /api/* to the backend at http://127.0.0.1:8000
(web/vite.config.ts). Override that server-side target with
CODENIB_API_BASE. The production Python server follows the same same-origin
contract: it proxies /api/* to the local FastAPI process and leaves
/runtime-config.js relative by default. This also keeps a Wiki bound to
0.0.0.0 usable from another machine without exposing a loopback API URL to
that browser.
Running over SSH¶
With the default or loopback API configuration, the browser talks only to the
Vite dev server on :3000; the dev server proxies /api/* to FastAPI on
the remote host. A single forward is enough:
(Address already in use on reconnect just means an earlier tunnel still
holds the port.) Set CODENIB_API_BASE on the host when the backend uses a
different address.
The Wiki page¶
Each wiki page (web/app/[repoId]/page.tsx) is a three-rail layout: a section
tree on the left, the narrative in the middle, and an "On this page" outline on
the right. The middle column leads with the graph, then the prose:
- Subsystem map. When the page's symbols resolve in the symbol graph, the
page renders a
GraphViewinvariant="wiki"mode inside a<details className="subsystem-map">block whose summary row reads "Subsystem map · N symbols · Last indexed \<sha>". This is built bybuild_page_subgraph()incodenib/web/codemap.py: it resolves each citation to a graph node, takes the page's cited symbols as seeds (highlighted as roots), and returns those nodes plus the direct reference edges between them. It deliberately does not expand a one-hop neighbourhood: the only non-cited symbols admitted are up to two bridge symbols, each of which must touch at least two cited symbols (ranked by call-site evidence), keeping the map grounded in the page's evidence. Edges carry the exact call-site anchors. - Relevant source files. A
<details>list of the distinct repo-relative files cited on the page, each linking to the exact GitHub blob at the indexed commit. - Source-grounded prose. The narrative markdown, rendered after generated Mermaid is stripped (see below). Headings drive scroll-spy in the right rail.
The Refresh this wiki button re-fetches the page tree and active page from
the backend. It does not clear the in-memory or on-disk generation cache, run a
new index build, or force model regeneration. To regenerate model-authored
pages after changing provider settings, stop the backend, remove
<data_dir>/wiki_cache, and start it again.
No more Mermaid¶
The page calls stripGeneratedDiagrams() on the markdown before rendering. It
removes any LLM-generated ```mermaid fence (and a preceding heading left
empty by the removal). The interactive Cytoscape graph replaces the static
diagram entirely. The <Mermaid> component still loads lazily in
web/components/Markdown.tsx for any stray fence, but wiki pages no longer
emit one.
The full-screen Dependency Map¶
The wiki header shows a Dependency Map button. Clicking it opens a
full-screen modal (graph-modal, Esc to close) hosting Codemap
(web/components/Codemap.tsx), the standalone explorer. Repositories without
the codemap capability get setup commands for the optional graph profile
instead of an empty canvas. When the capability is present, the same component
is reachable seeded on a symbol via "Focus here" in the wiki subsystem
map's node peek — that re-roots the explorer on the chosen symbol.
Codemap calls GET /api/repos/{id}/codemap, backed by build_codemap() in
codenib/web/codemap.py:
- Focus symbol. A free-text symbol box re-roots the map; blank picks the
repo's busiest symbol (
_default_seed()= highest reference out-degree function/method/class). - Direction. A dropdown chooses
both(callers + callees),callees(what it calls), orcallers(what calls it). - BFS walks reference edges out from the root (
depthclamped to 1–4,max_nodesclamped to 2–120; the explorer defaults to depth 1 and requests 28 nodes, or 40 at depth 2). - A row of chips below the graph lists every node; clicking a chip re-roots on it.
The modal is the full-screen surface; the wiki subsystem-map is the
inline one. Both render the same GraphView component — the difference is
variant ("explore" vs "wiki") and which payload feeds it.
The interactive graph (Cytoscape)¶
GraphView (web/components/GraphView.tsx) wraps CodeGraph
(web/components/CodeGraph.tsx), a Cytoscape view loaded dynamically (~3 MB of
Cytoscape + layout code) so the narrative paints first. It is a single
top-down dependency view — there is no Flow/Clusters toggle anymore.
Click an edge → exact source¶
Every edge is a real reference (call/use) relation, and CodeNib aggregates
the exact LSP/SCIP call sites (anchors) onto it. Clicking an edge opens a
source peek (SourcePeek in GraphView.tsx) at the precise line where the
call happens; if a call relation has several sites, a pager lets you step
through each one. Anchor lines are exposed 1-based (converted from the 0-based
tree-sitter/LSP origin in codemap.py). Hovering an edge shows its call-site
count.
Click a node → definition¶
Clicking a node opens its definition in the same source peek (with a "Focus here" shortcut that re-roots the explorer) and puts the node into persistent focus: the node and its neighbourhood pop via an accent border while unrelated edges fade — nodes are never greyed out, so every label stays legible. Esc or a background click clears focus. External symbols (defined outside the repo, no openable source) render dashed/italic and the peek says so instead of fetching source.
Top-down dependency layout¶
Layout is computed by computeFilesPositions() (web/lib/filesLayout.ts), a
pure, unit-tested function where position encodes meaning on both axes:
- Vertical = dependency depth. A file-level dependency graph is layered by longest path (cycle-safe bounded relaxation): callers / entry points sit at the top, their dependencies flow downward, and files that touch nothing else sink to a dedicated bottom band so they don't crowd the entry row.
- Per-file boxes. Each file becomes a compound box (titled with its basename); its symbols stack top→bottom by source line inside it. Boxes are packed into balanced lanes within each band, spaced by each node's measured width/height so wide or wrapped labels never overlap.
- Fallback. When there are no cross-file edges, it falls back to size-packed columns (largest files first, shortest-column-first packing).
Importance, community, and hub collapsing¶
Node and edge encodings come from _score() / _enrich() in
codenib/web/codemap.py, computed on the induced reference subgraph
(≤120 nodes), best-effort (never 500s the map):
- Importance (PageRank). Directed PageRank (damping 0.85) over the reference subgraph, exposed as a rank-percentile in [0, 1]. It drives node size: bigger node = more referenced / more "core" (the UI legend reads "bigger = more referenced").
- Community (Leiden). Modularity-based Leiden clustering on the undirected
projection (igraph 0.11.9 has no directed Leiden), gated so tiny or loose
graphs (< 12 nodes, or modularity < 0.2) collapse to one cluster. Exposed as
each node's
community. - Reference count & entry score.
ref_count= in-degree (how many symbols reference this one);entry_score= out/(in+out) in [0, 1], high for drivers / entry points. - Colour = kind. Node border colour is fixed per symbol kind (function, method, class, interface, struct, …) since position already encodes file + line. Roots render filled blue.
- Edge width = call-site count.
weight= number of distinct call-site anchors on the edge; a heavily-used call reads thicker. - Hub collapsing. A node with more than
hub_cap(8) outgoing edges keeps only the edges to its highest-importance targets; the rest are folded away and counted inhidden_callees, surfaced in the UI as a "+N" badge on the node label. Nodes left fully disconnected by that fold (and not a root) are dropped, so dense, high-fan-out maps don't become hairballs.
Per-commit graph snapshots¶
By default a repo serves one symbol graph, built at its indexed base_commit.
A repo can additionally carry a commit window — one graph snapshot per
commit for the last N first-parent commits of a ref — built offline by:
python scripts/build_commit_window.py --repo-dir /path/to/repo \
--n 5 --ref origin/main --language python
Only the oldest commit in the window pays a full cold build; the builder
attempts to reach every later commit by patching the previous graph forward
through the LSP-backed incremental patcher. This utility is best-effort: a
language whose server cannot start is omitted, and a failed per-language patch
leaves that language's prior facts in the saved graph. The resulting window is
therefore a demo artifact, not a commit-exact or fresh-rebuild-validated index;
operators should inspect warnings and per-language patch_stats before using
it for comparisons. Snapshots plus a commit_window.json manifest land under
the repository's CodeNib state directory below
$CODENIB_HOME/repositories/.../indexes/commit_window/ by default
(--out-dir overrides), and the backend picks them up per repo — no registry
change needed. With a window present:
- The graph-snapshot selector. The wiki's left rail swaps its static "Last
indexed \<sha>" label for a "Graph snapshot" dropdown fed by
GET /api/repos/{id}/commits(newest first). Picking a commit re-renders the dependency graph through thecommitquery parameter onGET /api/repos/{id}/codemap; with nocommitthe window's newest snapshot is served. This selection is graph-only: source peeks still read the repository checkout configured inRepoEntry.repo_dirand can show a different revision. Repos without a window returnavailable: falseand keep the single-commit label. - Snapshot fallback contract. The codemap response reports the
commitactually served. An unknowncommitvalue is a 404; a snapshot that exists in the manifest but cannot be loaded (e.g. an olderschema_version) falls back to the repo's default graph withfell_back: true, and the explorer says "The selected snapshot could not be loaded. Showing the default graph…" rather than labelling one commit's graph with another commit's SHA. - Warm patch cost. The displayed ratio divides cold graph-build time by
mean warm
patch_files()time. It excludes language-server startup, diff detection, checkout, and snapshot serialization, so it is not an end-to-end re-index speedup. The figures are derived in one place server-side (window_stats()incodenib/web/commit_window.py) and surfaced twice: the landing card shows "N commits · M× warm-patch speedup" (from theincrementalfield onGET /api/repos; just "N commits" when no defensible speedup exists), and the rail under the selector shows a per-commit cost line — "cold build · X.Xs" for the anchor commit, "warm patch · X.XXs · N files changed · ±D nodes" for commits reached incrementally.
The Ask page¶
The Ask flow (web/app/[repoId]/ask/page.tsx) is a two-column "codemap" layout:
the answer on the left, a code panel on the right. Submitting a question
re-fetches POST /api/chat; every submit is its own answer.
- Clickable citation chips. The answer is rendered through
Markdown(web/components/Markdown.tsx) with the run'scitationsand anonCitehandler.matchCitation()(web/lib/citations.ts) conservatively maps each inline code span to the citation it names — by exact symbol name, exact or suffix repo-relative path, file basename, the last segment of a qualified symbol (file.py:sym()), or apath:lineform. Ordinary inline code that matches nothing stays plain. A match renders as aCiteChipbutton; symbol matches also show a:line(or:start-end) badge fromlineLabel(). - Click-to-scroll + highlight. Clicking a chip calls
selectCitation(i), which sets the active citation (setActive) and bumps ascrollSignal. The sharedrefslist (fromcodeRefs(), filtered to repo-relative files and capped at 12) keeps a chip's index aligned with the code panel's fragments, so the right-handCodePanelscrolls to and highlights the cited span. The signal is keyed independently of the active index, so clicking the already-active chip re-scrolls to it. - Question truncation. A long question is clamped to
Q_TRUNCATE(200 chars) with a "Show full text" / "Show less" toggle. - Run metadata. Below the answer: tool-call count, turns, duration, and the number of references.
Troubleshooting¶
| Symptom | Cause / fix |
|---|---|
| Stuck on "Loading repositories…" | Backend unreachable from the Vite proxy or production runtime configuration. Confirm :8000 is up (curl 127.0.0.1:8000/api/health); over SSH only :3000 needs forwarding in source-development mode. Hard-refresh after the backend finishes loading. |
| Ask shows a backend error | The configured model endpoint is unreachable, provider authentication is unavailable, or the model call failed. Check the backend log and run codenib doctor --require agent with the same model settings; add --probe-model to send live text, tool, and structured-output probes. |
| Wiki prose looks deterministic after changing models | Generation fell back because the earlier model call failed, or the existing page cache was reused. Stop the backend, clear <data_dir>/wiki_cache, and restart it after fixing the provider. |
| Dependency Map shows setup commands | The repo has no usable symbol graph, so the codemap capability is off. Install codenib[graph] and rebuild with codenib wiki . --preset graph. |
repos: 0 at /api/health |
No registry. Run scripts/build_qa_index.py (or fix data_dir/prebuilt_dir). |