dsh-gitnexus-plus 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1722,7 +1722,686 @@ define("sections/activity", ["require", "exports", "react", "format", "navigatio
1722
1722
  */
1723
1723
  exports.MemoizedGitActivitySection = React.memo(GitActivitySection);
1724
1724
  });
1725
- define("sections/graph", ["require", "exports", "react", "primitives"], function (require, exports, React, primitives_3) {
1725
+ define("sections/graph3d", ["require", "exports", "react", "primitives"], function (require, exports, React, primitives_3) {
1726
+ "use strict";
1727
+ Object.defineProperty(exports, "__esModule", { value: true });
1728
+ exports.buildGraph3DLayout = buildGraph3DLayout;
1729
+ exports.graph3DPath = graph3DPath;
1730
+ exports.Graph3DView = Graph3DView;
1731
+ function hashHue(id) {
1732
+ let hash = 0;
1733
+ for (let index = 0; index < id.length; index += 1) {
1734
+ hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
1735
+ }
1736
+ return hash % 360;
1737
+ }
1738
+ function fibPoint(index, count, radius) {
1739
+ if (count <= 1)
1740
+ return { x: 0, y: 0, z: 0 };
1741
+ const k = index + 0.5;
1742
+ const phi = Math.acos(1 - (2 * k) / count);
1743
+ const theta = Math.PI * (1 + Math.sqrt(5)) * k;
1744
+ return {
1745
+ x: Math.cos(theta) * Math.sin(phi) * radius,
1746
+ y: Math.cos(phi) * radius,
1747
+ z: Math.sin(theta) * Math.sin(phi) * radius,
1748
+ };
1749
+ }
1750
+ function edgeKey(source, target) {
1751
+ return source < target ? `${source}|${target}` : `${target}|${source}`;
1752
+ }
1753
+ /**
1754
+ * Build the deterministic 3D positions.
1755
+ *
1756
+ * The largest connected component sits at the origin, so the first camera angle has
1757
+ * a centre of gravity instead of landing on an empty hemisphere. Other components
1758
+ * orbit on a sphere; isolated nodes sit on an outer shell. No `Math.random()`: a
1759
+ * view that reshuffles on every poll is impossible to follow or test.
1760
+ */
1761
+ function buildGraph3DLayout(nodes, edges) {
1762
+ const byId = new Map();
1763
+ const known = new Set(nodes.map((node) => node.id));
1764
+ const pairs = new Map();
1765
+ const adjacency = new Map();
1766
+ for (const node of nodes)
1767
+ adjacency.set(node.id, new Set());
1768
+ for (const edge of edges) {
1769
+ if (!known.has(edge.source) || !known.has(edge.target) || edge.source === edge.target)
1770
+ continue;
1771
+ const key = edgeKey(edge.source, edge.target);
1772
+ const pair = pairs.get(key);
1773
+ if (pair === undefined) {
1774
+ pairs.set(key, { source: edge.source, target: edge.target, weight: edge.weight });
1775
+ }
1776
+ else {
1777
+ pair.weight += edge.weight;
1778
+ }
1779
+ adjacency.get(edge.source)?.add(edge.target);
1780
+ adjacency.get(edge.target)?.add(edge.source);
1781
+ }
1782
+ const componentIds = [];
1783
+ const seen = new Set();
1784
+ for (const node of nodes) {
1785
+ if (seen.has(node.id))
1786
+ continue;
1787
+ const component = [];
1788
+ const queue = [node.id];
1789
+ seen.add(node.id);
1790
+ while (queue.length > 0) {
1791
+ const id = queue.pop();
1792
+ component.push(id);
1793
+ for (const neighbour of adjacency.get(id) ?? []) {
1794
+ if (seen.has(neighbour))
1795
+ continue;
1796
+ seen.add(neighbour);
1797
+ queue.push(neighbour);
1798
+ }
1799
+ }
1800
+ componentIds.push(component);
1801
+ }
1802
+ componentIds.sort((a, b) => b.length - a.length);
1803
+ let isolated = 0;
1804
+ componentIds.forEach((component, componentIndex) => {
1805
+ const isIsolated = component.length === 1;
1806
+ if (isIsolated)
1807
+ isolated += 1;
1808
+ const count = component.length;
1809
+ const radius = isIsolated ? 640 : 500;
1810
+ let centre = { x: 0, y: 0, z: 0 };
1811
+ if (componentIndex > 0) {
1812
+ const base = fibPoint(componentIndex - 1, Math.max(1, componentIds.length - 1), radius);
1813
+ const wobble = ((hashHue(component[0]) % 100) / 100 - 0.5) * 22;
1814
+ centre = { x: base.x + wobble, y: base.y + wobble * 0.5, z: base.z - wobble * 0.5 };
1815
+ }
1816
+ const clusterRadius = count === 1 ? 0 : 60 + 28 * Math.sqrt(count);
1817
+ component.forEach((id, index) => {
1818
+ const source = nodes.find((node) => node.id === id);
1819
+ if (source === undefined)
1820
+ return;
1821
+ const offset = fibPoint(index, count, clusterRadius);
1822
+ const symbols = source.symbolCount ?? 1;
1823
+ const degree = adjacency.get(id)?.size ?? 0;
1824
+ byId.set(id, {
1825
+ ...source,
1826
+ position: { x: centre.x + offset.x, y: centre.y + offset.y, z: centre.z + offset.z },
1827
+ radius: 7 + Math.sqrt(Math.max(1, symbols)) * 1.8,
1828
+ degree,
1829
+ color: degree === 0 ? 'hsl(220 9% 58%)' : `hsl(${hashHue(id)} 68% 64%)`,
1830
+ isolated: degree === 0,
1831
+ });
1832
+ });
1833
+ });
1834
+ const aggregated = [];
1835
+ for (const pair of pairs.values()) {
1836
+ aggregated.push({ source: pair.source, target: pair.target, type: 'DEPENDS', weight: pair.weight });
1837
+ }
1838
+ return {
1839
+ nodes: nodes.map((node) => byId.get(node.id)).filter((node) => node !== undefined),
1840
+ edges: aggregated,
1841
+ byId,
1842
+ components: componentIds.length,
1843
+ isolated,
1844
+ };
1845
+ }
1846
+ /** Shortest undirected path between two communities. */
1847
+ function graph3DPath(startId, endId, edges) {
1848
+ if (startId === endId)
1849
+ return [startId];
1850
+ const adjacency = new Map();
1851
+ const link = (from, to) => {
1852
+ const existing = adjacency.get(from);
1853
+ if (existing === undefined)
1854
+ adjacency.set(from, [to]);
1855
+ else
1856
+ existing.push(to);
1857
+ };
1858
+ for (const edge of edges) {
1859
+ link(edge.source, edge.target);
1860
+ link(edge.target, edge.source);
1861
+ }
1862
+ const previous = new Map([[startId, null]]);
1863
+ const queue = [startId];
1864
+ while (queue.length > 0) {
1865
+ const id = queue.shift();
1866
+ if (id === endId)
1867
+ break;
1868
+ for (const neighbour of adjacency.get(id) ?? []) {
1869
+ if (previous.has(neighbour))
1870
+ continue;
1871
+ previous.set(neighbour, id);
1872
+ queue.push(neighbour);
1873
+ }
1874
+ }
1875
+ if (!previous.has(endId))
1876
+ return null;
1877
+ const path = [];
1878
+ let current = endId;
1879
+ while (current !== null && current !== undefined) {
1880
+ path.push(current);
1881
+ current = previous.get(current);
1882
+ }
1883
+ return path.reverse();
1884
+ }
1885
+ function project(point, view, width, height) {
1886
+ const cosYaw = Math.cos(view.yaw);
1887
+ const sinYaw = Math.sin(view.yaw);
1888
+ const cosPitch = Math.cos(view.pitch);
1889
+ const sinPitch = Math.sin(view.pitch);
1890
+ const x1 = point.x * cosYaw + point.z * sinYaw;
1891
+ const z1 = -point.x * sinYaw + point.z * cosYaw;
1892
+ const y2 = point.y * cosPitch - z1 * sinPitch;
1893
+ const z2 = point.y * sinPitch + z1 * cosPitch;
1894
+ const z = view.dist + z2;
1895
+ if (z < 1)
1896
+ return null;
1897
+ const scale = 900 / z;
1898
+ return { x: width / 2 + x1 * scale, y: height / 2 - y2 * scale, scale, depth: z };
1899
+ }
1900
+ function clamp(value, low, high) {
1901
+ return Math.max(low, Math.min(high, value));
1902
+ }
1903
+ function fileBase(filePath) {
1904
+ if (filePath === null || filePath === undefined || filePath === '')
1905
+ return null;
1906
+ const parts = filePath.split(/[\\/]/);
1907
+ return parts[parts.length - 1] ?? filePath;
1908
+ }
1909
+ function labelFor(node) {
1910
+ const id = `#${node.id.replace(/^comm_/, '').slice(0, 10)}`;
1911
+ const base = fileBase(node.representativeFile);
1912
+ return base === null ? `${node.name} ${id}` : `${node.name} · ${base} ${id}`;
1913
+ }
1914
+ const CONTROL = {
1915
+ padding: '3px 6px',
1916
+ border: primitives_3.HAIRLINE,
1917
+ borderRadius: '4px',
1918
+ background: 'transparent',
1919
+ color: 'inherit',
1920
+ cursor: 'pointer',
1921
+ fontSize: '10px',
1922
+ lineHeight: 1.5,
1923
+ };
1924
+ const ACTIVE = {
1925
+ ...CONTROL,
1926
+ background: primitives_3.SOFT_BG,
1927
+ fontWeight: 600,
1928
+ };
1929
+ const OPTION_STYLE = {
1930
+ background: 'var(--dsw-alias-bg-base, #1e1e2e)',
1931
+ color: 'var(--dsw-alias-text-primary, #cdd6f4)',
1932
+ };
1933
+ function Graph3DView({ projection, onOpenCommunity }) {
1934
+ const layout = React.useMemo(() => buildGraph3DLayout(projection.nodes, projection.edges), [projection]);
1935
+ const canvasRef = React.useRef(null);
1936
+ const screenRef = React.useRef([]);
1937
+ const viewRef = React.useRef({ yaw: 0.7, pitch: 0.28, dist: 3400 });
1938
+ const dragRef = React.useRef({ x: 0, y: 0, moved: false, active: false });
1939
+ const [selectedId, setSelectedId] = React.useState(null);
1940
+ const [hoverId, setHoverId] = React.useState(null);
1941
+ const [labelsMode, setLabelsMode] = React.useState('auto');
1942
+ const [showIsolated, setShowIsolated] = React.useState(true);
1943
+ const [autoRotate, setAutoRotate] = React.useState(false);
1944
+ const [expanded, setExpanded] = React.useState(false);
1945
+ const [pathMode, setPathMode] = React.useState(false);
1946
+ const [startId, setStartId] = React.useState(null);
1947
+ const [endId, setEndId] = React.useState(null);
1948
+ const [query, setQuery] = React.useState('');
1949
+ const selected = selectedId === null ? null : (layout.byId.get(selectedId) ?? null);
1950
+ const start = startId === null ? null : (layout.byId.get(startId) ?? null);
1951
+ const end = endId === null ? null : (layout.byId.get(endId) ?? null);
1952
+ const path = React.useMemo(() => {
1953
+ if (startId === null || endId === null) {
1954
+ return { nodes: new Set(), edges: new Set(), list: [], error: null };
1955
+ }
1956
+ const list = graph3DPath(startId, endId, layout.edges);
1957
+ if (list === null) {
1958
+ return { nodes: new Set(), edges: new Set(), list: [], error: '这两个社区不在同一连通分量,没有路径。' };
1959
+ }
1960
+ const pathNodes = new Set(list);
1961
+ const pathEdges = new Set();
1962
+ for (let index = 1; index < list.length; index += 1) {
1963
+ pathEdges.add(edgeKey(list[index - 1], list[index]));
1964
+ }
1965
+ return { nodes: pathNodes, edges: pathEdges, list, error: null };
1966
+ }, [endId, layout, startId]);
1967
+ const sortedNodes = React.useMemo(() => [...layout.nodes].sort((a, b) => b.degree - a.degree || (b.symbolCount ?? 0) - (a.symbolCount ?? 0) || a.id.localeCompare(b.id)), [layout]);
1968
+ const filteredNodes = React.useMemo(() => {
1969
+ const needle = query.trim().toLowerCase();
1970
+ if (needle === '')
1971
+ return sortedNodes;
1972
+ return sortedNodes.filter((node) => labelFor(node).toLowerCase().includes(needle));
1973
+ }, [query, sortedNodes]);
1974
+ const hovered = hoverId === null ? null : (layout.byId.get(hoverId) ?? null);
1975
+ const draw = React.useCallback(() => {
1976
+ const canvas = canvasRef.current;
1977
+ if (canvas === null)
1978
+ return;
1979
+ const context = canvas.getContext('2d');
1980
+ if (context === null)
1981
+ return;
1982
+ const rect = canvas.getBoundingClientRect();
1983
+ const width = Math.max(1, rect.width);
1984
+ const height = Math.max(1, rect.height);
1985
+ const ratio = Math.min(2, window.devicePixelRatio || 1);
1986
+ if (canvas.width !== Math.round(width * ratio) || canvas.height !== Math.round(height * ratio)) {
1987
+ canvas.width = Math.round(width * ratio);
1988
+ canvas.height = Math.round(height * ratio);
1989
+ }
1990
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
1991
+ context.clearRect(0, 0, width, height);
1992
+ const background = context.createRadialGradient(width / 2, height / 2, 30, width / 2, height / 2, Math.max(width, height) * 0.8);
1993
+ background.addColorStop(0, '#191929');
1994
+ background.addColorStop(0.58, '#11111c');
1995
+ background.addColorStop(1, '#09090f');
1996
+ context.fillStyle = background;
1997
+ context.fillRect(0, 0, width, height);
1998
+ context.save();
1999
+ context.strokeStyle = 'rgba(137,180,250,0.055)';
2000
+ context.lineWidth = 1;
2001
+ for (const plane of ['xy', 'xz', 'yz']) {
2002
+ context.beginPath();
2003
+ let started = false;
2004
+ for (let index = 0; index <= 120; index += 1) {
2005
+ const angle = (index / 120) * Math.PI * 2;
2006
+ const point = plane === 'xy'
2007
+ ? { x: Math.cos(angle) * 620, y: Math.sin(angle) * 620, z: 0 }
2008
+ : plane === 'xz'
2009
+ ? { x: Math.cos(angle) * 620, y: 0, z: Math.sin(angle) * 620 }
2010
+ : { x: 0, y: Math.cos(angle) * 620, z: Math.sin(angle) * 620 };
2011
+ const projected = project(point, viewRef.current, width, height);
2012
+ if (projected === null) {
2013
+ started = false;
2014
+ continue;
2015
+ }
2016
+ if (!started) {
2017
+ context.moveTo(projected.x, projected.y);
2018
+ started = true;
2019
+ }
2020
+ else {
2021
+ context.lineTo(projected.x, projected.y);
2022
+ }
2023
+ }
2024
+ context.stroke();
2025
+ }
2026
+ context.restore();
2027
+ const projected = layout.nodes
2028
+ .map((node) => ({ node, p: project(node.position, viewRef.current, width, height) }))
2029
+ .filter((entry) => entry.p !== null && (showIsolated || !entry.node.isolated));
2030
+ screenRef.current = projected;
2031
+ const projectedById = new Map(projected.map((entry) => [entry.node.id, entry]));
2032
+ const edgeLines = [];
2033
+ for (const edge of layout.edges) {
2034
+ const source = projectedById.get(edge.source);
2035
+ const target = projectedById.get(edge.target);
2036
+ if (source === undefined || target === undefined)
2037
+ continue;
2038
+ edgeLines.push({
2039
+ source: source.p,
2040
+ target: target.p,
2041
+ weight: edge.weight,
2042
+ key: edgeKey(edge.source, edge.target),
2043
+ depth: source.p.depth + target.p.depth,
2044
+ });
2045
+ }
2046
+ edgeLines.sort((a, b) => b.depth - a.depth);
2047
+ for (const edge of edgeLines) {
2048
+ const isPath = path.edges.has(edge.key);
2049
+ const alpha = clamp(0.78 - (edge.depth / 2 - 700) / 2000, 0.08, 0.68);
2050
+ context.beginPath();
2051
+ context.moveTo(edge.source.x, edge.source.y);
2052
+ context.lineTo(edge.target.x, edge.target.y);
2053
+ context.strokeStyle = isPath ? 'rgba(250,179,135,0.95)' : `rgba(128,138,158,${alpha})`;
2054
+ context.lineWidth = (isPath ? 2.8 : 0.8) + Math.log2(1 + edge.weight) * (isPath ? 0.5 : 0.7);
2055
+ context.stroke();
2056
+ }
2057
+ for (const entry of projected) {
2058
+ const { node, p } = entry;
2059
+ const radius = Math.max(2.2, node.radius * p.scale);
2060
+ const isHot = node.id === selectedId || node.id === hoverId || path.nodes.has(node.id);
2061
+ context.beginPath();
2062
+ context.arc(p.x, p.y, radius + (isHot ? 4 : 0), 0, Math.PI * 2);
2063
+ context.fillStyle = isHot ? 'rgba(255,255,255,0.16)' : 'rgba(255,255,255,0.04)';
2064
+ context.fill();
2065
+ context.beginPath();
2066
+ context.arc(p.x, p.y, radius, 0, Math.PI * 2);
2067
+ context.fillStyle = node.color;
2068
+ context.globalAlpha = clamp(0.95 - (p.depth / 2 - 700) / 2000, 0.42, 0.95);
2069
+ context.fill();
2070
+ context.globalAlpha = 1;
2071
+ if (node.id === selectedId || node.id === startId || node.id === endId) {
2072
+ context.beginPath();
2073
+ context.arc(p.x, p.y, radius + 3, 0, Math.PI * 2);
2074
+ context.strokeStyle = node.id === selectedId ? '#f5f5ff' : '#fab387';
2075
+ context.lineWidth = 2;
2076
+ context.stroke();
2077
+ }
2078
+ }
2079
+ if (labelsMode !== 'off') {
2080
+ const boxes = [];
2081
+ const scored = projected
2082
+ .map((entry) => {
2083
+ const score = (entry.node.id === selectedId ? 1000 : 0) +
2084
+ (entry.node.id === hoverId ? 900 : 0) +
2085
+ (entry.node.id === startId || entry.node.id === endId ? 600 : 0) +
2086
+ (path.nodes.has(entry.node.id) ? 400 : 0) +
2087
+ entry.node.degree;
2088
+ return { ...entry, score };
2089
+ })
2090
+ .filter((entry) => labelsMode === 'all' || entry.score >= 4 || entry.node.id === selectedId || entry.node.id === hoverId)
2091
+ .sort((a, b) => b.score - a.score);
2092
+ const limit = labelsMode === 'all' ? scored.length : 8;
2093
+ for (const entry of scored.slice(0, limit)) {
2094
+ const { node, p } = entry;
2095
+ const text = labelFor(node);
2096
+ const force = node.id === selectedId || node.id === hoverId;
2097
+ context.font = '10px "Segoe UI", "Microsoft YaHei", system-ui, sans-serif';
2098
+ const textWidth = context.measureText(text).width;
2099
+ const boxWidth = textWidth + 6;
2100
+ const boxHeight = 14;
2101
+ const radius = Math.max(2.2, node.radius * p.scale);
2102
+ const x = clamp(p.x - boxWidth / 2, 2, Math.max(2, width - boxWidth - 2));
2103
+ const y = clamp(p.y + radius + 2, 2, Math.max(2, height - boxHeight - 2));
2104
+ const collides = boxes.some((box) => x < box.x + box.w && x + boxWidth > box.x && y < box.y + box.h && y + boxHeight > box.y);
2105
+ if (collides && !force)
2106
+ continue;
2107
+ boxes.push({ x, y, w: boxWidth, h: boxHeight });
2108
+ context.fillStyle = force ? 'rgba(30,30,45,0.94)' : 'rgba(16,16,26,0.80)';
2109
+ context.fillRect(x, y, boxWidth, boxHeight);
2110
+ context.strokeStyle = force ? 'rgba(137,180,250,0.55)' : 'rgba(42,42,61,0.8)';
2111
+ context.strokeRect(x, y, boxWidth, boxHeight);
2112
+ context.fillStyle = force ? '#ffffff' : '#e5e9f6';
2113
+ context.textAlign = 'left';
2114
+ context.textBaseline = 'middle';
2115
+ context.fillText(text, x + 3, y + boxHeight / 2 + 0.5);
2116
+ }
2117
+ }
2118
+ }, [endId, hoverId, labelsMode, layout, path, selectedId, showIsolated, startId]);
2119
+ React.useEffect(() => {
2120
+ const canvas = canvasRef.current;
2121
+ if (canvas === null || typeof ResizeObserver === 'undefined')
2122
+ return undefined;
2123
+ const observer = new ResizeObserver(() => draw());
2124
+ observer.observe(canvas);
2125
+ return () => observer.disconnect();
2126
+ }, [draw]);
2127
+ React.useEffect(() => {
2128
+ if (!autoRotate) {
2129
+ draw();
2130
+ return undefined;
2131
+ }
2132
+ let frame = 0;
2133
+ const tick = () => {
2134
+ if (!dragRef.current.active)
2135
+ viewRef.current.yaw += 0.0024;
2136
+ draw();
2137
+ frame = requestAnimationFrame(tick);
2138
+ };
2139
+ frame = requestAnimationFrame(tick);
2140
+ return () => cancelAnimationFrame(frame);
2141
+ }, [autoRotate, draw]);
2142
+ React.useEffect(() => {
2143
+ const canvas = canvasRef.current;
2144
+ if (canvas === null)
2145
+ return undefined;
2146
+ const onWheel = (event) => {
2147
+ event.preventDefault();
2148
+ viewRef.current.dist = clamp(viewRef.current.dist * Math.exp(event.deltaY * 0.0012), 1600, 5600);
2149
+ draw();
2150
+ };
2151
+ canvas.addEventListener('wheel', onWheel, { passive: false });
2152
+ return () => canvas.removeEventListener('wheel', onWheel);
2153
+ }, [draw]);
2154
+ const pointOf = (event) => {
2155
+ const rect = event.currentTarget.getBoundingClientRect();
2156
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
2157
+ };
2158
+ const nodeAt = (event) => {
2159
+ const point = pointOf(event);
2160
+ let best = null;
2161
+ let bestDistance = Infinity;
2162
+ for (const entry of screenRef.current) {
2163
+ const radius = Math.max(6, entry.node.radius * entry.p.scale + 4);
2164
+ const distance = Math.hypot(entry.p.x - point.x, entry.p.y - point.y);
2165
+ if (distance <= radius && distance < bestDistance) {
2166
+ best = entry.node;
2167
+ bestDistance = distance;
2168
+ }
2169
+ }
2170
+ return best;
2171
+ };
2172
+ const handleClick = (node) => {
2173
+ if (!pathMode) {
2174
+ setSelectedId(node.id);
2175
+ return;
2176
+ }
2177
+ if (startId === null || (startId !== null && endId !== null)) {
2178
+ setStartId(node.id);
2179
+ setEndId(null);
2180
+ setSelectedId(node.id);
2181
+ }
2182
+ else if (node.id === startId) {
2183
+ setStartId(null);
2184
+ setEndId(null);
2185
+ }
2186
+ else {
2187
+ setEndId(node.id);
2188
+ setSelectedId(node.id);
2189
+ }
2190
+ };
2191
+ const onPointerDown = (event) => {
2192
+ dragRef.current = { x: event.clientX, y: event.clientY, moved: false, active: true };
2193
+ event.currentTarget.setPointerCapture?.(event.pointerId);
2194
+ };
2195
+ const onPointerMove = (event) => {
2196
+ if (dragRef.current.active) {
2197
+ const dx = event.clientX - dragRef.current.x;
2198
+ const dy = event.clientY - dragRef.current.y;
2199
+ if (Math.abs(dx) + Math.abs(dy) > 3)
2200
+ dragRef.current.moved = true;
2201
+ if (dragRef.current.moved) {
2202
+ viewRef.current.yaw += dx * 0.006;
2203
+ viewRef.current.pitch = clamp(viewRef.current.pitch + dy * 0.005, -1.2, 1.2);
2204
+ dragRef.current.x = event.clientX;
2205
+ dragRef.current.y = event.clientY;
2206
+ draw();
2207
+ return;
2208
+ }
2209
+ }
2210
+ setHoverId(nodeAt(event)?.id ?? null);
2211
+ };
2212
+ const onPointerUp = (event) => {
2213
+ const moved = dragRef.current.moved;
2214
+ dragRef.current.active = false;
2215
+ if (moved)
2216
+ return;
2217
+ const node = nodeAt(event);
2218
+ if (node === null) {
2219
+ setSelectedId(null);
2220
+ return;
2221
+ }
2222
+ handleClick(node);
2223
+ };
2224
+ const onDoubleClick = (event) => {
2225
+ const node = nodeAt(event);
2226
+ if (node !== null)
2227
+ onOpenCommunity(node.id);
2228
+ };
2229
+ const resetView = () => {
2230
+ viewRef.current = { yaw: 0.7, pitch: 0.28, dist: expanded ? 3000 : 3400 };
2231
+ draw();
2232
+ };
2233
+ const cycleLabels = () => {
2234
+ setLabelsMode(labelsMode === 'auto' ? 'all' : labelsMode === 'all' ? 'off' : 'auto');
2235
+ };
2236
+ const canvasHeight = expanded ? 'min(56vh, 500px)' : 'clamp(240px, 28vw, 340px)';
2237
+ const pathNames = start === null || end === null ? [] : path.list.map((id) => layout.byId.get(id)).filter((node) => node !== undefined);
2238
+ React.useEffect(() => {
2239
+ if (!expanded)
2240
+ return undefined;
2241
+ const onKey = (event) => {
2242
+ if (event.key !== 'Escape')
2243
+ return;
2244
+ setExpanded(false);
2245
+ viewRef.current.dist = 3400;
2246
+ draw();
2247
+ };
2248
+ window.addEventListener('keydown', onKey);
2249
+ return () => window.removeEventListener('keydown', onKey);
2250
+ }, [expanded, draw]);
2251
+ const symbolTotal = projection.nodes.reduce((sum, node) => sum + (node.symbolCount ?? 0), 0);
2252
+ const canvasBlock = (0, primitives_3.h)('div', {
2253
+ style: {
2254
+ position: 'relative',
2255
+ width: '100%',
2256
+ height: canvasHeight,
2257
+ border: primitives_3.HAIRLINE,
2258
+ borderRadius: '6px',
2259
+ background: '#11111c',
2260
+ overflow: 'hidden',
2261
+ },
2262
+ }, (0, primitives_3.h)('canvas', {
2263
+ ref: canvasRef,
2264
+ 'aria-label': '社区关系三维图:拖拽旋转,滚轮缩放,单击社区查看详情,双击进入社区',
2265
+ style: { width: '100%', height: '100%', display: 'block', cursor: 'grab', touchAction: 'none' },
2266
+ onPointerDown,
2267
+ onPointerMove,
2268
+ onPointerUp,
2269
+ onDoubleClick,
2270
+ onPointerLeave: () => {
2271
+ dragRef.current.active = false;
2272
+ setHoverId(null);
2273
+ },
2274
+ }), hovered === null
2275
+ ? null
2276
+ : (0, primitives_3.h)('div', {
2277
+ style: {
2278
+ position: 'absolute',
2279
+ left: '6px',
2280
+ bottom: '6px',
2281
+ maxWidth: 'calc(100% - 12px)',
2282
+ padding: '3px 6px',
2283
+ borderRadius: '4px',
2284
+ background: 'rgba(13,13,23,0.9)',
2285
+ color: '#e5e9f6',
2286
+ fontSize: '10px',
2287
+ pointerEvents: 'none',
2288
+ overflow: 'hidden',
2289
+ textOverflow: 'ellipsis',
2290
+ whiteSpace: 'nowrap',
2291
+ },
2292
+ }, `${labelFor(hovered)} · ${hovered.degree} 度 · ${hovered.symbolCount ?? 0} 符号`), layout.nodes.length === 0
2293
+ ? (0, primitives_3.h)('div', {
2294
+ style: {
2295
+ position: 'absolute',
2296
+ inset: 0,
2297
+ display: 'flex',
2298
+ alignItems: 'center',
2299
+ justifyContent: 'center',
2300
+ ...primitives_3.MUTED,
2301
+ padding: '0 20px',
2302
+ textAlign: 'center',
2303
+ pointerEvents: 'none',
2304
+ },
2305
+ }, '该视图没有可画的社区。')
2306
+ : null);
2307
+ const controlsRow = (0, primitives_3.h)('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px', alignItems: 'center' } }, (0, primitives_3.h)('select', {
2308
+ value: selectedId ?? '',
2309
+ onChange: (event) => setSelectedId(event.target.value === '' ? null : event.target.value),
2310
+ style: { flex: '1 1 120px', minWidth: 0, padding: '3px 4px', border: primitives_3.HAIRLINE, borderRadius: '4px', background: 'var(--dsw-alias-bg-base, #1e1e2e)', color: 'var(--dsw-alias-text-primary, #cdd6f4)', colorScheme: 'dark', fontSize: '10px' },
2311
+ }, (0, primitives_3.h)('option', { value: '', style: OPTION_STYLE }, '选择社区…'), sortedNodes.map((node) => (0, primitives_3.h)('option', { key: node.id, value: node.id, style: OPTION_STYLE }, labelFor(node)))), (0, primitives_3.h)('button', { type: 'button', style: pathMode ? ACTIVE : CONTROL, onClick: () => { const next = !pathMode; setPathMode(next); if (!next) {
2312
+ setStartId(null);
2313
+ setEndId(null);
2314
+ } } }, '路径'), (0, primitives_3.h)('button', { type: 'button', style: CONTROL, onClick: cycleLabels }, `标签:${labelsMode === 'auto' ? '自动' : labelsMode === 'all' ? '全部' : '关闭'}`), (0, primitives_3.h)('button', { type: 'button', style: showIsolated ? ACTIVE : CONTROL, onClick: () => setShowIsolated(!showIsolated) }, `孤立:${showIsolated ? '显示' : '隐藏'}`), (0, primitives_3.h)('button', { type: 'button', style: autoRotate ? ACTIVE : CONTROL, onClick: () => setAutoRotate(!autoRotate) }, '自动'), (0, primitives_3.h)('button', { type: 'button', style: expanded ? ACTIVE : CONTROL, onClick: () => { const next = !expanded; setExpanded(next); viewRef.current.dist = next ? 3000 : 3400; draw(); } }, expanded ? '收起' : '展开'), (0, primitives_3.h)('button', { type: 'button', style: CONTROL, onClick: resetView }, '适配'));
2315
+ const detailBlock = selected === null
2316
+ ? (0, primitives_3.h)('div', { style: { ...primitives_3.MUTED, fontSize: '10px' } }, '拖拽旋转 Z 轴、滚轮缩放;单击看代表文件,双击进入社区。')
2317
+ : (0, primitives_3.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 } }, (0, primitives_3.h)('span', { style: { width: '8px', height: '8px', flex: '0 0 8px', borderRadius: '50%', background: selected.color } }), (0, primitives_3.h)('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: '11px' } }, `${labelFor(selected)} · ${selected.symbolCount ?? 0} 符号`), (0, primitives_3.h)('button', { type: 'button', style: CONTROL, onClick: () => onOpenCommunity(selected.id) }, '进入社区'));
2318
+ const pathBlock = pathMode
2319
+ ? (0, primitives_3.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px', fontSize: '10px' } }, (0, primitives_3.h)('div', { style: { display: 'flex', gap: '4px', alignItems: 'center' } }, (0, primitives_3.h)('span', { style: primitives_3.MUTED }, '路径'), (0, primitives_3.h)('span', { style: { flex: 1 } }), (0, primitives_3.h)('button', { type: 'button', style: CONTROL, onClick: () => { setStartId(null); setEndId(null); } }, '清除')), start === null || end === null
2320
+ ? (0, primitives_3.h)('div', { style: primitives_3.MUTED }, '点两个社区;路径按无向关系计算。')
2321
+ : path.error === null
2322
+ ? (0, primitives_3.h)('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '3px', color: '#f6d5bc' } }, pathNames.map((node) => (0, primitives_3.h)('span', { key: node.id, style: { padding: '1px 4px', borderRadius: '3px', background: 'rgba(250,179,135,0.12)' } }, node.name)))
2323
+ : (0, primitives_3.h)('div', { style: { color: '#f7768e' } }, path.error), start !== null && end !== null && path.error === null
2324
+ ? (0, primitives_3.h)('button', { type: 'button', style: CONTROL, onClick: () => setSelectedId(path.list[path.list.length - 1] ?? null) }, `终点 ${end.name}`)
2325
+ : null)
2326
+ : null;
2327
+ const notesBlock = projection.notes.length === 0
2328
+ ? null
2329
+ : (0, primitives_3.h)('div', { style: { ...primitives_3.MUTED, display: 'flex', flexDirection: 'column', gap: '1px', fontSize: '10px' } }, projection.notes.map((note, index) => (0, primitives_3.h)('div', { key: index }, `· ${note}`)));
2330
+ const footerBlock = (0, primitives_3.h)('div', { style: { ...primitives_3.MUTED, fontSize: '10px' } }, `拖拽旋转、滚轮缩放;单击选中,双击进入社区。节点与边预算 ${projection.budget.nodes}/${projection.budget.edges},超出部分会明确标注。`);
2331
+ const listBlock = (0, primitives_3.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', overflow: 'auto', maxHeight: 'min(38vh, 360px)', paddingRight: '2px' } }, filteredNodes.length === 0
2332
+ ? (0, primitives_3.h)('div', { style: { ...primitives_3.MUTED, fontSize: '10px' } }, '没有匹配的社区。')
2333
+ : filteredNodes.map((node) => (0, primitives_3.h)('button', {
2334
+ key: node.id,
2335
+ type: 'button',
2336
+ onClick: () => {
2337
+ setSelectedId(node.id);
2338
+ setHoverId(null);
2339
+ },
2340
+ style: {
2341
+ display: 'flex',
2342
+ alignItems: 'center',
2343
+ gap: '4px',
2344
+ minWidth: 0,
2345
+ padding: '3px 4px',
2346
+ border: 'none',
2347
+ borderLeft: node.id === selectedId ? '2px solid #89b4fa' : '2px solid transparent',
2348
+ borderRadius: '3px',
2349
+ background: node.id === selectedId ? primitives_3.SOFT_BG : 'transparent',
2350
+ color: 'inherit',
2351
+ textAlign: 'left',
2352
+ cursor: 'pointer',
2353
+ fontSize: '10px',
2354
+ },
2355
+ }, (0, primitives_3.h)('span', { style: { width: '7px', height: '7px', flex: '0 0 7px', borderRadius: '50%', background: node.color } }), (0, primitives_3.h)('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, labelFor(node)), (0, primitives_3.h)('span', { style: { ...primitives_3.MUTED, flex: '0 0 auto' } }, String(node.symbolCount ?? 0)))));
2356
+ const close = () => {
2357
+ setExpanded(false);
2358
+ viewRef.current.dist = 3400;
2359
+ draw();
2360
+ };
2361
+ const sidebar = (0, primitives_3.h)('aside', {
2362
+ 'aria-label': '社区列表与选中详情',
2363
+ style: { width: 'min(320px, 36vw)', flexShrink: 0, display: 'flex', flexDirection: 'column', gap: '6px', minWidth: 0, maxHeight: '72vh', overflow: 'auto' },
2364
+ }, (0, primitives_3.h)('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '6px', padding: '0 2px 2px' } }, (0, primitives_3.h)('div', { style: { flex: 1, minWidth: 0 } }, (0, primitives_3.h)('div', { style: { fontSize: '15px', fontWeight: 700, lineHeight: 1.25 } }, '社区图 3D'), (0, primitives_3.h)('div', { style: { ...primitives_3.MUTED, fontSize: '10px' } }, `${projection.nodes.length} / ${projection.total} 个社区`)), (0, primitives_3.h)('button', { type: 'button', style: CONTROL, onClick: close }, '收起')), (0, primitives_3.h)('div', { style: { ...primitives_3.MUTED, fontSize: '10px' } }, `${projection.nodes.length} 社区 · ${projection.edges.length} 对关系 · ${layout.isolated} 孤立 · ${symbolTotal} 符号`), detailBlock, pathBlock, (0, primitives_3.h)('input', {
2365
+ type: 'search',
2366
+ value: query,
2367
+ onChange: (event) => setQuery(event.target.value),
2368
+ placeholder: '搜索社区 / 文件 / #id…',
2369
+ style: { padding: '4px 6px', border: primitives_3.HAIRLINE, borderRadius: '4px', background: 'transparent', color: 'inherit', fontSize: '10px' },
2370
+ }), (0, primitives_3.h)('div', { style: { ...primitives_3.MUTED, fontSize: '10px', fontWeight: 600 } }, '社区列表'), listBlock);
2371
+ const card = (0, primitives_3.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } }, canvasBlock, controlsRow, detailBlock, pathBlock, notesBlock, footerBlock);
2372
+ if (!expanded)
2373
+ return card;
2374
+ return (0, primitives_3.h)('div', {
2375
+ style: {
2376
+ position: 'fixed',
2377
+ inset: 0,
2378
+ zIndex: 1000,
2379
+ display: 'flex',
2380
+ alignItems: 'center',
2381
+ justifyContent: 'center',
2382
+ padding: '18px',
2383
+ background: 'rgba(0,0,0,0.55)',
2384
+ },
2385
+ onClick: close,
2386
+ }, (0, primitives_3.h)('div', {
2387
+ role: 'dialog',
2388
+ 'aria-modal': 'true',
2389
+ 'aria-label': '社区图 3D 放大视图',
2390
+ style: {
2391
+ width: 'min(94vw, 1040px)',
2392
+ maxHeight: '86vh',
2393
+ overflow: 'auto',
2394
+ padding: '12px',
2395
+ border: primitives_3.HAIRLINE,
2396
+ borderRadius: '8px',
2397
+ background: 'var(--dsw-alias-bg-base, #11111b)',
2398
+ boxShadow: '0 18px 60px rgba(0,0,0,0.45)',
2399
+ },
2400
+ onClick: (event) => event.stopPropagation(),
2401
+ }, (0, primitives_3.h)('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px', minWidth: 0 } }, sidebar, (0, primitives_3.h)('div', { style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: '6px' } }, (0, primitives_3.h)('div', { style: { position: 'relative', minWidth: 0 } }, canvasBlock, (0, primitives_3.h)('div', { style: { position: 'absolute', top: '6px', left: '6px', right: '6px', display: 'flex', justifyContent: 'flex-end', pointerEvents: 'none' } }, (0, primitives_3.h)('div', { style: { pointerEvents: 'auto', maxWidth: '100%' } }, controlsRow))), notesBlock, footerBlock))));
2402
+ }
2403
+ });
2404
+ define("sections/graph", ["require", "exports", "react", "primitives", "sections/graph3d"], function (require, exports, React, primitives_4, graph3d_1) {
1726
2405
  "use strict";
1727
2406
  Object.defineProperty(exports, "__esModule", { value: true });
1728
2407
  exports.communityColors = communityColors;
@@ -1903,7 +2582,7 @@ define("sections/graph", ["require", "exports", "react", "primitives"], function
1903
2582
  };
1904
2583
  }
1905
2584
  function GraphSection({ state }) {
1906
- const { projection, loading, error, routeState, scope, onOpenCommunity, onBack, onReload, onOpenSymbol } = state;
2585
+ const { projection, loading, error, routeState, scope, onOpenCommunity, onOpenSymbols, onBack, onReload, onOpenSymbol } = state;
1907
2586
  const canvasRef = React.useRef(null);
1908
2587
  const transform = React.useRef({ x: 0, y: 0, k: 1 });
1909
2588
  const drag = React.useRef({
@@ -2137,103 +2816,113 @@ define("sections/graph", ["require", "exports", "react", "primitives"], function
2137
2816
  // screen-reader-reachable equivalent of the picture, and the only part of this card
2138
2817
  // that a non-canvas test can assert on.
2139
2818
  const listed = nodes.slice(0, 8);
2140
- return (0, primitives_3.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', paddingTop: '4px' } }, (0, primitives_3.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' } }, scope === null
2141
- ? (0, primitives_3.h)('span', { style: { fontWeight: 600 } }, '社区总览')
2142
- : (0, primitives_3.h)('span', { style: { fontWeight: 600 } }, `社区 ${scope}`), projection === null
2819
+ return (0, primitives_4.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', paddingTop: '4px' } }, (0, primitives_4.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' } }, scope === null
2820
+ ? (0, primitives_4.h)('span', { style: { fontWeight: 600 } }, projection?.view === 'symbols' ? '符号全图' : '社区总览')
2821
+ : (0, primitives_4.h)('span', { style: { fontWeight: 600 } }, `社区 ${scope}`), projection === null
2143
2822
  ? null
2144
- : (0, primitives_3.h)('span', { style: primitives_3.MUTED }, projection.view === 'communities'
2823
+ : (0, primitives_4.h)('span', { style: primitives_4.MUTED }, projection.view === 'communities'
2145
2824
  ? `${projection.nodes.length} / ${projection.total} 个社区`
2146
- : `${projection.nodes.length} / ${projection.total} 个符号`), projection?.truncated === true ? (0, primitives_3.h)(primitives_3.Tag, { tone: 'outline' }, '已截断') : null, (0, primitives_3.h)('span', { style: { flex: 1 } }), scope === null ? null : (0, primitives_3.h)(primitives_3.Button, { variant: 'ghost', onClick: onBack }, '← 全部社区'), (0, primitives_3.h)(primitives_3.Button, { variant: 'ghost', onClick: onReload, disabled: loading }, loading ? '读取中…' : '重读图')), error === null
2825
+ : `${projection.nodes.length} / ${projection.total} 个符号`), projection?.truncated === true ? (0, primitives_4.h)(primitives_4.Tag, { tone: 'outline' }, '已截断') : null, (0, primitives_4.h)('span', { style: { flex: 1 } }), projection === null
2147
2826
  ? null
2148
- : (0, primitives_3.h)('div', { style: { ...primitives_3.MUTED, color: 'var(--dsw-alias-state-error-primary, inherit)' } }, error), (0, primitives_3.h)('div', {
2149
- style: {
2150
- position: 'relative',
2151
- width: '100%',
2152
- // Responsive height: a fixed 300px box made a wide card letterbox the drawing,
2153
- // and stretching the bitmap to fill it distorted every circle instead.
2154
- height: 'clamp(260px, 32vw, 420px)',
2155
- border: primitives_3.HAIRLINE,
2156
- borderRadius: '6px',
2157
- background: primitives_3.SOFT_BG,
2158
- overflow: 'hidden',
2159
- },
2160
- }, (0, primitives_3.h)('canvas', {
2161
- ref: canvasRef,
2162
- style: { width: '100%', height: '100%', display: 'block', touchAction: 'none', cursor: 'grab' },
2163
- onPointerDown,
2164
- onPointerMove,
2165
- onPointerUp,
2166
- onPointerLeave: () => {
2167
- drag.current.active = false;
2168
- hovered.current = null;
2169
- setHoverLabel(null);
2170
- draw();
2171
- },
2172
- }),
2173
- // An empty canvas explains itself, centred, rather than leaving a blank frame that
2174
- // reads as a hang.
2175
- emptyMessage === null
2827
+ : (0, primitives_4.h)(primitives_4.Button, {
2828
+ variant: 'ghost',
2829
+ onClick: projection.view === 'symbols' ? onBack : onOpenSymbols,
2830
+ disabled: loading,
2831
+ }, projection.view === 'symbols' ? '社区视图' : '符号视图'), scope === null ? null : (0, primitives_4.h)(primitives_4.Button, { variant: 'ghost', onClick: onBack }, '← 全部社区'), (0, primitives_4.h)(primitives_4.Button, { variant: 'ghost', onClick: onReload, disabled: loading }, loading ? '读取中…' : '重读图')), error === null
2176
2832
  ? null
2177
- : (0, primitives_3.h)('div', {
2833
+ : (0, primitives_4.h)('div', { style: { ...primitives_4.MUTED, color: 'var(--dsw-alias-state-error-primary, inherit)' } }, error), projection?.view === 'communities'
2834
+ ? (0, primitives_4.h)(graph3d_1.Graph3DView, { projection, onOpenCommunity })
2835
+ : (0, primitives_4.h)(React.Fragment, null, (0, primitives_4.h)('div', {
2178
2836
  style: {
2179
- position: 'absolute',
2180
- inset: 0,
2181
- display: 'flex',
2182
- alignItems: 'center',
2183
- justifyContent: 'center',
2184
- padding: '0 24px',
2185
- textAlign: 'center',
2186
- ...primitives_3.MUTED,
2187
- pointerEvents: 'none',
2837
+ position: 'relative',
2838
+ width: '100%',
2839
+ // Responsive height: a fixed 300px box made a wide card letterbox the drawing,
2840
+ // and stretching the bitmap to fill it distorted every circle instead.
2841
+ height: 'clamp(260px, 32vw, 420px)',
2842
+ border: primitives_4.HAIRLINE,
2843
+ borderRadius: '6px',
2844
+ background: primitives_4.SOFT_BG,
2845
+ overflow: 'hidden',
2188
2846
  },
2189
- }, emptyMessage),
2190
- // A hover caption rather than redrawing a tooltip inside the canvas: it is
2191
- // readable at any zoom, and it stays honest about the node's file.
2192
- hoverLabel === null
2193
- ? null
2194
- : (0, primitives_3.h)('div', {
2195
- style: {
2196
- position: 'absolute',
2197
- left: '8px',
2198
- bottom: '6px',
2199
- padding: '2px 6px',
2200
- borderRadius: '4px',
2201
- background: 'rgba(15, 23, 42, 0.75)',
2202
- fontSize: '11px',
2203
- pointerEvents: 'none',
2847
+ }, (0, primitives_4.h)('canvas', {
2848
+ ref: canvasRef,
2849
+ style: { width: '100%', height: '100%', display: 'block', touchAction: 'none', cursor: 'grab' },
2850
+ onPointerDown,
2851
+ onPointerMove,
2852
+ onPointerUp,
2853
+ onPointerLeave: () => {
2854
+ drag.current.active = false;
2855
+ hovered.current = null;
2856
+ setHoverLabel(null);
2857
+ draw();
2204
2858
  },
2205
- }, hoverLabel)), projection === null || nodes.length === 0
2206
- ? null
2207
- : (0, primitives_3.h)('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } }, listed.map((node) => (0, primitives_3.h)(primitives_3.Button, {
2208
- key: node.id,
2209
- variant: 'ghost',
2210
- onClick: () => (node.kind === 'Community' ? onOpenCommunity(node.id) : onOpenSymbol(node)),
2211
- }, node.kind === 'Community'
2212
- ? [
2213
- (0, primitives_3.h)('span', {
2214
- key: 'dot',
2215
- style: {
2216
- display: 'inline-block',
2217
- width: '8px',
2218
- height: '8px',
2219
- borderRadius: '50%',
2220
- marginRight: '5px',
2221
- verticalAlign: 'middle',
2222
- background: colorsByCommunity.get(node.id) ?? COMMUNITY_COLOR,
2223
- },
2224
- }),
2225
- `${node.name} · ${node.symbolCount ?? 0}`,
2226
- ]
2227
- : node.name)), nodes.length > listed.length
2228
- ? (0, primitives_3.h)('span', { style: { ...primitives_3.MUTED, alignSelf: 'center' } }, `…还有 ${nodes.length - listed.length} 个`)
2229
- : null), projection !== null && Array.isArray(projection.notes) && projection.notes.length > 0
2230
- ? (0, primitives_3.h)('div', { style: { ...primitives_3.MUTED, display: 'flex', flexDirection: 'column', gap: '2px' } }, projection.notes.map((note, index) => (0, primitives_3.h)('div', { key: index }, `· ${note}`)))
2231
- : null, (0, primitives_3.h)('div', { style: primitives_3.MUTED }, scope === null
2232
- ? '点一个社区看它的成员符号;滚轮缩放、拖动平移。节点数与边数由宿主限制,超出部分会明确标注。'
2233
- : '点一个符号会在文件查看器中打开它并定位到行;滚轮缩放、拖动平移。'));
2859
+ }),
2860
+ // An empty canvas explains itself, centred, rather than leaving a blank frame that
2861
+ // reads as a hang.
2862
+ emptyMessage === null
2863
+ ? null
2864
+ : (0, primitives_4.h)('div', {
2865
+ style: {
2866
+ position: 'absolute',
2867
+ inset: 0,
2868
+ display: 'flex',
2869
+ alignItems: 'center',
2870
+ justifyContent: 'center',
2871
+ padding: '0 24px',
2872
+ textAlign: 'center',
2873
+ ...primitives_4.MUTED,
2874
+ pointerEvents: 'none',
2875
+ },
2876
+ }, emptyMessage),
2877
+ // A hover caption rather than redrawing a tooltip inside the canvas: it is
2878
+ // readable at any zoom, and it stays honest about the node's file.
2879
+ hoverLabel === null
2880
+ ? null
2881
+ : (0, primitives_4.h)('div', {
2882
+ style: {
2883
+ position: 'absolute',
2884
+ left: '8px',
2885
+ bottom: '6px',
2886
+ padding: '2px 6px',
2887
+ borderRadius: '4px',
2888
+ background: 'rgba(15, 23, 42, 0.75)',
2889
+ fontSize: '11px',
2890
+ pointerEvents: 'none',
2891
+ },
2892
+ }, hoverLabel)), projection === null || nodes.length === 0
2893
+ ? null
2894
+ : (0, primitives_4.h)('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } }, listed.map((node) => (0, primitives_4.h)(primitives_4.Button, {
2895
+ key: node.id,
2896
+ variant: 'ghost',
2897
+ onClick: () => (node.kind === 'Community' ? onOpenCommunity(node.id) : onOpenSymbol(node)),
2898
+ }, node.kind === 'Community'
2899
+ ? [
2900
+ (0, primitives_4.h)('span', {
2901
+ key: 'dot',
2902
+ style: {
2903
+ display: 'inline-block',
2904
+ width: '8px',
2905
+ height: '8px',
2906
+ borderRadius: '50%',
2907
+ marginRight: '5px',
2908
+ verticalAlign: 'middle',
2909
+ background: colorsByCommunity.get(node.id) ?? COMMUNITY_COLOR,
2910
+ },
2911
+ }),
2912
+ `${node.name} · ${node.symbolCount ?? 0}`,
2913
+ ]
2914
+ : node.name)), nodes.length > listed.length
2915
+ ? (0, primitives_4.h)('span', { style: { ...primitives_4.MUTED, alignSelf: 'center' } }, `…还有 ${nodes.length - listed.length} 个`)
2916
+ : null), projection !== null && Array.isArray(projection.notes) && projection.notes.length > 0
2917
+ ? (0, primitives_4.h)('div', { style: { ...primitives_4.MUTED, display: 'flex', flexDirection: 'column', gap: '2px' } }, projection.notes.map((note, index) => (0, primitives_4.h)('div', { key: index }, `· ${note}`)))
2918
+ : null, (0, primitives_4.h)('div', { style: primitives_4.MUTED }, scope === null
2919
+ ? projection?.view === 'symbols'
2920
+ ? '点一个符号会在文件查看器中打开它;这是全仓库符号的截断视图,社区视图更适合看整体结构。'
2921
+ : '点一个社区看它的成员符号;滚轮缩放、拖动平移。节点数与边数由宿主限制,超出部分会明确标注。'
2922
+ : '点一个符号会在文件查看器中打开它并定位到行;滚轮缩放、拖动平移。')));
2234
2923
  }
