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.
- package/README-CN.md +18 -7
- package/README.md +71 -12
- package/lib/client.js +1584 -351
- package/lib/gitnexus/analyze.d.ts +6 -0
- package/lib/gitnexus/analyze.d.ts.map +1 -1
- package/lib/gitnexus/analyze.js +10 -0
- package/lib/gitnexus/analyze.js.map +1 -1
- package/lib/gitnexus/commands.d.ts +11 -1
- package/lib/gitnexus/commands.d.ts.map +1 -1
- package/lib/gitnexus/commands.js +27 -8
- package/lib/gitnexus/commands.js.map +1 -1
- package/lib/gitnexus/coverage.d.ts +28 -0
- package/lib/gitnexus/coverage.d.ts.map +1 -1
- package/lib/gitnexus/coverage.js +64 -2
- package/lib/gitnexus/coverage.js.map +1 -1
- package/lib/gitnexus/delta.d.ts +25 -0
- package/lib/gitnexus/delta.d.ts.map +1 -1
- package/lib/gitnexus/delta.js +86 -3
- package/lib/gitnexus/delta.js.map +1 -1
- package/lib/gitnexus/discover.d.ts +91 -0
- package/lib/gitnexus/discover.d.ts.map +1 -0
- package/lib/gitnexus/discover.js +214 -0
- package/lib/gitnexus/discover.js.map +1 -0
- package/lib/gitnexus/graph.d.ts +18 -1
- package/lib/gitnexus/graph.d.ts.map +1 -1
- package/lib/gitnexus/graph.js +70 -4
- package/lib/gitnexus/graph.js.map +1 -1
- package/lib/gitnexus/index-meta.d.ts +34 -6
- package/lib/gitnexus/index-meta.d.ts.map +1 -1
- package/lib/gitnexus/index-meta.js +47 -7
- package/lib/gitnexus/index-meta.js.map +1 -1
- package/lib/gitnexus/install.d.ts +11 -0
- package/lib/gitnexus/install.d.ts.map +1 -1
- package/lib/gitnexus/install.js +65 -9
- package/lib/gitnexus/install.js.map +1 -1
- package/lib/gitnexus/search.d.ts +13 -0
- package/lib/gitnexus/search.d.ts.map +1 -1
- package/lib/gitnexus/search.js +2 -0
- package/lib/gitnexus/search.js.map +1 -1
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +27 -7
- package/lib/index.js.map +1 -1
- package/lib/routes/analyze.d.ts +5 -4
- package/lib/routes/analyze.d.ts.map +1 -1
- package/lib/routes/analyze.js +17 -4
- package/lib/routes/analyze.js.map +1 -1
- package/lib/routes/coverage.d.ts +16 -10
- package/lib/routes/coverage.d.ts.map +1 -1
- package/lib/routes/coverage.js +31 -22
- package/lib/routes/coverage.js.map +1 -1
- package/lib/routes/delta.js +8 -8
- package/lib/routes/delta.js.map +1 -1
- package/lib/routes/graph.d.ts +1 -1
- package/lib/routes/graph.js +2 -2
- package/lib/routes/graph.js.map +1 -1
- package/lib/routes/guard.d.ts +3 -6
- package/lib/routes/guard.d.ts.map +1 -1
- package/lib/routes/guard.js +3 -6
- package/lib/routes/guard.js.map +1 -1
- package/lib/routes/projects.d.ts +21 -0
- package/lib/routes/projects.d.ts.map +1 -0
- package/lib/routes/projects.js +61 -0
- package/lib/routes/projects.js.map +1 -0
- 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/
|
|
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,
|
|
2141
|
-
? (0,
|
|
2142
|
-
: (0,
|
|
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,
|
|
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,
|
|
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,
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
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,
|
|
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: '
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
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
|
-
},
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
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
|
-
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
:
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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,
|
|
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,
|
|
2249
|
-
? (0,
|
|
2250
|
-
: (0,
|
|
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,
|
|
2262
|
-
? (0,
|
|
2263
|
-
: (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
3024
|
+
return (0, primitives_6.h)(primitives_6.Section, {
|
|
2330
3025
|
title: `语言分布 · ${languages.rows.length} 种`,
|
|
2331
|
-
meta: (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
3193
|
+
border: primitives_8.HAIRLINE,
|
|
2495
3194
|
borderRadius: '6px',
|
|
2496
|
-
background:
|
|
3195
|
+
background: primitives_8.SOFT_BG,
|
|
2497
3196
|
},
|
|
2498
|
-
}, (0,
|
|
2499
|
-
? (0,
|
|
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,
|
|
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,
|
|
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,
|
|
3217
|
+
: (0, primitives_8.h)('details', { style: { marginTop: '2px' } }, (0, primitives_8.h)('summary', {
|
|
2519
3218
|
style: {
|
|
2520
|
-
...
|
|
3219
|
+
...primitives_8.MUTED,
|
|
2521
3220
|
cursor: 'pointer',
|
|
2522
3221
|
fontSize: '12px',
|
|
2523
3222
|
userSelect: 'none',
|
|
2524
3223
|
},
|
|
2525
|
-
}, '查看原始输出'), (0,
|
|
3224
|
+
}, '查看原始输出'), (0, primitives_8.h)('pre', {
|
|
2526
3225
|
style: {
|
|
2527
|
-
...
|
|
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,
|
|
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,
|
|
3243
|
+
? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, `差量检查失败:${deltaError} —— 刷新仍可直接执行。`)
|
|
2545
3244
|
: deltaRoute === 'missing'
|
|
2546
|
-
? (0,
|
|
3245
|
+
? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, '当前宿主未挂载差量路由:重启 DSH 后会显示"是否需要刷新"的依据;刷新按钮仍可使用。')
|
|
2547
3246
|
: gateKnown
|
|
2548
3247
|
? gateClean
|
|
2549
|
-
? (0,
|
|
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,
|
|
3260
|
+
? (0, primitives_8.h)('div', { style: primitives_8.MUTED }, `${excludedCount} 个改动属索引排除范围(超大文件等),刷新也不会收录,因此不计入刷新依据。`)
|
|
2562
3261
|
: null)
|
|
2563
|
-
: (0,
|
|
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:
|
|
3268
|
+
border: primitives_8.HAIRLINE,
|
|
2570
3269
|
borderRadius: '6px',
|
|
2571
|
-
background:
|
|
3270
|
+
background: primitives_8.SOFT_BG,
|
|
2572
3271
|
},
|
|
2573
|
-
}, (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,
|
|
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
|
-
...
|
|
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,
|
|
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,
|
|
2596
|
-
|
|
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
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
:
|
|
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,
|
|
3360
|
+
? (0, primitives_8.h)(primitives_8.Button, { variant: 'ghost', disabled: running || !hostCurrent, onClick: () => setConfirming('repair-fts') }, running ? '运行中' : '修复搜索索引')
|
|
2640
3361
|
: hostCurrent
|
|
2641
|
-
? (0,
|
|
2642
|
-
: (0,
|
|
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,
|
|
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
|
-
...
|
|
3369
|
+
...primitives_8.styles.mono,
|
|
2649
3370
|
display: 'block',
|
|
2650
3371
|
padding: '6px 8px',
|
|
2651
|
-
border:
|
|
3372
|
+
border: primitives_8.HAIRLINE,
|
|
2652
3373
|
borderRadius: '6px',
|
|
2653
|
-
background:
|
|
3374
|
+
background: primitives_8.SOFT_BG,
|
|
2654
3375
|
wordBreak: 'break-all',
|
|
2655
3376
|
userSelect: 'all',
|
|
2656
3377
|
},
|
|
2657
|
-
}, command.shell), (0,
|
|
2658
|
-
? (0,
|
|
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,
|
|
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,
|
|
3392
|
+
(0, primitives_8.h)(primitives.RiskConfirmation, {
|
|
2672
3393
|
open: confirming !== null,
|
|
2673
|
-
title: repairConfirm
|
|
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
|
-
|
|
2696
|
-
|
|
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
|
|
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/
|
|
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,
|
|
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,
|
|
3564
|
+
(0, primitives_10.h)('div', { style: { display: 'flex', gap: '12px', alignItems: 'center', minWidth: 0 } }, ratio === null
|
|
2739
3565
|
? null
|
|
2740
|
-
: (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
2760
|
-
{ label: '跳过合计', value: (0,
|
|
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,
|
|
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,
|
|
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,
|
|
2774
|
-
{ label: '边', value: (0,
|
|
2775
|
-
{ label: '社区', value: (0,
|
|
2776
|
-
{ label: '流程', value: (0,
|
|
2777
|
-
{ label: '向量', value: (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
...
|
|
3666
|
+
...primitives_11.styles.mono,
|
|
2841
3667
|
flex: '1 1 auto',
|
|
2842
3668
|
padding: '6px 8px',
|
|
2843
|
-
border:
|
|
3669
|
+
border: primitives_11.HAIRLINE,
|
|
2844
3670
|
borderRadius: '6px',
|
|
2845
|
-
background:
|
|
3671
|
+
background: primitives_11.SOFT_BG,
|
|
2846
3672
|
wordBreak: 'break-all',
|
|
2847
3673
|
userSelect: 'all',
|
|
2848
3674
|
},
|
|
2849
|
-
}, command), (0,
|
|
2850
|
-
? (0,
|
|
2851
|
-
: (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
3727
|
+
return (0, primitives_12.h)(primitives_12.Section, {
|
|
2902
3728
|
wide: true,
|
|
2903
3729
|
title: '关键词搜索',
|
|
2904
|
-
meta: (0,
|
|
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,
|
|
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:
|
|
3751
|
+
border: primitives_12.HAIRLINE,
|
|
2926
3752
|
borderRadius: '6px',
|
|
2927
|
-
background:
|
|
3753
|
+
background: primitives_12.SOFT_BG,
|
|
2928
3754
|
color: 'inherit',
|
|
2929
3755
|
font: 'inherit',
|
|
2930
3756
|
},
|
|
2931
|
-
}), (0,
|
|
2932
|
-
? (0,
|
|
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
|
-
...
|
|
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,
|
|
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,
|
|
2946
|
-
: (0,
|
|
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,
|
|
3775
|
+
`${search.truncated ? '(已截断)' : ''} · 点击结果打开文件`), search.results.map((hit) => (0, primitives_12.h)('div', {
|
|
2950
3776
|
key: keyOf(hit),
|
|
2951
|
-
style: { ...
|
|
2952
|
-
}, (0,
|
|
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,
|
|
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,
|
|
3806
|
+
}, hit.name), (0, primitives_12.h)('span', {
|
|
2981
3807
|
style: {
|
|
2982
|
-
...
|
|
2983
|
-
...
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
3064
|
-
//
|
|
3065
|
-
//
|
|
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,
|
|
3068
|
-
:
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
3101
|
-
?
|
|
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,
|
|
4077
|
+
meta: (0, primitives_13.h)('span', { style: { ...primitives_13.MUTED, fontSize: '12px' } }, '复制后到会话执行(沙箱与提权由 DSH 处理)'),
|
|
3106
4078
|
scroll: 320,
|
|
3107
|
-
}, (0,
|
|
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,
|
|
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 ??
|
|
3116
|
-
const cap = report.cap ??
|
|
3117
|
-
const analyzer = report.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,
|
|
3121
|
-
const ratio = (0,
|
|
3122
|
-
//
|
|
3123
|
-
//
|
|
3124
|
-
const
|
|
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,
|
|
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,
|
|
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,
|
|
3140
|
-
{ label: '选中文件', value: (0,
|
|
3141
|
-
{ label: '扫描字节', value: (0,
|
|
3142
|
-
{ label: '大小上限', value: cap.text, hint: (0,
|
|
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,
|
|
3145
|
-
?
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
3178
|
-
return (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
3209
|
-
}, (0,
|
|
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,
|
|
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,
|
|
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,
|
|
3228
|
-
}, (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
4268
|
+
: (0, primitives_13.h)('div', {
|
|
3279
4269
|
style: {
|
|
3280
4270
|
display: 'flex',
|
|
3281
4271
|
flexDirection: 'column',
|
|
3282
4272
|
gap: '4px',
|
|
3283
|
-
...
|
|
3284
|
-
...(notable.length > 0 ? { borderTop:
|
|
4273
|
+
...primitives_13.MUTED,
|
|
4274
|
+
...(notable.length > 0 ? { borderTop: primitives_13.HAIRLINE, paddingTop: '8px' } : {}),
|
|
3285
4275
|
},
|
|
3286
|
-
}, contextWarnings.map((warning, index) => (0,
|
|
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,
|
|
4280
|
+
? (0, primitives_13.h)(primitives_13.Section, {
|
|
3291
4281
|
wide: true,
|
|
3292
|
-
title: `被跳过的超大文件 · ${largeFiles.length} 个 · ${(0,
|
|
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,
|
|
3297
|
-
? (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
4309
|
+
border: primitives_13.HAIRLINE,
|
|
3320
4310
|
borderRadius: '4px',
|
|
3321
|
-
background:
|
|
4311
|
+
background: primitives_13.SOFT_BG,
|
|
3322
4312
|
color: 'inherit',
|
|
3323
4313
|
},
|
|
3324
|
-
}, [7, 14, 30].map((days) => (0,
|
|
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,
|
|
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
|
-
|
|
3337
|
-
?
|
|
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,
|
|
3342
|
-
(
|
|
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,
|
|
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
|
-
|
|
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 [
|
|
3362
|
-
const [
|
|
3363
|
-
const [
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
4832
|
+
setRawError(null);
|
|
4833
|
+
setReportProject(activeProject);
|
|
3725
4834
|
setProgress({ percent: 0, label: '连接索引服务…' });
|
|
3726
|
-
const query = `/api/gitnexus/coverage?repo=${encodeURIComponent(
|
|
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
|
-
|
|
3785
|
-
|
|
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
|
-
|
|
3799
|
-
|
|
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
|
-
|
|
3807
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
-
|
|
3905
|
-
const
|
|
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
|
|
3983
|
-
|
|
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
|
|
5134
|
+
maintenance,
|
|
3986
5135
|
projectPath,
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
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) =>
|
|
4041
|
-
|
|
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,
|
|
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,
|
|
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 },
|
|
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:
|
|
4170
|
-
fileSize:
|
|
4171
|
-
percent:
|
|
4172
|
-
stackSegments:
|
|
4173
|
-
splitWarnings:
|
|
4174
|
-
summaryLine:
|
|
4175
|
-
coverageRatio:
|
|
4176
|
-
healthState:
|
|
4177
|
-
stalenessView:
|
|
4178
|
-
capabilityRows:
|
|
4179
|
-
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
|
-
|
|
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:
|
|
4194
|
-
MetricGrid:
|
|
4195
|
-
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:
|
|
4202
|
-
classifyFileType:
|
|
4203
|
-
IconRefreshOutlineRegular:
|
|
5434
|
+
Section: primitives_15.Section,
|
|
5435
|
+
classifyFileType: primitives_15.classifyFileType,
|
|
5436
|
+
IconRefreshOutlineRegular: primitives_15.IconRefreshOutlineRegular,
|
|
4204
5437
|
};
|
|
4205
5438
|
});
|
|
4206
5439
|
|