Skip to content

CodeNib Web UI

For one local repository, use the packaged two-command path in the Quickstart:

pip install codenib
codenib wiki /path/to/repository

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.0 or >=22.12.0 (only for frontend source development, matching the checked-in Vite toolchain). Install deps once with make web-deps.
  • An LLM provider reachable through LiteLLM, for Ask answers and generated wiki pages:
  • OPENAI_API_KEY for the default gpt-4o (a non-thinking model; set model in qa_config.yaml or CODENIB_DEMO_MODEL to change it — openai/gpt-4o-mini is 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 ADC quota_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:

python scripts/build_qa_index.py        # needs network: HuggingFace + git

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:

export CODENIB_DEMO_PREBUILT_DIR=/path/to/prebuilt

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:

curl -s http://127.0.0.1:8000/api/health   # {"status":"ok","repos":N}

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

cd web
npm run dev                              # Vite dev server on :3000

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:

ssh -L 3000:localhost:3000 <host>

(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 GraphView in variant="wiki" mode inside a <details className="subsystem-map"> block whose summary row reads "Subsystem map · N symbols · Last indexed \<sha>". This is built by build_page_subgraph() in codenib/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), or callers (what calls it).
  • BFS walks reference edges out from the root (depth clamped to 1–4, max_nodes clamped 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 in hidden_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 the commit query parameter on GET /api/repos/{id}/codemap; with no commit the window's newest snapshot is served. This selection is graph-only: source peeks still read the repository checkout configured in RepoEntry.repo_dir and can show a different revision. Repos without a window return available: false and keep the single-commit label.
  • Snapshot fallback contract. The codemap response reports the commit actually served. An unknown commit value is a 404; a snapshot that exists in the manifest but cannot be loaded (e.g. an older schema_version) falls back to the repo's default graph with fell_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() in codenib/web/commit_window.py) and surfaced twice: the landing card shows "N commits · M× warm-patch speedup" (from the incremental field on GET /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's citations and an onCite handler. 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 a path:line form. Ordinary inline code that matches nothing stays plain. A match renders as a CiteChip button; symbol matches also show a :line (or :start-end) badge from lineLabel().
  • Click-to-scroll + highlight. Clicking a chip calls selectCitation(i), which sets the active citation (setActive) and bumps a scrollSignal. The shared refs list (from codeRefs(), filtered to repo-relative files and capped at 12) keeps a chip's index aligned with the code panel's fragments, so the right-hand CodePanel scrolls 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).