2235
2924
  });
2236
- define("sections/capabilities", ["require", "exports", "format", "primitives"], function (require, exports, format_4, primitives_4) {
2925
+ define("sections/capabilities", ["require", "exports", "format", "primitives"], function (require, exports, format_4, primitives_5) {
2237
2926
  "use strict";
2238
2927
  Object.defineProperty(exports, "__esModule", { value: true });
2239
2928
  exports.CapabilityStrip = CapabilityStrip;
@@ -2245,9 +2934,9 @@ define("sections/capabilities", ["require", "exports", "format", "primitives"],
2245
2934
  */
2246
2935
  function CapabilityStrip({ capabilities }) {
2247
2936
  const rows = (0, format_4.capabilityRows)(capabilities);
2248
- return (0, primitives_4.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px' } }, rows.map((row) => (0, primitives_4.h)('div', { key: row.key, style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_4.h)(primitives_4.StateDot, { state: row.ok ? 'done' : 'idle' }), (0, primitives_4.h)('span', { style: { minWidth: '68px' } }, row.label), row.ok
2249
- ? (0, primitives_4.h)('span', { style: primitives_4.MUTED }, row.provider)
2250
- : (0, primitives_4.h)(primitives_4.Tag, { tone: 'outline' }, row.note))));
2937
+ return (0, primitives_5.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px' } }, rows.map((row) => (0, primitives_5.h)('div', { key: row.key, style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_5.h)(primitives_5.StateDot, { state: row.ok ? 'done' : 'idle' }), (0, primitives_5.h)('span', { style: { minWidth: '68px' } }, row.label), row.ok
2938
+ ? (0, primitives_5.h)('span', { style: primitives_5.MUTED }, row.provider)
2939
+ : (0, primitives_5.h)(primitives_5.Tag, { tone: 'outline' }, row.note))));
2251
2940
  }
2252
2941
  /**
2253
2942
  * Resolver confidence and analysis provenance.
@@ -2256,26 +2945,32 @@ define("sections/capabilities", ["require", "exports", "format", "primitives"],
2256
2945
  *
2257
2946
  * @param {{quality: any, provenance: any}} props
2258
2947
  */
2259
- function ProvenanceBlock({ quality, provenance }) {
2948
+ function ProvenanceBlock({ quality, provenance, compatibility }) {
2260
2949
  const summary = (0, format_4.graphQualitySummary)(quality);
2261
- return (0, primitives_4.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', paddingTop: '4px', flex: '1 1 auto' } }, summary === null
2262
- ? (0, primitives_4.h)('div', { style: primitives_4.MUTED }, '该索引未记录未解析接收者统计(旧版本写入)')
2263
- : (0, primitives_4.h)('div', null, (0, primitives_4.h)('div', null, `未解析接收者 ${(0, format_4.compactNumber)(summary.total)} 处——存在这些位置时,impact / context 的结果是下界而非精确值`), (0, primitives_4.h)('div', { style: { ...primitives_4.MUTED, marginTop: '3px' } }, summary.names
2950
+ return (0, primitives_5.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', paddingTop: '4px', flex: '1 1 auto' } }, summary === null
2951
+ ? (0, primitives_5.h)('div', { style: primitives_5.MUTED }, '该索引未记录未解析接收者统计(旧版本写入)')
2952
+ : (0, primitives_5.h)('div', null, (0, primitives_5.h)('div', null, `未解析接收者 ${(0, format_4.compactNumber)(summary.total)} 处——存在这些位置时,impact / context 的结果是下界而非精确值`), (0, primitives_5.h)('div', { style: { ...primitives_5.MUTED, marginTop: '3px' } }, summary.names
2264
2953
  .slice(0, 6)
2265
2954
  .map((entry) => `${entry.name} ${entry.count}`)
2266
2955
  .join(' · '))), provenance === null
2267
2956
  ? null
2268
- : (0, primitives_4.h)('div',
2957
+ : (0, primitives_5.h)('div',
2269
2958
  // Pushed to the bottom: this card shares a row with the caveats card and
2270
2959
  // stretches to its height, so anchoring the metrics at the foot turns the
2271
2960
  // leftover space into a gap between two groups rather than a void below.
2272
- { style: { marginTop: 'auto', paddingTop: '8px' } }, (0, primitives_4.h)(primitives_4.MetricGrid, {
2961
+ { style: { marginTop: 'auto', paddingTop: '8px' } }, (0, primitives_5.h)(primitives_5.MetricGrid, {
2273
2962
  items: [
2274
2963
  { label: 'schema', value: provenance.schemaFingerprint ?? '—' },
2275
2964
  { label: '内容保留', value: provenance.contentRetention ?? '—' },
2276
2965
  { label: 'FTS 配置', value: provenance.ftsProfile ?? '—' },
2277
2966
  { label: '跨语言属性', value: (0, format_4.compactNumber)(provenance.crossLanguageProperties ?? 0) },
2278
2967
  { label: '分析器', value: provenance.runnerCliVersion ?? '—' },
2968
+ {
2969
+ label: '索引/CLI',
2970
+ value: compatibility === undefined || compatibility === null
2971
+ ? '—'
2972
+ : `${compatibility.indexedBy ?? '缺失'} / ${compatibility.installed ?? '缺失'}`,
2973
+ },
2279
2974
  ],
2280
2975
  })));
2281
2976
  }
@@ -2285,7 +2980,7 @@ define("sections/capabilities", ["require", "exports", "format", "primitives"],
2285
2980
  return summary === null ? '无未解析接收者记录' : `未解析接收者 ${(0, format_4.compactNumber)(summary.total)} 处`;
2286
2981
  }
2287
2982
  });
2288
- define("sections/languages", ["require", "exports", "react", "format", "primitives"], function (require, exports, React, format_5, primitives_5) {
2983
+ define("sections/languages", ["require", "exports", "react", "format", "primitives"], function (require, exports, React, format_5, primitives_6) {
2289
2984
  "use strict";
2290
2985
  Object.defineProperty(exports, "__esModule", { value: true });
2291
2986
  exports.MemoizedLanguageSection = void 0;
@@ -2297,11 +2992,11 @@ define("sections/languages", ["require", "exports", "react", "format", "primitiv
2297
2992
  return null;
2298
2993
  const visible = segments.filter((s) => s.percent >= 0.05);
2299
2994
  const hidden = segments.length - visible.length;
2300
- return (0, primitives_5.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, (0, primitives_5.h)('div', { style: primitives_5.styles.bar, role: 'img', 'aria-label': '语言分布' }, segments.map((segment, index) => (0, primitives_5.h)(primitives_5.Tooltip, { key: `${segment.key}-${index}`, label: `${segment.label} ${(0, format_5.fixed)(segment.percent, 1)}%`, side: 'bottom' }, (0, primitives_5.h)('div', {
2995
+ return (0, primitives_6.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, (0, primitives_6.h)('div', { style: primitives_6.styles.bar, role: 'img', 'aria-label': '语言分布' }, segments.map((segment, index) => (0, primitives_6.h)(primitives_6.Tooltip, { key: `${segment.key}-${index}`, label: `${segment.label} ${(0, format_5.fixed)(segment.percent, 1)}%`, side: 'bottom' }, (0, primitives_6.h)('div', {
2301
2996
  // A floor keeps a 0.01% language visible; the flex ratio keeps the
2302
2997
  // major segments proportional.
2303
2998
  style: { width: `${Math.max(0.4, segment.percent)}%`, background: segment.color, flexShrink: 0 },
2304
- })))), (0, primitives_5.h)('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px 12px' } }, visible.map((segment) => (0, primitives_5.h)('span', { key: segment.key, style: { display: 'inline-flex', gap: '5px', alignItems: 'center' } }, (0, primitives_5.h)('span', {
2999
+ })))), (0, primitives_6.h)('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px 12px' } }, visible.map((segment) => (0, primitives_6.h)('span', { key: segment.key, style: { display: 'inline-flex', gap: '5px', alignItems: 'center' } }, (0, primitives_6.h)('span', {
2305
3000
  style: {
2306
3001
  width: '8px',
2307
3002
  height: '8px',
@@ -2310,7 +3005,7 @@ define("sections/languages", ["require", "exports", "react", "format", "primitiv
2310
3005
  display: 'inline-block',
2311
3006
  flexShrink: 0,
2312
3007
  },
2313
- }), (0, primitives_5.h)('span', null, segment.label), (0, primitives_5.h)('span', { style: primitives_5.MUTED }, `${(0, format_5.fixed)(segment.percent, 1)}%`))), hidden > 0 ? (0, primitives_5.h)('span', { style: primitives_5.MUTED }, `…另有 ${hidden} 种`) : null));
3008
+ }), (0, primitives_6.h)('span', null, segment.label), (0, primitives_6.h)('span', { style: primitives_6.MUTED }, `${(0, format_5.fixed)(segment.percent, 1)}%`))), hidden > 0 ? (0, primitives_6.h)('span', { style: primitives_6.MUTED }, `…另有 ${hidden} 种`) : null));
2314
3009
  }
2315
3010
  /**
2316
3011
  * Language distribution, with the metric switchable.
@@ -2326,9 +3021,9 @@ define("sections/languages", ["require", "exports", "react", "format", "primitiv
2326
3021
  function LanguageSection({ languages }) {
2327
3022
  const [metric, setMetric] = React.useState('bytes');
2328
3023
  const unit = metric === 'files' ? '文件' : '字节';
2329
- return (0, primitives_5.h)(primitives_5.Section, {
3024
+ return (0, primitives_6.h)(primitives_6.Section, {
2330
3025
  title: `语言分布 · ${languages.rows.length} 种`,
2331
- meta: (0, primitives_5.h)(primitives_5.SegmentedControl, {
3026
+ meta: (0, primitives_6.h)(primitives_6.SegmentedControl, {
2332
3027
  id: 'gitnexus-language-metric',
2333
3028
  label: '语言分布统计口径',
2334
3029
  value: metric,
@@ -2343,12 +3038,12 @@ define("sections/languages", ["require", "exports", "react", "format", "primitiv
2343
3038
  }),
2344
3039
  // The card stretches to the row height beside the overview card; the footnote
2345
3040
  // below uses `marginTop: auto` to sit at the foot of that height.
2346
- }, (0, primitives_5.h)(LanguageBar, {
3041
+ }, (0, primitives_6.h)(LanguageBar, {
2347
3042
  rows: languages.rows,
2348
3043
  totalBytes: languages.totalBytes,
2349
3044
  totalFiles: languages.totalFiles,
2350
3045
  metric,
2351
- }), (0, primitives_5.h)('div', { style: { marginTop: 'auto', paddingTop: '8px', fontSize: '10px', ...primitives_5.MUTED } }, languages.detector === 'builtin'
3046
+ }), (0, primitives_6.h)('div', { style: { marginTop: 'auto', paddingTop: '8px', fontSize: '10px', ...primitives_6.MUTED } }, languages.detector === 'builtin'
2352
3047
  ? `按${unit}计的占比;GitNexus 不可用,语言由内置扩展名映射识别(仅作参考)`
2353
3048
  : `按${unit}计的占比;GitNexus 只解析其中一部分语言,其余仅作为文件节点存在`));
2354
3049
  }
@@ -2358,7 +3053,7 @@ define("sections/languages", ["require", "exports", "react", "format", "primitiv
2358
3053
  */
2359
3054
  exports.MemoizedLanguageSection = React.memo(LanguageSection);
2360
3055
  });
2361
- define("sections/large-files", ["require", "exports", "format", "primitives"], function (require, exports, format_6, primitives_6) {
3056
+ define("sections/large-files", ["require", "exports", "format", "primitives"], function (require, exports, format_6, primitives_7) {
2362
3057
  "use strict";
2363
3058
  Object.defineProperty(exports, "__esModule", { value: true });
2364
3059
  exports.LargeFileList = LargeFileList;
@@ -2372,10 +3067,10 @@ define("sections/large-files", ["require", "exports", "format", "primitives"], f
2372
3067
  * @param {{files: any[]}} props
2373
3068
  */
2374
3069
  function LargeFileList({ files }) {
2375
- return (0, primitives_6.h)('div', { style: { display: 'flex', flexDirection: 'column' } }, files.map((file) => (0, primitives_6.h)('div', { key: file.path, style: primitives_6.styles.fileRow }, (0, primitives_6.h)(primitives_6.FileTypeIcon, { path: file.path, size: 16 }), (0, primitives_6.h)('span', { style: { minWidth: 0, flex: '1 1 auto' } }, (0, primitives_6.h)(primitives_6.PathLabel, { path: file.path })), (0, primitives_6.h)('span', { style: primitives_6.styles.fileMeta }, (0, primitives_6.h)('span', { style: { ...primitives_6.MUTED, whiteSpace: 'nowrap' } }, file.sizeText), (0, primitives_6.h)(primitives_6.FileFlags, { file }), (0, primitives_6.h)('span', { style: { ...primitives_6.MUTED, whiteSpace: 'nowrap' } }, `${(0, format_6.fixed)((file.shareOfLanguage ?? 0) * 100, 0)}% ${file.language?.label ?? ''}`)))));
3070
+ return (0, primitives_7.h)('div', { style: { display: 'flex', flexDirection: 'column' } }, files.map((file) => (0, primitives_7.h)('div', { key: file.path, style: primitives_7.styles.fileRow }, (0, primitives_7.h)(primitives_7.FileTypeIcon, { path: file.path, size: 16 }), (0, primitives_7.h)('span', { style: { minWidth: 0, flex: '1 1 auto' } }, (0, primitives_7.h)(primitives_7.PathLabel, { path: file.path })), (0, primitives_7.h)('span', { style: primitives_7.styles.fileMeta }, (0, primitives_7.h)('span', { style: { ...primitives_7.MUTED, whiteSpace: 'nowrap' } }, file.sizeText), (0, primitives_7.h)(primitives_7.FileFlags, { file }), (0, primitives_7.h)('span', { style: { ...primitives_7.MUTED, whiteSpace: 'nowrap' } }, `${(0, format_6.fixed)((file.shareOfLanguage ?? 0) * 100, 0)}% ${file.language?.label ?? ''}`)))));
2376
3071
  }
2377
3072
  });
2378
- define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh-client-ui-primitives", "clock", "format", "primitives"], function (require, exports, React, primitives, clock_1, format_7, primitives_7) {
3073
+ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh-client-ui-primitives", "clock", "format", "primitives"], function (require, exports, React, primitives, clock_1, format_7, primitives_8) {
2379
3074
  "use strict";
2380
3075
  Object.defineProperty(exports, "__esModule", { value: true });
2381
3076
  exports.MaintenanceSection = MaintenanceSection;
@@ -2476,11 +3171,11 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2476
3171
  setCopied(null);
2477
3172
  }
2478
3173
  };
2479
- return (0, primitives_7.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px', paddingTop: '4px' } },
3174
+ return (0, primitives_8.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px', paddingTop: '4px' } },
2480
3175
  // The old line said "复制后到会话执行". That stopped being the whole story once the
2481
3176
  // plugin could start the run itself, so the sentence says which parts it runs and
2482
3177
  // which it only hands over.
2483
- (0, primitives_7.h)('div', { style: primitives_7.MUTED }, hostCurrent
3178
+ (0, primitives_8.h)('div', { style: primitives_8.MUTED }, hostCurrent
2484
3179
  ? '下面列出的命令都可由本插件执行(走 DSH 的 shell 执行器,沙箱与其它命令一致,本插件不申请扩权)。' +
2485
3180
  '全量重建只应在增量刷新卡死或结果反常时使用。'
2486
3181
  : '当前宿主是旧版本(不包含 FTS 自适应与注册表重定向修复);重启 DSH 后再执行维护。命令仍可复制。'),
@@ -2489,23 +3184,23 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2489
3184
  // separate ones.
2490
3185
  activeRun === null
2491
3186
  ? null
2492
- : (0, primitives_7.h)('div', {
3187
+ : (0, primitives_8.h)('div', {
2493
3188
  style: {
2494
3189
  display: 'flex',
2495
3190
  flexDirection: 'column',
2496
3191
  gap: '4px',
2497
3192
  padding: '6px 8px',
2498
- border: primitives_7.HAIRLINE,
3193
+ border: primitives_8.HAIRLINE,
2499
3194
  borderRadius: '6px',
2500
- background: primitives_7.SOFT_BG,
3195
+ background: primitives_8.SOFT_BG,
2501
3196
  },
2502
- }, (0, primitives_7.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_7.h)(primitives_7.StateDot, { state: (0, format_7.runStateTone)(activeRun.state) }), (0, primitives_7.h)('span', { style: { fontWeight: 600 } }, (0, format_7.runStateLabel)(activeRun.state)), activeRun.force === true ? (0, primitives_7.h)(primitives_7.Tag, { tone: 'outline' }, '全量') : null, activeRun.repairFts === true ? (0, primitives_7.h)(primitives_7.Tag, { tone: 'outline' }, '修复 FTS') : null, (0, primitives_7.h)('span', { style: primitives_7.MUTED }, activeRun.state === 'running' ? `已运行 ${elapsed}s` : `${elapsed}s`), (0, primitives_7.h)('span', { style: { flex: 1 } }), running
2503
- ? (0, primitives_7.h)(primitives_7.Button, { variant: 'ghost', onClick: onCancelRun }, '取消')
3197
+ }, (0, primitives_8.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_8.h)(primitives_8.StateDot, { state: (0, format_7.runStateTone)(activeRun.state) }), (0, primitives_8.h)('span', { style: { fontWeight: 600 } }, (0, format_7.runStateLabel)(activeRun.state)), activeRun.force === true ? (0, primitives_8.h)(primitives_8.Tag, { tone: 'outline' }, '全量') : null, activeRun.repairFts === true ? (0, primitives_8.h)(primitives_8.Tag, { tone: 'outline' }, '修复 FTS') : null, (0, primitives_8.h)('span', { style: primitives_8.MUTED }, activeRun.state === 'running' ? `已运行 ${elapsed}s` : `${elapsed}s`), (0, primitives_8.h)('span', { style: { flex: 1 } }), running
3198
+ ? (0, primitives_8.h)(primitives_8.Button, { variant: 'ghost', onClick: onCancelRun }, '取消')
2504
3199
  // Wrapped, not passed directly: once `onStartRun` takes a mode argument,
2505
3200
  // React's click event would arrive as that argument and every "run again"
2506
3201
  // would become a forced rebuild. Repeating the last mode is also the
2507
3202
  // honest reading of "run again" after a repair or a full rebuild.
2508
- : (0, primitives_7.h)(primitives_7.Button, {
3203
+ : (0, primitives_8.h)(primitives_8.Button, {
2509
3204
  variant: 'ghost',
2510
3205
  disabled: !hostCurrent,
2511
3206
  onClick: () => onStartRun(activeRun.repairFts === true
@@ -2513,22 +3208,22 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2513
3208
  : activeRun.force === true
2514
3209
  ? 'force'
2515
3210
  : 'refresh'),
2516
- }, '再次执行')), activeRun.message === null ? null : (0, primitives_7.h)('div', { style: primitives_7.MUTED }, activeRun.message),
3211
+ }, '再次执行')), activeRun.message === null ? null : (0, primitives_8.h)('div', { style: primitives_8.MUTED }, activeRun.message),
2517
3212
  // The output is the evidence, but a failure's first screen was thirty lines of
2518
3213
  // JavaScript stack with the one useful sentence buried in it. The verdict above
2519
3214
  // is the summary; the raw text stays one click away.
2520
3215
  activeRun.output === ''
2521
3216
  ? null
2522
- : (0, primitives_7.h)('details', { style: { marginTop: '2px' } }, (0, primitives_7.h)('summary', {
3217
+ : (0, primitives_8.h)('details', { style: { marginTop: '2px' } }, (0, primitives_8.h)('summary', {
2523
3218
  style: {
2524
- ...primitives_7.MUTED,
3219
+ ...primitives_8.MUTED,
2525
3220
  cursor: 'pointer',
2526
3221
  fontSize: '12px',
2527
3222
  userSelect: 'none',
2528
3223
  },
2529
- }, '查看原始输出'), (0, primitives_7.h)('pre', {
3224
+ }, '查看原始输出'), (0, primitives_8.h)('pre', {
2530
3225
  style: {
2531
- ...primitives_7.styles.mono,
3226
+ ...primitives_8.styles.mono,
2532
3227
  margin: '4px 0 0',
2533
3228
  whiteSpace: 'pre-wrap',
2534
3229
  maxHeight: '12em',
@@ -2536,7 +3231,7 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2536
3231
  },
2537
3232
  }, activeRun.output))), runError === null
2538
3233
  ? null
2539
- : (0, primitives_7.h)('div', { style: { ...primitives_7.MUTED, color: 'var(--dsw-alias-state-error-primary, inherit)' } }, runError),
3234
+ : (0, primitives_8.h)('div', { style: { ...primitives_8.MUTED, color: 'var(--dsw-alias-state-error-primary, inherit)' } }, runError),
2540
3235
  // The Tier 0 verdict sits directly above the buttons it governs. Showing "dirty"
2541
3236
  // without the reason would be the same opacity the gate exists to remove, and
2542
3237
  // showing "clean" without an escape hatch would take a decision away from the
@@ -2545,12 +3240,12 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2545
3240
  // deployment state worth naming — otherwise the gate is simply absent and the
2546
3241
  // operator has no way to tell that from "nothing changed".
2547
3242
  deltaError !== null && deltaError !== undefined
2548
- ? (0, primitives_7.h)('div', { style: primitives_7.MUTED }, `差量检查失败:${deltaError} —— 刷新仍可直接执行。`)
3243
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, `差量检查失败:${deltaError} —— 刷新仍可直接执行。`)
2549
3244
  : deltaRoute === 'missing'
2550
- ? (0, primitives_7.h)('div', { style: primitives_7.MUTED }, '当前宿主未挂载差量路由:重启 DSH 后会显示"是否需要刷新"的依据;刷新按钮仍可使用。')
3245
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, '当前宿主未挂载差量路由:重启 DSH 后会显示"是否需要刷新"的依据;刷新按钮仍可使用。')
2551
3246
  : gateKnown
2552
3247
  ? gateClean
2553
- ? (0, primitives_7.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px' } }, (0, primitives_7.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' } }, (0, primitives_7.h)(primitives_7.StateDot, { state: 'done' }), (0, primitives_7.h)('span', { style: { fontWeight: 600 } }, '无需刷新'), (0, primitives_7.h)(primitives_7.Tag, { tone: 'solid' }, '索引已是最新'), (0, primitives_7.h)('span', { style: primitives_7.MUTED }, gate?.notes[0] ?? ''), (0, primitives_7.h)('span', { style: { flex: 1 } }), (0, primitives_7.h)(primitives_7.Button, {
3248
+ ? (0, primitives_8.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px' } }, (0, primitives_8.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' } }, (0, primitives_8.h)(primitives_8.StateDot, { state: 'done' }), (0, primitives_8.h)('span', { style: { fontWeight: 600 } }, '无需刷新'), (0, primitives_8.h)(primitives_8.Tag, { tone: 'solid' }, '索引已是最新'), (0, primitives_8.h)('span', { style: primitives_8.MUTED }, gate?.notes[0] ?? ''), (0, primitives_8.h)('span', { style: { flex: 1 } }), (0, primitives_8.h)(primitives_8.Button, {
2554
3249
  variant: 'ghost',
2555
3250
  disabled: running || !hostCurrent,
2556
3251
  // Not a bypass of the gate: it is the documented way to refresh when
@@ -2562,24 +3257,28 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2562
3257
  // policy skips. Saying "no refresh" without that explanation would read as
2563
3258
  // "git says nothing changed", which is not what was measured.
2564
3259
  excludedCount > 0
2565
- ? (0, primitives_7.h)('div', { style: primitives_7.MUTED }, `${excludedCount} 个改动属索引排除范围(超大文件等),刷新也不会收录,因此不计入刷新依据。`)
3260
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, `${excludedCount} 个改动属索引排除范围(超大文件等),刷新也不会收录,因此不计入刷新依据。`)
2566
3261
  : null)
2567
- : (0, primitives_7.h)('div', {
3262
+ : (0, primitives_8.h)('div', {
2568
3263
  style: {
2569
3264
  display: 'flex',
2570
3265
  flexDirection: 'column',
2571
3266
  gap: '4px',
2572
3267
  padding: '6px 8px',
2573
- border: primitives_7.HAIRLINE,
3268
+ border: primitives_8.HAIRLINE,
2574
3269
  borderRadius: '6px',
2575
- background: primitives_7.SOFT_BG,
3270
+ background: primitives_8.SOFT_BG,
2576
3271
  },
2577
- }, (0, primitives_7.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' } }, (0, primitives_7.h)(primitives_7.StateDot, { state: 'warning' }), (0, primitives_7.h)('span', { style: { fontWeight: 600 } }, '索引内容已落后'), (0, primitives_7.h)('span', { style: primitives_7.MUTED }, `${gate?.counts.modified ?? 0} 改 / ${gate?.counts.added ?? 0} 增 / ` +
3272
+ }, (0, primitives_8.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' } }, (0, primitives_8.h)(primitives_8.StateDot, { state: 'warning' }), (0, primitives_8.h)('span', { style: { fontWeight: 600 } }, '索引内容已落后'), (0, primitives_8.h)('span', { style: primitives_8.MUTED }, `${gate?.counts.modified ?? 0} 改 / ${gate?.counts.added ?? 0} 增 / ` +
2578
3273
  `${gate?.counts.deleted ?? 0} 删` +
2579
3274
  (excludedCount > 0 ? ` · ${excludedCount} 个被排除` : '') +
2580
- ` · 检查用时 ${gate?.durationMs ?? 0}ms`), (0, primitives_7.h)('span', { style: { flex: 1 } }), gate?.truncated === true ? (0, primitives_7.h)(primitives_7.Tag, { tone: 'outline' }, '候选已截断') : null), (0, primitives_7.h)('div', {
3275
+ ` · 检查用时 ${gate?.durationMs ?? 0}ms`), (0, primitives_8.h)('span', { style: { flex: 1 } }), gate?.advice !== undefined && gate?.advice !== null
3276
+ ? (0, primitives_8.h)(primitives_8.Tag, { tone: 'outline' }, `建议:${gate.advice.label}`)
3277
+ : null, gate?.truncated === true ? (0, primitives_8.h)(primitives_8.Tag, { tone: 'outline' }, '候选已截断') : null), gate?.advice !== undefined && gate?.advice !== null
3278
+ ? (0, primitives_8.h)('div', { style: { ...primitives_8.MUTED, fontSize: '11px' } }, gate.advice.reason)
3279
+ : null, (0, primitives_8.h)('div', {
2581
3280
  style: {
2582
- ...primitives_7.styles.mono,
3281
+ ...primitives_8.styles.mono,
2583
3282
  maxHeight: '7em',
2584
3283
  overflow: 'auto',
2585
3284
  whiteSpace: 'pre-wrap',
@@ -2589,15 +3288,19 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2589
3288
  .slice(0, 8)
2590
3289
  .map((entry) => `${entry.kind === 'modified' ? 'M' : entry.kind === 'added' ? 'A' : 'D'} ${entry.path}`)
2591
3290
  .join('\n') + (changed.length > 8 ? `\n…还有 ${changed.length - 8} 个` : '')), excludedCount > 0
2592
- ? (0, primitives_7.h)('div', { style: primitives_7.MUTED }, `${excludedCount} 个改动属索引排除范围(超大文件等),刷新也不会收录,因此不计入刷新依据。`)
3291
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, `${excludedCount} 个改动属索引排除范围(超大文件等),刷新也不会收录,因此不计入刷新依据。`)
2593
3292
  : null)
2594
3293
  : gate !== null && Array.isArray(gate.notes) && gate.notes.length > 0
2595
3294
  ? // No gate: either the comparison was impossible (legacy manifest, no git) or
2596
3295
  // it was not anchored to the index's commit. Both are "cannot prove it is
2597
3296
  // current", and the notes say which — print them all, because the first one
2598
3297
  // alone explains the cause but not the consequence.
2599
- (0, primitives_7.h)('div', { style: { ...primitives_7.MUTED, display: 'flex', flexDirection: 'column', gap: '2px' } }, gate.notes.slice(0, 3).map((note, index) => (0, primitives_7.h)('div', { key: index }, `· ${note}`)))
2600
- : null, commands.map((command) => {
3298
+ (0, primitives_8.h)('div', { style: { ...primitives_8.MUTED, display: 'flex', flexDirection: 'column', gap: '2px' } }, gate.advice !== undefined && gate.advice !== null
3299
+ ? (0, primitives_8.h)('div', null, `建议:${gate.advice.label} —— ${gate.advice.reason}`)
3300
+ : null, gate.notes.slice(0, 3).map((note, index) => (0, primitives_8.h)('div', { key: index }, `· ${note}`)))
3301
+ : skipGit && indexed
3302
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, '该目录没有 .git:索引由 --skip-git 建立,没有提交基线,只能重新分析,不能增量刷新。')
3303
+ : null, commands.map((command) => {
2601
3304
  // The host's wording is written for the no-index state ("初始化索引", "该项目还没有
2602
3305
  // … 索引时使用"). Reusing it on an indexed project told the operator to initialise
2603
3306
  // an index that already existed. Only the label and the "when" line are stateful;
@@ -2615,7 +3318,7 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2615
3318
  when: '索引落后或有文件改动时使用;未变更文件的解析产物会复用。',
2616
3319
  }
2617
3320
  : command;
2618
- return (0, primitives_7.h)('div', {
3321
+ return (0, primitives_8.h)('div', {
2619
3322
  key: command.mode,
2620
3323
  style: {
2621
3324
  display: 'flex',
@@ -2626,13 +3329,13 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2626
3329
  // because the index says FTS is missing, so it keeps full weight.
2627
3330
  opacity: command.mode === applicable || command.mode === 'repair-fts' ? 1 : 0.55,
2628
3331
  },
2629
- }, (0, primitives_7.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_7.h)('span', { style: { fontWeight: 600 } }, shown.label), command.mode === applicable ? (0, primitives_7.h)(primitives_7.Tag, { tone: 'solid' }, '当前适用') : null, command.destructive ? (0, primitives_7.h)(primitives_7.Tag, { tone: 'outline' }, '破坏性') : null, (0, primitives_7.h)('span', { style: { flex: 1 } }),
3332
+ }, (0, primitives_8.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_8.h)('span', { style: { fontWeight: 600 } }, shown.label), command.mode === applicable ? (0, primitives_8.h)(primitives_8.Tag, { tone: 'solid' }, '当前适用') : null, command.destructive ? (0, primitives_8.h)(primitives_8.Tag, { tone: 'outline' }, '破坏性') : null, (0, primitives_8.h)('span', { style: { flex: 1 } }),
2630
3333
  // Each command is executable now, each behind its own confirmation. The
2631
3334
  // force button exists because the host route implements `--force`; a button
2632
3335
  // labelled "全量重建" that quietly ran an incremental refresh would be the
2633
3336
  // exact failure the earlier revision refused to ship.
2634
3337
  command.mode === 'init'
2635
- ? (0, primitives_7.h)(primitives_7.Button, {
3338
+ ? (0, primitives_8.h)(primitives_8.Button, {
2636
3339
  variant: 'ghost',
2637
3340
  // The gate is the whole point of Tier 0: a clean tree disables the
2638
3341
  // normal action and says so, and "仍然刷新" is the way past it.
@@ -2654,29 +3357,29 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2654
3357
  ? '执行增量刷新'
2655
3358
  : '执行增量刷新(需重启)')
2656
3359
  : command.mode === 'repair-fts'
2657
- ? (0, primitives_7.h)(primitives_7.Button, { variant: 'ghost', disabled: running || !hostCurrent, onClick: () => setConfirming('repair-fts') }, running ? '运行中' : '修复搜索索引')
3360
+ ? (0, primitives_8.h)(primitives_8.Button, { variant: 'ghost', disabled: running || !hostCurrent, onClick: () => setConfirming('repair-fts') }, running ? '运行中' : '修复搜索索引')
2658
3361
  : hostCurrent
2659
- ? (0, primitives_7.h)(primitives_7.Button, { variant: 'ghost', disabled: running, onClick: () => setConfirming('full') }, running ? '运行中' : '全量重建')
2660
- : (0, primitives_7.h)(primitives_7.Button, {
3362
+ ? (0, primitives_8.h)(primitives_8.Button, { variant: 'ghost', disabled: running, onClick: () => setConfirming('full') }, running ? '运行中' : '全量重建')
3363
+ : (0, primitives_8.h)(primitives_8.Button, {
2661
3364
  variant: 'ghost',
2662
3365
  disabled: true,
2663
3366
  title: '当前宿主版本尚未支持 force=1;重启 DSH 后再试,或复制命令执行',
2664
- }, '全量重建(需重启)'), (0, primitives_7.h)(primitives_7.Button, { variant: 'ghost', onClick: () => copy(command) }, copied === command.mode ? '已复制' : '复制')), (0, primitives_7.h)('code', {
3367
+ }, '全量重建(需重启)'), (0, primitives_8.h)(primitives_8.Button, { variant: 'ghost', onClick: () => copy(command) }, copied === command.mode ? '已复制' : '复制')), (0, primitives_8.h)('code', {
2665
3368
  style: {
2666
- ...primitives_7.styles.mono,
3369
+ ...primitives_8.styles.mono,
2667
3370
  display: 'block',
2668
3371
  padding: '6px 8px',
2669
- border: primitives_7.HAIRLINE,
3372
+ border: primitives_8.HAIRLINE,
2670
3373
  borderRadius: '6px',
2671
- background: primitives_7.SOFT_BG,
3374
+ background: primitives_8.SOFT_BG,
2672
3375
  wordBreak: 'break-all',
2673
3376
  userSelect: 'all',
2674
3377
  },
2675
- }, command.shell), (0, primitives_7.h)('div', { style: primitives_7.MUTED }, shown.when), command.caveat !== null && command.caveat !== undefined
2676
- ? (0, primitives_7.h)('div', { style: primitives_7.MUTED }, command.caveat)
3378
+ }, command.shell), (0, primitives_8.h)('div', { style: primitives_8.MUTED }, shown.when), command.caveat !== null && command.caveat !== undefined
3379
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, command.caveat)
2677
3380
  : null);
2678
3381
  }), Array.isArray(maintenance?.rationale) && maintenance.rationale.length > 0
2679
- ? (0, primitives_7.h)('div', { style: { ...primitives_7.MUTED, display: 'flex', flexDirection: 'column', gap: '2px' } }, maintenance.rationale.map((note, index) => (0, primitives_7.h)('div', { key: index }, `· ${note}`)))
3382
+ ? (0, primitives_8.h)('div', { style: { ...primitives_8.MUTED, display: 'flex', flexDirection: 'column', gap: '2px' } }, maintenance.rationale.map((note, index) => (0, primitives_8.h)('div', { key: index }, `· ${note}`)))
2680
3383
  : null,
2681
3384
  // The confirmation. Everything the operator needs before a process touches their
2682
3385
  // working tree goes in `description`, because that is the prop the modal renders:
@@ -2686,7 +3389,7 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2686
3389
  // One dialog, two modes. The full rebuild gets its own title, wording and
2687
3390
  // acknowledgement because it is a different risk: it discards the graph and FTS
2688
3391
  // and can take minutes, while the incremental path reuses unchanged files.
2689
- (0, primitives_7.h)(primitives.RiskConfirmation, {
3392
+ (0, primitives_8.h)(primitives.RiskConfirmation, {
2690
3393
  open: confirming !== null,
2691
3394
  title: repairConfirm
2692
3395
  ? '修复搜索索引(FTS)?'
@@ -2757,7 +3460,91 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2757
3460
  }));
2758
3461
  }
2759
3462
  });
2760
- define("sections/overview", ["require", "exports", "react", "format", "primitives", "sections/capabilities"], function (require, exports, React, format_8, primitives_8, capabilities_1) {
3463
+ define("sections/workspace", ["require", "exports", "format", "primitives"], function (require, exports, format_8, primitives_9) {
3464
+ "use strict";
3465
+ Object.defineProperty(exports, "__esModule", { value: true });
3466
+ exports.aggregateProjects = aggregateProjects;
3467
+ exports.WorkspaceOverview = WorkspaceOverview;
3468
+ /** Sum the manifest stats of every indexed project below the workspace. */
3469
+ function aggregateProjects(projects) {
3470
+ const totals = {
3471
+ indexed: 0,
3472
+ unindexed: 0,
3473
+ files: 0,
3474
+ nodes: 0,
3475
+ edges: 0,
3476
+ communities: 0,
3477
+ processes: 0,
3478
+ embeddings: 0,
3479
+ };
3480
+ for (const project of projects) {
3481
+ if (project.kind === 'index') {
3482
+ totals.indexed += 1;
3483
+ const stats = project.stats;
3484
+ if (stats !== null && stats !== undefined) {
3485
+ totals.files += stats.files;
3486
+ totals.nodes += stats.nodes;
3487
+ totals.edges += stats.edges;
3488
+ totals.communities += stats.communities;
3489
+ totals.processes += stats.processes;
3490
+ totals.embeddings += stats.embeddings;
3491
+ }
3492
+ }
3493
+ else if (project.kind === 'git') {
3494
+ totals.unindexed += 1;
3495
+ }
3496
+ }
3497
+ return totals;
3498
+ }
3499
+ const MAX_ROWS = 8;
3500
+ /**
3501
+ * Aggregate view for a multi-project workspace. Returns null for a single project:
3502
+ * there is nothing to aggregate, and the per-project cards already say the same thing.
3503
+ */
3504
+ function WorkspaceOverview({ projects, activePath, onSelectProject, }) {
3505
+ const indexed = projects.filter((project) => project.kind === 'index');
3506
+ if (indexed.length < 2)
3507
+ return null;
3508
+ const totals = aggregateProjects(projects);
3509
+ const rows = [...indexed]
3510
+ .sort((a, b) => (b.stats?.nodes ?? 0) - (a.stats?.nodes ?? 0) || a.path.localeCompare(b.path))
3511
+ .slice(0, MAX_ROWS);
3512
+ return (0, primitives_9.h)(primitives_9.Section, {
3513
+ wide: true,
3514
+ title: '工作区概览',
3515
+ meta: (0, primitives_9.h)('span', { style: { ...primitives_9.MUTED, fontSize: '11px' } }, `${totals.indexed} 个已索引` + (totals.unindexed > 0 ? ` · ${totals.unindexed} 个未索引` : '')),
3516
+ }, (0, primitives_9.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } }, (0, primitives_9.h)('div', { style: { ...primitives_9.MUTED, fontSize: '11px' } }, `合计 ${(0, format_8.compactNumber)(totals.files)} 个文件 · ${(0, format_8.compactNumber)(totals.nodes)} 个符号 · ` +
3517
+ `${(0, format_8.compactNumber)(totals.edges)} 条关系 · ${(0, format_8.compactNumber)(totals.communities)} 个社区 · ` +
3518
+ `${(0, format_8.compactNumber)(totals.processes)} 条流程`), (0, primitives_9.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } }, rows.map((project) => (0, primitives_9.h)('div', {
3519
+ key: project.path,
3520
+ role: 'button',
3521
+ tabIndex: 0,
3522
+ title: project.path,
3523
+ onClick: () => onSelectProject?.(project.path),
3524
+ onKeyDown: (event) => {
3525
+ if (event.key === 'Enter' || event.key === ' ')
3526
+ onSelectProject?.(project.path);
3527
+ },
3528
+ style: {
3529
+ display: 'grid',
3530
+ gridTemplateColumns: 'minmax(0, 1fr) auto auto auto',
3531
+ gap: '8px',
3532
+ alignItems: 'center',
3533
+ padding: '4px 6px',
3534
+ border: primitives_9.HAIRLINE,
3535
+ borderRadius: '4px',
3536
+ background: project.path === activePath ? primitives_9.SOFT_BG : 'transparent',
3537
+ cursor: onSelectProject === undefined ? 'default' : 'pointer',
3538
+ fontSize: '11px',
3539
+ },
3540
+ }, (0, primitives_9.h)('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, project.relPath === '' ? `${project.name}(本工作区)` : project.relPath), (0, primitives_9.h)('span', { style: { ...primitives_9.MUTED, textAlign: 'right' } }, `${(0, format_8.compactNumber)(project.stats?.nodes ?? 0)} 符号`), (0, primitives_9.h)('span', { style: { ...primitives_9.MUTED, textAlign: 'right' } }, `${(0, format_8.compactNumber)(project.stats?.files ?? 0)} 文件`), (0, primitives_9.h)('span', { style: { ...primitives_9.MUTED, textAlign: 'right' } }, project.complete ? '完整' : '缺图')))), indexed.length > MAX_ROWS
3541
+ ? (0, primitives_9.h)('div', { style: { ...primitives_9.MUTED, fontSize: '11px' } }, `还有 ${indexed.length - MAX_ROWS} 个项目;用标题栏切换器查看。`)
3542
+ : null, totals.unindexed > 0
3543
+ ? (0, primitives_9.h)('div', { style: { ...primitives_9.MUTED, fontSize: '11px' } }, `${totals.unindexed} 个 git 项目尚未建索引,未计入合计。`)
3544
+ : null));
3545
+ }
3546
+ });
3547
+ define("sections/overview", ["require", "exports", "react", "format", "primitives", "sections/capabilities"], function (require, exports, React, format_9, primitives_10, capabilities_1) {
2761
3548
  "use strict";
2762
3549
  Object.defineProperty(exports, "__esModule", { value: true });
2763
3550
  exports.MemoizedOverviewBody = void 0;
@@ -2770,36 +3557,36 @@ define("sections/overview", ["require", "exports", "react", "format", "primitive
2770
3557
  * section wrapper and the split-region layout around it.
2771
3558
  */
2772
3559
  function OverviewBody({ report, totals, cap, ratio, }) {
2773
- return (0, primitives_8.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
3560
+ return (0, primitives_10.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
2774
3561
  // Ring and scan summary on one line, metrics below at full card width. Keeping
2775
3562
  // the ring in a left rail stole ~90px from the metric grid at exactly the widths
2776
3563
  // where it needed two columns, so the card turned into a tall single column.
2777
- (0, primitives_8.h)('div', { style: { display: 'flex', gap: '12px', alignItems: 'center', minWidth: 0 } }, ratio === null
3564
+ (0, primitives_10.h)('div', { style: { display: 'flex', gap: '12px', alignItems: 'center', minWidth: 0 } }, ratio === null
2778
3565
  ? null
2779
- : (0, primitives_8.h)(primitives_8.Ring, {
3566
+ : (0, primitives_10.h)(primitives_10.Ring, {
2780
3567
  ratio,
2781
3568
  caption: '覆盖',
2782
3569
  tone: ratio >= 0.9
2783
3570
  ? 'var(--dsw-alias-state-success-primary, #3fb950)'
2784
3571
  : 'var(--dsw-alias-state-warn-primary, #d9a441)',
2785
- }), (0, primitives_8.h)('div', { style: { ...primitives_8.MUTED, minWidth: 0 } }, (0, format_8.summaryLine)(report))),
3572
+ }), (0, primitives_10.h)('div', { style: { ...primitives_10.MUTED, minWidth: 0 } }, (0, format_9.summaryLine)(report))),
2786
3573
  // The three readings sit in a grid instead of a column: 文件 and 图谱 share a
2787
3574
  // row, and the capability strip spans the full width below them. A stacked
2788
3575
  // layout left the right half of the card empty at every width above ~360px.
2789
- (0, primitives_8.h)('div', {
3576
+ (0, primitives_10.h)('div', {
2790
3577
  style: {
2791
3578
  display: 'grid',
2792
3579
  gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
2793
3580
  gap: '10px 20px',
2794
3581
  alignItems: 'start',
2795
3582
  },
2796
- }, (0, primitives_8.h)('div', { style: { minWidth: 0 } }, (0, primitives_8.h)('div', { style: { ...primitives_8.MUTED, marginBottom: '5px' } }, '文件'), (0, primitives_8.h)(primitives_8.MetricGrid, {
3583
+ }, (0, primitives_10.h)('div', { style: { minWidth: 0 } }, (0, primitives_10.h)('div', { style: { ...primitives_10.MUTED, marginBottom: '5px' } }, '文件'), (0, primitives_10.h)(primitives_10.MetricGrid, {
2797
3584
  items: [
2798
- { label: '大小上限', value: cap.text, hint: (0, format_8.capSourceLabel)(cap.source), color: '#d9a441' },
2799
- { label: '跳过合计', value: (0, format_8.fileSize)(totals.largeBytes), color: '#e0af68' },
3585
+ { label: '大小上限', value: cap.text, hint: (0, format_9.capSourceLabel)(cap.source), color: '#d9a441' },
3586
+ { label: '跳过合计', value: (0, format_9.fileSize)(totals.largeBytes), color: '#e0af68' },
2800
3587
  {
2801
3588
  label: '超限文件',
2802
- value: (0, format_8.compactNumber)(totals.large),
3589
+ value: (0, format_9.compactNumber)(totals.large),
2803
3590
  // Amber when something was actually dropped, grey when the count is
2804
3591
  // zero: a zero is good news and should not shout.
2805
3592
  color: totals.large > 0 ? '#f7768e' : undefined,
@@ -2807,17 +3594,17 @@ define("sections/overview", ["require", "exports", "react", "format", "primitive
2807
3594
  ],
2808
3595
  })), report.graph === null || report.graph === undefined
2809
3596
  ? null
2810
- : (0, primitives_8.h)('div', { style: { minWidth: 0 } }, (0, primitives_8.h)('div', { style: { ...primitives_8.MUTED, marginBottom: '5px' } }, '图谱'), (0, primitives_8.h)(primitives_8.MetricGrid, {
3597
+ : (0, primitives_10.h)('div', { style: { minWidth: 0 } }, (0, primitives_10.h)('div', { style: { ...primitives_10.MUTED, marginBottom: '5px' } }, '图谱'), (0, primitives_10.h)(primitives_10.MetricGrid, {
2811
3598
  items: [
2812
- { label: '符号', value: (0, format_8.compactNumber)(report.graph.nodes), color: '#7aa2f7' },
2813
- { label: '边', value: (0, format_8.compactNumber)(report.graph.edges), color: '#9ece6a' },
2814
- { label: '社区', value: (0, format_8.compactNumber)(report.graph.communities), color: '#bb9af7' },
2815
- { label: '流程', value: (0, format_8.compactNumber)(report.graph.processes), color: '#7dcfff' },
2816
- { label: '向量', value: (0, format_8.compactNumber)(report.graph.embeddings), color: '#f7768e' },
3599
+ { label: '符号', value: (0, format_9.compactNumber)(report.graph.nodes), color: '#7aa2f7' },
3600
+ { label: '边', value: (0, format_9.compactNumber)(report.graph.edges), color: '#9ece6a' },
3601
+ { label: '社区', value: (0, format_9.compactNumber)(report.graph.communities), color: '#bb9af7' },
3602
+ { label: '流程', value: (0, format_9.compactNumber)(report.graph.processes), color: '#7dcfff' },
3603
+ { label: '向量', value: (0, format_9.compactNumber)(report.graph.embeddings), color: '#f7768e' },
2817
3604
  ],
2818
- })), (0, primitives_8.h)('div', { style: { gridColumn: '1 / -1', borderTop: primitives_8.HAIRLINE, paddingTop: '8px' } }, (0, primitives_8.h)('div', { style: { ...primitives_8.MUTED, marginBottom: '5px' } }, '查询能力'), (0, primitives_8.h)(capabilities_1.CapabilityStrip, { capabilities: report.capabilities }), report.capabilities?.fts?.status === 'available'
3605
+ })), (0, primitives_10.h)('div', { style: { gridColumn: '1 / -1', borderTop: primitives_10.HAIRLINE, paddingTop: '8px' } }, (0, primitives_10.h)('div', { style: { ...primitives_10.MUTED, marginBottom: '5px' } }, '查询能力'), (0, primitives_10.h)(capabilities_1.CapabilityStrip, { capabilities: report.capabilities }), report.capabilities?.fts?.status === 'available'
2819
3606
  ? null
2820
- : (0, primitives_8.h)('div', { style: { ...primitives_8.MUTED, marginTop: '4px', fontSize: '10px' } }, '顶部「关键词搜索」会先查图;图查询不可用时回退到源码关键词。'))));
3607
+ : (0, primitives_10.h)('div', { style: { ...primitives_10.MUTED, marginTop: '4px', fontSize: '10px' } }, '顶部「关键词搜索」会先查图;图查询不可用时回退到源码关键词。'))));
2821
3608
  }
2822
3609
  /**
2823
3610
  * Memoized for the panel: all four props are stable for the lifetime of one report, so
@@ -2825,7 +3612,7 @@ define("sections/overview", ["require", "exports", "react", "format", "primitive
2825
3612
  */
2826
3613
  exports.MemoizedOverviewBody = React.memo(OverviewBody);
2827
3614
  });
2828
- define("sections/install", ["require", "exports", "react", "@deepseek-ai/dsh-client-ui-primitives", "clock", "format", "primitives"], function (require, exports, React, primitives, clock_2, format_9, primitives_9) {
3615
+ define("sections/install", ["require", "exports", "react", "@deepseek-ai/dsh-client-ui-primitives", "clock", "format", "primitives"], function (require, exports, React, primitives, clock_2, format_10, primitives_11) {
2829
3616
  "use strict";
2830
3617
  Object.defineProperty(exports, "__esModule", { value: true });
2831
3618
  exports.ToolchainNotice = ToolchainNotice;
@@ -2872,27 +3659,27 @@ define("sections/install", ["require", "exports", "react", "@deepseek-ai/dsh-cli
2872
3659
  setCopied(false);
2873
3660
  }
2874
3661
  };
2875
- return (0, primitives_9.h)(primitives_9.Section, { wide: true, title: libraryOnly ? '缺少 gitnexus 命令' : '本机没有 GitNexus' }, (0, primitives_9.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px', paddingTop: '4px' } }, (0, primitives_9.h)('div', { style: { ...primitives_9.MUTED, display: 'flex', gap: '6px', alignItems: 'flex-start' } }, (0, primitives_9.h)(primitives_9.StateDot, { state: 'warning' }), (0, primitives_9.h)('span', null, libraryOnly
3662
+ return (0, primitives_11.h)(primitives_11.Section, { wide: true, title: libraryOnly ? '缺少 gitnexus 命令' : '本机没有 GitNexus' }, (0, primitives_11.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px', paddingTop: '4px' } }, (0, primitives_11.h)('div', { style: { ...primitives_11.MUTED, display: 'flex', gap: '6px', alignItems: 'flex-start' } }, (0, primitives_11.h)(primitives_11.StateDot, { state: 'warning' }), (0, primitives_11.h)('span', null, libraryOnly
2876
3663
  ? '找到了 GitNexus 的库,但没有可执行的 gitnexus 命令(可能只作为依赖安装、未全局链接)。读取不受影响;修复后即可执行增量刷新。'
2877
- : '本插件读取 .gitnexus/gitnexus.json —— 那份文件由 GitNexus 分析仓库产生,包含每个函数定义、符号关系与执行流。没有它就没什么可展示的。')), (0, primitives_9.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } }, (0, primitives_9.h)('div', { style: { fontWeight: 600 } }, '① 自己安装(推荐)'), (0, primitives_9.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_9.h)('code', {
3664
+ : '本插件读取 .gitnexus/gitnexus.json —— 那份文件由 GitNexus 分析仓库产生,包含每个函数定义、符号关系与执行流。没有它就没什么可展示的。')), (0, primitives_11.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } }, (0, primitives_11.h)('div', { style: { fontWeight: 600 } }, '① 自己安装(推荐)'), (0, primitives_11.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_11.h)('code', {
2878
3665
  style: {
2879
- ...primitives_9.styles.mono,
3666
+ ...primitives_11.styles.mono,
2880
3667
  flex: '1 1 auto',
2881
3668
  padding: '6px 8px',
2882
- border: primitives_9.HAIRLINE,
3669
+ border: primitives_11.HAIRLINE,
2883
3670
  borderRadius: '6px',
2884
- background: primitives_9.SOFT_BG,
3671
+ background: primitives_11.SOFT_BG,
2885
3672
  wordBreak: 'break-all',
2886
3673
  userSelect: 'all',
2887
3674
  },
2888
- }, command), (0, primitives_9.h)(primitives_9.Button, { variant: 'ghost', onClick: copy }, copied ? '已复制' : '复制')), (0, primitives_9.h)('div', { style: primitives_9.MUTED }, '用你能访问到的源安装即可 —— 本插件不假设任何网络,也不会自行下载。')), (0, primitives_9.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } }, (0, primitives_9.h)('div', { style: { fontWeight: 600 } }, '② 授权本插件安装'), (0, primitives_9.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } }, installing
2889
- ? (0, primitives_9.h)(primitives_9.Button, { variant: 'ghost', onClick: onCancelInstall }, `取消(已进行 ${elapsed}s)`)
2890
- : (0, primitives_9.h)(primitives_9.Button, { variant: 'ghost', onClick: () => setConfirming(true) }, '授权并安装'), (0, primitives_9.h)('span', { style: primitives_9.MUTED }, '装到插件自己的目录,用你已配置的 npm 源,不改全局前缀、不需要管理员权限;' +
3675
+ }, command), (0, primitives_11.h)(primitives_11.Button, { variant: 'ghost', onClick: copy }, copied ? '已复制' : '复制')), (0, primitives_11.h)('div', { style: primitives_11.MUTED }, '用你能访问到的源安装即可 —— 本插件不假设任何网络,也不会自行下载。')), (0, primitives_11.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } }, (0, primitives_11.h)('div', { style: { fontWeight: 600 } }, '② 授权本插件安装'), (0, primitives_11.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } }, installing
3676
+ ? (0, primitives_11.h)(primitives_11.Button, { variant: 'ghost', onClick: onCancelInstall }, `取消(已进行 ${elapsed}s)`)
3677
+ : (0, primitives_11.h)(primitives_11.Button, { variant: 'ghost', onClick: () => setConfirming(true) }, '授权并安装'), (0, primitives_11.h)('span', { style: primitives_11.MUTED }, '装到插件自己的目录,用你已配置的 npm 源,不改全局前缀、不需要管理员权限;' +
2891
3678
  '若当前项目还没有索引,装完会继续初始化一次。')), activeRun === null
2892
3679
  ? null
2893
- : (0, primitives_9.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px', paddingTop: '2px' } }, (0, primitives_9.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_9.h)(primitives_9.StateDot, { state: (0, format_9.runStateTone)(activeRun.state) }), (0, primitives_9.h)('span', { style: { fontWeight: 600 } }, (0, format_9.runStateLabel)(activeRun.state)), (0, primitives_9.h)('span', { style: primitives_9.MUTED }, installing ? `已运行 ${elapsed}s` : `${elapsed}s`)), activeRun.message === null ? null : (0, primitives_9.h)('div', { style: primitives_9.MUTED }, activeRun.message), activeRun.output === ''
3680
+ : (0, primitives_11.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px', paddingTop: '2px' } }, (0, primitives_11.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_11.h)(primitives_11.StateDot, { state: (0, format_10.runStateTone)(activeRun.state) }), (0, primitives_11.h)('span', { style: { fontWeight: 600 } }, (0, format_10.runStateLabel)(activeRun.state)), (0, primitives_11.h)('span', { style: primitives_11.MUTED }, installing ? `已运行 ${elapsed}s` : `${elapsed}s`)), activeRun.message === null ? null : (0, primitives_11.h)('div', { style: primitives_11.MUTED }, activeRun.message), activeRun.output === ''
2894
3681
  ? null
2895
- : (0, primitives_9.h)('pre', { style: { ...primitives_9.styles.mono, margin: 0, whiteSpace: 'pre-wrap', maxHeight: '9em', overflow: 'auto' } }, activeRun.output)), runError === null ? null : (0, primitives_9.h)('div', { style: { ...primitives_9.MUTED, color: 'var(--dsw-alias-state-error-primary, inherit)' } }, runError)), (0, primitives_9.h)(primitives.RiskConfirmation, {
3682
+ : (0, primitives_11.h)('pre', { style: { ...primitives_11.styles.mono, margin: 0, whiteSpace: 'pre-wrap', maxHeight: '9em', overflow: 'auto' } }, activeRun.output)), runError === null ? null : (0, primitives_11.h)('div', { style: { ...primitives_11.MUTED, color: 'var(--dsw-alias-state-error-primary, inherit)' } }, runError)), (0, primitives_11.h)(primitives.RiskConfirmation, {
2896
3683
  open: confirming,
2897
3684
  title: '授权本插件安装 GitNexus?',
2898
3685
  description: '本插件将执行:\n\nnpm install --prefix <插件数据目录>/gitnexus gitnexus@^1.6.12\n\n' +
@@ -2921,7 +3708,7 @@ define("sections/install", ["require", "exports", "react", "@deepseek-ai/dsh-cli
2921
3708
  })));
2922
3709
  }
2923
3710
  });
2924
- define("sections/search", ["require", "exports", "primitives"], function (require, exports, primitives_10) {
3711
+ define("sections/search", ["require", "exports", "primitives"], function (require, exports, primitives_12) {
2925
3712
  "use strict";
2926
3713
  Object.defineProperty(exports, "__esModule", { value: true });
2927
3714
  exports.SearchSection = SearchSection;
@@ -2937,13 +3724,13 @@ define("sections/search", ["require", "exports", "primitives"], function (requir
2937
3724
  function SearchSection({ search }) {
2938
3725
  const keyOf = (hit) => `${hit.kind}:${hit.filePath}:${hit.startLine ?? ''}:${hit.name}`;
2939
3726
  const canSubmit = search.query.trim() !== '' && !search.searching;
2940
- return (0, primitives_10.h)(primitives_10.Section, {
3727
+ return (0, primitives_12.h)(primitives_12.Section, {
2941
3728
  wide: true,
2942
3729
  title: '关键词搜索',
2943
- meta: (0, primitives_10.h)('span', { style: { ...primitives_10.MUTED, fontSize: '12px' } }, search.mode === 'text' ? '源码关键词(回退)' : '符号名 / 文件路径'),
3730
+ meta: (0, primitives_12.h)('span', { style: { ...primitives_12.MUTED, fontSize: '12px' } }, search.mode === 'text' ? '源码关键词(回退)' : '符号名 / 文件路径'),
2944
3731
  // Twenty hits of `path:line` rows is a scroll area, not a page length.
2945
3732
  scroll: 420,
2946
- }, (0, primitives_10.h)('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', paddingTop: '4px', flexWrap: 'wrap' } }, (0, primitives_10.h)('input', {
3733
+ }, (0, primitives_12.h)('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', paddingTop: '4px', flexWrap: 'wrap' } }, (0, primitives_12.h)('input', {
2947
3734
  type: 'search',
2948
3735
  value: search.query,
2949
3736
  placeholder: '搜索符号或文件,例如 analyzeCoverage',
@@ -2961,34 +3748,34 @@ define("sections/search", ["require", "exports", "primitives"], function (requir
2961
3748
  flex: '1 1 auto',
2962
3749
  minWidth: 0,
2963
3750
  padding: '5px 8px',
2964
- border: primitives_10.HAIRLINE,
3751
+ border: primitives_12.HAIRLINE,
2965
3752
  borderRadius: '6px',
2966
- background: primitives_10.SOFT_BG,
3753
+ background: primitives_12.SOFT_BG,
2967
3754
  color: 'inherit',
2968
3755
  font: 'inherit',
2969
3756
  },
2970
- }), (0, primitives_10.h)(primitives_10.Button, { variant: 'primary', disabled: !canSubmit, onClick: search.onSubmit }, search.searching ? '搜索中…' : '搜索'), search.query === '' ? null : (0, primitives_10.h)(primitives_10.Button, { variant: 'ghost', onClick: search.onClear }, '清除')), search.error !== null
2971
- ? (0, primitives_10.h)('div', {
3757
+ }), (0, primitives_12.h)(primitives_12.Button, { variant: 'primary', disabled: !canSubmit, onClick: search.onSubmit }, search.searching ? '搜索中…' : '搜索'), search.query === '' ? null : (0, primitives_12.h)(primitives_12.Button, { variant: 'ghost', onClick: search.onClear }, '清除')), search.error !== null
3758
+ ? (0, primitives_12.h)('div', {
2972
3759
  style: {
2973
- ...primitives_10.MUTED,
3760
+ ...primitives_12.MUTED,
2974
3761
  color: 'var(--dsw-alias-state-error-primary, inherit)',
2975
3762
  marginTop: '6px',
2976
3763
  },
2977
3764
  }, search.error)
2978
- : null, (0, primitives_10.h)('div', { style: { ...primitives_10.MUTED, fontSize: '10px', marginTop: '4px' } }, search.warning ??
3765
+ : null, (0, primitives_12.h)('div', { style: { ...primitives_12.MUTED, fontSize: '10px', marginTop: '4px' } }, search.warning ??
2979
3766
  (search.mode === 'text'
2980
3767
  ? '图查询不可用,已回退到源码关键词搜索。'
2981
3768
  : '按符号名与文件路径匹配;不搜索正文。')), search.results === null
2982
3769
  ? null
2983
3770
  : search.results.length === 0
2984
- ? (0, primitives_10.h)('div', { style: { ...primitives_10.MUTED, marginTop: '6px' } }, `没有匹配“${search.query}”的符号或文件。`)
2985
- : (0, primitives_10.h)('div', { style: { display: 'flex', flexDirection: 'column', marginTop: '6px' } }, (0, primitives_10.h)('div', { style: { ...primitives_10.MUTED, fontSize: '10px', paddingBottom: '2px' } }, (search.mode === 'text'
3771
+ ? (0, primitives_12.h)('div', { style: { ...primitives_12.MUTED, marginTop: '6px' } }, `没有匹配“${search.query}”的符号或文件。`)
3772
+ : (0, primitives_12.h)('div', { style: { display: 'flex', flexDirection: 'column', marginTop: '6px' } }, (0, primitives_12.h)('div', { style: { ...primitives_12.MUTED, fontSize: '10px', paddingBottom: '2px' } }, (search.mode === 'text'
2986
3773
  ? `找到 ${search.total} 处源码匹配`
2987
3774
  : `找到 ${search.total} 个结果`) +
2988
- `${search.truncated ? '(已截断)' : ''} · 点击结果打开文件`), search.results.map((hit) => (0, primitives_10.h)('div', {
3775
+ `${search.truncated ? '(已截断)' : ''} · 点击结果打开文件`), search.results.map((hit) => (0, primitives_12.h)('div', {
2989
3776
  key: keyOf(hit),
2990
- style: { ...primitives_10.styles.fileRow, borderTop: primitives_10.HAIRLINE, padding: '5px 0' },
2991
- }, (0, primitives_10.h)('button', {
3777
+ style: { ...primitives_12.styles.fileRow, borderTop: primitives_12.HAIRLINE, padding: '5px 0' },
3778
+ }, (0, primitives_12.h)('button', {
2992
3779
  type: 'button',
2993
3780
  onClick: () => search.onOpen(hit),
2994
3781
  title: hit.startLine === null
@@ -3008,7 +3795,7 @@ define("sections/search", ["require", "exports", "primitives"], function (requir
3008
3795
  textAlign: 'left',
3009
3796
  cursor: 'pointer',
3010
3797
  },
3011
- }, (0, primitives_10.h)(primitives_10.Tag, { tone: 'neutral' }, hit.kind), (0, primitives_10.h)('span', {
3798
+ }, (0, primitives_12.h)(primitives_12.Tag, { tone: 'neutral' }, hit.kind), (0, primitives_12.h)('span', {
3012
3799
  style: {
3013
3800
  fontWeight: 600,
3014
3801
  minWidth: 0,
@@ -3016,20 +3803,20 @@ define("sections/search", ["require", "exports", "primitives"], function (requir
3016
3803
  textOverflow: 'ellipsis',
3017
3804
  whiteSpace: 'nowrap',
3018
3805
  },
3019
- }, hit.name), (0, primitives_10.h)('span', {
3806
+ }, hit.name), (0, primitives_12.h)('span', {
3020
3807
  style: {
3021
- ...primitives_10.MUTED,
3022
- ...primitives_10.styles.mono,
3808
+ ...primitives_12.MUTED,
3809
+ ...primitives_12.styles.mono,
3023
3810
  minWidth: 0,
3024
3811
  flex: '1 1 auto',
3025
3812
  overflow: 'hidden',
3026
3813
  textOverflow: 'ellipsis',
3027
3814
  whiteSpace: 'nowrap',
3028
3815
  },
3029
- }, (0, primitives_10.h)(primitives_10.PathLabel, { path: hit.filePath }), hit.startLine !== null ? `:${hit.startLine}` : '')), (0, primitives_10.h)(primitives_10.Button, { variant: 'ghost', onClick: () => search.onCopy(hit) }, search.copied === keyOf(hit) ? '已复制' : '复制')))));
3816
+ }, (0, primitives_12.h)(primitives_12.PathLabel, { path: hit.filePath }), hit.startLine !== null ? `:${hit.startLine}` : '')), (0, primitives_12.h)(primitives_12.Button, { variant: 'ghost', onClick: () => search.onCopy(hit) }, search.copied === keyOf(hit) ? '已复制' : '复制')))));
3030
3817
  }
3031
3818
  });
3032
- define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-ui-primitives", "format", "navigation", "primitives", "sections/overview", "sections/activity", "sections/capabilities", "sections/install", "sections/languages", "sections/large-files", "sections/maintenance", "sections/search", "sections/graph"], function (require, exports, React, primitives, format_10, navigation_3, primitives_11, overview_1, activity_1, capabilities_2, install_1, languages_1, large_files_1, maintenance_1, search_1, graph_1) {
3819
+ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-ui-primitives", "format", "navigation", "primitives", "sections/overview", "sections/activity", "sections/capabilities", "sections/install", "sections/languages", "sections/large-files", "sections/maintenance", "sections/workspace", "sections/search", "sections/graph"], function (require, exports, React, primitives, format_11, navigation_3, primitives_13, overview_1, activity_1, capabilities_2, install_1, languages_1, large_files_1, maintenance_1, workspace_1, search_1, graph_1) {
3033
3820
  "use strict";
3034
3821
  Object.defineProperty(exports, "__esModule", { value: true });
3035
3822
  exports.chooseProject = chooseProject;
@@ -3116,14 +3903,14 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3116
3903
  ? undefined
3117
3904
  : Math.max(0, Math.min(100, Math.round(percent)));
3118
3905
  const text = value === undefined ? label : `${label} · ${value}%`;
3119
- return (0, primitives_11.h)('div', {
3906
+ return (0, primitives_13.h)('div', {
3120
3907
  style: {
3121
3908
  gridColumn: '1 / -1',
3122
3909
  display: 'flex',
3123
3910
  flexDirection: 'column',
3124
3911
  gap: '4px',
3125
3912
  },
3126
- }, (0, primitives_11.h)('progress', {
3913
+ }, (0, primitives_13.h)('progress', {
3127
3914
  value,
3128
3915
  max: 100,
3129
3916
  'aria-label': label,
@@ -3139,13 +3926,45 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3139
3926
  accentColor: 'var(--dsw-alias-brand-primary, #4f8cff)',
3140
3927
  color: 'var(--dsw-alias-brand-primary, #4f8cff)',
3141
3928
  },
3142
- }), (0, primitives_11.h)('div', { style: { ...primitives_11.MUTED, fontSize: '10px' } }, text));
3929
+ }), (0, primitives_13.h)('div', { style: { ...primitives_13.MUTED, fontSize: '10px' } }, text));
3143
3930
  }
3144
- function CoverageView({ report, maintenance, projectPath, workspacePath, projects, projectsVisited, projectsTruncated, projectsError, onSelectProject, context, busy, progress, error, readAt, run, runError, delta, deltaError, deltaRoute, onRefresh, onStartRun, onCancelRun, installRun, installError, onInstall, onCancelInstall, search, graph, }) {
3931
+ function CoverageView({ report, maintenance, projectPath, workspacePath, projects, projectsVisited, projectsTruncated, projectsError, onSelectProject, context, busy, progress, error, readAt, run, runError, delta, deltaError, deltaRoute, onRefresh, onStartRun, onCancelRun, installRun, installError, onInstall, onCancelInstall, search, graph, onGraphNear, graphVisible = false, }) {
3145
3932
  // Whether an index exists decides which maintenance command is applicable. When
3146
3933
  // the project could not even be identified there is nothing to claim, so it is
3147
3934
  // false rather than assumed.
3148
3935
  const indexed = report?.repo?.indexed === true;
3936
+ const graphCardRef = React.useRef(null);
3937
+ // Hooks must stay above every early return below: the panel renders once with
3938
+ // `report === null` while the first request is in flight, then again with the
3939
+ // report. Declaring this state after the loading/no-index returns changed the
3940
+ // hook count between those renders and crashed the sidebar with React #310.
3941
+ const [activityDays, setActivityDays] = React.useState(7);
3942
+ /**
3943
+ * Tell the panel when the graph card is near the viewport.
3944
+ *
3945
+ * The view stays presentational: it owns the ref and the observer, but the fetch
3946
+ * lives in the panel. A browser without IntersectionObserver reports immediately;
3947
+ * the test harness records effects but deliberately does not run them.
3948
+ */
3949
+ React.useEffect(() => {
3950
+ if (!indexed || graph === undefined)
3951
+ return undefined;
3952
+ const node = graphCardRef.current;
3953
+ if (node === null)
3954
+ return undefined;
3955
+ if (typeof IntersectionObserver === 'undefined') {
3956
+ onGraphNear?.();
3957
+ return undefined;
3958
+ }
3959
+ const observer = new IntersectionObserver((entries) => {
3960
+ if (entries.some((entry) => entry.isIntersecting)) {
3961
+ onGraphNear?.();
3962
+ observer.disconnect();
3963
+ }
3964
+ }, { rootMargin: '200px', threshold: 0.01 });
3965
+ observer.observe(node);
3966
+ return () => observer.disconnect();
3967
+ }, [indexed, graph === undefined, onGraphNear]);
3149
3968
  // A maintenance payload with no commands is not a section: rendering its title
3150
3969
  // produced an empty card on failed reads and on hosts that predate the route.
3151
3970
  const maintenanceSection = maintenance !== null &&
@@ -3173,13 +3992,13 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3173
3992
  onSelectProject !== undefined &&
3174
3993
  projectPath !== null;
3175
3994
  const activeDiffers = projectPath !== null && workspacePath !== null && projectPath !== workspacePath;
3176
- const header = (0, primitives_11.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px', minWidth: 0 } }, (0, primitives_11.h)(primitives_11.Row, null, report !== null && report.repo?.indexed ? (0, primitives_11.h)(primitives_11.StateDot, { state: (0, format_10.healthState)(report) }) : null, (0, primitives_11.h)('span', { style: { fontWeight: 600 } }, 'GitNexus 索引'),
3995
+ const header = (0, primitives_13.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px', minWidth: 0 } }, (0, primitives_13.h)(primitives_13.Row, null, report !== null && report.repo?.indexed ? (0, primitives_13.h)(primitives_13.StateDot, { state: (0, format_11.healthState)(report) }) : null, (0, primitives_13.h)('span', { style: { fontWeight: 600 } }, 'GitNexus 索引'),
3177
3996
  // The project, not a global picker. A workspace-scoped switcher is the one
3178
3997
  // selection this panel offers; it changes what the cards describe, nothing else.
3179
3998
  projectPath === null
3180
- ? (0, primitives_11.h)('span', { style: primitives_11.MUTED }, '未解析到工作区')
3999
+ ? (0, primitives_13.h)('span', { style: primitives_13.MUTED }, '未解析到工作区')
3181
4000
  : showSwitcher
3182
- ? (0, primitives_11.h)('span', { style: { display: 'flex', alignItems: 'center', gap: '2px', minWidth: 0, flex: '1 1 auto' } }, (0, primitives_11.h)('select', {
4001
+ ? (0, primitives_13.h)('span', { style: { display: 'flex', alignItems: 'center', gap: '2px', minWidth: 0, flex: '1 1 auto' } }, (0, primitives_13.h)('select', {
3183
4002
  value: projectPath,
3184
4003
  onChange: (event) => onSelectProject?.(event.target.value),
3185
4004
  'aria-label': '切换工作区内的 GitNexus 索引',
@@ -3195,22 +4014,22 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3195
4014
  padding: 0,
3196
4015
  cursor: 'pointer',
3197
4016
  },
3198
- }, projectChoices.map((project) => (0, primitives_11.h)('option', { key: project.path, value: project.path }, projectLabel(project)))), (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '10px' } }, '▾'))
3199
- : (0, primitives_11.h)('span', { style: { minWidth: 0, flex: '1 1 auto' } }, (0, primitives_11.h)(primitives_11.PathLabel, { path: projectPath })), (0, primitives_11.h)('span', { style: { flex: 1 } }),
4017
+ }, projectChoices.map((project) => (0, primitives_13.h)('option', { key: project.path, value: project.path }, projectLabel(project)))), (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '10px' } }, '▾'))
4018
+ : (0, primitives_13.h)('span', { style: { minWidth: 0, flex: '1 1 auto' } }, (0, primitives_13.h)(primitives_13.PathLabel, { path: projectPath })), (0, primitives_13.h)('span', { style: { flex: 1 } }),
3200
4019
  // The label states the scope. "刷新" alone next to a staleness warning reads as
3201
4020
  // "rebuild the index", which this control has never done and — by design, since
3202
4021
  // the plugin spawns nothing — cannot do.
3203
- (0, primitives_11.h)(primitives_11.Button, {
4022
+ (0, primitives_13.h)(primitives_13.Button, {
3204
4023
  variant: 'ghost',
3205
4024
  onClick: onRefresh,
3206
4025
  disabled: busy,
3207
4026
  title: '重新读取报告(不会重建索引;重建见下方「维护命令」)',
3208
4027
  'aria-label': '重新读取报告',
3209
- }, (0, primitives_11.h)(primitives_11.IconRefreshOutlineRegular, { size: 14 }))), showSwitcher ||
4028
+ }, (0, primitives_13.h)(primitives_13.IconRefreshOutlineRegular, { size: 14 }))), showSwitcher ||
3210
4029
  activeDiffers ||
3211
4030
  projectsTruncated === true ||
3212
4031
  (projectsError !== null && projectsError !== undefined)
3213
- ? (0, primitives_11.h)('div', { style: { ...primitives_11.MUTED, fontSize: '11px', minWidth: 0, overflowWrap: 'anywhere' } }, [
4032
+ ? (0, primitives_13.h)('div', { style: { ...primitives_13.MUTED, fontSize: '11px', minWidth: 0, overflowWrap: 'anywhere' } }, [
3214
4033
  showSwitcher
3215
4034
  ? `工作区:${workspacePath ?? ''} · ${projectChoices.length} 个项目` +
3216
4035
  (indexedProjectCount > 0
@@ -3235,7 +4054,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3235
4054
  // `percent` is null when the host does not stream (old host, or a cached read):
3236
4055
  // the native progress element is then indeterminate, not a made-up number.
3237
4056
  const progressBar = report === null || busy
3238
- ? (0, primitives_11.h)(LoadingBar, {
4057
+ ? (0, primitives_13.h)(LoadingBar, {
3239
4058
  label: progress?.label ?? (report === null ? '正在读取索引…' : '正在刷新索引…'),
3240
4059
  percent: progress?.percent ?? null,
3241
4060
  })
@@ -3246,89 +4065,93 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3246
4065
  // index for a project it had not identified. The diagnostic exists so that state
3247
4066
  // is actionable rather than a dead end.
3248
4067
  if (projectPath === null) {
3249
- return (0, primitives_11.h)('div', { style: primitives_11.styles.root }, header, (0, primitives_11.h)(primitives_11.Section, { wide: true, style: { display: 'flex', gap: '8px', alignItems: 'flex-start' } }, (0, primitives_11.h)(primitives_11.StateDot, { state: 'warning' }), (0, primitives_11.h)('span', null, '未能确定当前项目路径。')), context === null || context === undefined
4068
+ return (0, primitives_13.h)('div', { style: primitives_13.styles.root }, header, (0, primitives_13.h)(primitives_13.Section, { wide: true, style: { display: 'flex', gap: '8px', alignItems: 'flex-start' } }, (0, primitives_13.h)(primitives_13.StateDot, { state: 'warning' }), (0, primitives_13.h)('span', null, '未能确定当前项目路径。')), context === null || context === undefined
3250
4069
  ? null
3251
- : (0, primitives_11.h)(primitives_11.Section, { wide: true, title: '面板收到的上下文' }, (0, primitives_11.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px' } }, (0, primitives_11.h)('div', null, `存在:${context.present.length > 0 ? context.present.join('、') : '(无)'}`), (0, primitives_11.h)('div', null, `缺失:${context.absent.length > 0 ? context.absent.join('、') : '(无)'}`))));
4070
+ : (0, primitives_13.h)(primitives_13.Section, { wide: true, title: '面板收到的上下文' }, (0, primitives_13.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px' } }, (0, primitives_13.h)('div', null, `存在:${context.present.length > 0 ? context.present.join('、') : '(无)'}`), (0, primitives_13.h)('div', null, `缺失:${context.absent.length > 0 ? context.absent.join('、') : '(无)'}`))));
3252
4071
  }
3253
4072
  if (error !== null) {
3254
- return (0, primitives_11.h)('div', { style: primitives_11.styles.root }, header, (0, primitives_11.h)(primitives_11.Section, { wide: true, style: { display: 'flex', gap: '8px', alignItems: 'flex-start' } }, (0, primitives_11.h)(primitives_11.StateDot, { state: 'error' }), (0, primitives_11.h)('span', null, error)), maintenanceSection !== null
3255
- ? (0, primitives_11.h)(primitives_11.Section, {
4073
+ return (0, primitives_13.h)('div', { style: primitives_13.styles.root }, header, (0, primitives_13.h)(primitives_13.Section, { wide: true, style: { display: 'flex', gap: '8px', alignItems: 'flex-start' } }, (0, primitives_13.h)(primitives_13.StateDot, { state: 'error' }), (0, primitives_13.h)('span', null, error)), maintenanceSection !== null
4074
+ ? (0, primitives_13.h)(primitives_13.Section, {
3256
4075
  wide: true,
3257
4076
  title: '维护命令',
3258
- meta: (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '12px' } }, '复制后到会话执行(沙箱与提权由 DSH 处理)'),
4077
+ meta: (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '12px' } }, '复制后到会话执行(沙箱与提权由 DSH 处理)'),
3259
4078
  scroll: 320,
3260
- }, (0, primitives_11.h)(maintenance_1.MaintenanceSection, { maintenance: maintenanceSection, projectPath, indexed: false, run, runError, delta, deltaError, deltaRoute, onStartRun, onCancelRun }))
4079
+ }, (0, primitives_13.h)(maintenance_1.MaintenanceSection, { maintenance: maintenanceSection, projectPath, indexed: false, run, runError, delta, deltaError, deltaRoute, onStartRun, onCancelRun }))
3261
4080
  : null);
3262
4081
  }
3263
4082
  if (report === null) {
3264
- return (0, primitives_11.h)('div', { style: primitives_11.styles.root }, header, progressBar);
4083
+ return (0, primitives_13.h)('div', { style: primitives_13.styles.root }, header, progressBar);
3265
4084
  }
3266
4085
  // Defaults are resolved before the no-index branch: a scan-only report still has
3267
4086
  // totals, a cap and languages even though it has no graph. Building them only for
3268
4087
  // an indexed report is what made "no index" mean "nothing to show".
3269
- const totals = report.totals ?? format_10.ZERO_TOTALS;
3270
- const cap = report.cap ?? format_10.ZERO_CAP;
3271
- const analyzer = report.analyzer ?? format_10.ZERO_ANALYZER;
4088
+ const totals = report.totals ?? format_11.ZERO_TOTALS;
4089
+ const cap = report.cap ?? format_11.ZERO_CAP;
4090
+ const analyzer = report.analyzer ?? format_11.ZERO_ANALYZER;
3272
4091
  const largeFiles = report.largeFiles ?? [];
3273
4092
  const languages = report.languages ?? null;
3274
- const { notable, context: contextWarnings } = (0, format_10.splitWarnings)(report.warnings);
3275
- const ratio = (0, format_10.coverageRatio)(report);
3276
- // The header names the window the timeline is showing; the selection itself lives in
3277
- // the section, since the rows below are now the only entry point.
3278
- const [activityDays, setActivityDays] = React.useState(7);
4093
+ const { notable, context: contextWarnings } = (0, format_11.splitWarnings)(report.warnings);
4094
+ const ratio = (0, format_11.coverageRatio)(report);
4095
+ // Multi-project workspaces get one aggregate card before the project-scoped ones.
4096
+ // It is null for a single project, so the common case is unchanged.
4097
+ const workspaceSection = (0, primitives_13.h)(workspace_1.WorkspaceOverview, {
4098
+ projects: projects ?? [],
4099
+ activePath: projectPath,
4100
+ onSelectProject,
4101
+ });
3279
4102
  // No index yet. This is the state the initialize command exists for, but it is
3280
4103
  // also a state in which the working tree is still readable: file counts, bytes,
3281
4104
  // the cap and the language split do not need GitNexus. The panel used to show
3282
4105
  // only "没有索引" and a command, which made an otherwise healthy repository look
3283
4106
  // empty. The scan is the "at least something to look at" floor.
3284
4107
  if (report.repo?.indexed !== true) {
3285
- return (0, primitives_11.h)('div', { style: primitives_11.styles.root }, (0, primitives_11.h)('div', { style: { gridColumn: '1 / -1', display: 'flex', flexDirection: 'column', gap: '6px' } }, header), progressBar, (0, primitives_11.h)(install_1.ToolchainNotice, {
4108
+ return (0, primitives_13.h)('div', { style: primitives_13.styles.root }, (0, primitives_13.h)('div', { style: { gridColumn: '1 / -1', display: 'flex', flexDirection: 'column', gap: '6px' } }, header), progressBar, (0, primitives_13.h)(install_1.ToolchainNotice, {
3286
4109
  toolchain: report.toolchain,
3287
4110
  run: installRun,
3288
4111
  runError: installError,
3289
4112
  onInstall,
3290
4113
  onCancelInstall,
3291
- }), (0, primitives_11.h)('div', { style: { gridColumn: '1 / -1', display: 'flex', gap: '8px', alignItems: 'flex-start' } }, (0, primitives_11.h)(primitives_11.StateDot, { state: 'idle' }), (0, primitives_11.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 } }, (0, primitives_11.h)('span', null, report.repo?.hasGit === false
4114
+ }), (0, primitives_13.h)('div', { style: { gridColumn: '1 / -1', display: 'flex', gap: '8px', alignItems: 'flex-start' } }, (0, primitives_13.h)(primitives_13.StateDot, { state: 'idle' }), (0, primitives_13.h)('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 } }, (0, primitives_13.h)('span', null, report.repo?.hasGit === false
3292
4115
  ? '该目录不是 git 仓库,也没有 GitNexus 索引。下面是工作区扫描结果;仍可建立索引(--skip-git),但没有提交基线与增量刷新,也不会有 Git 活动。'
3293
4116
  : '该项目还没有 GitNexus 索引。下面是工作区扫描结果;初始化后可看到图谱、查询能力与 Git 活动对账。'), Array.isArray(projects)
3294
- ? (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '11px' } }, projectsError !== null && projectsError !== undefined
4117
+ ? (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '11px' } }, projectsError !== null && projectsError !== undefined
3295
4118
  ? `项目列表读取失败:${projectsError};已按工作区描述。`
3296
4119
  : indexedProjectCount === 0 && gitProjectCount > 0
3297
4120
  ? `已在 ${projectsVisited ?? 0} 个目录中查找;工作区内有 ${gitProjectCount} 个 git 项目尚未建索引,可在标题栏切换后初始化。`
3298
4121
  : indexedProjectCount === 0
3299
4122
  ? `已在 ${projectsVisited ?? 0} 个目录中查找,没有找到 GitNexus 索引或 git 仓库。`
3300
4123
  : null)
3301
- : null)), (0, primitives_11.h)(primitives_11.Section, { wide: true, title: '扫描概览 · 尚未索引' }, (0, primitives_11.h)(primitives_11.MetricGrid, {
4124
+ : null)), (0, primitives_13.h)(primitives_13.Section, { wide: true, title: '扫描概览 · 尚未索引' }, (0, primitives_13.h)(primitives_13.MetricGrid, {
3302
4125
  items: [
3303
- { label: '扫描文件', value: (0, format_10.compactNumber)(totals.scanned) },
3304
- { label: '选中文件', value: (0, format_10.compactNumber)(totals.selected) },
3305
- { label: '扫描字节', value: (0, format_10.fileSize)(totals.scannedBytes ?? 0) },
3306
- { label: '大小上限', value: cap.text, hint: (0, format_10.capSourceLabel)(cap.source) },
4126
+ { label: '扫描文件', value: (0, format_11.compactNumber)(totals.scanned) },
4127
+ { label: '选中文件', value: (0, format_11.compactNumber)(totals.selected) },
4128
+ { label: '扫描字节', value: (0, format_11.fileSize)(totals.scannedBytes ?? 0) },
4129
+ { label: '大小上限', value: cap.text, hint: (0, format_11.capSourceLabel)(cap.source) },
3307
4130
  ],
3308
- }), (0, primitives_11.h)('div', { style: { ...primitives_11.MUTED, marginTop: '6px' } }, '这里的数字来自工作区,不是索引;索引里有什么,只有建立后才能回答。')), languages !== null && languages.rows.length > 0 ? (0, primitives_11.h)(languages_1.MemoizedLanguageSection, { languages }) : null, maintenanceSection !== null
3309
- ? (0, primitives_11.h)(primitives_11.Section, {
4131
+ }), (0, primitives_13.h)('div', { style: { ...primitives_13.MUTED, marginTop: '6px' } }, '这里的数字来自工作区,不是索引;索引里有什么,只有建立后才能回答。')), languages !== null && languages.rows.length > 0 ? (0, primitives_13.h)(languages_1.MemoizedLanguageSection, { languages }) : null, maintenanceSection !== null
4132
+ ? (0, primitives_13.h)(primitives_13.Section, {
3310
4133
  wide: true,
3311
4134
  title: '初始化索引',
3312
- meta: (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '11px' } }, maintenanceSection.supportsForce === true
4135
+ meta: (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '11px' } }, maintenanceSection.supportsForce === true
3313
4136
  ? '均可直接执行,也可复制到会话(沙箱与提权由 DSH 处理)'
3314
4137
  : '重启 DSH 后可执行;当前仅可复制(沙箱与提权由 DSH 处理)'),
3315
- }, (0, primitives_11.h)(maintenance_1.MaintenanceSection, { maintenance: maintenanceSection, projectPath, indexed, run, runError, delta, deltaError, deltaRoute, onStartRun, onCancelRun }))
4138
+ }, (0, primitives_13.h)(maintenance_1.MaintenanceSection, { maintenance: maintenanceSection, projectPath, indexed, run, runError, delta, deltaError, deltaRoute, onStartRun, onCancelRun }))
3316
4139
  : null);
3317
4140
  }
3318
- return (0, primitives_11.h)('div', { style: primitives_11.styles.root },
4141
+ return (0, primitives_13.h)('div', { style: primitives_13.styles.root },
3319
4142
  // The header and the two status lines span everything: they describe the whole
3320
4143
  // panel, so giving them a column would make them look like one card's heading.
3321
- (0, primitives_11.h)('div', { style: { gridColumn: '1 / -1', display: 'flex', flexDirection: 'column', gap: '6px' } }, header), progressBar,
4144
+ (0, primitives_13.h)('div', { style: { gridColumn: '1 / -1', display: 'flex', flexDirection: 'column', gap: '6px' } }, header), progressBar,
3322
4145
  // Immediately after the header, before any index content. If GitNexus is missing,
3323
4146
  // that single fact explains everything the panel is about to show or fail to show,
3324
4147
  // so it belongs above the numbers rather than beside them.
3325
- (0, primitives_11.h)(install_1.ToolchainNotice, {
4148
+ (0, primitives_13.h)(install_1.ToolchainNotice, {
3326
4149
  toolchain: report.toolchain,
3327
4150
  run: installRun,
3328
4151
  runError: installError,
3329
4152
  onInstall,
3330
4153
  onCancelInstall,
3331
- }), (0, primitives_11.h)('div', { style: { gridColumn: '1 / -1', display: 'flex', flexDirection: 'column', gap: '3px' } }, (0, primitives_11.h)('div', { style: primitives_11.MUTED }, `${report.repo.indexedAt ?? '未知时间'} · ${report.repo.branch ?? 'unknown'}@${String(report.repo.lastCommit ?? '').slice(0, 8)}` +
4154
+ }), workspaceSection, (0, primitives_13.h)('div', { style: { gridColumn: '1 / -1', display: 'flex', flexDirection: 'column', gap: '3px' } }, (0, primitives_13.h)('div', { style: primitives_13.MUTED }, `${report.repo.indexedAt ?? '未知时间'} · ${report.repo.branch ?? 'unknown'}@${String(report.repo.lastCommit ?? '').slice(0, 8)}` +
3332
4155
  ` · manifest ${report.repo.metaFile ?? '?'} · GitNexus ${analyzer.gitnexusVersion}` +
3333
4156
  // The read time is the only thing on screen a refresh can change when the
3334
4157
  // repository has not moved, so without it the control reads as broken.
@@ -3338,12 +4161,12 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3338
4161
  report.staleness === undefined
3339
4162
  ? null
3340
4163
  : (() => {
3341
- const s = (0, format_10.stalenessView)(report.staleness);
3342
- return (0, primitives_11.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_11.h)(primitives_11.StateDot, { state: s.state }), (0, primitives_11.h)('span', null, s.text));
4164
+ const s = (0, format_11.stalenessView)(report.staleness);
4165
+ return (0, primitives_13.h)('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, (0, primitives_13.h)(primitives_13.StateDot, { state: s.state }), (0, primitives_13.h)('span', null, s.text));
3343
4166
  })()),
3344
4167
  // Search sits above the report cards: it is the action a reader reaches for
3345
4168
  // before reading, not an annotation on the numbers below it.
3346
- search === undefined ? null : (0, primitives_11.h)(search_1.SearchSection, { search }),
4169
+ search === undefined ? null : (0, primitives_13.h)(search_1.SearchSection, { search }),
3347
4170
  // One card for the whole "what is this index" question — file coverage and
3348
4171
  // graph contents together.
3349
4172
  //
@@ -3355,7 +4178,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3355
4178
  // stacked on the right. The right column stretches to the overview's height, so the
3356
4179
  // quality card absorbs the difference instead of leaving a gap under it; a narrow
3357
4180
  // pane collapses the three into one column.
3358
- (0, primitives_11.h)('div', {
4181
+ (0, primitives_13.h)('div', {
3359
4182
  style: {
3360
4183
  gridColumn: '1 / -1',
3361
4184
  display: 'grid',
@@ -3363,14 +4186,14 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3363
4186
  gap: '8px',
3364
4187
  alignItems: 'stretch',
3365
4188
  },
3366
- }, (0, primitives_11.h)(primitives_11.Section, {
4189
+ }, (0, primitives_13.h)(primitives_13.Section, {
3367
4190
  // The left column of the split region: the one card that answers "what is in
3368
4191
  // this index". The right column stacks the two compact readings beside it.
3369
4192
  title: '索引概览',
3370
4193
  meta: report.graph === null || report.graph === undefined
3371
4194
  ? null
3372
- : (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '12px' } }, `${(0, format_10.fixed)(report.graph.edgesPerNode, 2)} 边/符号`),
3373
- }, (0, primitives_11.h)(overview_1.MemoizedOverviewBody, { report, totals, cap, ratio })), (0, primitives_11.h)('div',
4195
+ : (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '12px' } }, `${(0, format_11.fixed)(report.graph.edgesPerNode, 2)} 边/符号`),
4196
+ }, (0, primitives_13.h)(overview_1.MemoizedOverviewBody, { report, totals, cap, ratio })), (0, primitives_13.h)('div',
3374
4197
  // The column stretches to the overview's height; the quality card below takes
3375
4198
  // whatever the language card leaves, so the right side ends level with the left.
3376
4199
  { style: { display: 'flex', flexDirection: 'column', gap: '8px', minHeight: 0 } },
@@ -3379,36 +4202,39 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3379
4202
  // repository" answers side by side. The grid gives them one row on a wide pane and
3380
4203
  // one column in a narrow sidebar, which is the platform's own behaviour.
3381
4204
  languages !== null && languages.rows.length > 0
3382
- ? (0, primitives_11.h)('div', { style: { flex: '0 0 auto', minWidth: 0 } }, (0, primitives_11.h)(languages_1.MemoizedLanguageSection, { languages }))
4205
+ ? (0, primitives_13.h)('div', { style: { flex: '0 0 auto', minWidth: 0 } }, (0, primitives_13.h)(languages_1.MemoizedLanguageSection, { languages }))
3383
4206
  : null,
3384
4207
  // Fills the rest of the column: at the overview's height it ends level with the left
3385
4208
  // card, and its body scrolls rather than growing past it.
3386
- (0, primitives_11.h)(primitives_11.Section, {
4209
+ (0, primitives_13.h)(primitives_13.Section, {
3387
4210
  title: '解析质量与溯源',
3388
4211
  style: { flex: '1 1 auto', minHeight: 0 },
3389
4212
  bodyStyle: { maxHeight: 'none', flex: '1 1 auto', minHeight: 0 },
3390
4213
  scroll: true,
3391
- meta: (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '11px' } }, (0, capabilities_2.provenanceCollapsed)(report.graphQuality ?? null)),
3392
- }, (0, primitives_11.h)(capabilities_2.ProvenanceBlock, {
4214
+ meta: (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '11px' } }, (0, capabilities_2.provenanceCollapsed)(report.graphQuality ?? null)),
4215
+ }, (0, primitives_13.h)(capabilities_2.ProvenanceBlock, {
3393
4216
  quality: report.graphQuality ?? null,
3394
4217
  provenance: report.provenance ?? null,
4218
+ compatibility: report.compatibility ?? null,
3395
4219
  })))),
3396
4220
  // The graph is the index's own shape, not a derived statistic: it belongs directly
3397
4221
  // under the overview, before the caveats that explain its omissions.
3398
4222
  graph === undefined || !indexed
3399
4223
  ? null
3400
- : (0, primitives_11.h)(primitives_11.Section, {
4224
+ : (0, primitives_13.h)('div', { ref: graphCardRef, style: { gridColumn: '1 / -1', minWidth: 0 } }, (0, primitives_13.h)(primitives_13.Section, {
3401
4225
  wide: true,
3402
4226
  title: '图视图',
3403
- meta: (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '11px' } }, graph.projection === null
4227
+ meta: (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '11px' } }, graph.projection === null
3404
4228
  ? graph.routeState === 'missing'
3405
4229
  ? '宿主未挂载图路由'
3406
4230
  : graph.loading
3407
4231
  ? '读取中…'
3408
- : '暂无图数据'
4232
+ : graphVisible
4233
+ ? '暂无图数据'
4234
+ : '滚动到此处加载'
3409
4235
  : `${graph.projection.view === 'communities' ? '社区聚合' : '单个社区'} · ` +
3410
4236
  `上限 ${graph.projection.budget.nodes} 节点 / ${graph.projection.budget.edges} 边`),
3411
- }, (0, primitives_11.h)(graph_1.GraphSection, { state: graph })),
4237
+ }, (0, primitives_13.h)(graph_1.GraphSection, { state: graph }))),
3412
4238
  // Caveats, in one card, sharing a row with the provenance card.
3413
4239
  //
3414
4240
  // `notable` and `contextWarnings` were two cards — one amber, one an untitled
@@ -3417,13 +4243,13 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3417
4243
  // check two boxes to answer one question, and the untitled one had no heading
3418
4244
  // to say what it was.
3419
4245
  notable.length > 0 || contextWarnings.length > 0
3420
- ? (0, primitives_11.h)(primitives_11.Section, {
4246
+ ? (0, primitives_13.h)(primitives_13.Section, {
3421
4247
  // Full width: a list of warnings would otherwise pair with whatever card
3422
4248
  // happened to follow and leave a hole in the other column.
3423
4249
  wide: true,
3424
4250
  title: `需要注意 · ${notable.length + contextWarnings.length}`,
3425
4251
  scroll: 300,
3426
- }, notable.map((warning, index) => (0, primitives_11.h)('div', {
4252
+ }, notable.map((warning, index) => (0, primitives_13.h)('div', {
3427
4253
  key: `${warning.code}-${index}`,
3428
4254
  style: {
3429
4255
  display: 'flex',
@@ -3434,45 +4260,45 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3434
4260
  background: 'color-mix(in srgb, #d9a441 9%, transparent)',
3435
4261
  borderRadius: '6px',
3436
4262
  },
3437
- }, (0, primitives_11.h)(primitives_11.StateDot, { state: 'warning' }), (0, primitives_11.h)('span', { style: { minWidth: 0 } }, warning.message))),
4263
+ }, (0, primitives_13.h)(primitives_13.StateDot, { state: 'warning' }), (0, primitives_13.h)('span', { style: { minWidth: 0 } }, warning.message))),
3438
4264
  // The milder ones stay quiet rows: giving every item an amber box would
3439
4265
  // make a stale-index count look as urgent as a truncated activity window.
3440
4266
  contextWarnings.length === 0
3441
4267
  ? null
3442
- : (0, primitives_11.h)('div', {
4268
+ : (0, primitives_13.h)('div', {
3443
4269
  style: {
3444
4270
  display: 'flex',
3445
4271
  flexDirection: 'column',
3446
4272
  gap: '4px',
3447
- ...primitives_11.MUTED,
3448
- ...(notable.length > 0 ? { borderTop: primitives_11.HAIRLINE, paddingTop: '8px' } : {}),
4273
+ ...primitives_13.MUTED,
4274
+ ...(notable.length > 0 ? { borderTop: primitives_13.HAIRLINE, paddingTop: '8px' } : {}),
3449
4275
  },
3450
- }, contextWarnings.map((warning, index) => (0, primitives_11.h)('div', { key: `${warning.code}-${index}` }, `· ${warning.message}`))))
4276
+ }, contextWarnings.map((warning, index) => (0, primitives_13.h)('div', { key: `${warning.code}-${index}` }, `· ${warning.message}`))))
3451
4277
  : null,
3452
4278
  // Wide: file paths must keep their tails.
3453
4279
  largeFiles.length > 0
3454
- ? (0, primitives_11.h)(primitives_11.Section, {
4280
+ ? (0, primitives_13.h)(primitives_13.Section, {
3455
4281
  wide: true,
3456
- title: `被跳过的超大文件 · ${largeFiles.length} 个 · ${(0, format_10.fileSize)(totals.largeBytes)}`,
4282
+ title: `被跳过的超大文件 · ${largeFiles.length} 个 · ${(0, format_11.fileSize)(totals.largeBytes)}`,
3457
4283
  // A list of up to 40 paths would push every card below it off screen; the
3458
4284
  // platform's own panels cap long lists and scroll inside the card.
3459
4285
  scroll: 280,
3460
- }, (0, primitives_11.h)(large_files_1.LargeFileList, { files: largeFiles.slice(0, 40) }), largeFiles.length > 40
3461
- ? (0, primitives_11.h)('div', { style: { ...primitives_11.MUTED, marginTop: '6px' } }, `…另有 ${largeFiles.length - 40} 个未显示`)
4286
+ }, (0, primitives_13.h)(large_files_1.LargeFileList, { files: largeFiles.slice(0, 40) }), largeFiles.length > 40
4287
+ ? (0, primitives_13.h)('div', { style: { ...primitives_13.MUTED, marginTop: '6px' } }, `…另有 ${largeFiles.length - 40} 个未显示`)
3462
4288
  : null)
3463
4289
  : null, report.activity === undefined || report.activity === null
3464
4290
  ? null
3465
- : (0, primitives_11.h)(primitives_11.Section, {
4291
+ : (0, primitives_13.h)(primitives_13.Section, {
3466
4292
  wide: true,
3467
4293
  // The header names the window the reader is actually looking at (the
3468
4294
  // timeline's 7-day default), not the 90-day fetch window — the two used to
3469
4295
  // disagree, so the card read "近 90 天" above a list of seven days.
3470
4296
  title: `Git 活动 · 近 ${activityDays} 天`,
3471
- meta: (0, primitives_11.h)('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '6px', ...primitives_11.MUTED, fontSize: '11px' } },
4297
+ meta: (0, primitives_13.h)('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '6px', ...primitives_13.MUTED, fontSize: '11px' } },
3472
4298
  // The range control belongs to the card, not to the folded timeline: it
3473
4299
  // scopes every number on the card, and it should be reachable without
3474
4300
  // opening the list.
3475
- (0, primitives_11.h)('select', {
4301
+ (0, primitives_13.h)('select', {
3476
4302
  value: String(activityDays),
3477
4303
  'aria-label': 'Git 活动窗口',
3478
4304
  onChange: (event) => setActivityDays(Number(event.target.value)),
@@ -3480,17 +4306,17 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3480
4306
  font: 'inherit',
3481
4307
  fontSize: '11px',
3482
4308
  padding: '1px 4px',
3483
- border: primitives_11.HAIRLINE,
4309
+ border: primitives_13.HAIRLINE,
3484
4310
  borderRadius: '4px',
3485
- background: primitives_11.SOFT_BG,
4311
+ background: primitives_13.SOFT_BG,
3486
4312
  color: 'inherit',
3487
4313
  },
3488
- }, [7, 14, 30].map((days) => (0, primitives_11.h)('option', { key: days, value: String(days) }, `${days} 天`))), (0, primitives_11.h)(primitives_11.StateDot, {
4314
+ }, [7, 14, 30].map((days) => (0, primitives_13.h)('option', { key: days, value: String(days) }, `${days} 天`))), (0, primitives_13.h)(primitives_13.StateDot, {
3489
4315
  state: (0, activity_1.activityView)(report.activity)?.verdicts.find((v) => v.key === 'missing')?.tone ?? 'done',
3490
4316
  }), (0, activity_1.activityCollapsed)(report.activity)),
3491
4317
  // Timeline + cross-tab + baseline is the longest card in the panel.
3492
4318
  scroll: 560,
3493
- }, (0, primitives_11.h)(activity_1.MemoizedGitActivitySection, {
4319
+ }, (0, primitives_13.h)(activity_1.MemoizedGitActivitySection, {
3494
4320
  activity: report.activity,
3495
4321
  projectPath,
3496
4322
  days: activityDays,
@@ -3498,16 +4324,16 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3498
4324
  })),
3499
4325
  // Wide, and always open: the commands are the action this panel offers.
3500
4326
  maintenanceSection !== null
3501
- ? (0, primitives_11.h)(primitives_11.Section, {
4327
+ ? (0, primitives_13.h)(primitives_13.Section, {
3502
4328
  wide: true,
3503
4329
  title: '维护命令',
3504
- meta: (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '12px' } }, `${maintenanceSection.commands?.length ?? 0} 条 · ` +
4330
+ meta: (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '12px' } }, `${maintenanceSection.commands?.length ?? 0} 条 · ` +
3505
4331
  (maintenanceSection.supportsForce === true
3506
4332
  ? '均可直接执行,也可复制到会话'
3507
4333
  : '重启 DSH 后可执行;当前仅可复制') +
3508
4334
  '(沙箱与提权由 DSH 处理)'),
3509
4335
  scroll: 360,
3510
- }, (0, primitives_11.h)(maintenance_1.MaintenanceSection, { maintenance: maintenanceSection, projectPath, indexed, run, runError, delta, deltaError, deltaRoute, onStartRun, onCancelRun }))
4336
+ }, (0, primitives_13.h)(maintenance_1.MaintenanceSection, { maintenance: maintenanceSection, projectPath, indexed, run, runError, delta, deltaError, deltaRoute, onStartRun, onCancelRun }))
3511
4337
  : null);
3512
4338
  }
3513
4339
  /**
@@ -3604,7 +4430,11 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3604
4430
  */
3605
4431
  const [graphRoute, setGraphRoute] = React.useState('unknown');
3606
4432
  const [graphScope, setGraphScope] = React.useState(null);
4433
+ const [graphMode, setGraphMode] = React.useState('communities');
3607
4434
  const [graphReload, setGraphReload] = React.useState(0);
4435
+ /** Set once the graph card has entered the viewport; keeps the first fetch lazy. */
4436
+ const [graphVisible, setGraphVisible] = React.useState(false);
4437
+ const onGraphNear = React.useCallback(() => setGraphVisible(true), []);
3608
4438
  /** Which project the current projection belongs to, so a switch can clear it. */
3609
4439
  const graphRepoRef = React.useRef(null);
3610
4440
  const running = run?.state === 'running' && runProject === projectPath;
@@ -4179,6 +5009,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4179
5009
  setGraphError(null);
4180
5010
  setGraphLoading(false);
4181
5011
  setGraphScope(null);
5012
+ setGraphMode('communities');
4182
5013
  return undefined;
4183
5014
  }
4184
5015
  const projectChanged = graphRepoRef.current !== projectPath;
@@ -4190,6 +5021,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4190
5021
  // next render fetch the overview instead of asking the new project for an id it
4191
5022
  // cannot have.
4192
5023
  setGraphScope(null);
5024
+ setGraphMode('communities');
4193
5025
  }
4194
5026
  if (!graphAvailable) {
4195
5027
  setGraphProjection(null);
@@ -4197,6 +5029,10 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4197
5029
  setGraphLoading(false);
4198
5030
  return undefined;
4199
5031
  }
5032
+ // The visibility effect flips this once the card is near the viewport; until
5033
+ // then the card keeps its empty state instead of issuing a request.
5034
+ if (!graphVisible)
5035
+ return undefined;
4200
5036
  if (projectChanged && graphScope !== null) {
4201
5037
  // The setGraphScope(null) above re-runs this effect; do not also fire a request
4202
5038
  // for the old scope against the new project.
@@ -4206,7 +5042,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4206
5042
  setGraphLoading(true);
4207
5043
  setGraphError(null);
4208
5044
  const effectiveScope = projectChanged ? null : graphScope;
4209
- const view = effectiveScope === null ? 'communities' : 'community';
5045
+ const view = effectiveScope === null ? graphMode : 'community';
4210
5046
  const suffix = effectiveScope === null ? '' : `&id=${encodeURIComponent(effectiveScope)}`;
4211
5047
  void (async () => {
4212
5048
  try {
@@ -4245,7 +5081,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4245
5081
  }
4246
5082
  })();
4247
5083
  return () => controller.abort();
4248
- }, [projectPath, reloadToken, graphScope, graphReload, graphAvailable]);
5084
+ }, [projectPath, reloadToken, graphScope, graphMode, graphReload, graphAvailable, graphVisible]);
4249
5085
  /**
4250
5086
  * Open a symbol node, with the same fallbacks the search hits use.
4251
5087
  *
@@ -4293,7 +5129,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4293
5129
  }, [workspacePath]);
4294
5130
  const visibleRun = runProject === projectPath ? run : null;
4295
5131
  const visibleRunError = runProject === projectPath ? runError : null;
4296
- return (0, primitives_11.h)(CoverageView, {
5132
+ return (0, primitives_13.h)(CoverageView, {
4297
5133
  report,
4298
5134
  maintenance,
4299
5135
  projectPath,
@@ -4356,17 +5192,71 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4356
5192
  error: graphError,
4357
5193
  routeState: graphRoute,
4358
5194
  scope: graphScope,
4359
- onOpenCommunity: (id) => setGraphScope(id),
4360
- onBack: () => setGraphScope(null),
5195
+ onOpenCommunity: (id) => {
5196
+ setGraphScope(id);
5197
+ setGraphMode('communities');
5198
+ },
5199
+ onOpenSymbols: () => {
5200
+ setGraphScope(null);
5201
+ setGraphMode('symbols');
5202
+ },
5203
+ onBack: () => {
5204
+ setGraphScope(null);
5205
+ setGraphMode('communities');
5206
+ },
4361
5207
  onReload: () => setGraphReload((n) => n + 1),
4362
5208
  onOpenSymbol: openGraphNode,
4363
5209
  },
5210
+ onGraphNear,
5211
+ graphVisible,
4364
5212
  });
4365
5213
  }
4366
5214
  /** The tab chip's live title. */
4367
5215
  function CoverageTitle() {
4368
- return (0, primitives_11.h)('span', null, 'GitNexus');
5216
+ return (0, primitives_13.h)('span', null, 'GitNexus');
5217
+ }
5218
+ });
5219
+ define("views/boundary", ["require", "exports", "react", "primitives"], function (require, exports, React, primitives_14) {
5220
+ "use strict";
5221
+ Object.defineProperty(exports, "__esModule", { value: true });
5222
+ exports.PanelBoundary = void 0;
5223
+ /**
5224
+ * Catch a render failure inside the sidebar panel.
5225
+ *
5226
+ * A thrown component currently takes the whole right pane down to a black
5227
+ * rectangle, which is indistinguishable from "the plugin did not load". This
5228
+ * boundary keeps the pane usable and puts the actual error on screen so the
5229
+ * failure can be reported instead of guessed at.
5230
+ */
5231
+ class PanelBoundary extends React.Component {
5232
+ state = { error: null, componentStack: '' };
5233
+ static getDerivedStateFromError(error) {
5234
+ return { error, componentStack: '' };
5235
+ }
5236
+ componentDidCatch(error, info) {
5237
+ // Also leave a trail for a terminal/journal reader when the panel is embedded.
5238
+ // eslint-disable-next-line no-console
5239
+ console.error('gitnexus panel render failed', error, info.componentStack);
5240
+ this.setState({ componentStack: info.componentStack ?? '' });
5241
+ }
5242
+ render() {
5243
+ const { error, componentStack } = this.state;
5244
+ if (error === null)
5245
+ return this.props.children;
5246
+ return (0, primitives_14.h)('div', {
5247
+ style: {
5248
+ padding: '12px',
5249
+ color: 'var(--dsw-alias-state-error-primary, #f38ba8)',
5250
+ fontFamily: 'ui-monospace, SFMono-Regular, Consolas, monospace',
5251
+ fontSize: '11px',
5252
+ lineHeight: 1.5,
5253
+ whiteSpace: 'pre-wrap',
5254
+ overflow: 'auto',
5255
+ },
5256
+ }, (0, primitives_14.h)('div', { style: { fontWeight: 700, marginBottom: '6px' } }, 'GitNexus 面板渲染失败'), (0, primitives_14.h)('div', { style: primitives_14.MUTED }, error.message), componentStack === '' ? null : (0, primitives_14.h)('div', { style: { ...primitives_14.MUTED, marginTop: '8px' } }, componentStack), error.stack === undefined ? null : (0, primitives_14.h)('div', { style: { ...primitives_14.MUTED, marginTop: '8px' } }, error.stack));
5257
+ }
4369
5258
  }
5259
+ exports.PanelBoundary = PanelBoundary;
4370
5260
  });
4371
5261
  /**
4372
5262
  * dsh-gitnexus-plus — browser half.
@@ -4407,7 +5297,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4407
5297
  * index actually represent?", the warning block says what was dropped, and the
4408
5298
  * file list says whether what was dropped is source or build output.
4409
5299
  */
4410
- define("index", ["require", "exports", "format", "navigation", "primitives", "sections/activity", "highlight", "sections/graph", "sections/capabilities", "sections/file-history", "sections/languages", "sections/large-files", "sections/maintenance", "views/panel"], function (require, exports, format_11, navigation_4, primitives_12, activity_2, highlight_2, graph_2, capabilities_3, file_history_2, languages_2, large_files_2, maintenance_2, panel_1) {
5300
+ define("index", ["require", "exports", "format", "react", "navigation", "primitives", "sections/activity", "highlight", "sections/graph", "sections/graph3d", "sections/capabilities", "sections/file-history", "sections/languages", "sections/large-files", "sections/maintenance", "sections/workspace", "views/panel", "views/boundary"], function (require, exports, format_12, React, navigation_4, primitives_15, activity_2, highlight_2, graph_2, graph3d_2, capabilities_3, file_history_2, languages_2, large_files_2, maintenance_2, workspace_2, panel_1, boundary_1) {
4411
5301
  "use strict";
4412
5302
  Object.defineProperty(exports, "__esModule", { value: true });
4413
5303
  exports.__internals = exports.inject = void 0;
@@ -4472,8 +5362,26 @@ define("index", ["require", "exports", "format", "navigation", "primitives", "se
4472
5362
  (0, navigation_4.bindSidebarRight)(svcCtx.sidebarRight);
4473
5363
  });
4474
5364
  });
5365
+ /**
5366
+ * Wrapping the whole panel keeps a render failure inside the pane: without it
5367
+ * React unmounts the subtree and the sidebar is simply black, which is
5368
+ * indistinguishable from the plugin never loading.
5369
+ */
5370
+ const requiredComponents = [
5371
+ ['PanelBoundary', boundary_1.PanelBoundary],
5372
+ ['CoveragePanel', panel_1.CoveragePanel],
5373
+ ['WorkspaceOverview', workspace_2.WorkspaceOverview],
5374
+ ['Graph3DView', graph3d_2.Graph3DView],
5375
+ ['ProvenanceBlock', capabilities_3.ProvenanceBlock],
5376
+ ];
5377
+ const missingComponents = requiredComponents
5378
+ .filter(([, component]) => typeof component !== 'function')
5379
+ .map(([name]) => name);
5380
+ const CoveragePanelWithBoundary = (props) => missingComponents.length === 0
5381
+ ? React.createElement(boundary_1.PanelBoundary, null, React.createElement(panel_1.CoveragePanel, props))
5382
+ : React.createElement('div', { style: { padding: 12, color: '#fff', background: '#3a1111' } }, 'GitNexus 缺少组件: ' + missingComponents.join(', '));
4475
5383
  safe('sidebar.right.pane.tab', () => {
4476
- ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: PLUGIN_ID }, panel_1.CoveragePanel));
5384
+ ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: PLUGIN_ID }, CoveragePanelWithBoundary));
4477
5385
  });
4478
5386
  safe('sidebar.right.pane.tab.title', () => {
4479
5387
  ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: PLUGIN_ID }, panel_1.CoverageTitle));
@@ -4485,22 +5393,25 @@ define("index", ["require", "exports", "format", "navigation", "primitives", "se
4485
5393
  TAB_KIND,
4486
5394
  routeUrl: navigation_4.routeUrl,
4487
5395
  fileResourceAddress: navigation_4.fileResourceAddress,
4488
- formatBytes: format_11.formatBytes,
4489
- fileSize: format_11.fileSize,
4490
- percent: format_11.percent,
4491
- stackSegments: format_11.stackSegments,
4492
- splitWarnings: format_11.splitWarnings,
4493
- summaryLine: format_11.summaryLine,
4494
- coverageRatio: format_11.coverageRatio,
4495
- healthState: format_11.healthState,
4496
- stalenessView: format_11.stalenessView,
4497
- capabilityRows: format_11.capabilityRows,
4498
- graphQualitySummary: format_11.graphQualitySummary,
5396
+ formatBytes: format_12.formatBytes,
5397
+ fileSize: format_12.fileSize,
5398
+ percent: format_12.percent,
5399
+ stackSegments: format_12.stackSegments,
5400
+ splitWarnings: format_12.splitWarnings,
5401
+ summaryLine: format_12.summaryLine,
5402
+ coverageRatio: format_12.coverageRatio,
5403
+ healthState: format_12.healthState,
5404
+ stalenessView: format_12.stalenessView,
5405
+ capabilityRows: format_12.capabilityRows,
5406
+ graphQualitySummary: format_12.graphQualitySummary,
4499
5407
  activityView: activity_2.activityView,
4500
5408
  highlightLine: highlight_2.highlightLine,
4501
5409
  languageOf: highlight_2.languageOf,
4502
5410
  communityColors: graph_2.communityColors,
4503
- compactNumber: format_11.compactNumber,
5411
+ buildGraph3DLayout: graph3d_2.buildGraph3DLayout,
5412
+ graph3DPath: graph3d_2.graph3DPath,
5413
+ Graph3DView: graph3d_2.Graph3DView,
5414
+ compactNumber: format_12.compactNumber,
4504
5415
  useProjectPath: navigation_4.useProjectPath,
4505
5416
  contextDiagnostic: navigation_4.contextDiagnostic,
4506
5417
  chooseProject: panel_1.chooseProject,
@@ -4510,17 +5421,19 @@ define("index", ["require", "exports", "format", "navigation", "primitives", "se
4510
5421
  LanguageBar: languages_2.LanguageBar,
4511
5422
  LanguageSection: languages_2.LanguageSection,
4512
5423
  LargeFileList: large_files_2.LargeFileList,
4513
- Ring: primitives_12.Ring,
4514
- MetricGrid: primitives_12.MetricGrid,
4515
- Histogram: primitives_12.Histogram,
5424
+ Ring: primitives_15.Ring,
5425
+ MetricGrid: primitives_15.MetricGrid,
5426
+ Histogram: primitives_15.Histogram,
4516
5427
  GitActivitySection: activity_2.GitActivitySection,
4517
5428
  FileHistorySection: file_history_2.FileHistorySection,
4518
5429
  ProvenanceBlock: capabilities_3.ProvenanceBlock,
4519
5430
  MaintenanceSection: maintenance_2.MaintenanceSection,
5431
+ WorkspaceOverview: workspace_2.WorkspaceOverview,
5432
+ aggregateProjects: workspace_2.aggregateProjects,
4520
5433
  CapabilityStrip: capabilities_3.CapabilityStrip,
4521
- Section: primitives_12.Section,
4522
- classifyFileType: primitives_12.classifyFileType,
4523
- IconRefreshOutlineRegular: primitives_12.IconRefreshOutlineRegular,
5434
+ Section: primitives_15.Section,
5435
+ classifyFileType: primitives_15.classifyFileType,
5436
+ IconRefreshOutlineRegular: primitives_15.IconRefreshOutlineRegular,
4524
5437
  };
4525
5438
  });
4526
5439