The UI security audit (security-ui.sh, layer A1) forbids hardcoded
external URLs in graph-ui source — the callout's GitHub issues link
tripped it on all pr-smoke legs. Follow the established pattern for
external targets (/api/repo-info deep-links): /api/ui-config now
carries upstream_issues_url and the callout consumes it, rendering the
issue button only when the backend provides an https URL. The protocol
check uses a regex literal on purpose: a bare protocol string in source
also aborts the audit's URL extraction.
Verified: security-ui.sh passes locally; /api/ui-config serves the URL;
the callout opens with the prefilled link in the browser; frontend
build + tests green.
Refs #963
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Clicking a node of the missed skeleton now opens a dedicated right-panel
callout instead of the standard node panel (code snippet and
callers/callees are meaningless for a not-fully-indexed file):
- explains the gap in plain words (best-effort detection; the file
content itself is ground truth)
- asks the user to have their agent summarize what fails to parse and
report it upstream so the edge case can be handled
- two working actions: a prefilled upstream GitHub issue (title
'Indexing gap: <file>'; body carries ONLY the file path and project
name, with an explicit add-snippets-only-if-shareable note) and a
copy-to-clipboard agent prompt (index_status -> summarize flagged
ranges -> file the issue) with visible copied feedback
Verified in the browser: skeleton click opens the callout with both
actions; frontend build + tests green.
Refs #963
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Render the miss graph side by side with the code graph instead of
swapping layouts:
- /api/layout (code graph) now also attaches "missed_graph": {nodes,
edges, offset} — the shadow-project layout placed below the primary
cluster (same satellite pattern as linked_projects; -Y slot so
cross-repo satellites collide last). graph=missed stays the isolated
view for API users
- the UI paints the skeleton white and ghostly beside the galaxy,
auto-frames BOTH clusters on load, and navigates naturally: clicking
the skeleton flies the camera into it (file labels + detail panel),
clicking a code node flies back to the code side, and clicking empty
space while the skeleton has focus returns to the overview (the
galaxy can be entirely off-screen at that point)
- the sidebar toggle now shows/hides the skeleton (with a missed-file
count) rather than swapping the layout
Verified in the browser: overview composition, skeleton focus with
split.c labeled, and the empty-space return flight; ui/httpd suites and
frontend build + tests green.
Refs #963
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Four refinements to the coverage signal:
- extraction: subtract DEFINITE recovery before flagging. Tree-sitter
error recovery plus the ERROR-descending def walker often re-extract
constructs inside a failed region (verified: `def broken(:` comes
back as a def); a region whose every line is covered by definitions
that START inside it is not a miss, and a fully recovered file is not
flagged at all. Container defs (Module/Package) don't count as
evidence, and partially covered regions stay flagged — the
#ifdef-split case keeps its flag because the first branch's function
is genuinely lost
- naming: the query_graph option is graph="missed" (the graph shows
ONLY misses — "coverage" was misleading); shadow project renamed to
"<name>::missed"; tool descriptions updated so agents discover both
the option and its semantics
- hook: the CLI-installed PreToolUse augmenter now also matches Read
and injects a coverage note when the file being read is listed as not
fully indexed ("line ranges X-Y could not be parsed — the file
content you are reading is ground truth"). Safe against the old
issue-362 hazard: the augmenter is structurally non-blocking (always
exit 0, additionalContext only), mirroring the Gemini matcher that
already includes read_file; matcher upgrade bookkeeping updated
- ui: "Missed files" toggle in the graph sidebar renders the miss
graph as a second graph option — /api/layout gains graph=missed
(same db file, shadow-project scoping; base project name validated
as before)
Tests: recovery-subtraction cases (recovered def unflagged, garbage
region flagged, trailing recovered defs keep the flag), CLI matcher
tests updated, e2e resilience fixtures switched to an unrecovered
miss; verified end-to-end: hook emits the note for a flagged file and
stays silent otherwise, /api/layout?graph=missed serves the miss graph
and the code graph is unchanged, frontend build + tests green.
Refs #963
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
High-density graphs washed out to a cyan-white blob: edges blend additively
and the per-node boost rewarded luminance, so ~80k overlapping edges plus the
white/yellow mid-tier stars saturated the center.
Compensate by density so contrast stays roughly constant as the graph grows:
- edges dim by ~1/sqrt(edgeCount) (they cause the blob) while nodes and bloom
stay at full strength up to 25k nodes and only ease gently past that, so the
bright-star look is preserved on moderate graphs;
- the per-node glow is now colour-aware by channel dominance instead of
luminance: blue hubs (high-degree) glow brightest, red leaves modestly,
white/yellow least — matching the star-class meaning and giving the graph
depth instead of a uniform white core.
Add a Display menu (edge brightness / node glow / bloom multipliers, 0–3×,
persisted) layered on top of the adaptive defaults, so the look is tunable
live. A selection is never density-scaled and stays bright against the
dimmed rest.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Add a node-budget control next to Refresh: type any count (snapped to
5,000 steps, 5,000 default, 10M max) and the graph reloads with that many
nodes plus every edge between them. The choice persists per project.
Loading gets real feedback: the layout response streams with a live
MB counter under an animated constellation (static under
prefers-reduced-motion), replacing the bare spinner.
Rendering scales with the budget: instance matrices are rebuilt only when
the node set or highlight changes instead of every frame, sphere
tessellation steps down as counts grow, and past 75k nodes the cloud
switches to soft point sprites — one position per node — so six-figure
budgets stay interactive. The truncation notice now names the loaded edge
count and points at the budget control.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Distilled from #789 (safe features only). Adds backend dead-code classification
(status + in_calls in the layout JSON), a GET /api/repo-info endpoint for GitHub
deep-links, and the frontend dead-code filters, node code preview, and deep-links.
The render-cap revert was dropped (kept 2000 for DEFAULT_MAX_NODES/HARD_MAX_NODES
and GRAPH_RENDER_NODE_LIMIT); the sidebar regex-search refactor was omitted.
Security fixes over the original: repo-info strips credentials from any returned
remote_url; the legitimate https blob-URL construction is allow-listed so the
static gate passes; libgit2 is not re-initialized/shutdown per request (reuses the
process-wide init from cbm_alloc_init); deep-link path segments are URL-encoded.
Co-authored-by: Andy Zehady <azehady@ciroos.ai>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The fly-to animation moved only the camera and called lookAt, leaving the OrbitControls pivot at the origin, so OrbitControls re-centred the view on the next frame and snapped the camera back once the animation ended.
Lerp the controls target to the focus point as well so the view stays on the clicked node or cluster and orbits around it afterwards.
Signed-off-by: Zadak <rarepops@protonmail.com>
Rename the ambiguous Clear button to Clear selection so it reads distinctly from Refresh, and point the empty-state hint at the Projects tab instead of the renamed Stats tab.
Signed-off-by: Zadak <rarepops@protonmail.com>
Store the active tab and selected project in the URL query string so the view survives refreshes and can be bookmarked or shared, syncing back on browser back and forward via popstate.
Query params are used rather than path segments because the embedded server only serves index.html at the root path. Opening the Projects tab now clears the active project, and the Graph tab stays disabled until a project is selected.
Signed-off-by: Zadak <rarepops@protonmail.com>
Cap the filter panel height and move the node and edge chips into a scroll area so a long type list no longer pushes the folder tree off screen.
Regroup the chips under Node types and Relationships, pin the show-labels toggle as a footer, and add a Folders heading (en and zh) above the folder tree so the section is clearly labelled.
Signed-off-by: Zadak <rarepops@protonmail.com>
The New Index modal presented the project name as an optional display
name, but the value becomes permanent identity: the database filename,
the projects primary key, and the QN prefix of every node. It can never
be renamed afterwards.
Implements the decision from the #805 review: keep the field, make it
honest. The label now reads "Project ID (optional — permanent, cannot
be renamed)", the placeholder and a new help line explain that the ID
becomes the database name and query prefix and that leaving it blank
derives it from the path. Both EN and zh locales updated, test asserting
the old label adjusted.
Co-authored-by: rarepops <rarepops@protonmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Pin formatGraphLimitNotice to 'en-US' to resolve unit test failures under non-en_US (e.g. German de_*) locales.
Fixes#825
Signed-off-by: sahil-mangla <manglasahil2017@gmail.com>
The debounced folder-list refresh (and Enter-to-navigate) on the Repository path field now only fires for Windows drive paths (e.g. 'D:/'), where typing is the mechanism for switching drives. POSIX path navigation is left exactly as before.
Add a regression test asserting a typed POSIX path does not trigger a re-browse.
Signed-off-by: Zadak <rarepops@protonmail.com>
On Windows the POSIX '/' quick-jump root is meaningless (browsing it returns an empty listing), yet the picker showed a '/' button whenever the backend did not enumerate drives (older builds return roots=['/']). Clicking it stranded the user on an empty view.
Derive Windows-aware quick-jump roots: drop non-drive roots and always include the current drive (parsed from the browsed path), so the button lists the drive root. Other drives remain reachable by typing. Add a regression test.
Signed-off-by: Zadak <rarepops@protonmail.com>
Typing a path into the Repository path field updated currentPath (and thus the breadcrumb) but never re-fetched the directory listing, so switching drives by typing (e.g. 'D:/') left the previous location's folders showing. This is the only way to change drives when the backend does not enumerate drive roots.
Debounce a silent /api/browse refresh when the typed path changes (keeping the last good listing instead of flashing errors mid-typing), and navigate immediately on Enter. Add a regression test.
Signed-off-by: Zadak <rarepops@protonmail.com>
The index file picker built breadcrumb targets as '/' + segments, so on a Windows drive path (C:/Users/rap) clicking a crumb browsed to '/C:/...', which the backend rejected as 'not a directory'. Only the '.. (up)' button worked.
Build drive-aware crumb targets (C:/, C:/Users) and drop the bogus unified '/' root crumb on Windows drive paths; POSIX behavior is unchanged. Add a regression test for Windows breadcrumb navigation and cleanup() for test isolation.
Signed-off-by: Zadak <rarepops@protonmail.com>
The backend /api/index-status reports status:"error" plus an error
message for failed indexing jobs, but IndexProgress treated any
non-"indexing" state as successful completion: the spinner vanished
with no feedback (e.g. after an OOM-killed indexer subprocess), the
project never appeared, and no error was shown.
Render a visible error banner (path + error text) with a Dismiss
button instead, and keep the success flow unchanged. Beyond the
original PR:
- Restore the empty-jobs guard the PR dropped: the backend keeps
finished jobs listed as "done"/"error" (handle_index_status only
skips idle slots), so an empty list mid-index only occurs on
transient state loss and must not be treated as completion.
- Route the new user-facing strings through the i18n system
(projects.indexingFailed, common.dismiss; EN + zh entries).
- Cover error banner + dismiss, success flow and the empty-jobs
guard with vitest cases on the now-exported IndexProgress.
Distilled from #549.
Refs #524
Co-authored-by: sahil-mangla <manglasahil2017@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Clicking "None" (disable all filters) clears every label and edge type,
making filteredData.nodes empty. The early return then replaced the whole
layout — including the left filter sidebar — with a centered "All nodes
filtered out / Reset Filters" message, leaving no way to re-enable
individual filters without resetting everything.
Narrow the early return to the genuinely-empty / no-data case and render
the "all filtered out" state inside the graph area instead, so the
FilterPanel and Sidebar stay mounted. As a side effect, a project with
zero nodes now correctly reads "No nodes in this project" rather than
"All nodes filtered out".
Add a regression test that disables all filters via "None" and asserts the
Filters sidebar remains in the document.
Signed-off-by: Zadak <rarepops@protonmail.com>
- graph-ui: bump vite ^6.4.2 -> ^6.4.3 and add overrides pinning the transitive
form-data >=4.0.6 and @babel/core >=7.29.6 (all dev-scope build/test deps, not
shipped in the binary). Clears the 4 open Dependabot alerts and Scorecard's
VulnerabilitiesID. `npm audit` now reports 0 vulnerabilities.
- codeql.yml: move `security-events: write` from the workflow top level to the
`analyze` job (top level is now `contents: read`), resolving Scorecard's
TokenPermissionsID least-privilege finding. CodeQL still uploads results from the
job-scoped token.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Structural layer: drop the hardcoded component list — any directory
containing vendored source or data files must now be covered by a
license file in itself or an ancestor within the vendored tree, so
newly vendored code without a license fails immediately. New third
layer: the graph-UI npm production tree is resolved (postinstall
scripts disabled) and every bundled package must be on the policy
allow-list; unknown licenses fail. Reclassifies @tailwindcss/vite as a
devDependency — it is a build plugin and was inflating the production
tree with native tooling.
Resolves the critical Dependabot/Scorecard alert for vitest <4.1.0 (UI server
arbitrary file read/exec). graph-ui has no test files, so the major bump is
risk-free here (CI uses 'vitest run'); lockfile regenerated, vitest 4.1.8.
ws 8.19.0 had a moderate uninitialized-memory-disclosure issue in websocket.close() (fixed in 8.20.1). Transitive dev dependency; the existing ^8.18.0 range already permits the fix, so this is a lockfile-only bump. npm audit now reports 0 vulnerabilities.
Two visualization gaps in the cross-repo graph view:
1. Fixed LAYOUT_GALAXY_SPACING=600 buried satellite galaxies inside the
primary cluster on any non-trivial project — a 1000-node primary
cluster has bounding radius ~1500. Added layout_radius() (max distance
from origin across all node positions) and compute the per-satellite
distance as primary_radius + sat_radius + LAYOUT_GALAXY_PAD, with the
old constant retained as a lower bound for tiny projects.
2. cross_edges was hard-coded to an empty array (TODO placeholder), so
inter-galaxy CROSS_* edges never rendered even when the matcher had
produced them. Populate from a join on the source store's CROSS_*
edges → Route node qualified_name, then resolve the canonical QN in
the linked store to get its node id. The Route QN is the cross-repo
matching contract; properties.target_function isn't unique.
Required keeping both `store` and `lp_store` open through the linked-
projects loop instead of closing them right after layout compute.
Added cbm_store_close to all early-error paths to balance the lifetime.
Frontend side:
- EdgeLines accepts an optional targetNodes prop so a single component
can render edges with source in one node array and target in another
(offset-adjusted satellite nodes).
- GraphScene renders an EdgeLines layer per linked project for its
cross_edges, using primary nodes as source and offsetNodes as target.
- GraphTab folds linked-project labels and edge types into the filter
init / enableAll / filteredData paths so cross_edges respect the
current filter state.
- Edge color palette extended with GRPC/GRAPHQL/TRPC/CROSS_* tones.
Co-authored-by: sponger94 <45746997+sponger94@users.noreply.github.com>