Skip to content

CodeNib Web UI

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

python -m pip install "codenib[semantic]==0.2.3"
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/codenib-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.

Native vector reuse also requires a secure source fingerprint v2 in the repository manifest. The interactive Web service treats a missing or legacy fingerprint as an unavailable vector view: it does not open the vector artifact, logs a warning, and continues with BM25 retrieval. Once a v2 fingerprint is present, authorization rejection and artifact-integrity failures still fail closed. Rebuild both the manifest and vector artifact with the current CodeNib version to restore hybrid retrieval; do not edit fingerprint fields by hand.

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). A profile may set extends to one relative parent path or an ordered list of parents. Parents are applied left to right, the child wins, nested mappings are merged, and environment variables beat the final YAML (load_config() in codenib/web/config.py). Missing parents and inheritance cycles fail at startup instead of silently selecting defaults.

The checked-in templates support two generation profiles without duplicating index and cache settings:

Profile Template Generation route
Local serve qa_config.local.yaml.example OpenAI-compatible local Qwen/vLLM endpoint
API service qa_config.api.yaml.example DeepSeek hosted API for Ask and Wiki

Copy them to the ignored qa_config.local.yaml and qa_config.api.yaml files. The API profile extends the local profile, so both routes use the same source registry, retrieval artifacts, embeddings, and Wiki cache. Set CODENIB_DEMO_PROFILE=api with scripts/start_web.sh, or point a service directly at CODENIB_DEMO_CONFIG=qa_config.api.yaml. Keep API keys in CODENIB_DEMO_API_KEY or the provider's standard environment variable.

