dsh-gitnexus-plus 0.1.4 → 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.
Files changed (65) hide show
  1. package/README-CN.md +18 -7
  2. package/README.md +71 -12
  3. package/lib/client.js +1584 -351
  4. package/lib/gitnexus/analyze.d.ts +6 -0
  5. package/lib/gitnexus/analyze.d.ts.map +1 -1
  6. package/lib/gitnexus/analyze.js +10 -0
  7. package/lib/gitnexus/analyze.js.map +1 -1
  8. package/lib/gitnexus/commands.d.ts +11 -1
  9. package/lib/gitnexus/commands.d.ts.map +1 -1
  10. package/lib/gitnexus/commands.js +27 -8
  11. package/lib/gitnexus/commands.js.map +1 -1
  12. package/lib/gitnexus/coverage.d.ts +28 -0
  13. package/lib/gitnexus/coverage.d.ts.map +1 -1
  14. package/lib/gitnexus/coverage.js +64 -2
  15. package/lib/gitnexus/coverage.js.map +1 -1
  16. package/lib/gitnexus/delta.d.ts +25 -0
  17. package/lib/gitnexus/delta.d.ts.map +1 -1
  18. package/lib/gitnexus/delta.js +86 -3
  19. package/lib/gitnexus/delta.js.map +1 -1
  20. package/lib/gitnexus/discover.d.ts +91 -0
  21. package/lib/gitnexus/discover.d.ts.map +1 -0
  22. package/lib/gitnexus/discover.js +214 -0
  23. package/lib/gitnexus/discover.js.map +1 -0
  24. package/lib/gitnexus/graph.d.ts +18 -1
  25. package/lib/gitnexus/graph.d.ts.map +1 -1
  26. package/lib/gitnexus/graph.js +70 -4
  27. package/lib/gitnexus/graph.js.map +1 -1
  28. package/lib/gitnexus/index-meta.d.ts +34 -6
  29. package/lib/gitnexus/index-meta.d.ts.map +1 -1
  30. package/lib/gitnexus/index-meta.js +47 -7
  31. package/lib/gitnexus/index-meta.js.map +1 -1
  32. package/lib/gitnexus/install.d.ts +11 -0
  33. package/lib/gitnexus/install.d.ts.map +1 -1
  34. package/lib/gitnexus/install.js +65 -9
  35. package/lib/gitnexus/install.js.map +1 -1
  36. package/lib/gitnexus/search.d.ts +13 -0
  37. package/lib/gitnexus/search.d.ts.map +1 -1
  38. package/lib/gitnexus/search.js +2 -0
  39. package/lib/gitnexus/search.js.map +1 -1
  40. package/lib/index.d.ts +7 -0
  41. package/lib/index.d.ts.map +1 -1
  42. package/lib/index.js +27 -7
  43. package/lib/index.js.map +1 -1
  44. package/lib/routes/analyze.d.ts +5 -4
  45. package/lib/routes/analyze.d.ts.map +1 -1
  46. package/lib/routes/analyze.js +17 -4
  47. package/lib/routes/analyze.js.map +1 -1
  48. package/lib/routes/coverage.d.ts +16 -10
  49. package/lib/routes/coverage.d.ts.map +1 -1
  50. package/lib/routes/coverage.js +31 -22
  51. package/lib/routes/coverage.js.map +1 -1
  52. package/lib/routes/delta.js +8 -8
  53. package/lib/routes/delta.js.map +1 -1
  54. package/lib/routes/graph.d.ts +1 -1
  55. package/lib/routes/graph.js +2 -2
  56. package/lib/routes/graph.js.map +1 -1
  57. package/lib/routes/guard.d.ts +3 -6
  58. package/lib/routes/guard.d.ts.map +1 -1
  59. package/lib/routes/guard.js +3 -6
  60. package/lib/routes/guard.js.map +1 -1
  61. package/lib/routes/projects.d.ts +21 -0
  62. package/lib/routes/projects.d.ts.map +1 -0
  63. package/lib/routes/projects.js +61 -0
  64. package/lib/routes/projects.js.map +1 -0
  65. package/package.json +5 -1
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;
@@ -2444,6 +3139,10 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2444
3139
  // install or the registry write. Refuse to start it and say why, rather than
2445
3140
  // leaving a button that is known to be wrong on this host.
2446
3141
  const hostCurrent = maintenance.supportsForce === true;
3142
+ // A folder with no `.git` is indexable, but only with `--skip-git`, and GitNexus
3143
+ // disables commit tracking and incremental updates for it. Calling that action an
3144
+ // "incremental refresh" would describe a mode the index cannot have.
3145
+ const skipGit = maintenance.skipGit === true;
2447
3146
  // Normalised rather than trusted. The declared type says `ClientAnalyzeRun | null`,
2448
3147
  // but a React element built without the prop arrives as `undefined` — which is
2449
3148
  // exactly what happened the first time this component was rendered by a caller that
@@ -2472,11 +3171,11 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2472
3171
  setCopied(null);
2473
3172
  }
2474
3173
  };
2475
- 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' } },
2476
3175
  // The old line said "复制后到会话执行". That stopped being the whole story once the
2477
3176
  // plugin could start the run itself, so the sentence says which parts it runs and
2478
3177
  // which it only hands over.
2479
- (0, primitives_7.h)('div', { style: primitives_7.MUTED }, hostCurrent
3178
+ (0, primitives_8.h)('div', { style: primitives_8.MUTED }, hostCurrent
2480
3179
  ? '下面列出的命令都可由本插件执行(走 DSH 的 shell 执行器,沙箱与其它命令一致,本插件不申请扩权)。' +
2481
3180
  '全量重建只应在增量刷新卡死或结果反常时使用。'
2482
3181
  : '当前宿主是旧版本(不包含 FTS 自适应与注册表重定向修复);重启 DSH 后再执行维护。命令仍可复制。'),
@@ -2485,23 +3184,23 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2485
3184
  // separate ones.
2486
3185
  activeRun === null
2487
3186
  ? null
2488
- : (0, primitives_7.h)('div', {
3187
+ : (0, primitives_8.h)('div', {
2489
3188
  style: {
2490
3189
  display: 'flex',
2491
3190
  flexDirection: 'column',
2492
3191
  gap: '4px',
2493
3192
  padding: '6px 8px',
2494
- border: primitives_7.HAIRLINE,
3193
+ border: primitives_8.HAIRLINE,
2495
3194
  borderRadius: '6px',
2496
- background: primitives_7.SOFT_BG,
3195
+ background: primitives_8.SOFT_BG,
2497
3196
  },
2498
- }, (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
2499
- ? (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 }, '取消')
2500
3199
  // Wrapped, not passed directly: once `onStartRun` takes a mode argument,
2501
3200
  // React's click event would arrive as that argument and every "run again"
2502
3201
  // would become a forced rebuild. Repeating the last mode is also the
2503
3202
  // honest reading of "run again" after a repair or a full rebuild.
2504
- : (0, primitives_7.h)(primitives_7.Button, {
3203
+ : (0, primitives_8.h)(primitives_8.Button, {
2505
3204
  variant: 'ghost',
2506
3205
  disabled: !hostCurrent,
2507
3206
  onClick: () => onStartRun(activeRun.repairFts === true
@@ -2509,22 +3208,22 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2509
3208
  : activeRun.force === true
2510
3209
  ? 'force'
2511
3210
  : 'refresh'),
2512
- }, '再次执行')), 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),
2513
3212
  // The output is the evidence, but a failure's first screen was thirty lines of
2514
3213
  // JavaScript stack with the one useful sentence buried in it. The verdict above
2515
3214
  // is the summary; the raw text stays one click away.
2516
3215
  activeRun.output === ''
2517
3216
  ? null
2518
- : (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', {
2519
3218
  style: {
2520
- ...primitives_7.MUTED,
3219
+ ...primitives_8.MUTED,
2521
3220
  cursor: 'pointer',
2522
3221
  fontSize: '12px',
2523
3222
  userSelect: 'none',
2524
3223
  },
2525
- }, '查看原始输出'), (0, primitives_7.h)('pre', {
3224
+ }, '查看原始输出'), (0, primitives_8.h)('pre', {
2526
3225
  style: {
2527
- ...primitives_7.styles.mono,
3226
+ ...primitives_8.styles.mono,
2528
3227
  margin: '4px 0 0',
2529
3228
  whiteSpace: 'pre-wrap',
2530
3229
  maxHeight: '12em',
@@ -2532,7 +3231,7 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2532
3231
  },
2533
3232
  }, activeRun.output))), runError === null
2534
3233
  ? null
2535
- : (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),
2536
3235
  // The Tier 0 verdict sits directly above the buttons it governs. Showing "dirty"
2537
3236
  // without the reason would be the same opacity the gate exists to remove, and
2538
3237
  // showing "clean" without an escape hatch would take a decision away from the
@@ -2541,12 +3240,12 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2541
3240
  // deployment state worth naming — otherwise the gate is simply absent and the
2542
3241
  // operator has no way to tell that from "nothing changed".
2543
3242
  deltaError !== null && deltaError !== undefined
2544
- ? (0, primitives_7.h)('div', { style: primitives_7.MUTED }, `差量检查失败:${deltaError} —— 刷新仍可直接执行。`)
3243
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, `差量检查失败:${deltaError} —— 刷新仍可直接执行。`)
2545
3244
  : deltaRoute === 'missing'
2546
- ? (0, primitives_7.h)('div', { style: primitives_7.MUTED }, '当前宿主未挂载差量路由:重启 DSH 后会显示"是否需要刷新"的依据;刷新按钮仍可使用。')
3245
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, '当前宿主未挂载差量路由:重启 DSH 后会显示"是否需要刷新"的依据;刷新按钮仍可使用。')
2547
3246
  : gateKnown
2548
3247
  ? gateClean
2549
- ? (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, {
2550
3249
  variant: 'ghost',
2551
3250
  disabled: running || !hostCurrent,
2552
3251
  // Not a bypass of the gate: it is the documented way to refresh when
@@ -2558,24 +3257,28 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2558
3257
  // policy skips. Saying "no refresh" without that explanation would read as
2559
3258
  // "git says nothing changed", which is not what was measured.
2560
3259
  excludedCount > 0
2561
- ? (0, primitives_7.h)('div', { style: primitives_7.MUTED }, `${excludedCount} 个改动属索引排除范围(超大文件等),刷新也不会收录,因此不计入刷新依据。`)
3260
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, `${excludedCount} 个改动属索引排除范围(超大文件等),刷新也不会收录,因此不计入刷新依据。`)
2562
3261
  : null)
