Skip to content

Commit a902263

Browse files
authored
Merge branch 'main' into fix/856-native-fetch-detection
2 parents d288a21 + ee68144 commit a902263

50 files changed

Lines changed: 2852 additions & 436 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Makefile.cbm‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ FOUNDATION_SRCS = \
112112
src/foundation/profile.c \
113113
src/foundation/dump_verify.c \
114114
src/foundation/limits.c \
115-
src/foundation/subprocess.c
115+
src/foundation/subprocess.c \
116+
src/foundation/sha256.c
116117

117118
# Existing extraction C code (compiled from current location)
118119
EXTRACTION_SRCS = \

‎README.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ The `install` command auto-detects all installed coding agents and configures MC
104104

105105
### Graph Visualization UI
106106

107-
If you downloaded the `ui` variant:
107+
The UI ships as a separate `ui` build (it embeds the frontend). The default install on every channel is the lean, headless server; opt into the UI build with:
108+
109+
- **install.sh:** add `--ui` (see [Quick Start](#quick-start))
110+
- **npm:** `CBM_VARIANT=ui npm install -g codebase-memory-mcp`
111+
- **PyPI:** `CBM_VARIANT=ui pip install codebase-memory-mcp`
112+
- **Manual:** download the `codebase-memory-mcp-ui-<os>-<arch>` archive
113+
114+
Then run it:
108115

109116
```bash
110117
codebase-memory-mcp --ui=true --port=9749
@@ -476,6 +483,7 @@ codebase-memory-mcp config reset auto_index # reset to default
476483

477484
| Variable | Default | Description |
478485
|----------|---------|-------------|
486+
| `CBM_ALLOWED_ROOT` | *(unset)* | Restrict `index_repository` to paths within this directory. When set, a `repo_path` that resolves (after symlink / `..` resolution) outside this root is refused; unset imposes no restriction. Useful when the server may be driven by an untrusted caller, e.g. agentic or multi-tenant deployments. |
479487
| `CBM_CACHE_DIR` | `~/.cache/codebase-memory-mcp` | Override the database storage directory. All project indexes and config are stored here. |
480488
| `CBM_DIAGNOSTICS` | `false` | Set to `1` or `true` to enable periodic diagnostics output to `/tmp/cbm-diagnostics-<pid>.json`. |
481489
| `CBM_DOWNLOAD_URL` | *(GitHub releases)* | Override the download URL for updates. Used for testing or self-hosted deployments. |

‎docs/CONFIGURATION.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ These environment variables affect runtime behavior:
110110

111111
| Variable | Default | Description |
112112
|---|---|---|
113+
| `CBM_ALLOWED_ROOT` | *(unset)* | Restrict `index_repository` to paths within this directory. When set, a `repo_path` that resolves (after symlink / `..` resolution) outside this root is refused; unset imposes no restriction. Useful when the server may be driven by an untrusted caller (agentic or multi-tenant deployments). |
113114
| `CBM_CACHE_DIR` | `~/.cache/codebase-memory-mcp` | Override the cache directory used for indexes, `_config.db`, and UI `config.json`. |
114115
| `CBM_DIAGNOSTICS` | `false` | Enable periodic diagnostics output to `/tmp/cbm-diagnostics-<pid>.json`. |
115116
| `CBM_DOWNLOAD_URL` | GitHub releases | Override the update download URL. |
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { useEffect, useRef, useState } from "react";
2+
import { Button } from "@/components/ui/button";
3+
import {
4+
DEFAULT_DISPLAY_SETTINGS,
5+
DISPLAY_LIMITS,
6+
type DisplaySettings,
7+
} from "../lib/density";
8+
9+
interface DisplaySettingsMenuProps {
10+
settings: DisplaySettings;
11+
onChange: (next: DisplaySettings) => void;
12+
}
13+
14+
interface SliderRowProps {
15+
label: string;
16+
hint: string;
17+
value: number;
18+
min: number;
19+
max: number;
20+
onChange: (value: number) => void;
21+
}
22+
23+
function SliderRow({ label, hint, value, min, max, onChange }: SliderRowProps) {
24+
return (
25+
<label className="block">
26+
<div className="flex items-center justify-between mb-1">
27+
<span className="text-[11px] text-foreground/70">{label}</span>
28+
<span className="text-[10px] font-mono text-cyan-300/70 tabular-nums">
29+
{value.toFixed(2)}×
30+
</span>
31+
</div>
32+
<input
33+
type="range"
34+
min={min}
35+
max={max}
36+
step={0.05}
37+
value={value}
38+
onChange={(e) => onChange(parseFloat(e.target.value))}
39+
className="w-full accent-cyan-400 cursor-pointer"
40+
aria-label={`${label} (${hint})`}
41+
/>
42+
<p className="text-[9px] text-foreground/30 mt-0.5">{hint}</p>
43+
</label>
44+
);
45+
}
46+
47+
/* Contrast / brightness controls for the 3D graph. These ride on top of the
48+
* automatic density compensation — the defaults already adapt to graph size,
49+
* so 1.00× is "auto"; the sliders let the user push it. */
50+
export function DisplaySettingsMenu({
51+
settings,
52+
onChange,
53+
}: DisplaySettingsMenuProps) {
54+
const [open, setOpen] = useState(false);
55+
const rootRef = useRef<HTMLDivElement>(null);
56+
57+
/* Close on outside click / Escape */
58+
useEffect(() => {
59+
if (!open) return;
60+
const onDown = (e: MouseEvent) => {
61+
if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
62+
setOpen(false);
63+
}
64+
};
65+
const onKey = (e: KeyboardEvent) => {
66+
if (e.key === "Escape") setOpen(false);
67+
};
68+
document.addEventListener("mousedown", onDown);
69+
document.addEventListener("keydown", onKey);
70+
return () => {
71+
document.removeEventListener("mousedown", onDown);
72+
document.removeEventListener("keydown", onKey);
73+
};
74+
}, [open]);
75+
76+
const set = (patch: Partial<DisplaySettings>) =>
77+
onChange({ ...settings, ...patch });
78+
79+
const isDefault =
80+
settings.edgeBrightness === DEFAULT_DISPLAY_SETTINGS.edgeBrightness &&
81+
settings.nodeGlow === DEFAULT_DISPLAY_SETTINGS.nodeGlow &&
82+
settings.bloom === DEFAULT_DISPLAY_SETTINGS.bloom;
83+
84+
return (
85+
<div ref={rootRef} className="relative">
86+
<Button
87+
variant="outline"
88+
size="sm"
89+
onClick={() => setOpen((v) => !v)}
90+
aria-expanded={open}
91+
aria-haspopup="dialog"
92+
title="Contrast & brightness"
93+
>
94+
Display{!isDefault && <span className="ml-1 text-cyan-300">•</span>}
95+
</Button>
96+
97+
{open && (
98+
<div
99+
role="dialog"
100+
aria-label="Display settings"
101+
className="absolute top-10 right-0 w-64 p-4 rounded-lg border border-border/60 bg-[#0b1920]/95 backdrop-blur-md shadow-xl z-20 space-y-3.5"
102+
>
103+
<div className="flex items-center justify-between">
104+
<span className="text-[11px] font-medium text-foreground/50 uppercase tracking-widest">
105+
Contrast
106+
</span>
107+
<button
108+
onClick={() => onChange(DEFAULT_DISPLAY_SETTINGS)}
109+
className="text-[10px] text-primary/70 hover:text-primary transition-colors disabled:opacity-30"
110+
disabled={isDefault}
111+
>
112+
Reset
113+
</button>
114+
</div>
115+
116+
<SliderRow
117+
label="Edge brightness"
118+
hint="Dim the web of links on dense graphs"
119+
value={settings.edgeBrightness}
120+
min={DISPLAY_LIMITS.edgeBrightness.min}
121+
max={DISPLAY_LIMITS.edgeBrightness.max}
122+
onChange={(edgeBrightness) => set({ edgeBrightness })}
123+
/>
124+
<SliderRow
125+
label="Node glow"
126+
hint="Halo boost around each node"
127+
value={settings.nodeGlow}
128+
min={DISPLAY_LIMITS.nodeGlow.min}
129+
max={DISPLAY_LIMITS.nodeGlow.max}
130+
onChange={(nodeGlow) => set({ nodeGlow })}
131+
/>
132+
<SliderRow
133+
label="Bloom"
134+
hint="Overall glow bloom strength"
135+
value={settings.bloom}
136+
min={DISPLAY_LIMITS.bloom.min}
137+
max={DISPLAY_LIMITS.bloom.max}
138+
onChange={(bloom) => set({ bloom })}
139+
/>
140+
141+
<p className="text-[9px] text-foreground/30 pt-1 border-t border-border/30">
142+
1.00× follows the automatic density compensation. Lower the
143+
edge/glow/bloom values when a large graph washes out to white.
144+
</p>
145+
</div>
146+
)}
147+
</div>
148+
);
149+
}

‎graph-ui/src/components/EdgeLines.tsx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { useMemo } from "react";
22
import * as THREE from "three";
33
import type { GraphNode, GraphEdge } from "../lib/types";
4+
import { edgeIntensityScale } from "../lib/density";
45

56
interface EdgeLinesProps {
67
nodes: GraphNode[];
78
edges: GraphEdge[];
89
highlightedIds: Set<number> | null;
910
opacity?: number;
11+
/* User edge-brightness multiplier (see DisplaySettings). Layered on top of
12+
* the automatic density scale. */
13+
brightness?: number;
1014
/* When set, edge.target is looked up in this array instead of `nodes`.
1115
* Used for cross-galaxy edges where source lives in the primary graph
1216
* and target lives in a linked project's offset-adjusted nodes. */
@@ -52,9 +56,13 @@ export function EdgeLines({
5256
edges,
5357
highlightedIds,
5458
opacity = 1.0,
59+
brightness = 1.0,
5560
targetNodes,
5661
}: EdgeLinesProps) {
5762
const geometry = useMemo(() => {
63+
/* Shrink per-edge glow as the edge count grows so the additively-blended
64+
* center doesn't saturate to white; the user multiplier rides on top. */
65+
const densityScale = edgeIntensityScale(edges.length) * brightness;
5866
const srcMap = new Map<number, number>();
5967
for (let i = 0; i < nodes.length; i++) {
6068
srcMap.set(nodes[i].id, i);
@@ -91,7 +99,11 @@ export function EdgeLines({
9199
* With additive blending + dark background, these glow nicely. */
92100
let intensity = sameCluster ? 0.25 : 0.06;
93101
if (hasHighlight) {
94-
intensity = sHL && tHL ? 0.5 : 0.04;
102+
/* A selection stays at full strength (never density-scaled) so it
103+
* pops against the dimmed rest; only the un-selected bulk is scaled. */
104+
intensity = sHL && tHL ? 0.5 : 0.04 * densityScale;
105+
} else {
106+
intensity *= densityScale;
95107
}
96108

97109
const off = validCount * 6;
@@ -125,7 +137,7 @@ export function EdgeLines({
125137
new THREE.BufferAttribute(colors.slice(0, validCount * 6), 3),
126138
);
127139
return geo;
128-
}, [nodes, edges, highlightedIds, targetNodes]);
140+
}, [nodes, edges, highlightedIds, targetNodes, brightness]);
129141

130142
return (
131143
<lineSegments geometry={geometry}>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { LoadProgress } from "../hooks/useGraphData";
2+
3+
/* Small constellation whose nodes pulse and whose edges draw themselves in a
4+
* staggered loop — a miniature of the graph being loaded. Pure SVG/CSS
5+
* (keyframes in globals.css) so it costs nothing while the real payload
6+
* streams in, and it degrades to a static constellation under
7+
* prefers-reduced-motion. */
8+
9+
const LOADER_NODES: Array<{ cx: number; cy: number; r: number; delay: string }> = [
10+
{ cx: 60, cy: 18, r: 5, delay: "0s" },
11+
{ cx: 22, cy: 42, r: 4, delay: "0.35s" },
12+
{ cx: 98, cy: 40, r: 4, delay: "0.7s" },
13+
{ cx: 42, cy: 78, r: 4.5, delay: "1.05s" },
14+
{ cx: 84, cy: 82, r: 3.5, delay: "1.4s" },
15+
{ cx: 60, cy: 52, r: 6, delay: "0.15s" },
16+
{ cx: 14, cy: 88, r: 3, delay: "1.75s" },
17+
];
18+
19+
const LOADER_EDGES: Array<{ x1: number; y1: number; x2: number; y2: number; delay: string }> = [
20+
{ x1: 60, y1: 18, x2: 60, y2: 52, delay: "0s" },
21+
{ x1: 22, y1: 42, x2: 60, y2: 52, delay: "0.3s" },
22+
{ x1: 98, y1: 40, x2: 60, y2: 52, delay: "0.6s" },
23+
{ x1: 42, y1: 78, x2: 60, y2: 52, delay: "0.9s" },
24+
{ x1: 84, y1: 82, x2: 60, y2: 52, delay: "1.2s" },
25+
{ x1: 42, y1: 78, x2: 14, y2: 88, delay: "1.5s" },
26+
{ x1: 22, y1: 42, x2: 60, y2: 18, delay: "1.8s" },
27+
];
28+
29+
function formatMegabytes(bytes: number): string {
30+
return (bytes / (1024 * 1024)).toFixed(1);
31+
}
32+
33+
interface GraphLoaderProps {
34+
nodeBudget: number;
35+
progress: LoadProgress;
36+
}
37+
38+
export function GraphLoader({ nodeBudget, progress }: GraphLoaderProps) {
39+
const receiving = progress.receivedBytes > 0;
40+
return (
41+
<div className="text-center" role="status" aria-live="polite">
42+
<svg
43+
width="120"
44+
height="104"
45+
viewBox="0 0 120 104"
46+
className="mx-auto"
47+
aria-hidden="true"
48+
>
49+
{LOADER_EDGES.map((e, i) => (
50+
<line
51+
key={`e${i}`}
52+
x1={e.x1}
53+
y1={e.y1}
54+
x2={e.x2}
55+
y2={e.y2}
56+
className="graph-loader-edge"
57+
style={{ animationDelay: e.delay }}
58+
/>
59+
))}
60+
{LOADER_NODES.map((n, i) => (
61+
<circle
62+
key={`n${i}`}
63+
cx={n.cx}
64+
cy={n.cy}
65+
r={n.r}
66+
className="graph-loader-node"
67+
style={{ animationDelay: n.delay }}
68+
/>
69+
))}
70+
</svg>
71+
<p className="text-white/50 text-sm mt-4">
72+
{receiving ? "Receiving graph" : "Computing layout"} — up to{" "}
73+
{nodeBudget.toLocaleString("en-US")} nodes
74+
</p>
75+
<p className="text-cyan-300/60 text-xs font-mono mt-1 h-4">
76+
{receiving
77+
? progress.totalBytes
78+
? `${formatMegabytes(progress.receivedBytes)} of ${formatMegabytes(progress.totalBytes)} MB`
79+
: `${formatMegabytes(progress.receivedBytes)} MB received`
80+
: " "}
81+
</p>
82+
</div>
83+
);
84+
}

0 commit comments

Comments
 (0)