Environment overrides are:

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) or openai (BYO 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

The manifest's embedding route is authoritative when the service reopens a vector view. Runtime configuration may provide its credential, but cannot swap the artifact to another provider, model, dimension, or endpoint. A BYO credential can be copied into CODENIB_EMBEDDING_API_KEY through the codenib wiki --embedding-api-key-env option. No credential is persisted in the manifest or vector-store configuration.

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

Interactive requests are bounded before repository or model work:

Boundary Public budget
Ask (POST /api/chat) 128 messages; 65,536 characters per message; 262,144 characters across the conversation; 512 characters for repo_id
Edge label (POST /api/repos/{id}/edge-label) 4,096 characters per file path; 1,024 characters per display label; 128 characters for the commit; 32 call-site anchors with positive line numbers
Direct FastAPI request body 8 MiB, enforced while streaming even without Content-Length
Installed same-origin proxy 8 MiB request body and 32 MiB upstream response body

The browser sends only the first three edge-label anchors because prompt generation consumes at most that sample. The installed proxy rejects transfer-encoded request bodies and malformed, negative, or oversized Content-Length values instead of buffering them. Validation errors omit the rejected input so oversized prompts are not reflected back to clients.

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.

For a long-lived reverse-proxied demo, build once and serve the production bundle instead of leaving Vite's development server running:

cd web
npm run build
CODENIB_API_BASE=http://127.0.0.1:8000 \
  npm run preview -- --host 127.0.0.1 --port 3001

The document base keeps relative Vite chunks rooted correctly when a browser opens or refreshes a nested route such as /<repo>/ask. Static export removes that document base and rewrites the same assets to its configured Pages mount, so sub-path exports remain portable.

What the loading states mean

  • The route-level Loading CodeNib… is a lazily loaded frontend chunk. A production build avoids the development transform and React StrictMode request replay costs.
  • A Wiki page is normally a disk- or memory-cache read. On its first load, the backend retrieves source evidence and may ask the configured Wiki model to generate prose. Concurrent requests for the same cold outline or page are coalesced into one generation call.
  • A degraded fact-plan fallback is retained as diagnostic evidence, but it is not treated as durable page content. A fresh backend process retries that page from the original evidence, and the reader UI withholds the internal fallback instead of displaying is indexed from inventory text.
  • Ask is a multi-turn model run. Retrieval itself is usually short; model turns and the growing evidence context dominate latency. max_turns is the main latency/coverage trade-off, so tune it with repeated, source-checked questions rather than a ping prompt.
  • Source panes and dependency graphs load independently after the narrative; they do not need to hold the page in a generic loading state.
  • Repository cards inspect the small leading schema field of graph.pkl without loading the graph. An older persisted schema disables codemap immediately; the page-graph API returns the required rebuild version instead of advertising an indexed graph and then returning an unexplained empty map.

The Wiki and Ask views replace a bare spinner with the active stage and elapsed time. A direct Wiki deep link starts with its requested page instead of first fetching overview. API responses also expose backend processing time through the Server-Timing: codenib;dur=... header. Requests that spend at least two seconds in the backend are logged by method, path, and duration; query strings and Ask contents are deliberately omitted.

Audit the Wiki cache

The cache auditor reads only entries reachable from the current registry and outline identities; it never generates an outline or page and therefore never calls the Wiki model:

make wiki-cache-audit \
  WIKI_CACHE_AUDIT_ARGS="--config qa_config.local.yaml --require-overviews"

Its JSON report separates Overview, root-page, and child-page coverage; lists degraded, fallback, and quality-invalid pages; and reports stale/orphaned Wiki database entries and payload bytes. Newly generated pages also contribute retrieval, planning, model-call, repair-count, and total-latency summaries. Existing cache entries created before those metrics were introduced remain readable and are simply excluded from the latency sample. Add --fail-on-fallback and --fail-on-quality-invalid to use the report as an acceptance gate.

Prewarm the landing page for every registered repository with a small bounded worker pool after reviewing the selection in dry-run mode:

make wiki-cache-prewarm \
  WIKI_CACHE_PREWARM_ARGS="--config qa_config.local.yaml --scope overview --dry-run"
make wiki-cache-prewarm \
  WIKI_CACHE_PREWARM_ARGS="--config qa_config.local.yaml --scope overview --workers 2 --retry-degraded-now --fail-on-error --fail-on-quality-invalid"

overview is the safe public-demo default; root includes every top-level section and all includes child pages. The prewarmer skips healthy entries and quality failures still inside their retry cooldown. The sidebar reports each page as cached, cold, retryable, or needing review; the browser preloads at most two adjacent pages only when they are already cached, so navigation does not silently create model spend.

--retry-degraded-now is an explicit operator action: it bypasses the retry cooldown for degraded pages in this prewarm run without changing the bounded reader-traffic policy. It does not regenerate healthy cached prose. The standalone prewarmer groups pages by repository and releases that repository's BM25/vector views as soon as its group finishes, so a 26-repository run retains at most the configured worker count of large retrieval bundles. Pass --keep-views only when profiling retained-view memory deliberately.

Rebuild demo indexes safely

When a manifest predates secure source fingerprints, rebuild its BM25, vector, and symbol-graph views together instead of editing the manifest. Preview the registry selection first, then publish either all repositories or a repeated set of repository ids:

make demo-index-rebuild \
  DEMO_INDEX_REBUILD_ARGS="--config qa_config.local.yaml --dry-run"
make demo-index-rebuild \
  DEMO_INDEX_REBUILD_ARGS="--config qa_config.local.yaml --repo sharkdp__bat"

The command builds into <data_dir>/rebuilds-source-v2, validates the source binding, commit, fresh-view status, and complete vector artifact, then replaces only the manifest while holding the compiler cache lock. A demo graph with fewer than 25 nodes is also rejected, catching tools that exit successfully after indexing the wrong workspace boundary. The command preserves the original as repo_manifest.json.pre-source-v2 and refuses publication if the live manifest changed while the private generation was being built.

Some demo checkouts populate optional dependency SDKs after cloning. Keep a repository-specific exclusion explicit and persisted in the BM25/vector artifact identity instead of silently adding that directory to the global source policy. For example, MicroPython's populated lib/ tree is external dependency material and otherwise contributes hundreds of thousands of noisy chunks:

make demo-index-rebuild \
  DEMO_INDEX_REBUILD_ARGS="--config qa_config.local.yaml --repo micropython__micropython --ignore-dir micropython__micropython:lib"

The source fingerprint still binds the entire visible checkout; the exclusion only defines the retrieval corpus. Consequently, a dependency-tree change is detected conservatively even though those files are not searchable.

The managed Rust analyzer remains the default. If one repository exposes an upstream analyzer crash, set CODENIB_RUST_ANALYZER to an explicit validated executable for that rebuild; this opt-in override does not replace the pinned toolchain for other repositories.

Compare Ask model candidates

With the candidate served behind the configured OpenAI-compatible endpoint, run the fixed Requests, Gin, and Vue source-grounding gate and retain its JSON:

make demo-ask-benchmark \
  DEMO_ASK_BENCHMARK_ARGS="--candidate qwen-current --repeat 2" \
  > demo-ask-qwen-current.json

The report records end-to-end and backend-reported latency, citation location validity, expected-file recall, and expected-term coverage. The command exits nonzero if any case has invalid citation locations or falls below the default 0.3 file-recall or term-coverage threshold. Use --min-file-recall and --min-term-coverage to raise those per-case thresholds for a release gate. Compare candidates with identical indexes, backend settings, cases, thresholds, and repeat counts; a successful model ping is not a demo-quality or latency result.

Wiki prose has a separate gate because it uses a separate model and quality contract. It reuses each repository's current cached outline, writes candidate pages only to an isolated temporary cache, and retains metrics rather than the generated prose:

make demo-wiki-benchmark \
  DEMO_WIKI_BENCHMARK_ARGS="--config qa_config.local.yaml --candidate deepseek-current --repo psf__requests"
make demo-wiki-benchmark \
  DEMO_WIKI_BENCHMARK_ARGS="--config qa_config.local.yaml --candidate local-candidate --model openai/local-model --api-base http://127.0.0.1:8080/v1 --repo psf__requests"

Run cache prewarming first: the benchmark intentionally refuses to generate a new outline, so every candidate receives the same source-bound page plan. A run fails when generation errors, falls back to diagnostic mode, or produces a page that does not pass the normal Wiki quality gate.

Use the retained JSON reports to compare Qwen3.6, Qwen3.8-27B, DeepSeek V4 Flash, Muse Glimmer 30B, or another candidate under the same indexes, backend settings, cases, and repeat counts.

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 retain the exact call-site count and carry up to 12 source-linked anchor samples.
  • 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, run the bounded prewarmer with --retry-degraded-now; healthy pages remain cached. A deliberate prompt or index identity change selects new cache entries without deleting unrelated healthy content.

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. CodeNib retains the exact number of distinct LSP/SCIP call sites while serializing at most 12 source anchors per edge. Clicking an edge opens a source peek (SourcePeek in GraphView.tsx) at one of those precise lines; the pager states both the total and displayed sample when an edge is truncated. Anchor lines are exposed 1-based (converted from the 0-based tree-sitter/LSP origin in codemap.py). Hovering an edge shows the complete 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 is the complete number of distinct call sites even when anchors is sampled; 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.
Direct /<repo>/ask refresh is blank Rebuild the frontend from a checkout whose index.html contains the live document base. Without it, relative Vite assets resolve below the nested route and return 404. Static exports remove and remount this base automatically.
Vite says This host is not allowed Add the exact public proxy hostname to server.allowedHosts (and preview.allowedHosts when using vite preview). Keep host checking enabled; do not use allowedHosts: true. The checked-in config already admits demo.codenib.ai.
Wiki says "Couldn't load this page" Read the status and backend detail now shown in the page. A provider or generation error points to the Wiki model; a legacy source-fingerprint warning means the Web service is serving BM25 until the manifest and vector artifact are rebuilt. Older or strict runtimes may instead return native vector authorization requires source fingerprint v2.
Wiki stays on "Preparing a source-linked page…" This is a cold page generation, not the Ask model. Check the Wiki provider and its timeout, then inspect whether another identical request is already in flight. Each blocked generation-lock acquisition gives up after 30 seconds without interrupting the owner or starting duplicate work; retry later to reuse the published page.
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 Source-checked page caches are model-independent by design, so changing the configured model does not rewrite healthy pages. Degraded fact-plan fallbacks are retried automatically by a fresh backend process; clear the matching cache only when intentionally regenerating a healthy page with another model.
Frontend changes do not appear Run make web-status and inspect the Vite process cwd. Stop a current-user process rooted in an old checkout or detached snapshot, then start the frontend from the checkout being edited. Never reclaim a listener owned by another user.
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.
Page graph says its schema is incompatible The prose and graph are separate artifacts. The page can remain source-cited while an older graph.pkl is rejected. Rebuild symbol_graph with the current CodeNib version; do not edit the pickle's version field.
repos: 0 at /api/health No registry. Run scripts/build_qa_index.py (or fix data_dir/prebuilt_dir).