2563
- : (0, primitives_7.h)('div', {
3262
+ : (0, primitives_8.h)('div', {
2564
3263
  style: {
2565
3264
  display: 'flex',
2566
3265
  flexDirection: 'column',
2567
3266
  gap: '4px',
2568
3267
  padding: '6px 8px',
2569
- border: primitives_7.HAIRLINE,
3268
+ border: primitives_8.HAIRLINE,
2570
3269
  borderRadius: '6px',
2571
- background: primitives_7.SOFT_BG,
3270
+ background: primitives_8.SOFT_BG,
2572
3271
  },
2573
- }, (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} 增 / ` +
2574
3273
  `${gate?.counts.deleted ?? 0} 删` +
2575
3274
  (excludedCount > 0 ? ` · ${excludedCount} 个被排除` : '') +
2576
- ` · 检查用时 ${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', {
2577
3280
  style: {
2578
- ...primitives_7.styles.mono,
3281
+ ...primitives_8.styles.mono,
2579
3282
  maxHeight: '7em',
2580
3283
  overflow: 'auto',
2581
3284
  whiteSpace: 'pre-wrap',
@@ -2585,27 +3288,37 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2585
3288
  .slice(0, 8)
2586
3289
  .map((entry) => `${entry.kind === 'modified' ? 'M' : entry.kind === 'added' ? 'A' : 'D'} ${entry.path}`)
2587
3290
  .join('\n') + (changed.length > 8 ? `\n…还有 ${changed.length - 8} 个` : '')), excludedCount > 0
2588
- ? (0, primitives_7.h)('div', { style: primitives_7.MUTED }, `${excludedCount} 个改动属索引排除范围(超大文件等),刷新也不会收录,因此不计入刷新依据。`)
3291
+ ? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, `${excludedCount} 个改动属索引排除范围(超大文件等),刷新也不会收录,因此不计入刷新依据。`)
2589
3292
  : null)
2590
3293
  : gate !== null && Array.isArray(gate.notes) && gate.notes.length > 0
2591
3294
  ? // No gate: either the comparison was impossible (legacy manifest, no git) or
2592
3295
  // it was not anchored to the index's commit. Both are "cannot prove it is
2593
3296
  // current", and the notes say which — print them all, because the first one
2594
3297
  // alone explains the cause but not the consequence.
2595
- (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}`)))
2596
- : 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) => {
2597
3304
  // The host's wording is written for the no-index state ("初始化索引", "该项目还没有
2598
3305
  // … 索引时使用"). Reusing it on an indexed project told the operator to initialise
2599
3306
  // an index that already existed. Only the label and the "when" line are stateful;
2600
3307
  // the command itself is identical.
2601
3308
  const shown = command.mode === 'init' && indexed
2602
- ? {
2603
- ...command,
2604
- label: '增量刷新',
2605
- when: '索引落后或有文件改动时使用;未变更文件的解析产物会复用。',
2606
- }
3309
+ ? skipGit
3310
+ ? {
3311
+ ...command,
3312
+ label: '重新分析',
3313
+ when: '该目录不是 git 仓库:没有增量刷新,每次都会完整重分析;未变更文件的解析缓存仍会复用。',
3314
+ }
3315
+ : {
3316
+ ...command,
3317
+ label: '增量刷新',
3318
+ when: '索引落后或有文件改动时使用;未变更文件的解析产物会复用。',
3319
+ }
2607
3320
  : command;
2608
- return (0, primitives_7.h)('div', {
3321
+ return (0, primitives_8.h)('div', {
2609
3322
  key: command.mode,
2610
3323
  style: {
2611
3324
  display: 'flex',
@@ -2616,13 +3329,13 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2616
3329
  // because the index says FTS is missing, so it keeps full weight.
2617
3330
  opacity: command.mode === applicable || command.mode === 'repair-fts' ? 1 : 0.55,
2618
3331
  },
2619
- }, (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 } }),
2620
3333
  // Each command is executable now, each behind its own confirmation. The
2621
3334
  // force button exists because the host route implements `--force`; a button
2622
3335
  // labelled "全量重建" that quietly ran an incremental refresh would be the
2623
3336
  // exact failure the earlier revision refused to ship.
2624
3337
  command.mode === 'init'
2625
- ? (0, primitives_7.h)(primitives_7.Button, {
3338
+ ? (0, primitives_8.h)(primitives_8.Button, {
2626
3339
  variant: 'ghost',
2627
3340
  // The gate is the whole point of Tier 0: a clean tree disables the
2628
3341
  // normal action and says so, and "仍然刷新" is the way past it.
@@ -2632,33 +3345,41 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2632
3345
  ? '运行中'
2633
3346
  : gateClean
2634
3347
  ? '无需刷新'
2635
- : hostCurrent
2636
- ? '执行增量刷新'
2637
- : '执行增量刷新(需重启)')
3348
+ : !indexed
3349
+ ? hostCurrent
3350
+ ? '初始化索引'
3351
+ : '初始化索引(需重启)'
3352
+ : skipGit
3353
+ ? hostCurrent
3354
+ ? '执行重新分析'
3355
+ : '执行重新分析(需重启)'
3356
+ : hostCurrent
3357
+ ? '执行增量刷新'
3358
+ : '执行增量刷新(需重启)')
2638
3359
  : command.mode === 'repair-fts'
2639
- ? (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 ? '运行中' : '修复搜索索引')
2640
3361
  : hostCurrent
2641
- ? (0, primitives_7.h)(primitives_7.Button, { variant: 'ghost', disabled: running, onClick: () => setConfirming('full') }, running ? '运行中' : '全量重建')
2642
- : (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, {
2643
3364
  variant: 'ghost',
2644
3365
  disabled: true,
2645
3366
  title: '当前宿主版本尚未支持 force=1;重启 DSH 后再试,或复制命令执行',
2646
- }, '全量重建(需重启)'), (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', {
2647
3368
  style: {
2648
- ...primitives_7.styles.mono,
3369
+ ...primitives_8.styles.mono,
2649
3370
  display: 'block',
2650
3371
  padding: '6px 8px',
2651
- border: primitives_7.HAIRLINE,
3372
+ border: primitives_8.HAIRLINE,
2652
3373
  borderRadius: '6px',
2653
- background: primitives_7.SOFT_BG,
3374
+ background: primitives_8.SOFT_BG,
2654
3375
  wordBreak: 'break-all',
2655
3376
  userSelect: 'all',
2656
3377
  },
2657
- }, command.shell), (0, primitives_7.h)('div', { style: primitives_7.MUTED }, shown.when), command.caveat !== null && command.caveat !== undefined
2658
- ? (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)
2659
3380
  : null);
2660
3381
  }), Array.isArray(maintenance?.rationale) && maintenance.rationale.length > 0
2661
- ? (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}`)))
2662
3383
  : null,
2663
3384
  // The confirmation. Everything the operator needs before a process touches their
2664
3385
  // working tree goes in `description`, because that is the prop the modal renders:
@@ -2668,14 +3389,23 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2668
3389
  // One dialog, two modes. The full rebuild gets its own title, wording and
2669
3390
  // acknowledgement because it is a different risk: it discards the graph and FTS
2670
3391
  // and can take minutes, while the incremental path reuses unchanged files.
2671
- (0, primitives_7.h)(primitives.RiskConfirmation, {
3392
+ (0, primitives_8.h)(primitives.RiskConfirmation, {
2672
3393
  open: confirming !== null,
2673
- title: repairConfirm ? '修复搜索索引(FTS)?' : fullConfirm ? '执行全量重建?' : '执行增量刷新?',
3394
+ title: repairConfirm
3395
+ ? '修复搜索索引(FTS)?'
3396
+ : fullConfirm
3397
+ ? '执行全量重建?'
3398
+ : !indexed
3399
+ ? '初始化索引?'
3400
+ : skipGit
3401
+ ? '重新分析这个目录?'
3402
+ : '执行增量刷新?',
2674
3403
  description: repairConfirm
2675
3404
  ? '将执行:\n\n' +
2676
3405
  (confirmCommand?.shell ?? 'gitnexus analyze --index-only --repair-fts') +
2677
3406
  '\n\n' +
2678
3407
  '只重建搜索索引(FTS),不重跑分析、不改图,耗时通常在秒级。' +
3408
+ (skipGit ? '该目录没有 .git,命令带 --skip-git。' : '') +
2679
3409
  '只写入 .gitnexus/,不改动源文件。' +
2680
3410
  '如果 FTS 扩展文件本身缺失(Windows 上常见),修复也会失败 —— 那是上游安装问题,不是索引坏了。' +
2681
3411
  '命令由 DSH 的 shell 执行器运行,沙箱与其它命令一致 —— 本插件不申请扩权。'
@@ -2686,20 +3416,32 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2686
3416
  '这会重建图与 FTS;未变更文件的解析产物仍会复用。' +
2687
3417
  '但耗时以分钟计,索引很大时更久(上限 10 分钟,可随时取消)。' +
2688
3418
  '重建期间索引可能短暂不可用。' +
3419
+ (skipGit ? '该目录没有 .git,命令带 --skip-git;重建后仍没有提交基线与增量刷新。' : '') +
2689
3420
  '只写入 .gitnexus/,不改动源文件,也不会重写 AGENTS.md / CLAUDE.md。' +
2690
3421
  '命令由 DSH 的 shell 执行器运行,沙箱与其它命令一致 —— 本插件不申请扩权。'
2691
3422
  : '将在项目目录下运行:\n\n' +
2692
3423
  (confirmCommand?.shell ?? 'gitnexus analyze --index-only') +
2693
3424
  '\n\n' +
2694
3425
  '只写入 .gitnexus/,不改动任何源文件,也不会重写 AGENTS.md / CLAUDE.md。' +
2695
- '耗时取决于改动量:无改动时约 7 秒,改动几个文件时实测可到 3 分钟,' +
2696
- '大仓库的初始化或刷新可能更久(上限 10 分钟,可随时取消)。' +
3426
+ (skipGit
3427
+ ? '该目录没有 .git,命令带 --skip-git:GitNexus 会直接索引这个目录,' +
3428
+ '但没有提交基线,也没有增量刷新 —— 每次都会完整重分析,也不会有 Git 活动。'
3429
+ : '耗时取决于改动量:无改动时约 7 秒,改动几个文件时实测可到 3 分钟,' +
3430
+ '大仓库的初始化或刷新可能更久(上限 10 分钟,可随时取消)。') +
2697
3431
  '命令由 DSH 的 shell 执行器运行,沙箱与其它命令一致 —— 本插件不申请扩权。',
2698
3432
  acknowledgeLabel: fullConfirm
2699
3433
  ? '我了解这会重建图与 FTS、耗时数分钟,并可能让索引短暂不可用'
2700
3434
  : '我了解这会运行一个进程并写入 .gitnexus/',
2701
3435
  cancelLabel: '取消',
2702
- confirmLabel: repairConfirm ? '开始修复' : fullConfirm ? '开始重建' : '执行',
3436
+ confirmLabel: repairConfirm
3437
+ ? '开始修复'
3438
+ : fullConfirm
3439
+ ? '开始重建'
3440
+ : !indexed
3441
+ ? '开始初始化'
3442
+ : skipGit
3443
+ ? '开始重新分析'
3444
+ : '执行',
2703
3445
  closeLabel: '关闭',
2704
3446
  // The checkbox is controlled, so it needs state of its own, and it is reset on
2705
3447
  // close: the acknowledgement is per-action rather than remembered.
@@ -2718,7 +3460,91 @@ define("sections/maintenance", ["require", "exports", "react", "@deepseek-ai/dsh
2718
3460
  }));
2719
3461
  }
2720
3462
  });
2721
- 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) {
2722
3548
  "use strict";
2723
3549
  Object.defineProperty(exports, "__esModule", { value: true });
2724
3550
  exports.MemoizedOverviewBody = void 0;
@@ -2731,36 +3557,36 @@ define("sections/overview", ["require", "exports", "react", "format", "primitive
2731
3557
  * section wrapper and the split-region layout around it.
2732
3558
  */
2733
3559
  function OverviewBody({ report, totals, cap, ratio, }) {
2734
- 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' } },
2735
3561
  // Ring and scan summary on one line, metrics below at full card width. Keeping
2736
3562
  // the ring in a left rail stole ~90px from the metric grid at exactly the widths
2737
3563
  // where it needed two columns, so the card turned into a tall single column.
2738
- (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
2739
3565
  ? null
2740
- : (0, primitives_8.h)(primitives_8.Ring, {
3566
+ : (0, primitives_10.h)(primitives_10.Ring, {
2741
3567
  ratio,
2742
3568
  caption: '覆盖',
2743
3569
  tone: ratio >= 0.9
2744
3570
  ? 'var(--dsw-alias-state-success-primary, #3fb950)'
2745
3571
  : 'var(--dsw-alias-state-warn-primary, #d9a441)',
2746
- }), (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))),
2747
3573
  // The three readings sit in a grid instead of a column: 文件 and 图谱 share a
2748
3574
  // row, and the capability strip spans the full width below them. A stacked
2749
3575
  // layout left the right half of the card empty at every width above ~360px.
2750
- (0, primitives_8.h)('div', {
3576
+ (0, primitives_10.h)('div', {
2751
3577
  style: {
2752
3578
  display: 'grid',
2753
3579
  gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
2754
3580
  gap: '10px 20px',
2755
3581
  alignItems: 'start',
2756
3582
  },
2757
- }, (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, {
2758
3584
  items: [
2759
- { label: '大小上限', value: cap.text, hint: (0, format_8.capSourceLabel)(cap.source), color: '#d9a441' },
2760
- { 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' },
2761
3587
  {
2762
3588
  label: '超限文件',
2763
- value: (0, format_8.compactNumber)(totals.large),
3589
+ value: (0, format_9.compactNumber)(totals.large),
2764
3590
  // Amber when something was actually dropped, grey when the count is
2765
3591
  // zero: a zero is good news and should not shout.
2766
3592
  color: totals.large > 0 ? '#f7768e' : undefined,
@@ -2768,17 +3594,17 @@ define("sections/overview", ["require", "exports", "react", "format", "primitive
2768
3594
  ],
2769
3595
  })), report.graph === null || report.graph === undefined
2770
3596
  ? null
2771
- : (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, {
2772
3598
  items: [
2773
- { label: '符号', value: (0, format_8.compactNumber)(report.graph.nodes), color: '#7aa2f7' },
2774
- { label: '边', value: (0, format_8.compactNumber)(report.graph.edges), color: '#9ece6a' },
2775
- { label: '社区', value: (0, format_8.compactNumber)(report.graph.communities), color: '#bb9af7' },
2776
- { label: '流程', value: (0, format_8.compactNumber)(report.graph.processes), color: '#7dcfff' },
2777
- { 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' },
2778
3604
  ],
2779
- })), (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'
2780
3606
  ? null
2781
- : (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' } }, '顶部「关键词搜索」会先查图;图查询不可用时回退到源码关键词。'))));
2782
3608
  }
2783
3609
  /**
2784
3610
  * Memoized for the panel: all four props are stable for the lifetime of one report, so
@@ -2786,7 +3612,7 @@ define("sections/overview", ["require", "exports", "react", "format", "primitive
2786
3612
  */
2787
3613
  exports.MemoizedOverviewBody = React.memo(OverviewBody);
2788
3614
  });
2789
- 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) {
2790
3616
  "use strict";
2791
3617
  Object.defineProperty(exports, "__esModule", { value: true });
2792
3618
  exports.ToolchainNotice = ToolchainNotice;
@@ -2833,27 +3659,27 @@ define("sections/install", ["require", "exports", "react", "@deepseek-ai/dsh-cli
2833
3659
  setCopied(false);
2834
3660
  }
2835
3661
  };
2836
- 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
2837
3663
  ? '找到了 GitNexus 的库,但没有可执行的 gitnexus 命令(可能只作为依赖安装、未全局链接)。读取不受影响;修复后即可执行增量刷新。'
2838
- : '本插件读取 .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', {
2839
3665
  style: {
2840
- ...primitives_9.styles.mono,
3666
+ ...primitives_11.styles.mono,
2841
3667
  flex: '1 1 auto',
2842
3668
  padding: '6px 8px',
2843
- border: primitives_9.HAIRLINE,
3669
+ border: primitives_11.HAIRLINE,
2844
3670
  borderRadius: '6px',
2845
- background: primitives_9.SOFT_BG,
3671
+ background: primitives_11.SOFT_BG,
2846
3672
  wordBreak: 'break-all',
2847
3673
  userSelect: 'all',
2848
3674
  },
2849
- }, 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
2850
- ? (0, primitives_9.h)(primitives_9.Button, { variant: 'ghost', onClick: onCancelInstall }, `取消(已进行 ${elapsed}s)`)
2851
- : (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 源,不改全局前缀、不需要管理员权限;' +
2852
3678
  '若当前项目还没有索引,装完会继续初始化一次。')), activeRun === null
2853
3679
  ? null
2854
- : (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 === ''
2855
3681
  ? null
2856
- : (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, {
2857
3683
  open: confirming,
2858
3684
  title: '授权本插件安装 GitNexus?',
2859
3685
  description: '本插件将执行:\n\nnpm install --prefix <插件数据目录>/gitnexus gitnexus@^1.6.12\n\n' +
@@ -2882,7 +3708,7 @@ define("sections/install", ["require", "exports", "react", "@deepseek-ai/dsh-cli
2882
3708
  })));
2883
3709
  }
2884
3710
  });
2885
- define("sections/search", ["require", "exports", "primitives"], function (require, exports, primitives_10) {
3711
+ define("sections/search", ["require", "exports", "primitives"], function (require, exports, primitives_12) {
2886
3712
  "use strict";
2887
3713
  Object.defineProperty(exports, "__esModule", { value: true });
2888
3714
  exports.SearchSection = SearchSection;
@@ -2898,13 +3724,13 @@ define("sections/search", ["require", "exports", "primitives"], function (requir
2898
3724
  function SearchSection({ search }) {
2899
3725
  const keyOf = (hit) => `${hit.kind}:${hit.filePath}:${hit.startLine ?? ''}:${hit.name}`;
2900
3726
  const canSubmit = search.query.trim() !== '' && !search.searching;
2901
- return (0, primitives_10.h)(primitives_10.Section, {
3727
+ return (0, primitives_12.h)(primitives_12.Section, {
2902
3728
  wide: true,
2903
3729
  title: '关键词搜索',
2904
- 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' ? '源码关键词(回退)' : '符号名 / 文件路径'),
2905
3731
  // Twenty hits of `path:line` rows is a scroll area, not a page length.
2906
3732
  scroll: 420,
2907
- }, (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', {
2908
3734
  type: 'search',
2909
3735
  value: search.query,
2910
3736
  placeholder: '搜索符号或文件,例如 analyzeCoverage',
@@ -2922,34 +3748,34 @@ define("sections/search", ["require", "exports", "primitives"], function (requir
2922
3748
  flex: '1 1 auto',
2923
3749
  minWidth: 0,
2924
3750
  padding: '5px 8px',
2925
- border: primitives_10.HAIRLINE,
3751
+ border: primitives_12.HAIRLINE,
2926
3752
  borderRadius: '6px',
2927
- background: primitives_10.SOFT_BG,
3753
+ background: primitives_12.SOFT_BG,
2928
3754
  color: 'inherit',
2929
3755
  font: 'inherit',
2930
3756
  },
2931
- }), (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
2932
- ? (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', {
2933
3759
  style: {
2934
- ...primitives_10.MUTED,
3760
+ ...primitives_12.MUTED,
2935
3761
  color: 'var(--dsw-alias-state-error-primary, inherit)',
2936
3762
  marginTop: '6px',
2937
3763
  },
2938
3764
  }, search.error)
2939
- : 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 ??
2940
3766
  (search.mode === 'text'
2941
3767
  ? '图查询不可用,已回退到源码关键词搜索。'
2942
3768
  : '按符号名与文件路径匹配;不搜索正文。')), search.results === null
2943
3769
  ? null
2944
3770
  : search.results.length === 0
2945
- ? (0, primitives_10.h)('div', { style: { ...primitives_10.MUTED, marginTop: '6px' } }, `没有匹配“${search.query}”的符号或文件。`)
2946
- : (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'
2947
3773
  ? `找到 ${search.total} 处源码匹配`
2948
3774
  : `找到 ${search.total} 个结果`) +
2949
- `${search.truncated ? '(已截断)' : ''} · 点击结果打开文件`), search.results.map((hit) => (0, primitives_10.h)('div', {
3775
+ `${search.truncated ? '(已截断)' : ''} · 点击结果打开文件`), search.results.map((hit) => (0, primitives_12.h)('div', {
2950
3776
  key: keyOf(hit),
2951
- style: { ...primitives_10.styles.fileRow, borderTop: primitives_10.HAIRLINE, padding: '5px 0' },
2952
- }, (0, primitives_10.h)('button', {
3777
+ style: { ...primitives_12.styles.fileRow, borderTop: primitives_12.HAIRLINE, padding: '5px 0' },
3778
+ }, (0, primitives_12.h)('button', {
2953
3779
  type: 'button',
2954
3780
  onClick: () => search.onOpen(hit),
2955
3781
  title: hit.startLine === null
@@ -2969,7 +3795,7 @@ define("sections/search", ["require", "exports", "primitives"], function (requir
2969
3795
  textAlign: 'left',
2970
3796
  cursor: 'pointer',
2971
3797
  },
2972
- }, (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', {
2973
3799
  style: {
2974
3800
  fontWeight: 600,
2975
3801
  minWidth: 0,
@@ -2977,26 +3803,73 @@ define("sections/search", ["require", "exports", "primitives"], function (requir
2977
3803
  textOverflow: 'ellipsis',
2978
3804
  whiteSpace: 'nowrap',
2979
3805
  },
2980
- }, hit.name), (0, primitives_10.h)('span', {
3806
+ }, hit.name), (0, primitives_12.h)('span', {
2981
3807
  style: {
2982
- ...primitives_10.MUTED,
2983
- ...primitives_10.styles.mono,
3808
+ ...primitives_12.MUTED,
3809
+ ...primitives_12.styles.mono,
2984
3810
  minWidth: 0,
2985
3811
  flex: '1 1 auto',
2986
3812
  overflow: 'hidden',
2987
3813
  textOverflow: 'ellipsis',
2988
3814
  whiteSpace: 'nowrap',
2989
3815
  },
2990
- }, (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) ? '已复制' : '复制')))));
2991
3817
  }
2992
3818
  });
2993
- 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) {
2994
3820
  "use strict";
2995
3821
  Object.defineProperty(exports, "__esModule", { value: true });
3822
+ exports.chooseProject = chooseProject;
2996
3823
  exports.LoadingBar = LoadingBar;
2997
3824
  exports.CoverageView = CoverageView;
2998
3825
  exports.CoveragePanel = CoveragePanel;
2999
3826
  exports.CoverageTitle = CoverageTitle;
3827
+ /**
3828
+ * Pick the project a workspace opens on.
3829
+ *
3830
+ * An explicit remembered choice wins while it still exists. Otherwise an indexed
3831
+ * project is preferred over an unindexed git repository, and the workspace's own index
3832
+ * wins over a descendant. A workspace with no indexes opens on the workspace itself —
3833
+ * the scan-only state, with the unindexed git projects available in the switcher.
3834
+ */
3835
+ function chooseProject(projects, workspace, remembered) {
3836
+ if (remembered !== null) {
3837
+ const hit = projects.find((project) => project.path === remembered);
3838
+ if (hit !== undefined)
3839
+ return hit.path;
3840
+ }
3841
+ const selfIndex = projects.find((project) => project.direction === 'self' && project.kind === 'index');
3842
+ if (selfIndex !== undefined)
3843
+ return selfIndex.path;
3844
+ const indexed = projects.find((project) => project.kind === 'index');
3845
+ if (indexed !== undefined)
3846
+ return indexed.path;
3847
+ const self = projects.find((project) => project.direction === 'self');
3848
+ if (self !== undefined)
3849
+ return self.path;
3850
+ return projects[0]?.path ?? workspace;
3851
+ }
3852
+ const ACTIVE_PROJECT_KEY = 'dsh-gitnexus-plus:active-project:';
3853
+ function readRememberedProject(workspace) {
3854
+ try {
3855
+ if (typeof localStorage === 'undefined')
3856
+ return null;
3857
+ return localStorage.getItem(`${ACTIVE_PROJECT_KEY}${workspace}`);
3858
+ }
3859
+ catch {
3860
+ return null;
3861
+ }
3862
+ }
3863
+ function rememberProject(workspace, project) {
3864
+ try {
3865
+ if (typeof localStorage === 'undefined')
3866
+ return;
3867
+ localStorage.setItem(`${ACTIVE_PROJECT_KEY}${workspace}`, project);
3868
+ }
3869
+ catch {
3870
+ /* storage may be unavailable in a hardened webview; selection stays in memory */
3871
+ }
3872
+ }
3000
3873
  /**
3001
3874
  * Whether two polled run snapshots say the same thing.
3002
3875
  *
@@ -3030,14 +3903,14 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3030
3903
  ? undefined
3031
3904
  : Math.max(0, Math.min(100, Math.round(percent)));
3032
3905
  const text = value === undefined ? label : `${label} · ${value}%`;
3033
- return (0, primitives_11.h)('div', {
3906
+ return (0, primitives_13.h)('div', {
3034
3907
  style: {
3035
3908
  gridColumn: '1 / -1',
3036
3909
  display: 'flex',
3037
3910
  flexDirection: 'column',
3038
3911
  gap: '4px',
3039
3912
  },
3040
- }, (0, primitives_11.h)('progress', {
3913
+ }, (0, primitives_13.h)('progress', {
3041
3914
  value,
3042
3915
  max: 100,
3043
3916
  'aria-label': label,
@@ -3053,35 +3926,135 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3053
3926
  accentColor: 'var(--dsw-alias-brand-primary, #4f8cff)',
3054
3927
  color: 'var(--dsw-alias-brand-primary, #4f8cff)',
3055
3928
  },
3056
- }), (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));
3057
3930
  }
3058
- function CoverageView({ report, maintenance, projectPath, 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, }) {
3059
3932
  // Whether an index exists decides which maintenance command is applicable. When
3060
3933
  // the project could not even be identified there is nothing to claim, so it is
3061
3934
  // false rather than assumed.
3062
3935
  const indexed = report?.repo?.indexed === true;
3063
- const header = (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 索引'),
3064
- // The project, not a picker. Shown so the reader can confirm which tree the
3065
- // numbers describe — a path is the only unambiguous answer to that.
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]);
3968
+ // A maintenance payload with no commands is not a section: rendering its title
3969
+ // produced an empty card on failed reads and on hosts that predate the route.
3970
+ const maintenanceSection = maintenance !== null &&
3971
+ maintenance !== undefined &&
3972
+ Array.isArray(maintenance.commands) &&
3973
+ maintenance.commands.length > 0
3974
+ ? maintenance
3975
+ : null;
3976
+ // The workspace and the active project are different facts when the index lives in a
3977
+ // subdirectory. The path position becomes a switcher when there is more than one.
3978
+ const projectChoices = projects ?? [];
3979
+ const indexedProjectCount = projectChoices.filter((project) => project.kind === 'index').length;
3980
+ const gitProjectCount = projectChoices.filter((project) => project.kind === 'git').length;
3981
+ const projectLabel = (project) => {
3982
+ const base = project.relPath === ''
3983
+ ? `${project.name}(本工作区)`
3984
+ : project.relPath === project.name
3985
+ ? project.name
3986
+ : `${project.name} — ${project.relPath}`;
3987
+ return project.kind === 'git' ? `${base}(未索引)` : base;
3988
+ };
3989
+ const activeInChoices = projectChoices.some((project) => project.path === projectPath);
3990
+ const showSwitcher = projectChoices.length > 1 &&
3991
+ activeInChoices &&
3992
+ onSelectProject !== undefined &&
3993
+ projectPath !== null;
3994
+ const activeDiffers = projectPath !== null && workspacePath !== null && projectPath !== workspacePath;
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 索引'),
3996
+ // The project, not a global picker. A workspace-scoped switcher is the one
3997
+ // selection this panel offers; it changes what the cards describe, nothing else.
3066
3998
  projectPath === null
3067
- ? (0, primitives_11.h)('span', { style: primitives_11.MUTED }, '未解析到工作区')
3068
- : (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 } }),
3999
+ ? (0, primitives_13.h)('span', { style: primitives_13.MUTED }, '未解析到工作区')
4000
+ : showSwitcher
4001
+ ? (0, primitives_13.h)('span', { style: { display: 'flex', alignItems: 'center', gap: '2px', minWidth: 0, flex: '1 1 auto' } }, (0, primitives_13.h)('select', {
4002
+ value: projectPath,
4003
+ onChange: (event) => onSelectProject?.(event.target.value),
4004
+ 'aria-label': '切换工作区内的 GitNexus 索引',
4005
+ title: `工作区:${workspacePath ?? ''} · ${projectChoices.length} 个索引`,
4006
+ style: {
4007
+ appearance: 'none',
4008
+ border: 0,
4009
+ background: 'transparent',
4010
+ color: 'inherit',
4011
+ font: 'inherit',
4012
+ minWidth: 0,
4013
+ maxWidth: '100%',
4014
+ padding: 0,
4015
+ cursor: 'pointer',
4016
+ },
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 } }),
3069
4019
  // The label states the scope. "刷新" alone next to a staleness warning reads as
3070
4020
  // "rebuild the index", which this control has never done and — by design, since
3071
4021
  // the plugin spawns nothing — cannot do.
3072
- (0, primitives_11.h)(primitives_11.Button, {
4022
+ (0, primitives_13.h)(primitives_13.Button, {
3073
4023
  variant: 'ghost',
3074
4024
  onClick: onRefresh,
3075
4025
  disabled: busy,
3076
4026
  title: '重新读取报告(不会重建索引;重建见下方「维护命令」)',
3077
4027
  'aria-label': '重新读取报告',
3078
- }, (0, primitives_11.h)(primitives_11.IconRefreshOutlineRegular, { size: 14 })));
4028
+ }, (0, primitives_13.h)(primitives_13.IconRefreshOutlineRegular, { size: 14 }))), showSwitcher ||
4029
+ activeDiffers ||
4030
+ projectsTruncated === true ||
4031
+ (projectsError !== null && projectsError !== undefined)
4032
+ ? (0, primitives_13.h)('div', { style: { ...primitives_13.MUTED, fontSize: '11px', minWidth: 0, overflowWrap: 'anywhere' } }, [
4033
+ showSwitcher
4034
+ ? `工作区:${workspacePath ?? ''} · ${projectChoices.length} 个项目` +
4035
+ (indexedProjectCount > 0
4036
+ ? `(${indexedProjectCount} 个已索引)`
4037
+ : gitProjectCount > 0
4038
+ ? `(${gitProjectCount} 个 git 项目未索引)`
4039
+ : '') +
4040
+ ` · 当前 ${projectPath ?? ''}`
4041
+ : activeDiffers
4042
+ ? `工作区:${workspacePath ?? ''} · 当前 ${projectPath ?? ''}`
4043
+ : null,
4044
+ projectsTruncated === true ? '项目搜索达到上限,可能还有未列出的项目。' : null,
4045
+ projectsError !== null && projectsError !== undefined
4046
+ ? `项目列表读取失败:${projectsError}`
4047
+ : null,
4048
+ ]
4049
+ .filter((line) => line !== null)
4050
+ .join(' '))
4051
+ : null);
3079
4052
  // A report already on screen stays readable while a manual refresh runs; the bar
3080
4053
  // rides above it. Before the first report exists, the bar is the whole body.
3081
4054
  // `percent` is null when the host does not stream (old host, or a cached read):
3082
4055
  // the native progress element is then indeterminate, not a made-up number.
3083
4056
  const progressBar = report === null || busy
3084
- ? (0, primitives_11.h)(LoadingBar, {
4057
+ ? (0, primitives_13.h)(LoadingBar, {
3085
4058
  label: progress?.label ?? (report === null ? '正在读取索引…' : '正在刷新索引…'),
3086
4059
  percent: progress?.percent ?? null,
3087
4060
  })
@@ -3092,79 +4065,93 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3092
4065
  // index for a project it had not identified. The diagnostic exists so that state
3093
4066
  // is actionable rather than a dead end.
3094
4067
  if (projectPath === null) {
3095
- 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
3096
4069
  ? null
3097
- : (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('、') : '(无)'}`))));
3098
4071
  }
3099
4072
  if (error !== null) {
3100
- 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)), maintenance === null || maintenance === undefined
3101
- ? null
3102
- : (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, {
3103
4075
  wide: true,
3104
4076
  title: '维护命令',
3105
- 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 处理)'),
3106
4078
  scroll: 320,
3107
- }, (0, primitives_11.h)(maintenance_1.MaintenanceSection, { maintenance, 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 }))
4080
+ : null);
3108
4081
  }
3109
4082
  if (report === null) {
3110
- 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);
3111
4084
  }
3112
4085
  // Defaults are resolved before the no-index branch: a scan-only report still has
3113
4086
  // totals, a cap and languages even though it has no graph. Building them only for
3114
4087
  // an indexed report is what made "no index" mean "nothing to show".
3115
- const totals = report.totals ?? format_10.ZERO_TOTALS;
3116
- const cap = report.cap ?? format_10.ZERO_CAP;
3117
- 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;
3118
4091
  const largeFiles = report.largeFiles ?? [];
3119
4092
  const languages = report.languages ?? null;
3120
- const { notable, context: contextWarnings } = (0, format_10.splitWarnings)(report.warnings);
3121
- const ratio = (0, format_10.coverageRatio)(report);
3122
- // The header names the window the timeline is showing; the selection itself lives in
3123
- // the section, since the rows below are now the only entry point.
3124
- 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
+ });
3125
4102
  // No index yet. This is the state the initialize command exists for, but it is
3126
4103
  // also a state in which the working tree is still readable: file counts, bytes,
3127
4104
  // the cap and the language split do not need GitNexus. The panel used to show
3128
4105
  // only "没有索引" and a command, which made an otherwise healthy repository look
3129
4106
  // empty. The scan is the "at least something to look at" floor.
3130
4107
  if (report.repo?.indexed !== true) {
3131
- 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, {
3132
4109
  toolchain: report.toolchain,
3133
4110
  run: installRun,
3134
4111
  runError: installError,
3135
4112
  onInstall,
3136
4113
  onCancelInstall,
3137
- }), (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)('span', null, '该项目还没有 GitNexus 索引。下面是工作区扫描结果;初始化后可看到图谱、查询能力与 Git 活动对账。')), (0, primitives_11.h)(primitives_11.Section, { wide: true, title: '扫描概览 · 尚未索引' }, (0, primitives_11.h)(primitives_11.MetricGrid, {
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
4115
+ ? '该目录不是 git 仓库,也没有 GitNexus 索引。下面是工作区扫描结果;仍可建立索引(--skip-git),但没有提交基线与增量刷新,也不会有 Git 活动。'
4116
+ : '该项目还没有 GitNexus 索引。下面是工作区扫描结果;初始化后可看到图谱、查询能力与 Git 活动对账。'), Array.isArray(projects)
4117
+ ? (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '11px' } }, projectsError !== null && projectsError !== undefined
4118
+ ? `项目列表读取失败:${projectsError};已按工作区描述。`
4119
+ : indexedProjectCount === 0 && gitProjectCount > 0
4120
+ ? `已在 ${projectsVisited ?? 0} 个目录中查找;工作区内有 ${gitProjectCount} 个 git 项目尚未建索引,可在标题栏切换后初始化。`
4121
+ : indexedProjectCount === 0
4122
+ ? `已在 ${projectsVisited ?? 0} 个目录中查找,没有找到 GitNexus 索引或 git 仓库。`
4123
+ : null)
4124
+ : null)), (0, primitives_13.h)(primitives_13.Section, { wide: true, title: '扫描概览 · 尚未索引' }, (0, primitives_13.h)(primitives_13.MetricGrid, {
3138
4125
  items: [
3139
- { label: '扫描文件', value: (0, format_10.compactNumber)(totals.scanned) },
3140
- { label: '选中文件', value: (0, format_10.compactNumber)(totals.selected) },
3141
- { label: '扫描字节', value: (0, format_10.fileSize)(totals.scannedBytes ?? 0) },
3142
- { 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) },
3143
4130
  ],
3144
- }), (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, maintenance === null || maintenance === undefined
3145
- ? null
3146
- : (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, {
3147
4133
  wide: true,
3148
4134
  title: '初始化索引',
3149
- meta: (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '11px' } }, maintenance.supportsForce === true
4135
+ meta: (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '11px' } }, maintenanceSection.supportsForce === true
3150
4136
  ? '均可直接执行,也可复制到会话(沙箱与提权由 DSH 处理)'
3151
4137
  : '重启 DSH 后可执行;当前仅可复制(沙箱与提权由 DSH 处理)'),
3152
- }, (0, primitives_11.h)(maintenance_1.MaintenanceSection, { maintenance, 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 }))
4139
+ : null);
3153
4140
  }
3154
- return (0, primitives_11.h)('div', { style: primitives_11.styles.root },
4141
+ return (0, primitives_13.h)('div', { style: primitives_13.styles.root },
3155
4142
  // The header and the two status lines span everything: they describe the whole
3156
4143
  // panel, so giving them a column would make them look like one card's heading.
3157
- (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,
3158
4145
  // Immediately after the header, before any index content. If GitNexus is missing,
3159
4146
  // that single fact explains everything the panel is about to show or fail to show,
3160
4147
  // so it belongs above the numbers rather than beside them.
3161
- (0, primitives_11.h)(install_1.ToolchainNotice, {
4148
+ (0, primitives_13.h)(install_1.ToolchainNotice, {
3162
4149
  toolchain: report.toolchain,
3163
4150
  run: installRun,
3164
4151
  runError: installError,
3165
4152
  onInstall,
3166
4153
  onCancelInstall,
3167
- }), (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)}` +
3168
4155
  ` · manifest ${report.repo.metaFile ?? '?'} · GitNexus ${analyzer.gitnexusVersion}` +
3169
4156
  // The read time is the only thing on screen a refresh can change when the
3170
4157
  // repository has not moved, so without it the control reads as broken.
@@ -3174,12 +4161,12 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3174
4161
  report.staleness === undefined
3175
4162
  ? null
3176
4163
  : (() => {
3177
- const s = (0, format_10.stalenessView)(report.staleness);
3178
- 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));
3179
4166
  })()),
3180
4167
  // Search sits above the report cards: it is the action a reader reaches for
3181
4168
  // before reading, not an annotation on the numbers below it.
3182
- search === undefined ? null : (0, primitives_11.h)(search_1.SearchSection, { search }),
4169
+ search === undefined ? null : (0, primitives_13.h)(search_1.SearchSection, { search }),
3183
4170
  // One card for the whole "what is this index" question — file coverage and
3184
4171
  // graph contents together.
3185
4172
  //
@@ -3191,7 +4178,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3191
4178
  // stacked on the right. The right column stretches to the overview's height, so the
3192
4179
  // quality card absorbs the difference instead of leaving a gap under it; a narrow
3193
4180
  // pane collapses the three into one column.
3194
- (0, primitives_11.h)('div', {
4181
+ (0, primitives_13.h)('div', {
3195
4182
  style: {
3196
4183
  gridColumn: '1 / -1',
3197
4184
  display: 'grid',
@@ -3199,14 +4186,14 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3199
4186
  gap: '8px',
3200
4187
  alignItems: 'stretch',
3201
4188
  },
3202
- }, (0, primitives_11.h)(primitives_11.Section, {
4189
+ }, (0, primitives_13.h)(primitives_13.Section, {
3203
4190
  // The left column of the split region: the one card that answers "what is in
3204
4191
  // this index". The right column stacks the two compact readings beside it.
3205
4192
  title: '索引概览',
3206
4193
  meta: report.graph === null || report.graph === undefined
3207
4194
  ? null
3208
- : (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '12px' } }, `${(0, format_10.fixed)(report.graph.edgesPerNode, 2)} 边/符号`),
3209
- }, (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',
3210
4197
  // The column stretches to the overview's height; the quality card below takes
3211
4198
  // whatever the language card leaves, so the right side ends level with the left.
3212
4199
  { style: { display: 'flex', flexDirection: 'column', gap: '8px', minHeight: 0 } },
@@ -3215,36 +4202,39 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3215
4202
  // repository" answers side by side. The grid gives them one row on a wide pane and
3216
4203
  // one column in a narrow sidebar, which is the platform's own behaviour.
3217
4204
  languages !== null && languages.rows.length > 0
3218
- ? (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 }))
3219
4206
  : null,
3220
4207
  // Fills the rest of the column: at the overview's height it ends level with the left
3221
4208
  // card, and its body scrolls rather than growing past it.
3222
- (0, primitives_11.h)(primitives_11.Section, {
4209
+ (0, primitives_13.h)(primitives_13.Section, {
3223
4210
  title: '解析质量与溯源',
3224
4211
  style: { flex: '1 1 auto', minHeight: 0 },
3225
4212
  bodyStyle: { maxHeight: 'none', flex: '1 1 auto', minHeight: 0 },
3226
4213
  scroll: true,
3227
- meta: (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '11px' } }, (0, capabilities_2.provenanceCollapsed)(report.graphQuality ?? null)),
3228
- }, (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, {
3229
4216
  quality: report.graphQuality ?? null,
3230
4217
  provenance: report.provenance ?? null,
4218
+ compatibility: report.compatibility ?? null,
3231
4219
  })))),
3232
4220
  // The graph is the index's own shape, not a derived statistic: it belongs directly
3233
4221
  // under the overview, before the caveats that explain its omissions.
3234
4222
  graph === undefined || !indexed
3235
4223
  ? null
3236
- : (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, {
3237
4225
  wide: true,
3238
4226
  title: '图视图',
3239
- 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
3240
4228
  ? graph.routeState === 'missing'
3241
4229
  ? '宿主未挂载图路由'
3242
4230
  : graph.loading
3243
4231
  ? '读取中…'
3244
- : '暂无图数据'
4232
+ : graphVisible
4233
+ ? '暂无图数据'
4234
+ : '滚动到此处加载'
3245
4235
  : `${graph.projection.view === 'communities' ? '社区聚合' : '单个社区'} · ` +
3246
4236
  `上限 ${graph.projection.budget.nodes} 节点 / ${graph.projection.budget.edges} 边`),
3247
- }, (0, primitives_11.h)(graph_1.GraphSection, { state: graph })),
4237
+ }, (0, primitives_13.h)(graph_1.GraphSection, { state: graph }))),
3248
4238
  // Caveats, in one card, sharing a row with the provenance card.
3249
4239
  //
3250
4240
  // `notable` and `contextWarnings` were two cards — one amber, one an untitled
@@ -3253,13 +4243,13 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3253
4243
  // check two boxes to answer one question, and the untitled one had no heading
3254
4244
  // to say what it was.
3255
4245
  notable.length > 0 || contextWarnings.length > 0
3256
- ? (0, primitives_11.h)(primitives_11.Section, {
4246
+ ? (0, primitives_13.h)(primitives_13.Section, {
3257
4247
  // Full width: a list of warnings would otherwise pair with whatever card
3258
4248
  // happened to follow and leave a hole in the other column.
3259
4249
  wide: true,
3260
4250
  title: `需要注意 · ${notable.length + contextWarnings.length}`,
3261
4251
  scroll: 300,
3262
- }, notable.map((warning, index) => (0, primitives_11.h)('div', {
4252
+ }, notable.map((warning, index) => (0, primitives_13.h)('div', {
3263
4253
  key: `${warning.code}-${index}`,
3264
4254
  style: {
3265
4255
  display: 'flex',
@@ -3270,45 +4260,45 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3270
4260
  background: 'color-mix(in srgb, #d9a441 9%, transparent)',
3271
4261
  borderRadius: '6px',
3272
4262
  },
3273
- }, (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))),
3274
4264
  // The milder ones stay quiet rows: giving every item an amber box would
3275
4265
  // make a stale-index count look as urgent as a truncated activity window.
3276
4266
  contextWarnings.length === 0
3277
4267
  ? null
3278
- : (0, primitives_11.h)('div', {
4268
+ : (0, primitives_13.h)('div', {
3279
4269
  style: {
3280
4270
  display: 'flex',
3281
4271
  flexDirection: 'column',
3282
4272
  gap: '4px',
3283
- ...primitives_11.MUTED,
3284
- ...(notable.length > 0 ? { borderTop: primitives_11.HAIRLINE, paddingTop: '8px' } : {}),
4273
+ ...primitives_13.MUTED,
4274
+ ...(notable.length > 0 ? { borderTop: primitives_13.HAIRLINE, paddingTop: '8px' } : {}),
3285
4275
  },
3286
- }, 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}`))))
3287
4277
  : null,
3288
4278
  // Wide: file paths must keep their tails.
3289
4279
  largeFiles.length > 0
3290
- ? (0, primitives_11.h)(primitives_11.Section, {
4280
+ ? (0, primitives_13.h)(primitives_13.Section, {
3291
4281
  wide: true,
3292
- title: `被跳过的超大文件 · ${largeFiles.length} 个 · ${(0, format_10.fileSize)(totals.largeBytes)}`,
4282
+ title: `被跳过的超大文件 · ${largeFiles.length} 个 · ${(0, format_11.fileSize)(totals.largeBytes)}`,
3293
4283
  // A list of up to 40 paths would push every card below it off screen; the
3294
4284
  // platform's own panels cap long lists and scroll inside the card.
3295
4285
  scroll: 280,
3296
- }, (0, primitives_11.h)(large_files_1.LargeFileList, { files: largeFiles.slice(0, 40) }), largeFiles.length > 40
3297
- ? (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} 个未显示`)
3298
4288
  : null)
3299
4289
  : null, report.activity === undefined || report.activity === null
3300
4290
  ? null
3301
- : (0, primitives_11.h)(primitives_11.Section, {
4291
+ : (0, primitives_13.h)(primitives_13.Section, {
3302
4292
  wide: true,
3303
4293
  // The header names the window the reader is actually looking at (the
3304
4294
  // timeline's 7-day default), not the 90-day fetch window — the two used to
3305
4295
  // disagree, so the card read "近 90 天" above a list of seven days.
3306
4296
  title: `Git 活动 · 近 ${activityDays} 天`,
3307
- 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' } },
3308
4298
  // The range control belongs to the card, not to the folded timeline: it
3309
4299
  // scopes every number on the card, and it should be reachable without
3310
4300
  // opening the list.
3311
- (0, primitives_11.h)('select', {
4301
+ (0, primitives_13.h)('select', {
3312
4302
  value: String(activityDays),
3313
4303
  'aria-label': 'Git 活动窗口',
3314
4304
  onChange: (event) => setActivityDays(Number(event.target.value)),
@@ -3316,35 +4306,35 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3316
4306
  font: 'inherit',
3317
4307
  fontSize: '11px',
3318
4308
  padding: '1px 4px',
3319
- border: primitives_11.HAIRLINE,
4309
+ border: primitives_13.HAIRLINE,
3320
4310
  borderRadius: '4px',
3321
- background: primitives_11.SOFT_BG,
4311
+ background: primitives_13.SOFT_BG,
3322
4312
  color: 'inherit',
3323
4313
  },
3324
- }, [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, {
3325
4315
  state: (0, activity_1.activityView)(report.activity)?.verdicts.find((v) => v.key === 'missing')?.tone ?? 'done',
3326
4316
  }), (0, activity_1.activityCollapsed)(report.activity)),
3327
4317
  // Timeline + cross-tab + baseline is the longest card in the panel.
3328
4318
  scroll: 560,
3329
- }, (0, primitives_11.h)(activity_1.MemoizedGitActivitySection, {
4319
+ }, (0, primitives_13.h)(activity_1.MemoizedGitActivitySection, {
3330
4320
  activity: report.activity,
3331
4321
  projectPath,
3332
4322
  days: activityDays,
3333
4323
  onDaysChange: setActivityDays,
3334
4324
  })),
3335
4325
  // Wide, and always open: the commands are the action this panel offers.
3336
- maintenance === null || maintenance === undefined
3337
- ? null
3338
- : (0, primitives_11.h)(primitives_11.Section, {
4326
+ maintenanceSection !== null
4327
+ ? (0, primitives_13.h)(primitives_13.Section, {
3339
4328
  wide: true,
3340
4329
  title: '维护命令',
3341
- meta: (0, primitives_11.h)('span', { style: { ...primitives_11.MUTED, fontSize: '12px' } }, `${maintenance.commands?.length ?? 0} 条 · ` +
3342
- (maintenance.supportsForce === true
4330
+ meta: (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '12px' } }, `${maintenanceSection.commands?.length ?? 0} 条 · ` +
4331
+ (maintenanceSection.supportsForce === true
3343
4332
  ? '均可直接执行,也可复制到会话'
3344
4333
  : '重启 DSH 后可执行;当前仅可复制') +
3345
4334
  '(沙箱与提权由 DSH 处理)'),
3346
4335
  scroll: 360,
3347
- }, (0, primitives_11.h)(maintenance_1.MaintenanceSection, { maintenance, 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 }))
4337
+ : null);
3348
4338
  }
3349
4339
  /**
3350
4340
  * Stateful wrapper: resolves the current project, loads its coverage.
@@ -3356,11 +4346,36 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3356
4346
  * @param {{params?: {repo?: string}, sessionId?: string, useWorkspaces?: Function}} props
3357
4347
  */
3358
4348
  function CoveragePanel(props) {
3359
- const projectPath = (0, navigation_3.useProjectPath)(props);
4349
+ // The path the operator opened. It is the search root for the project list; the
4350
+ // active project may be the workspace itself or one of its descendants.
4351
+ const workspacePath = (0, navigation_3.useProjectPath)(props);
3360
4352
  const sessionId = typeof props.sessionId === 'string' ? props.sessionId : null;
3361
- const [report, setReport] = React.useState(null);
3362
- const [maintenance, setMaintenance] = React.useState(null);
3363
- const [error, setError] = React.useState(null);
4353
+ const [rawReport, setRawReport] = React.useState(null);
4354
+ const [rawMaintenance, setRawMaintenance] = React.useState(null);
4355
+ const [rawError, setRawError] = React.useState(null);
4356
+ /** The project the current report belongs to. */
4357
+ const [reportProject, setReportProject] = React.useState(null);
4358
+ const [projectList, setProjectList] = React.useState(null);
4359
+ /** The workspace `projectList` was resolved for. Guards against a workspace switch. */
4360
+ const [projectListFor, setProjectListFor] = React.useState(null);
4361
+ const [activeProject, setActiveProject] = React.useState(null);
4362
+ const [projectsError, setProjectsError] = React.useState(null);
4363
+ const [resolving, setResolving] = React.useState(false);
4364
+ /**
4365
+ * Bumped after the project list resolves, so the coverage effect waits for it.
4366
+ * Without this, a manual refresh would read coverage for the old active project
4367
+ * while the resolve was still deciding whether that project still exists.
4368
+ */
4369
+ const [loadToken, setLoadToken] = React.useState(0);
4370
+ const projectsReady = projectListFor === workspacePath;
4371
+ const visibleProjects = projectsReady ? projectList?.projects ?? [] : undefined;
4372
+ const projectPath = activeProject ?? workspacePath;
4373
+ const reportVisible = reportProject === projectPath;
4374
+ const report = reportVisible ? rawReport : null;
4375
+ const maintenance = reportVisible ? rawMaintenance : null;
4376
+ const error = reportVisible ? rawError : null;
4377
+ /** The graph is only meaningful for an index; a scan-only project has no projection. */
4378
+ const graphAvailable = report?.repo?.indexed === true;
3364
4379
  const [busy, setBusy] = React.useState(false);
3365
4380
  const [progress, setProgress] = React.useState(null);
3366
4381
  const [reloadToken, setReloadToken] = React.useState(0);
@@ -3388,6 +4403,8 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3388
4403
  // "not run yet" rather than as a failure.
3389
4404
  const [run, setRun] = React.useState(null);
3390
4405
  const [runError, setRunError] = React.useState(null);
4406
+ /** Which project the run status belongs to; a switch must not show it elsewhere. */
4407
+ const [runProject, setRunProject] = React.useState(null);
3391
4408
  /** The Tier 0 verdict, or null while unknown (no route on this host, or not read yet). */
3392
4409
  const [delta, setDelta] = React.useState(null);
3393
4410
  const [deltaError, setDeltaError] = React.useState(null);
@@ -3398,6 +4415,8 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3398
4415
  * gate and no explanation. Same lesson as the graph card: absent is a state to name.
3399
4416
  */
3400
4417
  const [deltaRoute, setDeltaRoute] = React.useState('unknown');
4418
+ /** Which project the current gate reading belongs to, so a switch can clear it. */
4419
+ const deltaRepoRef = React.useRef(null);
3401
4420
  /** The bounded graph projection, its scope, and the load state around it. */
3402
4421
  const [graphProjection, setGraphProjection] = React.useState(null);
3403
4422
  const [graphLoading, setGraphLoading] = React.useState(false);
@@ -3411,8 +4430,14 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3411
4430
  */
3412
4431
  const [graphRoute, setGraphRoute] = React.useState('unknown');
3413
4432
  const [graphScope, setGraphScope] = React.useState(null);
4433
+ const [graphMode, setGraphMode] = React.useState('communities');
3414
4434
  const [graphReload, setGraphReload] = React.useState(0);
3415
- const running = run?.state === 'running';
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), []);
4438
+ /** Which project the current projection belongs to, so a switch can clear it. */
4439
+ const graphRepoRef = React.useRef(null);
4440
+ const running = run?.state === 'running' && runProject === projectPath;
3416
4441
  /**
3417
4442
  * Poll while a run is in flight.
3418
4443
  *
@@ -3433,6 +4458,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3433
4458
  const next = body.run ?? null;
3434
4459
  // Polls fire every second; only a changed verdict should re-render the report.
3435
4460
  setRun((previous) => (sameRun(previous, next) ? previous : next));
4461
+ setRunProject(projectPath);
3436
4462
  // A finished run changed the index, so the report on screen is now a reading of
3437
4463
  // the previous one. This is the whole reason the run was started.
3438
4464
  if (next !== null && next.state !== 'running')
@@ -3454,6 +4480,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3454
4480
  if (projectPath === null)
3455
4481
  return;
3456
4482
  setRunError(null);
4483
+ setRunProject(projectPath);
3457
4484
  try {
3458
4485
  // One request, three modes. Sending them as flags rather than a body keeps the
3459
4486
  // operation visible in the URL the host logs alongside the command it ran.
@@ -3463,6 +4490,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3463
4490
  if (!body?.ok)
3464
4491
  throw new Error(body?.message ?? `请求失败 (${res.status})`);
3465
4492
  setRun(body.run ?? null);
4493
+ setRunProject(projectPath);
3466
4494
  }
3467
4495
  catch (cause) {
3468
4496
  setRunError(String(cause?.message ?? cause));
@@ -3471,6 +4499,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3471
4499
  const cancelRun = React.useCallback(async () => {
3472
4500
  if (projectPath === null)
3473
4501
  return;
4502
+ setRunProject(projectPath);
3474
4503
  try {
3475
4504
  const res = await fetch((0, navigation_3.routeUrl)(`/api/gitnexus/analyze?repo=${encodeURIComponent(projectPath)}`), {
3476
4505
  method: 'DELETE',
@@ -3717,13 +4746,93 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3717
4746
  void startRun('refresh');
3718
4747
  }, [installRun, projectPath, report, startRun]);
3719
4748
  React.useEffect(() => {
3720
- if (projectPath === null)
4749
+ if (workspacePath === null)
3721
4750
  return undefined;
3722
4751
  const controller = new AbortController();
4752
+ setResolving(true);
4753
+ setProjectsError(null);
4754
+ // A workspace switch must not leave the previous project's report on screen. A
4755
+ // refresh of the same workspace keeps it readable while the list is re-read.
4756
+ if (projectListFor !== workspacePath) {
4757
+ setRawReport(null);
4758
+ setRawMaintenance(null);
4759
+ setRawError(null);
4760
+ setReadAt(null);
4761
+ setReportProject(null);
4762
+ setActiveProject(null);
4763
+ }
4764
+ void (async () => {
4765
+ try {
4766
+ const res = await fetch((0, navigation_3.routeUrl)(`/api/gitnexus/projects?root=${encodeURIComponent(workspacePath)}`), { signal: controller.signal });
4767
+ const body = (await res.json().catch(() => null));
4768
+ if (!res.ok || body?.ok !== true || !Array.isArray(body.projects)) {
4769
+ throw new Error(body?.message ?? `读取项目列表失败 (${res.status})`);
4770
+ }
4771
+ if (controller.signal.aborted)
4772
+ return;
4773
+ const list = {
4774
+ root: workspacePath,
4775
+ projects: body.projects,
4776
+ visited: typeof body.visited === 'number' ? body.visited : 0,
4777
+ truncated: body.truncated === true,
4778
+ };
4779
+ setProjectList(list);
4780
+ setProjectListFor(workspacePath);
4781
+ const remembered = readRememberedProject(workspacePath);
4782
+ const next = chooseProject(list.projects, workspacePath, remembered);
4783
+ setActiveProject(next);
4784
+ if (remembered !== null && next !== remembered && list.truncated !== true) {
4785
+ // The remembered project disappeared; persist the fallback so the next open
4786
+ // does not look for it again. A truncated walk never proves it disappeared:
4787
+ // it may simply not have reached it before the bound, so keep the preference
4788
+ // and let a later complete resolve restore it.
4789
+ rememberProject(workspacePath, next);
4790
+ }
4791
+ setLoadToken((n) => n + 1);
4792
+ }
4793
+ catch (cause) {
4794
+ if (controller.signal.aborted)
4795
+ return;
4796
+ setProjectsError(String(cause?.message ?? cause));
4797
+ // First load: fall back to scanning the workspace. A refresh keeps the list
4798
+ // already on screen; a transient resolve failure should not remove it.
4799
+ if (projectListFor !== workspacePath) {
4800
+ setProjectList({ root: workspacePath, projects: [], visited: 0, truncated: false });
4801
+ setProjectListFor(workspacePath);
4802
+ setActiveProject(workspacePath);
4803
+ }
4804
+ setLoadToken((n) => n + 1);
4805
+ }
4806
+ finally {
4807
+ if (!controller.signal.aborted)
4808
+ setResolving(false);
4809
+ }
4810
+ })();
4811
+ return () => controller.abort();
4812
+ }, [workspacePath, reloadToken]);
4813
+ React.useEffect(() => {
4814
+ if (activeProject === null || workspacePath === null)
4815
+ return undefined;
4816
+ // Wait until the resolve has produced the list for this workspace. A manual
4817
+ // refresh bumps loadToken after the resolve, so coverage never reads the old
4818
+ // active project while the list is still being decided.
4819
+ if (projectListFor !== workspacePath || resolving)
4820
+ return undefined;
4821
+ const controller = new AbortController();
4822
+ // A project switch must not leave the previous report, maintenance payload or
4823
+ // read time under the new path. A refresh of the same project keeps them readable
4824
+ // while the new reading is fetched.
4825
+ if (reportProject !== activeProject) {
4826
+ setRawReport(null);
4827
+ setRawMaintenance(null);
4828
+ setRawError(null);
4829
+ setReadAt(null);
4830
+ }
3723
4831
  setBusy(true);
3724
- setError(null);
4832
+ setRawError(null);
4833
+ setReportProject(activeProject);
3725
4834
  setProgress({ percent: 0, label: '连接索引服务…' });
3726
- const query = `/api/gitnexus/coverage?repo=${encodeURIComponent(projectPath)}` +
4835
+ const query = `/api/gitnexus/coverage?repo=${encodeURIComponent(activeProject)}` +
3727
4836
  (wantsFresh ? '&refresh=1' : '') +
3728
4837
  '&stream=1';
3729
4838
  const load = async () => {
@@ -3781,8 +4890,8 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3781
4890
  throw new Error('读取中断:没有收到完整报告。');
3782
4891
  if (controller.signal.aborted)
3783
4892
  return;
3784
- setReport(payload.report);
3785
- setMaintenance(payload.maintenance ?? null);
4893
+ setRawReport(payload.report);
4894
+ setRawMaintenance(payload.maintenance ?? null);
3786
4895
  setReadAt(typeof payload.readAt === 'string' ? payload.readAt : new Date().toISOString());
3787
4896
  return;
3788
4897
  }
@@ -3795,16 +4904,16 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3795
4904
  // point of leading with the initialize command is that reaching it is
3796
4905
  // expected on a project that has never been analysed.
3797
4906
  if (res.status === 404 && body?.code === 'not-indexed') {
3798
- setReport({ repo: { indexed: false, path: projectPath } });
3799
- setMaintenance(null);
4907
+ setRawReport({ repo: { indexed: false, path: activeProject } });
4908
+ setRawMaintenance(null);
3800
4909
  setReadAt(body?.readAt ?? new Date().toISOString());
3801
4910
  return;
3802
4911
  }
3803
4912
  if (!body?.ok || body.report === undefined) {
3804
4913
  throw new Error(body?.message ?? `请求失败 (${res.status})`);
3805
4914
  }
3806
- setReport(body.report);
3807
- setMaintenance(body.maintenance ?? null);
4915
+ setRawReport(body.report);
4916
+ setRawMaintenance(body.maintenance ?? null);
3808
4917
  setReadAt(typeof body.readAt === 'string' ? body.readAt : new Date().toISOString());
3809
4918
  }
3810
4919
  catch (cause) {
@@ -3813,7 +4922,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3813
4922
  const error = cause;
3814
4923
  if (error?.name === 'AbortError')
3815
4924
  return;
3816
- setError(String(error?.message ?? cause));
4925
+ setRawError(String(error?.message ?? cause));
3817
4926
  }
3818
4927
  finally {
3819
4928
  if (!controller.signal.aborted) {
@@ -3824,7 +4933,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3824
4933
  };
3825
4934
  void load();
3826
4935
  return () => controller.abort();
3827
- }, [projectPath, reloadToken]);
4936
+ }, [activeProject, projectListFor, workspacePath, loadToken, resolving]);
3828
4937
  /**
3829
4938
  * Read the Tier 0 verdict whenever the project or the index changes.
3830
4939
  *
@@ -3837,10 +4946,15 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3837
4946
  * changed; there is no polling here for the same reason the run poll is conditional.
3838
4947
  */
3839
4948
  React.useEffect(() => {
3840
- if (projectPath === null) {
4949
+ // A gate read for the previous project must not be shown under the new one. A
4950
+ // refresh of the same project keeps the readable verdict while it is re-read.
4951
+ if (deltaRepoRef.current !== projectPath) {
4952
+ deltaRepoRef.current = projectPath;
3841
4953
  setDelta(null);
3842
4954
  setDeltaError(null);
3843
4955
  setDeltaRoute('unknown');
4956
+ }
4957
+ if (projectPath === null) {
3844
4958
  return undefined;
3845
4959
  }
3846
4960
  const controller = new AbortController();
@@ -3878,11 +4992,6 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3878
4992
  })();
3879
4993
  return () => controller.abort();
3880
4994
  }, [projectPath, reloadToken]);
3881
- // A different project means the previous drill-down is another repository's
3882
- // community: keeping the scope would ask the new project for an id that cannot exist.
3883
- React.useEffect(() => {
3884
- setGraphScope(null);
3885
- }, [projectPath]);
3886
4995
  /**
3887
4996
  * Read the graph projection for the current scope.
3888
4997
  *
@@ -3895,14 +5004,46 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3895
5004
  */
3896
5005
  React.useEffect(() => {
3897
5006
  if (projectPath === null) {
5007
+ graphRepoRef.current = null;
3898
5008
  setGraphProjection(null);
3899
5009
  setGraphError(null);
5010
+ setGraphLoading(false);
5011
+ setGraphScope(null);
5012
+ setGraphMode('communities');
5013
+ return undefined;
5014
+ }
5015
+ const projectChanged = graphRepoRef.current !== projectPath;
5016
+ if (projectChanged) {
5017
+ graphRepoRef.current = projectPath;
5018
+ setGraphProjection(null);
5019
+ setGraphError(null);
5020
+ // The previous scope is another repository's community. Clearing it makes the
5021
+ // next render fetch the overview instead of asking the new project for an id it
5022
+ // cannot have.
5023
+ setGraphScope(null);
5024
+ setGraphMode('communities');
5025
+ }
5026
+ if (!graphAvailable) {
5027
+ setGraphProjection(null);
5028
+ setGraphError(null);
5029
+ setGraphLoading(false);
5030
+ return undefined;
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;
5036
+ if (projectChanged && graphScope !== null) {
5037
+ // The setGraphScope(null) above re-runs this effect; do not also fire a request
5038
+ // for the old scope against the new project.
3900
5039
  return undefined;
3901
5040
  }
3902
5041
  const controller = new AbortController();
3903
5042
  setGraphLoading(true);
3904
- const view = graphScope === null ? 'communities' : 'community';
3905
- const suffix = graphScope === null ? '' : `&id=${encodeURIComponent(graphScope)}`;
5043
+ setGraphError(null);
5044
+ const effectiveScope = projectChanged ? null : graphScope;
5045
+ const view = effectiveScope === null ? graphMode : 'community';
5046
+ const suffix = effectiveScope === null ? '' : `&id=${encodeURIComponent(effectiveScope)}`;
3906
5047
  void (async () => {
3907
5048
  try {
3908
5049
  const res = await fetch((0, navigation_3.routeUrl)(`/api/gitnexus/graph?repo=${encodeURIComponent(projectPath)}&view=${view}${suffix}`), { signal: controller.signal });
@@ -3940,7 +5081,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3940
5081
  }
3941
5082
  })();
3942
5083
  return () => controller.abort();
3943
- }, [projectPath, reloadToken, graphScope, graphReload]);
5084
+ }, [projectPath, reloadToken, graphScope, graphMode, graphReload, graphAvailable, graphVisible]);
3944
5085
  /**
3945
5086
  * Open a symbol node, with the same fallbacks the search hits use.
3946
5087
  *
@@ -3979,18 +5120,32 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
3979
5120
  setGraphError(`当前宿主没有可用的文件打开能力;位置:${text}`);
3980
5121
  }
3981
5122
  }, [sessionId]);
3982
- const fallbackMaintenance = React.useMemo(() => (maintenance === null && projectPath !== null ? { commands: [], rationale: [] } : maintenance), [maintenance, projectPath]);
3983
- return (0, primitives_11.h)(CoverageView, {
5123
+ const selectProject = React.useCallback((path) => {
5124
+ if (workspacePath === null)
5125
+ return;
5126
+ setActiveProject(path);
5127
+ setProjectsError(null);
5128
+ rememberProject(workspacePath, path);
5129
+ }, [workspacePath]);
5130
+ const visibleRun = runProject === projectPath ? run : null;
5131
+ const visibleRunError = runProject === projectPath ? runError : null;
5132
+ return (0, primitives_13.h)(CoverageView, {
3984
5133
  report,
3985
- maintenance: fallbackMaintenance,
5134
+ maintenance,
3986
5135
  projectPath,
3987
- context: projectPath === null ? (0, navigation_3.contextDiagnostic)(props) : null,
3988
- busy,
3989
- progress,
5136
+ workspacePath,
5137
+ projects: visibleProjects,
5138
+ projectsVisited: projectsReady ? projectList?.visited ?? 0 : undefined,
5139
+ projectsTruncated: projectsReady ? projectList?.truncated === true : undefined,
5140
+ projectsError: projectsReady ? projectsError : null,
5141
+ onSelectProject: selectProject,
5142
+ context: workspacePath === null ? (0, navigation_3.contextDiagnostic)(props) : null,
5143
+ busy: busy || resolving,
5144
+ progress: resolving ? { percent: 0, label: '正在查找项目索引…' } : progress,
3990
5145
  error,
3991
5146
  readAt,
3992
- run,
3993
- runError,
5147
+ run: visibleRun,
5148
+ runError: visibleRunError,
3994
5149
  delta,
3995
5150
  deltaError,
3996
5151
  deltaRoute,
@@ -4037,18 +5192,72 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4037
5192
  error: graphError,
4038
5193
  routeState: graphRoute,
4039
5194
  scope: graphScope,
4040
- onOpenCommunity: (id) => setGraphScope(id),
4041
- 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
+ },
4042
5207
  onReload: () => setGraphReload((n) => n + 1),
4043
5208
  onOpenSymbol: openGraphNode,
4044
5209
  },
5210
+ onGraphNear,
5211
+ graphVisible,
4045
5212
  });
4046
5213
  }
4047
5214
  /** The tab chip's live title. */
4048
5215
  function CoverageTitle() {
4049
- return (0, primitives_11.h)('span', null, 'GitNexus');
5216
+ return (0, primitives_13.h)('span', null, 'GitNexus');
4050
5217
  }
4051
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
+ }
5258
+ }
5259
+ exports.PanelBoundary = PanelBoundary;
5260
+ });
4052
5261
  /**
4053
5262
  * dsh-gitnexus-plus — browser half.
4054
5263
  *
@@ -4088,7 +5297,7 @@ define("views/panel", ["require", "exports", "react", "@deepseek-ai/dsh-client-u
4088
5297
  * index actually represent?", the warning block says what was dropped, and the
4089
5298
  * file list says whether what was dropped is source or build output.
4090
5299
  */
4091
- 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) {
4092
5301
  "use strict";
4093
5302
  Object.defineProperty(exports, "__esModule", { value: true });
4094
5303
  exports.__internals = exports.inject = void 0;
@@ -4153,8 +5362,26 @@ define("index", ["require", "exports", "format", "navigation", "primitives", "se
4153
5362
  (0, navigation_4.bindSidebarRight)(svcCtx.sidebarRight);
4154
5363
  });
4155
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(', '));
4156
5383
  safe('sidebar.right.pane.tab', () => {
4157
- 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));
4158
5385
  });
4159
5386
  safe('sidebar.right.pane.tab.title', () => {
4160
5387
  ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: PLUGIN_ID }, panel_1.CoverageTitle));
@@ -4166,41 +5393,47 @@ define("index", ["require", "exports", "format", "navigation", "primitives", "se
4166
5393
  TAB_KIND,
4167
5394
  routeUrl: navigation_4.routeUrl,
4168
5395
  fileResourceAddress: navigation_4.fileResourceAddress,
4169
- formatBytes: format_11.formatBytes,
4170
- fileSize: format_11.fileSize,
4171
- percent: format_11.percent,
4172
- stackSegments: format_11.stackSegments,
4173
- splitWarnings: format_11.splitWarnings,
4174
- summaryLine: format_11.summaryLine,
4175
- coverageRatio: format_11.coverageRatio,
4176
- healthState: format_11.healthState,
4177
- stalenessView: format_11.stalenessView,
4178
- capabilityRows: format_11.capabilityRows,
4179
- 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,
4180
5407
  activityView: activity_2.activityView,
4181
5408
  highlightLine: highlight_2.highlightLine,
4182
5409
  languageOf: highlight_2.languageOf,
4183
5410
  communityColors: graph_2.communityColors,
4184
- compactNumber: format_11.compactNumber,
5411
+ buildGraph3DLayout: graph3d_2.buildGraph3DLayout,
5412
+ graph3DPath: graph3d_2.graph3DPath,
5413
+ Graph3DView: graph3d_2.Graph3DView,
5414
+ compactNumber: format_12.compactNumber,
4185
5415
  useProjectPath: navigation_4.useProjectPath,
4186
5416
  contextDiagnostic: navigation_4.contextDiagnostic,
5417
+ chooseProject: panel_1.chooseProject,
4187
5418
  CoveragePanel: panel_1.CoveragePanel,
4188
5419
  CoverageView: panel_1.CoverageView,
4189
5420
  CoverageTitle: panel_1.CoverageTitle,
4190
5421
  LanguageBar: languages_2.LanguageBar,
4191
5422
  LanguageSection: languages_2.LanguageSection,
4192
5423
  LargeFileList: large_files_2.LargeFileList,
4193
- Ring: primitives_12.Ring,
4194
- MetricGrid: primitives_12.MetricGrid,
4195
- Histogram: primitives_12.Histogram,
5424
+ Ring: primitives_15.Ring,
5425
+ MetricGrid: primitives_15.MetricGrid,
5426
+ Histogram: primitives_15.Histogram,
4196
5427
  GitActivitySection: activity_2.GitActivitySection,
4197
5428
  FileHistorySection: file_history_2.FileHistorySection,
4198
5429
  ProvenanceBlock: capabilities_3.ProvenanceBlock,
4199
5430
  MaintenanceSection: maintenance_2.MaintenanceSection,
5431
+ WorkspaceOverview: workspace_2.WorkspaceOverview,
5432
+ aggregateProjects: workspace_2.aggregateProjects,
4200
5433
  CapabilityStrip: capabilities_3.CapabilityStrip,
4201
- Section: primitives_12.Section,
4202
- classifyFileType: primitives_12.classifyFileType,
4203
- IconRefreshOutlineRegular: primitives_12.IconRefreshOutlineRegular,
5434
+ Section: primitives_15.Section,
5435
+ classifyFileType: primitives_15.classifyFileType,
5436
+ IconRefreshOutlineRegular: primitives_15.IconRefreshOutlineRegular,
4204
5437
  };
4205
5438
  });
4206
5439