codebase-onboarder 0.5.0 → 0.7.0
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.md +196 -47
- package/bin/build-if-source.js +23 -0
- package/cli/commands.d.ts +26 -0
- package/cli/commands.js +836 -811
- package/cli/commands.js.map +1 -0
- package/cli/explorer/advanced.d.ts +14 -0
- package/cli/explorer/advanced.js +449 -0
- package/cli/explorer/advanced.js.map +1 -0
- package/cli/explorer/app.d.ts +1 -0
- package/cli/explorer/app.js +396 -229
- package/cli/explorer/app.js.map +1 -0
- package/cli/explorer/assistant.d.ts +9 -0
- package/cli/explorer/assistant.js +58 -0
- package/cli/explorer/assistant.js.map +1 -0
- package/cli/explorer/commands.d.ts +17 -0
- package/cli/explorer/commands.js +400 -162
- package/cli/explorer/commands.js.map +1 -0
- package/cli/explorer/featureViews.d.ts +124 -0
- package/cli/explorer/featureViews.js +179 -0
- package/cli/explorer/featureViews.js.map +1 -0
- package/cli/explorer/github.d.ts +21 -0
- package/cli/explorer/github.js +73 -0
- package/cli/explorer/github.js.map +1 -0
- package/cli/explorer/graphs.d.ts +7 -0
- package/cli/explorer/graphs.js +173 -0
- package/cli/explorer/graphs.js.map +1 -0
- package/cli/explorer/picker.d.ts +22 -0
- package/cli/explorer/picker.js +150 -0
- package/cli/explorer/picker.js.map +1 -0
- package/cli/explorer/report.d.ts +85 -0
- package/cli/explorer/report.js +60 -0
- package/cli/explorer/report.js.map +1 -0
- package/cli/explorer/session.d.ts +449 -0
- package/cli/explorer/session.js +184 -95
- package/cli/explorer/session.js.map +1 -0
- package/cli/explorer/views.d.ts +19 -0
- package/cli/explorer/views.js +496 -412
- package/cli/explorer/views.js.map +1 -0
- package/cli/explorer/wrap.d.ts +1 -0
- package/cli/explorer/wrap.js +33 -0
- package/cli/explorer/wrap.js.map +1 -0
- package/cli/main.d.ts +1 -0
- package/cli/main.js +235 -138
- package/cli/main.js.map +1 -0
- package/cli/prompt.d.ts +5 -0
- package/cli/prompt.js +84 -80
- package/cli/prompt.js.map +1 -0
- package/cli/scanOptions.d.ts +2 -0
- package/cli/scanOptions.js +11 -0
- package/cli/scanOptions.js.map +1 -0
- package/cli/ui.d.ts +33 -0
- package/cli/ui.js +55 -35
- package/cli/ui.js.map +1 -0
- package/cli/wizard.d.ts +187 -0
- package/cli/wizard.js +304 -284
- package/cli/wizard.js.map +1 -0
- package/package.json +15 -4
- package/public/app.d.ts +1 -0
- package/public/app.js +1057 -1009
- package/public/app.js.map +1 -0
- package/public/browser.d.ts +68 -0
- package/public/browser.js +2 -0
- package/public/browser.js.map +1 -0
- package/public/index.html +30 -13
- package/public/js/about.d.ts +6 -0
- package/public/js/about.js +165 -172
- package/public/js/about.js.map +1 -0
- package/public/js/aiDraft.d.ts +3 -0
- package/public/js/aiDraft.js +116 -124
- package/public/js/aiDraft.js.map +1 -0
- package/public/js/analysisPanel.d.ts +7 -0
- package/public/js/analysisPanel.js +166 -166
- package/public/js/analysisPanel.js.map +1 -0
- package/public/js/analysisReport.d.ts +38 -0
- package/public/js/analysisReport.js +105 -110
- package/public/js/analysisReport.js.map +1 -0
- package/public/js/api.d.ts +26 -0
- package/public/js/api.js +176 -167
- package/public/js/api.js.map +1 -0
- package/public/js/atlas.d.ts +6 -0
- package/public/js/atlas.js +50 -58
- package/public/js/atlas.js.map +1 -0
- package/public/js/blameView.d.ts +2 -0
- package/public/js/blameView.js +36 -37
- package/public/js/blameView.js.map +1 -0
- package/public/js/codeTab.d.ts +3 -0
- package/public/js/codeTab.js +175 -180
- package/public/js/codeTab.js.map +1 -0
- package/public/js/codeViewer.d.ts +5 -0
- package/public/js/codeViewer.js +162 -150
- package/public/js/codeViewer.js.map +1 -0
- package/public/js/components/FileChip.d.ts +9 -0
- package/public/js/components/FileChip.js +45 -49
- package/public/js/components/FileChip.js.map +1 -0
- package/public/js/components/MetricSparkline.d.ts +7 -0
- package/public/js/components/MetricSparkline.js +35 -44
- package/public/js/components/MetricSparkline.js.map +1 -0
- package/public/js/components/RiskBadge.d.ts +9 -0
- package/public/js/components/RiskBadge.js +55 -63
- package/public/js/components/RiskBadge.js.map +1 -0
- package/public/js/deepAnalysisView.d.ts +99 -0
- package/public/js/deepAnalysisView.js +282 -302
- package/public/js/deepAnalysisView.js.map +1 -0
- package/public/js/diagramPane.d.ts +8 -0
- package/public/js/diagramPane.js +133 -135
- package/public/js/diagramPane.js.map +1 -0
- package/public/js/diffView.d.ts +6 -0
- package/public/js/diffView.js +222 -209
- package/public/js/diffView.js.map +1 -0
- package/public/js/docsView.d.ts +2 -0
- package/public/js/docsView.js +441 -442
- package/public/js/docsView.js.map +1 -0
- package/public/js/dom.d.ts +189 -0
- package/public/js/dom.js +5 -0
- package/public/js/dom.js.map +1 -0
- package/public/js/fileSourceBrowser.d.ts +5 -0
- package/public/js/fileSourceBrowser.js +33 -41
- package/public/js/fileSourceBrowser.js.map +1 -0
- package/public/js/flameGraph.d.ts +5 -0
- package/public/js/flameGraph.js +62 -72
- package/public/js/flameGraph.js.map +1 -0
- package/public/js/forceGraph.d.ts +10 -0
- package/public/js/forceGraph.js +581 -556
- package/public/js/forceGraph.js.map +1 -0
- package/public/js/heatmap.d.ts +4 -0
- package/public/js/heatmap.js +142 -164
- package/public/js/heatmap.js.map +1 -0
- package/public/js/highlight.d.ts +2 -0
- package/public/js/highlight.js +116 -107
- package/public/js/highlight.js.map +1 -0
- package/public/js/html.d.ts +2 -0
- package/public/js/html.js +14 -44
- package/public/js/html.js.map +1 -0
- package/public/js/insightsView.d.ts +5 -0
- package/public/js/insightsView.js +93 -111
- package/public/js/insightsView.js.map +1 -0
- package/public/js/inspector.d.ts +37 -0
- package/public/js/inspector.js +416 -445
- package/public/js/inspector.js.map +1 -0
- package/public/js/llm.d.ts +113 -0
- package/public/js/llm.js +233 -272
- package/public/js/llm.js.map +1 -0
- package/public/js/mapView.d.ts +3 -0
- package/public/js/mapView.js +121 -120
- package/public/js/mapView.js.map +1 -0
- package/public/js/markdown.d.ts +3 -0
- package/public/js/markdown.js +84 -91
- package/public/js/markdown.js.map +1 -0
- package/public/js/mindmap.d.ts +32 -0
- package/public/js/mindmap.js +244 -259
- package/public/js/mindmap.js.map +1 -0
- package/public/js/mobilePanels.d.ts +5 -0
- package/public/js/mobilePanels.js +50 -0
- package/public/js/mobilePanels.js.map +1 -0
- package/public/js/repoFiles.d.ts +12 -0
- package/public/js/repoFiles.js +28 -27
- package/public/js/repoFiles.js.map +1 -0
- package/public/js/reviewView.d.ts +28 -0
- package/public/js/reviewView.js +256 -0
- package/public/js/reviewView.js.map +1 -0
- package/public/js/sbomView.d.ts +3 -0
- package/public/js/sbomView.js +59 -67
- package/public/js/sbomView.js.map +1 -0
- package/public/js/scanCache.d.ts +16 -0
- package/public/js/scanCache.js +67 -65
- package/public/js/scanCache.js.map +1 -0
- package/public/js/search.d.ts +7 -0
- package/public/js/search.js +868 -800
- package/public/js/search.js.map +1 -0
- package/public/js/searchLocal.d.ts +42 -0
- package/public/js/searchLocal.js +87 -0
- package/public/js/searchLocal.js.map +1 -0
- package/public/js/serverSettings.d.ts +5 -0
- package/public/js/serverSettings.js +153 -164
- package/public/js/serverSettings.js.map +1 -0
- package/public/js/state.d.ts +798 -0
- package/public/js/state.js +114 -122
- package/public/js/state.js.map +1 -0
- package/public/js/tour.d.ts +6 -0
- package/public/js/tour.js +5 -7
- package/public/js/tour.js.map +1 -0
- package/public/js/transitions.d.ts +2 -0
- package/public/js/transitions.js +9 -10
- package/public/js/transitions.js.map +1 -0
- package/public/js/tree.d.ts +12 -0
- package/public/js/tree.js +157 -154
- package/public/js/tree.js.map +1 -0
- package/public/js/workflowsView.d.ts +3 -0
- package/public/js/workflowsView.js +15 -22
- package/public/js/workflowsView.js.map +1 -0
- package/public/search.css +495 -0
- package/public/styles.css +163 -346
- package/public/workbench.css +166 -0
- package/server/apiAuth.d.ts +3 -0
- package/server/apiAuth.js +18 -20
- package/server/apiAuth.js.map +1 -0
- package/server/apiDiff.d.ts +2 -0
- package/server/apiDiff.js +20 -18
- package/server/apiDiff.js.map +1 -0
- package/server/apiDocs.d.ts +2 -0
- package/server/apiDocs.js +38 -40
- package/server/apiDocs.js.map +1 -0
- package/server/apiFile.d.ts +1 -0
- package/server/apiFile.js +14 -17
- package/server/apiFile.js.map +1 -0
- package/server/apiGitBlame.d.ts +2 -0
- package/server/apiGitBlame.js +50 -51
- package/server/apiGitBlame.js.map +1 -0
- package/server/apiMcp.d.ts +4 -0
- package/server/apiMcp.js +64 -66
- package/server/apiMcp.js.map +1 -0
- package/server/apiReview.d.ts +2 -0
- package/server/apiReview.js +45 -0
- package/server/apiReview.js.map +1 -0
- package/server/apiScan.d.ts +5 -0
- package/server/apiScan.js +90 -71
- package/server/apiScan.js.map +1 -0
- package/server/apiSearch.d.ts +33 -0
- package/server/apiSearch.js +184 -200
- package/server/apiSearch.js.map +1 -0
- package/server/apiSettings.d.ts +4 -0
- package/server/apiSettings.js +84 -85
- package/server/apiSettings.js.map +1 -0
- package/server/apiTools.d.ts +3 -0
- package/server/apiTools.js +55 -56
- package/server/apiTools.js.map +1 -0
- package/server/auth.d.ts +5 -0
- package/server/auth.js +54 -51
- package/server/auth.js.map +1 -0
- package/server/config.d.ts +42 -0
- package/server/config.js +189 -194
- package/server/config.js.map +1 -0
- package/server/daemon.d.ts +12 -0
- package/server/daemon.js +124 -114
- package/server/daemon.js.map +1 -0
- package/server/fileSourceNode.d.ts +2 -0
- package/server/fileSourceNode.js +34 -36
- package/server/fileSourceNode.js.map +1 -0
- package/server/gitClone.d.ts +9 -0
- package/server/gitClone.js +59 -61
- package/server/gitClone.js.map +1 -0
- package/server/gitDiff.d.ts +36 -0
- package/server/gitDiff.js +271 -169
- package/server/gitDiff.js.map +1 -0
- package/server/gitHistory.d.ts +12 -0
- package/server/gitHistory.js +80 -80
- package/server/gitHistory.js.map +1 -0
- package/server/gitRemote.d.ts +12 -0
- package/server/gitRemote.js +60 -0
- package/server/gitRemote.js.map +1 -0
- package/server/htmlText.d.ts +4 -0
- package/server/htmlText.js +16 -23
- package/server/htmlText.js.map +1 -0
- package/server/http.d.ts +5 -0
- package/server/http.js +27 -30
- package/server/http.js.map +1 -0
- package/server/httpGuards.d.ts +2 -0
- package/server/httpGuards.js +47 -43
- package/server/httpGuards.js.map +1 -0
- package/server/https.d.ts +34 -0
- package/server/https.js +121 -111
- package/server/https.js.map +1 -0
- package/server/index.d.ts +40 -0
- package/server/index.js +139 -147
- package/server/index.js.map +1 -0
- package/server/layout.d.ts +26 -0
- package/server/layout.js +59 -37
- package/server/layout.js.map +1 -0
- package/server/llmProxy.d.ts +3 -0
- package/server/llmProxy.js +137 -135
- package/server/llmProxy.js.map +1 -0
- package/server/logger.d.ts +38 -0
- package/server/logger.js +198 -211
- package/server/logger.js.map +1 -0
- package/server/mcp/analysis.d.ts +29 -0
- package/server/mcp/analysis.js +99 -120
- package/server/mcp/analysis.js.map +1 -0
- package/server/mcp/http.d.ts +9 -0
- package/server/mcp/http.js +128 -137
- package/server/mcp/http.js.map +1 -0
- package/server/mcp/runner.d.ts +68 -0
- package/server/mcp/runner.js +208 -218
- package/server/mcp/runner.js.map +1 -0
- package/server/mcp/server.d.ts +29 -0
- package/server/mcp/server.js +166 -179
- package/server/mcp/server.js.map +1 -0
- package/server/mcp/standalone.d.ts +1 -0
- package/server/mcp/standalone.js +8 -10
- package/server/mcp/standalone.js.map +1 -0
- package/server/mcp/tools.d.ts +2020 -0
- package/server/mcp/tools.js +523 -574
- package/server/mcp/tools.js.map +1 -0
- package/server/paths.d.ts +3 -0
- package/server/paths.js +17 -15
- package/server/paths.js.map +1 -0
- package/server/pidfile.d.ts +9 -0
- package/server/pidfile.js +43 -45
- package/server/pidfile.js.map +1 -0
- package/server/review.d.ts +5 -0
- package/server/review.js +35 -0
- package/server/review.js.map +1 -0
- package/server/router.d.ts +33 -0
- package/server/router.js +191 -197
- package/server/router.js.map +1 -0
- package/server/scanRequest.d.ts +3 -0
- package/server/scanRequest.js +36 -0
- package/server/scanRequest.js.map +1 -0
- package/server/searchIndex.d.ts +17 -0
- package/server/searchIndex.js +83 -80
- package/server/searchIndex.js.map +1 -0
- package/server/sessions.d.ts +14 -0
- package/server/sessions.js +62 -70
- package/server/sessions.js.map +1 -0
- package/server/startup.d.ts +118 -0
- package/server/startup.js +143 -148
- package/server/startup.js.map +1 -0
- package/server/static.d.ts +6 -0
- package/server/static.js +28 -31
- package/server/static.js.map +1 -0
- package/server/tools/install.d.ts +4 -0
- package/server/tools/install.js +176 -185
- package/server/tools/install.js.map +1 -0
- package/server/tools/parse.d.ts +8 -0
- package/server/tools/parse.js +112 -114
- package/server/tools/parse.js.map +1 -0
- package/server/tools/platform.d.ts +9 -0
- package/server/tools/platform.js +49 -47
- package/server/tools/platform.js.map +1 -0
- package/server/tools/registry.d.ts +137 -0
- package/server/tools/registry.js +173 -172
- package/server/tools/registry.js.map +1 -0
- package/server/tools/scan.d.ts +34 -0
- package/server/tools/scan.js +89 -98
- package/server/tools/scan.js.map +1 -0
- package/server/tools.d.ts +34 -0
- package/server/tools.js +94 -107
- package/server/tools.js.map +1 -0
- package/server/tunnel.d.ts +29 -0
- package/server/tunnel.js +61 -66
- package/server/tunnel.js.map +1 -0
- package/shared/analyzer/docs.d.ts +16 -0
- package/shared/analyzer/docs.js +99 -93
- package/shared/analyzer/docs.js.map +1 -0
- package/shared/analyzer/explainLocal.d.ts +4 -0
- package/shared/analyzer/explainLocal.js +122 -142
- package/shared/analyzer/explainLocal.js.map +1 -0
- package/shared/analyzer/github.d.ts +37 -0
- package/shared/analyzer/github.js +79 -0
- package/shared/analyzer/github.js.map +1 -0
- package/shared/analyzer/graph.d.ts +41 -0
- package/shared/analyzer/graph.js +393 -400
- package/shared/analyzer/graph.js.map +1 -0
- package/shared/analyzer/health.d.ts +33 -0
- package/shared/analyzer/health.js +197 -193
- package/shared/analyzer/health.js.map +1 -0
- package/shared/analyzer/history.d.ts +29 -0
- package/shared/analyzer/history.js +110 -111
- package/shared/analyzer/history.js.map +1 -0
- package/shared/analyzer/jsonConfig.d.ts +2 -0
- package/shared/analyzer/jsonConfig.js +61 -0
- package/shared/analyzer/jsonConfig.js.map +1 -0
- package/shared/analyzer/languages/csharp.d.ts +26 -0
- package/shared/analyzer/languages/csharp.js +83 -88
- package/shared/analyzer/languages/csharp.js.map +1 -0
- package/shared/analyzer/languages/generic.d.ts +23 -0
- package/shared/analyzer/languages/generic.js +94 -105
- package/shared/analyzer/languages/generic.js.map +1 -0
- package/shared/analyzer/languages/go.d.ts +24 -0
- package/shared/analyzer/languages/go.js +45 -54
- package/shared/analyzer/languages/go.js.map +1 -0
- package/shared/analyzer/languages/index.d.ts +6 -0
- package/shared/analyzer/languages/index.js +19 -20
- package/shared/analyzer/languages/index.js.map +1 -0
- package/shared/analyzer/languages/java.d.ts +20 -0
- package/shared/analyzer/languages/java.js +73 -78
- package/shared/analyzer/languages/java.js.map +1 -0
- package/shared/analyzer/languages/javascript.d.ts +12 -0
- package/shared/analyzer/languages/javascript.js +258 -209
- package/shared/analyzer/languages/javascript.js.map +1 -0
- package/shared/analyzer/languages/python.d.ts +23 -0
- package/shared/analyzer/languages/python.js +92 -100
- package/shared/analyzer/languages/python.js.map +1 -0
- package/shared/analyzer/languages/rust.d.ts +17 -0
- package/shared/analyzer/languages/rust.js +96 -98
- package/shared/analyzer/languages/rust.js.map +1 -0
- package/shared/analyzer/languages/typescript.d.ts +11 -0
- package/shared/analyzer/languages/typescript.js +117 -116
- package/shared/analyzer/languages/typescript.js.map +1 -0
- package/shared/analyzer/licenses.d.ts +18 -0
- package/shared/analyzer/licenses.js +130 -133
- package/shared/analyzer/licenses.js.map +1 -0
- package/shared/analyzer/metrics.d.ts +20 -0
- package/shared/analyzer/metrics.js +112 -104
- package/shared/analyzer/metrics.js.map +1 -0
- package/shared/analyzer/pathUtil.d.ts +10 -0
- package/shared/analyzer/pathUtil.js +29 -35
- package/shared/analyzer/pathUtil.js.map +1 -0
- package/shared/analyzer/patterns.d.ts +11 -0
- package/shared/analyzer/patterns.js +231 -253
- package/shared/analyzer/patterns.js.map +1 -0
- package/shared/analyzer/scan.d.ts +62 -0
- package/shared/analyzer/scan.js +451 -442
- package/shared/analyzer/scan.js.map +1 -0
- package/shared/analyzer/scanControl.d.ts +11 -0
- package/shared/analyzer/scanControl.js +62 -0
- package/shared/analyzer/scanControl.js.map +1 -0
- package/shared/analyzer/security.d.ts +18 -0
- package/shared/analyzer/security.js +159 -159
- package/shared/analyzer/security.js.map +1 -0
- package/shared/analyzer/services.d.ts +5 -0
- package/shared/analyzer/services.js +141 -138
- package/shared/analyzer/services.js.map +1 -0
- package/shared/analyzer/stack.d.ts +6 -0
- package/shared/analyzer/stack.js +134 -141
- package/shared/analyzer/stack.js.map +1 -0
- package/shared/analyzer/tour.d.ts +2 -0
- package/shared/analyzer/tour.js +45 -53
- package/shared/analyzer/tour.js.map +1 -0
- package/shared/analyzer/util.d.ts +8 -0
- package/shared/analyzer/util.js +130 -76
- package/shared/analyzer/util.js.map +1 -0
- package/shared/analyzer/workflows.d.ts +8 -0
- package/shared/analyzer/workflows.js +139 -134
- package/shared/analyzer/workflows.js.map +1 -0
- package/shared/contracts.d.ts +250 -0
- package/shared/contracts.js +2 -0
- package/shared/contracts.js.map +1 -0
- package/shared/diagram/aiFacts.d.ts +35 -0
- package/shared/diagram/aiFacts.js +102 -105
- package/shared/diagram/aiFacts.js.map +1 -0
- package/shared/diagram/aiMermaid.d.ts +5 -0
- package/shared/diagram/aiMermaid.js +46 -45
- package/shared/diagram/aiMermaid.js.map +1 -0
- package/shared/diagram/atlas.d.ts +16 -0
- package/shared/diagram/atlas.js +51 -60
- package/shared/diagram/atlas.js.map +1 -0
- package/shared/diagram/mermaid.d.ts +43 -0
- package/shared/diagram/mermaid.js +348 -370
- package/shared/diagram/mermaid.js.map +1 -0
- package/shared/review/contracts.d.ts +95 -0
- package/shared/review/contracts.js +2 -0
- package/shared/review/contracts.js.map +1 -0
- package/shared/review/review.d.ts +10 -0
- package/shared/review/review.js +184 -0
- package/shared/review/review.js.map +1 -0
- package/shared/search/query.d.ts +32 -0
- package/shared/search/query.js +395 -388
- package/shared/search/query.js.map +1 -0
package/public/app.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { abortable, throwIfAborted } from '/shared/analyzer/scanControl.js';
|
|
2
|
+
import { eventElement } from '/js/dom.js';
|
|
1
3
|
// Onboarder — bootstrap, routing, and wiring.
|
|
2
4
|
//
|
|
3
5
|
// Two ways a repo arrives here:
|
|
@@ -13,7 +15,6 @@
|
|
|
13
15
|
// codeTab, about, aiDraft — and is handed its host element and a set of callbacks
|
|
14
16
|
// here. The callbacks are the reason the module graph stays acyclic: a feature
|
|
15
17
|
// module never imports back into app.js.
|
|
16
|
-
|
|
17
18
|
import { scanRepo } from '/shared/analyzer/scan.js';
|
|
18
19
|
import { computeFacts, scanIndex } from '/shared/analyzer/graph.js';
|
|
19
20
|
import { analyzeStack } from '/shared/analyzer/stack.js';
|
|
@@ -21,17 +22,11 @@ import { detectManifest } from '/shared/analyzer/services.js';
|
|
|
21
22
|
import { computeLayers, detectPatterns, couplingMatrix } from '/shared/analyzer/patterns.js';
|
|
22
23
|
import { explainOverview } from '/shared/analyzer/explainLocal.js';
|
|
23
24
|
import { dirOf, pathFilter } from '/shared/analyzer/pathUtil.js';
|
|
24
|
-
import {
|
|
25
|
-
overviewDiagram, folderDiagram, fileDetailDiagram, servicesDiagram, emptyDiagram,
|
|
26
|
-
layersDiagram, healthDiagram, securityDiagram, historyDiagram, setDiagramTheme, diagramThemeBlock,
|
|
27
|
-
} from '/shared/diagram/mermaid.js';
|
|
25
|
+
import { overviewDiagram, folderDiagram, fileDetailDiagram, servicesDiagram, emptyDiagram, layersDiagram, healthDiagram, securityDiagram, historyDiagram, setDiagramTheme, diagramThemeBlock, } from '/shared/diagram/mermaid.js';
|
|
28
26
|
import { analyzeHealth } from '/shared/analyzer/health.js';
|
|
29
27
|
import { summarizeSecurity } from '/shared/analyzer/security.js';
|
|
30
28
|
import { escapeHtml } from '/js/html.js';
|
|
31
|
-
import {
|
|
32
|
-
scanOnServer, cleanupClone, streamExplain,
|
|
33
|
-
fetchMcpStatus, startMcpServer, stopMcpServer, fetchMcpCommand,
|
|
34
|
-
} from '/js/api.js';
|
|
29
|
+
import { scanOnServer, cleanupClone, streamExplain, fetchMcpStatus, startMcpServer, stopMcpServer, fetchMcpCommand, } from '/js/api.js';
|
|
35
30
|
import { canPickFolder, pickDirectory, browserFileSource } from '/js/fileSourceBrowser.js';
|
|
36
31
|
import { renderInto, wireNodeClicks, makePanzoom, downloadSvg } from '/js/diagramPane.js';
|
|
37
32
|
import { setViewerTheme } from '/js/codeViewer.js';
|
|
@@ -54,265 +49,313 @@ import { initSearch } from '/js/search.js';
|
|
|
54
49
|
import { withTransition } from '/js/transitions.js';
|
|
55
50
|
import { renderInsights } from '/js/insightsView.js';
|
|
56
51
|
import { createServerDrawer } from '/js/serverSettings.js';
|
|
57
|
-
import { renderDiffView } from '/js/diffView.js';
|
|
52
|
+
import { renderDiffView, stopDiff } from '/js/diffView.js';
|
|
53
|
+
import { renderReviewView, stopReview } from '/js/reviewView.js';
|
|
58
54
|
import { renderWorkflows } from '/js/workflowsView.js';
|
|
59
55
|
import { renderSbom } from '/js/sbomView.js';
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
focusFile, focusFolder, clearFocus as clearFocusState, inspectorSubject, aiViewKey,
|
|
63
|
-
} from '/js/state.js';
|
|
64
|
-
|
|
56
|
+
import { initMobilePanels } from '/js/mobilePanels.js';
|
|
57
|
+
import { state as appState, isExplorerView, loadRepo, unloadRepo, focusFile, focusFolder, clearFocus as clearFocusState, inspectorSubject, aiViewKey, } from '/js/state.js';
|
|
65
58
|
const $ = (id) => document.getElementById(id);
|
|
66
|
-
|
|
67
59
|
const dom = {};
|
|
68
60
|
[
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
61
|
+
'landing', 'landingError', 'landingStatus', 'cancelScanBtn', 'scanProgress', 'pathForm', 'pathInput', 'gitForm', 'gitInput',
|
|
62
|
+
'pickBtn', 'demoBtn', 'explorer', 'viewTabs', 'repoChip', 'newRepoBtn', 'settingsBtn',
|
|
63
|
+
'landingAbout', 'creditsHide', 'creditsRemove', 'creditsBar', 'creditsShow', 'creditsBarRemove',
|
|
64
|
+
'treeFilter', 'fileTree', 'crumbs', 'copyMermaidBtn', 'svgBtn', 'fitBtn',
|
|
65
|
+
'canvas', 'canvasContent', 'canvasEmpty', 'stageFoot', 'themeBtn', 'aiDrawBtn', 'docView', 'aboutView', 'codeView',
|
|
66
|
+
'mmToggleBtn', 'stageFilters', 'modeSwitch', 'nodeFilter', 'testsToggle', 'depthSelect',
|
|
67
|
+
'tourbar', 'tourTitle', 'tourCount', 'tourPrev', 'tourNext', 'tourExit',
|
|
68
|
+
'drawerScrim', 'settingsDrawer', 'drawerClose', 'setBaseUrl', 'setApiKey', 'setModel',
|
|
69
|
+
'saveSettings', 'testSettings', 'settingsStatus', 'toast', 'brandNote', 'askBtn', 'askInput',
|
|
70
|
+
'graphView', 'graphCanvas', 'graphControls', 'graphReset', 'graphZoomIn', 'graphZoomOut', 'graphFit', 'graphSearchInput', 'graphTooltip',
|
|
71
|
+
'heatmapView', 'heatmapCanvas', 'heatmapTooltip', 'blameGutter', 'codeTabContainer',
|
|
72
|
+
'insightsView', 'diffView', 'reviewView', 'workflowsView', 'sbomView', 'searchTriggerBtn', 'inspSecurityTools', 'analysisView',
|
|
73
|
+
'mcpBtn', 'mcpBtnLabel', 'mcpDot', 'mcpDrawer', 'mcpScrim', 'mcpClose', 'mcpPanelDot', 'mcpPanelState',
|
|
74
|
+
'mcpStart', 'mcpStop', 'mcpStatus', 'mcpConfig', 'mcpCommandInput', 'mcpCopy', 'mcpTabJson', 'mcpTabToml',
|
|
75
|
+
'mcpConfigBlock', 'mcpToolsHint', 'serverBtn',
|
|
84
76
|
].forEach((id) => (dom[id] = $(id)));
|
|
85
|
-
|
|
86
77
|
let forceGraphCtl = null;
|
|
78
|
+
let graphScan = null;
|
|
79
|
+
let graphHealth = null;
|
|
87
80
|
let heatmapCtl = null;
|
|
88
81
|
let searchCtl = null;
|
|
89
|
-
|
|
90
82
|
const state = appState;
|
|
91
|
-
|
|
92
83
|
const panzoom = makePanzoom(dom.canvas, dom.canvasContent);
|
|
93
|
-
|
|
84
|
+
const mobilePanels = initMobilePanels();
|
|
94
85
|
// ---------------------------------------------------------------- landing --
|
|
95
|
-
|
|
86
|
+
let activeScan = null;
|
|
96
87
|
function landingBusy(message) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
88
|
+
dom.landingError.hidden = true;
|
|
89
|
+
dom.landing.setAttribute('aria-busy', 'true');
|
|
90
|
+
dom.landingStatus.textContent = message;
|
|
91
|
+
dom.landingStatus.hidden = false;
|
|
92
|
+
for (const btn of dom.landing.querySelectorAll('button'))
|
|
93
|
+
btn.disabled = true;
|
|
94
|
+
dom.cancelScanBtn.hidden = false;
|
|
95
|
+
dom.cancelScanBtn.disabled = false;
|
|
96
|
+
dom.scanProgress.hidden = false;
|
|
97
|
+
dom.scanProgress.removeAttribute('value');
|
|
101
98
|
}
|
|
102
|
-
|
|
103
99
|
function landingIdle(error) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
dom.
|
|
108
|
-
dom.
|
|
109
|
-
|
|
100
|
+
dom.landingStatus.hidden = true;
|
|
101
|
+
dom.cancelScanBtn.hidden = true;
|
|
102
|
+
dom.scanProgress.hidden = true;
|
|
103
|
+
dom.landing.removeAttribute('aria-busy');
|
|
104
|
+
for (const btn of dom.landing.querySelectorAll('button'))
|
|
105
|
+
btn.disabled = false;
|
|
106
|
+
dom.pickBtn.disabled = !canPickFolder();
|
|
107
|
+
if (error) {
|
|
108
|
+
dom.landingError.textContent = error;
|
|
109
|
+
dom.landingError.hidden = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function beginScan(message) {
|
|
113
|
+
activeScan?.abort();
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
activeScan = controller;
|
|
116
|
+
landingBusy(message);
|
|
117
|
+
return controller;
|
|
110
118
|
}
|
|
111
|
-
|
|
119
|
+
function updateScanProgress(progress) {
|
|
120
|
+
if (progress.phase === 'walk')
|
|
121
|
+
dom.landingStatus.textContent = `Walking the file tree… ${progress.found} files`;
|
|
122
|
+
if (progress.phase === 'parse' || progress.phase === 'resolve') {
|
|
123
|
+
dom.landingStatus.textContent = `${progress.phase === 'parse' ? 'Reading code' : 'Tracing imports'}… ${progress.done} / ${progress.total} files`;
|
|
124
|
+
dom.scanProgress.max = Math.max(1, progress.total);
|
|
125
|
+
dom.scanProgress.value = progress.done;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
dom.cancelScanBtn.addEventListener('click', () => {
|
|
129
|
+
activeScan?.abort();
|
|
130
|
+
activeScan = null;
|
|
131
|
+
landingIdle();
|
|
132
|
+
dom.landingStatus.hidden = false;
|
|
133
|
+
dom.landingStatus.textContent = 'Scan cancelled. Choose a repository when you’re ready.';
|
|
134
|
+
});
|
|
112
135
|
async function loadFromServer(payload, busyText) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
const controller = beginScan(busyText);
|
|
137
|
+
try {
|
|
138
|
+
const result = await scanOnServer(payload, { signal: controller.signal });
|
|
139
|
+
if (activeScan !== controller)
|
|
140
|
+
return;
|
|
141
|
+
enterExplorer(result, {});
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (activeScan === controller)
|
|
145
|
+
landingIdle(err.message);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (activeScan === controller)
|
|
149
|
+
activeScan = null;
|
|
150
|
+
}
|
|
120
151
|
}
|
|
121
|
-
|
|
122
152
|
dom.pathForm.addEventListener('submit', (event) => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
const path = dom.pathInput.value.trim();
|
|
155
|
+
if (!path)
|
|
156
|
+
return landingIdle('Give me a path first — something like ~/work/that-repo.');
|
|
157
|
+
loadFromServer({ path }, 'Walking the file tree…');
|
|
127
158
|
});
|
|
128
|
-
|
|
129
159
|
dom.gitForm.addEventListener('submit', (event) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
const gitUrl = dom.gitInput.value.trim();
|
|
162
|
+
if (!gitUrl)
|
|
163
|
+
return landingIdle('Paste a git URL first.');
|
|
164
|
+
loadFromServer({ gitUrl }, 'Cloning, then scanning. Big repos take a minute…');
|
|
134
165
|
});
|
|
135
|
-
|
|
136
166
|
dom.demoBtn.addEventListener('click', () => {
|
|
137
|
-
|
|
167
|
+
loadFromServer({ demo: true }, 'Reading our own source…');
|
|
138
168
|
});
|
|
139
|
-
|
|
140
169
|
// The landing credits card can be tucked away or permanently removed; the choice sticks.
|
|
141
170
|
const CREDITS_KEY = 'onboarder.credits';
|
|
142
171
|
function applyCreditsVisibility() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
const status = localStorage.getItem(CREDITS_KEY);
|
|
173
|
+
if (status === 'removed') {
|
|
174
|
+
dom.landingAbout?.remove();
|
|
175
|
+
dom.creditsBar?.remove();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const hidden = status === 'hidden';
|
|
179
|
+
if (dom.landingAbout)
|
|
180
|
+
dom.landingAbout.hidden = hidden;
|
|
181
|
+
if (dom.creditsBar)
|
|
182
|
+
dom.creditsBar.hidden = !hidden;
|
|
152
183
|
}
|
|
153
|
-
|
|
154
184
|
function removeCreditsPermanently() {
|
|
155
|
-
|
|
156
|
-
|
|
185
|
+
localStorage.setItem(CREDITS_KEY, 'removed');
|
|
186
|
+
applyCreditsVisibility();
|
|
157
187
|
}
|
|
158
|
-
|
|
159
188
|
dom.creditsHide?.addEventListener('click', () => {
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
localStorage.setItem(CREDITS_KEY, 'hidden');
|
|
190
|
+
applyCreditsVisibility();
|
|
162
191
|
});
|
|
163
192
|
dom.creditsShow?.addEventListener('click', () => {
|
|
164
|
-
|
|
165
|
-
|
|
193
|
+
localStorage.setItem(CREDITS_KEY, 'shown');
|
|
194
|
+
applyCreditsVisibility();
|
|
166
195
|
});
|
|
167
196
|
dom.creditsRemove?.addEventListener('click', removeCreditsPermanently);
|
|
168
197
|
dom.creditsBarRemove?.addEventListener('click', removeCreditsPermanently);
|
|
169
|
-
|
|
170
198
|
applyCreditsVisibility();
|
|
171
|
-
|
|
172
199
|
dom.pickBtn.addEventListener('click', async () => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
if (!canPickFolder()) {
|
|
201
|
+
return landingIdle('This browser can’t pick folders. Chrome or Edge can; the other two options work anywhere.');
|
|
202
|
+
}
|
|
203
|
+
let handle;
|
|
204
|
+
try {
|
|
205
|
+
handle = await pickDirectory();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return; // the picker was dismissed — no news is good news
|
|
209
|
+
}
|
|
210
|
+
const controller = beginScan('Walking the file tree…');
|
|
211
|
+
try {
|
|
212
|
+
const source = browserFileSource(handle);
|
|
213
|
+
const scan = await scanRepo(source, {
|
|
214
|
+
signal: controller.signal,
|
|
215
|
+
onProgress: updateScanProgress,
|
|
216
|
+
});
|
|
217
|
+
dom.landingStatus.textContent = 'Counting connections…';
|
|
218
|
+
const manifest = await abortable(detectManifest(source), controller.signal);
|
|
219
|
+
throwIfAborted(controller.signal);
|
|
220
|
+
if (activeScan !== controller)
|
|
221
|
+
return;
|
|
222
|
+
const facts = computeFacts(scan, manifest);
|
|
223
|
+
enterExplorer({ scan, facts, manifest }, { browserSource: source });
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
if (activeScan === controller)
|
|
227
|
+
landingIdle(err.message);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
if (activeScan === controller)
|
|
231
|
+
activeScan = null;
|
|
232
|
+
}
|
|
198
233
|
});
|
|
199
|
-
|
|
200
234
|
if (!canPickFolder()) {
|
|
201
|
-
|
|
202
|
-
|
|
235
|
+
dom.pickBtn.disabled = true;
|
|
236
|
+
dom.pickBtn.title = 'Folder picking needs Chrome or Edge.';
|
|
203
237
|
}
|
|
204
|
-
|
|
205
238
|
// -------------------------------------------------------------- explorer --
|
|
206
|
-
|
|
207
239
|
function enterExplorer(payload, opts) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
renderSidebar();
|
|
246
|
-
setView('map');
|
|
240
|
+
loadRepo(payload, {
|
|
241
|
+
browserSource: opts.browserSource || null,
|
|
242
|
+
history: payload.history || null, // computed server-side; null for browser picks
|
|
243
|
+
});
|
|
244
|
+
state.stack = analyzeStack(state.manifest, state.scan.stats.languages);
|
|
245
|
+
resetOpenState();
|
|
246
|
+
state.treeData = buildTree(state.scan.allFiles);
|
|
247
|
+
state.patterns = buildPatterns(state.scan, state.facts, state.manifest);
|
|
248
|
+
state.health = analyzeHealth(state.scan, state.facts);
|
|
249
|
+
state.security = summarizeSecurity(state.scan);
|
|
250
|
+
dom.nodeFilter.value = '';
|
|
251
|
+
dom.testsToggle.classList.add('is-on');
|
|
252
|
+
dom.depthSelect.value = '0';
|
|
253
|
+
if (searchCtl) {
|
|
254
|
+
searchCtl.updateFiles(state.scan.files, state.scanId);
|
|
255
|
+
searchCtl.hide();
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
searchCtl = initSearch(state.scan.files, { getScanId: () => state.scanId });
|
|
259
|
+
}
|
|
260
|
+
// Compute everything first, then flip the UI over — a failure here should
|
|
261
|
+
// land on the landing page with the error visible, not a half-dead explorer.
|
|
262
|
+
landingIdle();
|
|
263
|
+
dom.landing.hidden = true;
|
|
264
|
+
dom.explorer.hidden = false;
|
|
265
|
+
dom.viewTabs.hidden = false;
|
|
266
|
+
dom.repoChip.hidden = false;
|
|
267
|
+
dom.newRepoBtn.hidden = false;
|
|
268
|
+
dom.searchTriggerBtn.hidden = false;
|
|
269
|
+
dom.repoChip.innerHTML =
|
|
270
|
+
`<span class="repo-name">${escapeHtml(state.scan.name)}</span>` +
|
|
271
|
+
`<span class="repo-sub">${escapeHtml(repoSubLabel())}</span>`;
|
|
272
|
+
dom.repoChip.title = state.scan.root;
|
|
273
|
+
dom.brandNote.textContent = state.scan.stats.filesParsed + ' files · ' + state.scan.stats.edgeCount + ' connections';
|
|
274
|
+
renderSidebar();
|
|
275
|
+
setView(state.scanId ? 'review' : 'map');
|
|
247
276
|
}
|
|
248
|
-
|
|
249
277
|
// The big name is the repo's own; the small line says where it actually lives.
|
|
250
278
|
function repoSubLabel() {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
279
|
+
if (state.scan.tempId)
|
|
280
|
+
return state.scan.tempId + ' · temp clone';
|
|
281
|
+
if (state.browserSource)
|
|
282
|
+
return 'picked in the browser';
|
|
283
|
+
return state.scan.root;
|
|
254
284
|
}
|
|
255
|
-
|
|
256
285
|
function buildPatterns(scan, facts, manifest) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
286
|
+
const layersInfo = computeLayers(scan, facts);
|
|
287
|
+
return {
|
|
288
|
+
layersInfo,
|
|
289
|
+
findings: detectPatterns(scan, facts, manifest, layersInfo),
|
|
290
|
+
coupling: couplingMatrix(scan, facts),
|
|
291
|
+
};
|
|
263
292
|
}
|
|
264
|
-
|
|
265
293
|
dom.newRepoBtn.addEventListener('click', () => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
stopReview();
|
|
295
|
+
stopDiff();
|
|
296
|
+
mobilePanels.close();
|
|
297
|
+
if (state.cloneId)
|
|
298
|
+
cleanupClone(state.cloneId);
|
|
299
|
+
// The graph and heatmap views attach `window` resize listeners when
|
|
300
|
+
// they initialize. The view tab is unmounted by hiding the container,
|
|
301
|
+
// not by removing the listener, so without this call every new repo
|
|
302
|
+
// would attach another listener to the same global and the page would
|
|
303
|
+
// fan out work on every resize. Destroy tears them down before the
|
|
304
|
+
// next `init*` rebuilds them for a new repo.
|
|
305
|
+
if (forceGraphCtl) {
|
|
306
|
+
forceGraphCtl.destroy();
|
|
307
|
+
forceGraphCtl = null;
|
|
308
|
+
}
|
|
309
|
+
if (heatmapCtl) {
|
|
310
|
+
heatmapCtl.destroy();
|
|
311
|
+
heatmapCtl = null;
|
|
312
|
+
}
|
|
313
|
+
graphScan = null;
|
|
314
|
+
graphHealth = null;
|
|
315
|
+
unloadRepo();
|
|
316
|
+
dom.explorer.hidden = true;
|
|
317
|
+
dom.tourbar.hidden = true;
|
|
318
|
+
dom.viewTabs.hidden = true;
|
|
319
|
+
dom.repoChip.hidden = true;
|
|
320
|
+
dom.newRepoBtn.hidden = true;
|
|
321
|
+
dom.brandNote.textContent = 'a map for any codebase';
|
|
322
|
+
dom.landing.hidden = false;
|
|
323
|
+
landingIdle();
|
|
284
324
|
});
|
|
285
|
-
|
|
286
325
|
function renderSidebar() {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
326
|
+
renderTree(dom.fileTree, state.treeData, {
|
|
327
|
+
facts: state.facts,
|
|
328
|
+
filter: dom.treeFilter.value,
|
|
329
|
+
selected: state.selected,
|
|
330
|
+
parsedPaths: state.scan.files.map((f) => f.path),
|
|
331
|
+
onFile: openFile,
|
|
332
|
+
onFolder: (folder) => {
|
|
333
|
+
focusFolder(folder);
|
|
334
|
+
state.folder = folder;
|
|
335
|
+
state.detail = null;
|
|
336
|
+
if (state.view === 'files')
|
|
337
|
+
renderCanvas();
|
|
338
|
+
syncInspector();
|
|
339
|
+
},
|
|
340
|
+
});
|
|
301
341
|
}
|
|
302
|
-
|
|
303
|
-
dom.treeFilter.addEventListener('input',
|
|
304
|
-
|
|
342
|
+
let sidebarFrame = null;
|
|
343
|
+
dom.treeFilter.addEventListener('input', () => {
|
|
344
|
+
if (sidebarFrame !== null)
|
|
345
|
+
cancelAnimationFrame(sidebarFrame);
|
|
346
|
+
sidebarFrame = requestAnimationFrame(() => { sidebarFrame = null; if (state.scan)
|
|
347
|
+
renderSidebar(); });
|
|
348
|
+
});
|
|
305
349
|
// ------------------------------------------------------------------ views --
|
|
306
|
-
|
|
307
350
|
dom.viewTabs.addEventListener('click', (event) => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
351
|
+
const tab = eventElement(event)?.closest('.view-tab');
|
|
352
|
+
if (!tab)
|
|
353
|
+
return;
|
|
354
|
+
const view = tab.dataset.view === 'explorer' ? state.lastExplorer || 'map' : tab.dataset.view;
|
|
355
|
+
if (state.view === view)
|
|
356
|
+
return;
|
|
357
|
+
withTransition(() => setView(view));
|
|
314
358
|
});
|
|
315
|
-
|
|
316
359
|
// The inspector's single source of truth. Whatever is focused — a file, a
|
|
317
360
|
// folder — is shown in every tab; with nothing focused, each view shows its
|
|
318
361
|
// own default panel. Every selection path funnels here, so the panel is
|
|
@@ -320,432 +363,467 @@ dom.viewTabs.addEventListener('click', (event) => {
|
|
|
320
363
|
// comes from `inspectorSubject` in state.js, which is pure and tested; this
|
|
321
364
|
// function only does the showing.
|
|
322
365
|
function syncInspector() {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
366
|
+
const subject = inspectorSubject(state);
|
|
367
|
+
const opts = { ...state, backLabel: subject.backLabel };
|
|
368
|
+
switch (subject.kind) {
|
|
369
|
+
case 'file': return inspector.showFile(subject.path, opts);
|
|
370
|
+
case 'folder': return inspector.showFolder(subject.path, opts);
|
|
371
|
+
case 'patterns': return inspector.showPatterns(state);
|
|
372
|
+
case 'health': return inspector.showHealth(state);
|
|
373
|
+
case 'security': return inspector.showSecurity(state);
|
|
374
|
+
case 'history': return inspector.showHistory(state);
|
|
375
|
+
default: return inspector.showOverview(state);
|
|
376
|
+
}
|
|
334
377
|
}
|
|
335
|
-
|
|
336
378
|
function clearFocus() {
|
|
337
|
-
|
|
338
|
-
|
|
379
|
+
clearFocusState();
|
|
380
|
+
syncInspector();
|
|
339
381
|
}
|
|
340
|
-
|
|
341
382
|
function setView(view) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
383
|
+
if (view !== 'review')
|
|
384
|
+
stopReview();
|
|
385
|
+
if (view !== 'diff')
|
|
386
|
+
stopDiff();
|
|
387
|
+
dom.explorer.classList.toggle('review-workspace', view === 'review');
|
|
388
|
+
state.view = view;
|
|
389
|
+
if (isExplorerView(view))
|
|
390
|
+
state.lastExplorer = view;
|
|
391
|
+
for (const tab of dom.viewTabs.querySelectorAll('.view-tab')) {
|
|
392
|
+
tab.classList.toggle('is-active', tab.dataset.view === view || (tab.dataset.view === 'explorer' && isExplorerView(view)));
|
|
393
|
+
}
|
|
394
|
+
for (const chip of dom.modeSwitch.querySelectorAll('.mode-chip')) {
|
|
395
|
+
chip.classList.toggle('is-active', chip.dataset.mode === view);
|
|
396
|
+
}
|
|
397
|
+
dom.tourbar.hidden = view !== 'tour';
|
|
398
|
+
if (view === 'tour') {
|
|
399
|
+
state.tour.stops = buildTourStops(state.scan, state.facts);
|
|
400
|
+
state.tour.idx = 0;
|
|
401
|
+
state.selected = null; // the tour drives its own focus
|
|
402
|
+
}
|
|
403
|
+
for (const tab of dom.viewTabs.querySelectorAll('.view-tab'))
|
|
404
|
+
tab.setAttribute('aria-current', tab.classList.contains('is-active') ? 'page' : 'false');
|
|
405
|
+
renderCanvas();
|
|
406
|
+
if (view === 'tour')
|
|
407
|
+
showTourStop();
|
|
408
|
+
else
|
|
409
|
+
syncInspector();
|
|
365
410
|
}
|
|
366
|
-
|
|
367
411
|
function openFile(path) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
412
|
+
focusFile(path);
|
|
413
|
+
state.code.path = path; // the Code tab follows the current file
|
|
414
|
+
if (state.view !== 'files' && state.view !== 'tour' && state.view !== 'code') {
|
|
415
|
+
state.folder = dirOf(path);
|
|
416
|
+
setView('files');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (state.view === 'files') {
|
|
420
|
+
state.folder = dirOf(path);
|
|
421
|
+
state.detail = path;
|
|
422
|
+
renderCanvas();
|
|
423
|
+
}
|
|
424
|
+
if (state.view === 'code')
|
|
425
|
+
renderCode();
|
|
426
|
+
syncInspector();
|
|
427
|
+
renderSidebar();
|
|
383
428
|
}
|
|
384
|
-
|
|
385
429
|
// The Code tab, optionally at a line. Search results for symbols and content
|
|
386
430
|
// hits come through here: the scan already knows where a symbol was declared and
|
|
387
431
|
// where a phrase matched, so opening the file at line 1 would throw away the
|
|
388
432
|
// only part of the answer the person did not already know.
|
|
389
433
|
function openFileInCode(path, line) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
434
|
+
focusFile(path);
|
|
435
|
+
state.code.path = path;
|
|
436
|
+
if (line)
|
|
437
|
+
revealLineInCode(line);
|
|
438
|
+
setView('code');
|
|
394
439
|
}
|
|
395
|
-
|
|
396
440
|
function openDeepDive(path) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
441
|
+
focusFile(path);
|
|
442
|
+
state.folder = dirOf(path);
|
|
443
|
+
state.detail = path;
|
|
444
|
+
setView('files');
|
|
401
445
|
}
|
|
402
|
-
|
|
403
446
|
function onNodeClick(payload) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
447
|
+
if (payload.kind === 'folder') {
|
|
448
|
+
state.folder = payload.path;
|
|
449
|
+
state.detail = null;
|
|
450
|
+
focusFolder(payload.path);
|
|
451
|
+
setView('files');
|
|
452
|
+
}
|
|
453
|
+
else if (payload.kind === 'file') {
|
|
454
|
+
openFile(payload.path);
|
|
455
|
+
}
|
|
412
456
|
}
|
|
413
|
-
|
|
414
457
|
inspector.onInspectorNavigate(openFile);
|
|
415
|
-
|
|
416
458
|
document.addEventListener('search-select', (event) => {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
459
|
+
const detail = event.detail || {};
|
|
460
|
+
if (!detail.path)
|
|
461
|
+
return;
|
|
462
|
+
// The palette says where a result belongs. A file's home is the graph; a
|
|
463
|
+
// symbol or a content hit belongs in the Code tab, at its line.
|
|
464
|
+
if (detail.target === 'code')
|
|
465
|
+
return openFileInCode(detail.path, detail.line);
|
|
466
|
+
return openFile(detail.path);
|
|
423
467
|
});
|
|
424
|
-
|
|
425
468
|
document.addEventListener('file-select', (event) => {
|
|
426
|
-
|
|
469
|
+
if (event.detail?.path)
|
|
470
|
+
openFile(event.detail.path);
|
|
427
471
|
});
|
|
428
|
-
|
|
429
472
|
document.addEventListener('graph-node-click', (event) => {
|
|
430
|
-
|
|
473
|
+
if (event.detail?.path)
|
|
474
|
+
openFile(event.detail.path);
|
|
431
475
|
});
|
|
432
|
-
|
|
433
476
|
document.addEventListener('heatmap-node-click', (event) => {
|
|
434
|
-
|
|
477
|
+
if (event.detail?.path)
|
|
478
|
+
openFile(event.detail.path);
|
|
435
479
|
});
|
|
436
|
-
|
|
437
480
|
async function renderCanvas() {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
dom.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
481
|
+
const isDocs = state.view === 'docs';
|
|
482
|
+
const isAbout = state.view === 'about';
|
|
483
|
+
const isCode = state.view === 'code';
|
|
484
|
+
const isGraph = state.view === 'graph';
|
|
485
|
+
const isHeatmap = state.view === 'heatmap';
|
|
486
|
+
const isInsights = state.view === 'insights';
|
|
487
|
+
const isDiff = state.view === 'diff';
|
|
488
|
+
const isReview = state.view === 'review';
|
|
489
|
+
const isWorkflows = state.view === 'workflows';
|
|
490
|
+
const isSbom = state.view === 'sbom';
|
|
491
|
+
const isAnalysis = state.view === 'analysis';
|
|
492
|
+
const isPage = isDocs || isAbout || isCode || isGraph || isHeatmap || isInsights || isDiff || isReview || isWorkflows || isSbom || isAnalysis;
|
|
493
|
+
forceGraphCtl?.setActive(isGraph && !searchCtl?.isShowing());
|
|
494
|
+
const isAtlasList = state.view === 'atlas' && !state.atlasOpen;
|
|
495
|
+
dom.canvas.hidden = isPage;
|
|
496
|
+
dom.docView.hidden = !isDocs;
|
|
497
|
+
dom.aboutView.hidden = !isAbout;
|
|
498
|
+
dom.codeView.hidden = !isCode;
|
|
499
|
+
if (dom.codeTabContainer)
|
|
500
|
+
dom.codeTabContainer.hidden = !isCode;
|
|
501
|
+
if (dom.graphView)
|
|
502
|
+
dom.graphView.hidden = !isGraph;
|
|
503
|
+
if (dom.heatmapView)
|
|
504
|
+
dom.heatmapView.hidden = !isHeatmap;
|
|
505
|
+
if (dom.insightsView)
|
|
506
|
+
dom.insightsView.hidden = !isInsights;
|
|
507
|
+
if (dom.diffView)
|
|
508
|
+
dom.diffView.hidden = !isDiff;
|
|
509
|
+
if (dom.reviewView)
|
|
510
|
+
dom.reviewView.hidden = !isReview;
|
|
511
|
+
if (dom.workflowsView)
|
|
512
|
+
dom.workflowsView.hidden = !isWorkflows;
|
|
513
|
+
if (dom.sbomView)
|
|
514
|
+
dom.sbomView.hidden = !isSbom;
|
|
515
|
+
if (dom.analysisView)
|
|
516
|
+
dom.analysisView.hidden = !isAnalysis;
|
|
517
|
+
dom.copyMermaidBtn.hidden = isPage || isAtlasList;
|
|
518
|
+
dom.svgBtn.hidden = isPage;
|
|
519
|
+
dom.fitBtn.hidden = isPage || isAtlasList;
|
|
520
|
+
dom.stageFilters.hidden = isPage;
|
|
521
|
+
dom.mmToggleBtn.hidden = isPage || state.view !== 'map';
|
|
522
|
+
if (!isAtlasList)
|
|
523
|
+
dom.canvasContent.classList.remove('atlas-mode');
|
|
524
|
+
if (isPage) {
|
|
525
|
+
dom.aiDrawBtn.hidden = true;
|
|
526
|
+
if (isDocs)
|
|
527
|
+
renderDocs();
|
|
528
|
+
else if (isCode)
|
|
529
|
+
renderCode();
|
|
530
|
+
else if (isAbout)
|
|
531
|
+
renderAbout();
|
|
532
|
+
else if (isInsights)
|
|
533
|
+
renderInsights(dom.insightsView, state);
|
|
534
|
+
else if (isDiff)
|
|
535
|
+
renderDiffView(dom.diffView, state);
|
|
536
|
+
else if (isReview)
|
|
537
|
+
renderReviewView(dom.reviewView, state);
|
|
538
|
+
else if (isWorkflows)
|
|
539
|
+
renderWorkflows(dom.workflowsView, state);
|
|
540
|
+
else if (isSbom)
|
|
541
|
+
renderSbom(dom.sbomView, state);
|
|
542
|
+
else if (isAnalysis)
|
|
543
|
+
renderDeepAnalysis(dom.analysisView, state);
|
|
544
|
+
if (isGraph) {
|
|
545
|
+
if (!forceGraphCtl)
|
|
546
|
+
forceGraphCtl = initForceGraph(dom.graphCanvas);
|
|
547
|
+
forceGraphCtl.setActive(!searchCtl?.isShowing());
|
|
548
|
+
if (graphScan !== state.scan || graphHealth !== state.health) {
|
|
549
|
+
const edges = state.scan.edges || [];
|
|
550
|
+
const facts = state.facts || {};
|
|
551
|
+
const healthMap = new Map((state.health?.perFile || []).map(h => [h.path, h]));
|
|
552
|
+
const nodes = state.scan.files.map(f => {
|
|
553
|
+
const h = healthMap.get(f.path);
|
|
554
|
+
return {
|
|
555
|
+
path: f.path,
|
|
556
|
+
fanIn: facts.fanIn?.[f.path] || 0,
|
|
557
|
+
fanOut: facts.fanOut?.[f.path] || 0,
|
|
558
|
+
community: facts.communities?.[f.path] || 0,
|
|
559
|
+
risk: h?.risk || 0,
|
|
560
|
+
complexity: h?.complexity || f.cognitive || 0,
|
|
561
|
+
lang: f.lang || 'unknown',
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
forceGraphCtl.update(nodes, edges, facts.communities || {});
|
|
565
|
+
graphScan = state.scan;
|
|
566
|
+
graphHealth = state.health;
|
|
567
|
+
}
|
|
568
|
+
forceGraphCtl.resize();
|
|
569
|
+
}
|
|
570
|
+
if (isHeatmap) {
|
|
571
|
+
const healthByPath = new Map((state.health?.perFile || []).map(f => [f.path, f]));
|
|
572
|
+
const heatFiles = state.scan.files.map(f => ({
|
|
573
|
+
path: f.path,
|
|
574
|
+
risk: healthByPath.get(f.path)?.risk || 0,
|
|
575
|
+
loc: f.size
|
|
576
|
+
}));
|
|
577
|
+
if (!heatmapCtl)
|
|
578
|
+
heatmapCtl = initHeatmap(dom.heatmapCanvas, heatFiles);
|
|
579
|
+
else
|
|
580
|
+
heatmapCtl.update(heatFiles);
|
|
581
|
+
}
|
|
582
|
+
renderCrumbs();
|
|
583
|
+
renderFoot();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const { scan, facts, manifest } = state;
|
|
587
|
+
const key = aiViewKey();
|
|
588
|
+
updateAiBtn(key);
|
|
589
|
+
// Tree-map furniture only belongs to the Map; SVG export can't take cells.
|
|
590
|
+
dom.svgBtn.hidden = isPage || isAtlasList || state.view === 'map';
|
|
591
|
+
const filterable = ['map', 'files', 'patterns'].includes(state.view);
|
|
592
|
+
dom.nodeFilter.hidden = !filterable && state.view !== 'atlas';
|
|
593
|
+
dom.testsToggle.hidden = !filterable && state.view !== 'atlas';
|
|
594
|
+
dom.depthSelect.hidden = state.view !== 'map';
|
|
595
|
+
if (state.view === 'map') {
|
|
596
|
+
// Copy Mermaid still hands over the classic folder overview.
|
|
597
|
+
state.currentDiagram = { source: overviewDiagram(scan, facts).source, nodes: {} };
|
|
598
|
+
renderMap(true);
|
|
599
|
+
renderCrumbs();
|
|
600
|
+
renderFoot();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (isAtlasList) {
|
|
604
|
+
dom.aiDrawBtn.hidden = true;
|
|
605
|
+
dom.canvasEmpty.hidden = true; // the list is its own content; no placeholder
|
|
606
|
+
renderAtlasList();
|
|
607
|
+
renderCrumbs();
|
|
608
|
+
renderFoot();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
// An active AI draft replaces the static diagram for its exact view.
|
|
612
|
+
if (state.aiActiveKey === key && state.aiDiagrams[key]) {
|
|
613
|
+
const ai = state.aiDiagrams[key];
|
|
614
|
+
const source = diagramThemeBlock() + '\n' + ai.body; // theme follows the toggle
|
|
615
|
+
dom.canvasEmpty.hidden = true;
|
|
616
|
+
try {
|
|
617
|
+
await renderInto(dom.canvasContent, source);
|
|
618
|
+
wireAIClicks();
|
|
619
|
+
requestAnimationFrame(() => panzoom.fit());
|
|
620
|
+
}
|
|
621
|
+
catch (err) {
|
|
622
|
+
console.error(err);
|
|
623
|
+
delete state.aiDiagrams[key];
|
|
624
|
+
state.aiActiveKey = null;
|
|
625
|
+
toast('That AI sketch would not draw — back to the static map.');
|
|
626
|
+
renderCanvas();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
state.currentDiagram = { source, nodes: {} };
|
|
630
|
+
renderCrumbs();
|
|
631
|
+
renderFoot(ai.caption);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// Honest empty states: better than a sparse, confusing diagram.
|
|
635
|
+
const nothing = nothingToShow();
|
|
636
|
+
if (nothing) {
|
|
637
|
+
dom.canvasContent.innerHTML = '';
|
|
638
|
+
dom.canvasEmpty.textContent = nothing;
|
|
639
|
+
dom.canvasEmpty.hidden = false;
|
|
640
|
+
state.currentDiagram = { source: '', nodes: {} };
|
|
641
|
+
renderCrumbs();
|
|
642
|
+
renderFoot();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
let d;
|
|
646
|
+
if (state.view === 'patterns') {
|
|
647
|
+
d = layersDiagram(scan, facts, state.patterns.layersInfo, { include: pathFilter(state.filters) });
|
|
648
|
+
}
|
|
649
|
+
else if (state.view === 'health') {
|
|
650
|
+
d = healthDiagram(scan, facts, state.health, { include: pathFilter(state.filters) });
|
|
651
|
+
}
|
|
652
|
+
else if (state.view === 'security') {
|
|
653
|
+
d = securityDiagram(scan, facts, state.security, { include: pathFilter(state.filters) });
|
|
654
|
+
}
|
|
655
|
+
else if (state.view === 'history') {
|
|
656
|
+
d = historyDiagram(scan, facts, state.history);
|
|
657
|
+
}
|
|
658
|
+
else if (state.view === 'services') {
|
|
659
|
+
d = servicesDiagram(scan, manifest);
|
|
660
|
+
}
|
|
661
|
+
else if (state.view === 'tour') {
|
|
662
|
+
const stop = state.tour.stops[state.tour.idx];
|
|
663
|
+
d = stop ? fileDetailDiagram(scan, facts, stop.path) : emptyDiagram('No tour stops could be picked from this repo.');
|
|
664
|
+
}
|
|
665
|
+
else if (state.view === 'atlas' && state.atlasOpen) {
|
|
666
|
+
d = openCardDiagram();
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
d = state.detail
|
|
670
|
+
? fileDetailDiagram(scan, facts, state.detail)
|
|
671
|
+
: folderDiagram(scan, facts, state.folder, { include: pathFilter(state.filters) });
|
|
672
|
+
}
|
|
673
|
+
state.currentDiagram = d;
|
|
549
674
|
dom.canvasEmpty.hidden = true;
|
|
550
675
|
try {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
state.currentDiagram = { source, nodes: {} };
|
|
563
|
-
renderCrumbs();
|
|
564
|
-
renderFoot(ai.caption);
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Honest empty states: better than a sparse, confusing diagram.
|
|
569
|
-
const nothing = nothingToShow();
|
|
570
|
-
if (nothing) {
|
|
571
|
-
dom.canvasContent.innerHTML = '';
|
|
572
|
-
dom.canvasEmpty.textContent = nothing;
|
|
573
|
-
dom.canvasEmpty.hidden = false;
|
|
574
|
-
state.currentDiagram = { source: '', nodes: {} };
|
|
676
|
+
await renderInto(dom.canvasContent, d.source);
|
|
677
|
+
wireNodeClicks(dom.canvasContent, d.nodes, onNodeClick);
|
|
678
|
+
requestAnimationFrame(() => panzoom.fit());
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
console.error(err);
|
|
682
|
+
dom.canvasEmpty.textContent = 'That diagram would not draw: ' + err.message;
|
|
683
|
+
dom.canvasEmpty.hidden = false;
|
|
684
|
+
}
|
|
575
685
|
renderCrumbs();
|
|
576
686
|
renderFoot();
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
let d;
|
|
581
|
-
if (state.view === 'patterns') {
|
|
582
|
-
d = layersDiagram(scan, facts, state.patterns.layersInfo, { include: pathFilter(state.filters) });
|
|
583
|
-
} else if (state.view === 'health') {
|
|
584
|
-
d = healthDiagram(scan, facts, state.health, { include: pathFilter(state.filters) });
|
|
585
|
-
} else if (state.view === 'security') {
|
|
586
|
-
d = securityDiagram(scan, facts, state.security, { include: pathFilter(state.filters) });
|
|
587
|
-
} else if (state.view === 'history') {
|
|
588
|
-
d = historyDiagram(scan, facts, state.history);
|
|
589
|
-
} else if (state.view === 'services') {
|
|
590
|
-
d = servicesDiagram(scan, manifest);
|
|
591
|
-
} else if (state.view === 'tour') {
|
|
592
|
-
const stop = state.tour.stops[state.tour.idx];
|
|
593
|
-
d = stop ? fileDetailDiagram(scan, facts, stop.path) : emptyDiagram('No tour stops could be picked from this repo.');
|
|
594
|
-
} else if (state.view === 'atlas' && state.atlasOpen) {
|
|
595
|
-
d = openCardDiagram();
|
|
596
|
-
} else {
|
|
597
|
-
d = state.detail
|
|
598
|
-
? fileDetailDiagram(scan, facts, state.detail)
|
|
599
|
-
: folderDiagram(scan, facts, state.folder, { include: pathFilter(state.filters) });
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
state.currentDiagram = d;
|
|
603
|
-
dom.canvasEmpty.hidden = true;
|
|
604
|
-
try {
|
|
605
|
-
await renderInto(dom.canvasContent, d.source);
|
|
606
|
-
wireNodeClicks(dom.canvasContent, d.nodes, onNodeClick);
|
|
607
|
-
requestAnimationFrame(() => panzoom.fit());
|
|
608
|
-
} catch (err) {
|
|
609
|
-
console.error(err);
|
|
610
|
-
dom.canvasEmpty.textContent = 'That diagram would not draw: ' + err.message;
|
|
611
|
-
dom.canvasEmpty.hidden = false;
|
|
612
|
-
}
|
|
613
|
-
renderCrumbs();
|
|
614
|
-
renderFoot();
|
|
615
687
|
}
|
|
616
|
-
|
|
617
688
|
// Returns a message when the current view has nothing worth drawing, else
|
|
618
689
|
// null. The inspector still has plenty to say in these cases — this is only
|
|
619
690
|
// about the canvas.
|
|
620
691
|
function nothingToShow() {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
692
|
+
const { scan, facts } = state;
|
|
693
|
+
const parsed = scan.stats.filesParsed;
|
|
694
|
+
const edges = scan.stats.edgeCount;
|
|
695
|
+
if (state.view === 'patterns') {
|
|
696
|
+
if (!parsed)
|
|
697
|
+
return 'No code files parsed, so there are no patterns to read.';
|
|
698
|
+
if (!edges)
|
|
699
|
+
return 'No import connections anywhere — a shape needs edges, and there are none.';
|
|
700
|
+
}
|
|
701
|
+
if (state.view === 'files') {
|
|
702
|
+
if (state.detail) {
|
|
703
|
+
const f = scanIndex(scan).fileAt(state.detail);
|
|
704
|
+
if (!f)
|
|
705
|
+
return 'That file is not in the scan.';
|
|
706
|
+
const fin = facts.fanIn[state.detail] || 0;
|
|
707
|
+
const fout = facts.fanOut[state.detail] || 0;
|
|
708
|
+
if (!fin && !fout) {
|
|
709
|
+
return f.functions.length
|
|
710
|
+
? `${f.name} stands alone — nothing imports it and it imports nothing.`
|
|
711
|
+
: `${f.name} stands alone — nothing imports it, it imports nothing, and no functions to chart.`;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
const here = scanIndex(scan).filesIn(state.folder);
|
|
716
|
+
if (!here.length)
|
|
717
|
+
return 'Nothing parsed in this folder.';
|
|
718
|
+
const localSet = new Set(here.map((x) => x.path));
|
|
719
|
+
const touched = scan.edges.some((e) => localSet.has(e.from) || localSet.has(e.to));
|
|
720
|
+
if (!touched)
|
|
721
|
+
return 'The files here import nothing we can see, and nothing imports them.';
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (state.view === 'services') {
|
|
725
|
+
if (!state.manifest?.services?.length) {
|
|
726
|
+
return 'No services detected — no docker-compose, Procfile, or workspace setup we recognize.';
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return null;
|
|
657
730
|
}
|
|
658
|
-
|
|
659
731
|
// ------------------------------------------------------- filters & routing --
|
|
660
|
-
|
|
661
732
|
// Typing re-renders, so it is debounced; the toggles are single clicks and are
|
|
662
733
|
// not. The Map redraws in place, everything else goes through the canvas router.
|
|
663
734
|
let filterTimer = null;
|
|
664
735
|
function applyFilters() {
|
|
665
|
-
|
|
666
|
-
|
|
736
|
+
if (state.view === 'map')
|
|
737
|
+
renderMap(false);
|
|
738
|
+
else
|
|
739
|
+
renderCanvas();
|
|
667
740
|
}
|
|
668
741
|
function applyFiltersSoon() {
|
|
669
|
-
|
|
670
|
-
|
|
742
|
+
clearTimeout(filterTimer);
|
|
743
|
+
filterTimer = setTimeout(applyFilters, 160);
|
|
671
744
|
}
|
|
672
|
-
|
|
673
745
|
dom.modeSwitch.addEventListener('click', (event) => {
|
|
674
|
-
|
|
675
|
-
|
|
746
|
+
const chip = eventElement(event)?.closest('.mode-chip');
|
|
747
|
+
if (chip)
|
|
748
|
+
setView(chip.dataset.mode);
|
|
676
749
|
});
|
|
677
750
|
dom.nodeFilter.addEventListener('input', () => {
|
|
678
|
-
|
|
679
|
-
|
|
751
|
+
state.filters.text = dom.nodeFilter.value;
|
|
752
|
+
applyFiltersSoon();
|
|
680
753
|
});
|
|
681
754
|
dom.testsToggle.addEventListener('click', () => {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
755
|
+
state.filters.showTests = !state.filters.showTests;
|
|
756
|
+
dom.testsToggle.classList.toggle('is-on', state.filters.showTests);
|
|
757
|
+
applyFilters();
|
|
685
758
|
});
|
|
686
759
|
dom.depthSelect.addEventListener('change', () => {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
760
|
+
state.filters.depth = Number(dom.depthSelect.value);
|
|
761
|
+
if (state.filters.depth > 0)
|
|
762
|
+
state.mm.expanded = expandedToDepth(state.treeData, state.filters.depth);
|
|
763
|
+
if (state.view === 'map')
|
|
764
|
+
renderMap(true);
|
|
690
765
|
});
|
|
691
|
-
|
|
692
766
|
// ---------------------------------------------------------- crumbs & foot --
|
|
693
|
-
|
|
694
767
|
function renderCrumbs() {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (state.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
768
|
+
const parts = [];
|
|
769
|
+
parts.push(`<button data-go="map">${escapeHtml(state.scan.name)}</button>`);
|
|
770
|
+
if (state.view === 'files' || state.view === 'tour') {
|
|
771
|
+
const folder = state.view === 'tour' ? dirOf(state.tour.stops[state.tour.idx]?.path || '') : state.folder;
|
|
772
|
+
if (folder) {
|
|
773
|
+
const segs = folder.split('/');
|
|
774
|
+
segs.forEach((seg, i) => {
|
|
775
|
+
parts.push(`<button data-folder="${escapeHtml(segs.slice(0, i + 1).join('/'))}">${escapeHtml(seg)}</button>`);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
const detail = state.view === 'tour' ? state.tour.stops[state.tour.idx]?.path : state.detail;
|
|
779
|
+
if (detail)
|
|
780
|
+
parts.push(`<span>${escapeHtml(detail.split('/').pop())}</span>`);
|
|
781
|
+
}
|
|
782
|
+
else if (state.view === 'services') {
|
|
783
|
+
parts.push('<span>services</span>');
|
|
784
|
+
}
|
|
785
|
+
else if (state.view === 'atlas') {
|
|
786
|
+
parts.push('<button data-atlas="list">atlas</button>');
|
|
787
|
+
if (state.atlasOpen) {
|
|
788
|
+
const t = state.atlasOpen.title;
|
|
789
|
+
parts.push(`<span>${escapeHtml(t.length > 34 ? '…' + t.slice(-33) : t)}</span>`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
else if (state.view === 'about') {
|
|
793
|
+
parts.push('<span>about</span>');
|
|
794
|
+
}
|
|
795
|
+
else if (state.view === 'docs') {
|
|
796
|
+
parts.push('<span>docs</span>');
|
|
797
|
+
}
|
|
798
|
+
else if (state.view === 'patterns') {
|
|
799
|
+
parts.push('<span>patterns</span>');
|
|
800
|
+
}
|
|
801
|
+
else if (state.view === 'history') {
|
|
802
|
+
parts.push('<span>history</span>');
|
|
803
|
+
}
|
|
804
|
+
dom.crumbs.innerHTML = parts.join('<span class="sep">/</span>');
|
|
805
|
+
for (const b of dom.crumbs.querySelectorAll('button')) {
|
|
806
|
+
b.addEventListener('click', () => {
|
|
807
|
+
if (b.dataset.atlas) {
|
|
808
|
+
state.atlasOpen = null;
|
|
809
|
+
renderCanvas();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (b.dataset.go === 'map')
|
|
813
|
+
return setView('map');
|
|
814
|
+
state.folder = b.dataset.folder || '';
|
|
815
|
+
state.detail = null;
|
|
816
|
+
setView('files');
|
|
817
|
+
});
|
|
818
|
+
}
|
|
740
819
|
}
|
|
741
|
-
|
|
742
820
|
function renderFoot(aiCaption) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
821
|
+
const s = state.scan.stats;
|
|
822
|
+
const f = state.facts;
|
|
823
|
+
const langList = Object.entries(s.languages).map(([k, v]) => `${v} ${k}`).join(', ');
|
|
824
|
+
dom.stageFoot.innerHTML = [
|
|
825
|
+
aiCaption ? `<span class="ai-cap" title="${escapeHtml(aiCaption)}">“${escapeHtml(aiCaption)}”</span>` : '',
|
|
826
|
+
`<span class="foot-stats">
|
|
749
827
|
<b>${s.filesParsed}</b> files ·
|
|
750
828
|
<b>${s.edgeCount}</b> connections ·
|
|
751
829
|
<b>${f.entries.length}</b> entries ·
|
|
@@ -753,281 +831,258 @@ function renderFoot(aiCaption) {
|
|
|
753
831
|
<b>${f.orphans.length}</b> unconnected
|
|
754
832
|
${f.cycles.length ? ` · <b class="warn">${f.cycles.length}</b> circular` : ''}
|
|
755
833
|
</span>`,
|
|
756
|
-
|
|
834
|
+
`<span class="foot-right">
|
|
757
835
|
${langList ? `<span class="langs" title="${escapeHtml(langList)}">${escapeHtml(langList)}</span>` : ''}
|
|
758
836
|
<span>scanned in <b>${s.tookMs}</b> ms</span>
|
|
759
837
|
</span>`,
|
|
760
|
-
|
|
838
|
+
].filter(Boolean).join('');
|
|
761
839
|
}
|
|
762
|
-
|
|
763
840
|
// ------------------------------------------------------------------- tour --
|
|
764
|
-
|
|
765
841
|
function showTourStop() {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
842
|
+
const { stops, idx } = state.tour;
|
|
843
|
+
const stop = stops[idx];
|
|
844
|
+
if (!stop) {
|
|
845
|
+
dom.tourTitle.textContent = 'No stops could be picked from this repo.';
|
|
846
|
+
dom.tourCount.textContent = '0 / 0';
|
|
847
|
+
inspector.showOverview(state);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
dom.tourTitle.textContent = stop.path;
|
|
851
|
+
dom.tourTitle.title = stop.why;
|
|
852
|
+
dom.tourCount.textContent = describeStop(stop, idx, stops.length).count;
|
|
853
|
+
focusFile(stop.path);
|
|
854
|
+
syncInspector();
|
|
855
|
+
renderSidebar();
|
|
780
856
|
}
|
|
781
|
-
|
|
782
857
|
dom.tourPrev.addEventListener('click', () => {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
858
|
+
if (state.tour.idx > 0) {
|
|
859
|
+
state.tour.idx--;
|
|
860
|
+
renderCanvas();
|
|
861
|
+
showTourStop();
|
|
862
|
+
}
|
|
788
863
|
});
|
|
789
864
|
dom.tourNext.addEventListener('click', () => {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
865
|
+
if (state.tour.idx < state.tour.stops.length - 1) {
|
|
866
|
+
state.tour.idx++;
|
|
867
|
+
renderCanvas();
|
|
868
|
+
showTourStop();
|
|
869
|
+
}
|
|
795
870
|
});
|
|
796
871
|
dom.tourExit.addEventListener('click', () => setView('map'));
|
|
797
|
-
|
|
798
872
|
// ---------------------------------------------------------- stage actions --
|
|
799
|
-
|
|
800
873
|
dom.copyMermaidBtn.addEventListener('click', async () => {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
874
|
+
try {
|
|
875
|
+
await navigator.clipboard.writeText(state.currentDiagram.source);
|
|
876
|
+
toast('Mermaid source copied.');
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
toast('The clipboard said no.');
|
|
880
|
+
}
|
|
807
881
|
});
|
|
808
|
-
|
|
809
882
|
dom.svgBtn.addEventListener('click', () => {
|
|
810
|
-
|
|
811
|
-
|
|
883
|
+
const name = (state.scan?.name || 'codebase') + '-' + state.view + '.svg';
|
|
884
|
+
if (downloadSvg(dom.canvasContent, name))
|
|
885
|
+
toast('SVG saved.');
|
|
812
886
|
});
|
|
813
|
-
|
|
814
887
|
dom.fitBtn.addEventListener('click', () => panzoom.fit());
|
|
815
|
-
dom.searchTriggerBtn.addEventListener('click', () => searchCtl?.show());
|
|
816
|
-
|
|
888
|
+
dom.searchTriggerBtn.addEventListener('click', () => searchCtl?.show(dom.searchTriggerBtn));
|
|
889
|
+
document.addEventListener('search-visibility', () => forceGraphCtl?.setActive(state.view === 'graph' && !searchCtl?.isShowing()));
|
|
890
|
+
dom.graphSearchInput?.addEventListener('input', (e) => forceGraphCtl?.search(e.currentTarget.value));
|
|
817
891
|
dom.graphFit?.addEventListener('click', () => forceGraphCtl?.fit());
|
|
818
|
-
|
|
819
892
|
// ------------------------------------------------------------- AI explain --
|
|
820
|
-
|
|
821
893
|
inspector.initInspector({
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
894
|
+
onAskAI: askAI,
|
|
895
|
+
onClearFocus: clearFocus,
|
|
896
|
+
// The inspector's "Code" button means the Code tab, not the Explorer —
|
|
897
|
+
// openFile would drop the person back on the map they came from.
|
|
898
|
+
onOpenCode: openFileInCode,
|
|
899
|
+
onDeepDive: openDeepDive,
|
|
900
|
+
onToast: toast,
|
|
829
901
|
});
|
|
830
|
-
|
|
831
902
|
// The About view owns its own rendering; it borrows these three from the shell.
|
|
832
903
|
initAbout({
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
904
|
+
host: dom.aboutView,
|
|
905
|
+
onToast: toast,
|
|
906
|
+
onOpenSettings: openSettings,
|
|
907
|
+
repoOverview,
|
|
837
908
|
});
|
|
838
|
-
|
|
839
909
|
initDocs({
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
910
|
+
host: dom.docView,
|
|
911
|
+
onToast: toast,
|
|
912
|
+
onOpenSettings: openSettings,
|
|
913
|
+
onOpenFile: openFile,
|
|
914
|
+
repoOverview,
|
|
845
915
|
});
|
|
846
|
-
|
|
847
916
|
initCodeTab({
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
917
|
+
host: dom.codeView,
|
|
918
|
+
blameGutter: dom.blameGutter,
|
|
919
|
+
onSyncInspector: syncInspector,
|
|
920
|
+
onRenderSidebar: renderSidebar,
|
|
921
|
+
onOpenFile: openFile,
|
|
922
|
+
onDeepDive: openDeepDive,
|
|
923
|
+
onToast: toast,
|
|
924
|
+
getTheme,
|
|
856
925
|
});
|
|
857
|
-
|
|
858
926
|
// The deep-analysis panel lives in the security view's inspector. It runs the
|
|
859
927
|
// external engines through the server, then merges what they find back into the
|
|
860
928
|
// scan so the grade and the finding list update to include them.
|
|
861
929
|
initAnalysisPanel({
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
930
|
+
host: dom.inspSecurityTools,
|
|
931
|
+
onToast: toast,
|
|
932
|
+
onOpenFile: openFileInCode,
|
|
865
933
|
});
|
|
866
|
-
|
|
867
934
|
// The full-page Deep Analysis view. Same store, same run, more room: the whole
|
|
868
935
|
// report with file links and an AI explainer.
|
|
869
936
|
initDeepAnalysis({
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
937
|
+
host: dom.analysisView,
|
|
938
|
+
onToast: toast,
|
|
939
|
+
onOpenFile: openFileInCode,
|
|
940
|
+
onOpenSettings: openSettings,
|
|
941
|
+
repoOverview,
|
|
875
942
|
});
|
|
876
|
-
|
|
877
943
|
// A run started from either surface changes the scan's findings, so whatever
|
|
878
944
|
// panel is on screen has to be told. The merge itself happens in the store.
|
|
879
945
|
subscribeAnalysis(() => {
|
|
880
|
-
|
|
946
|
+
if (state.scan)
|
|
947
|
+
syncInspector();
|
|
881
948
|
});
|
|
882
|
-
|
|
883
949
|
initAiDraft({
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
950
|
+
host: dom.aiDrawBtn,
|
|
951
|
+
statusEl: dom.canvasEmpty, // the placeholder doubles as the progress line
|
|
952
|
+
nodeHost: dom.canvasContent,
|
|
953
|
+
onRender: renderCanvas,
|
|
954
|
+
onOpenFile: openFile,
|
|
955
|
+
onOpenSettings: openSettings,
|
|
956
|
+
onToast: toast,
|
|
891
957
|
});
|
|
892
|
-
|
|
893
958
|
initAtlas({
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
959
|
+
host: dom.canvasContent,
|
|
960
|
+
canvas: dom.canvas,
|
|
961
|
+
onSyncInspector: syncInspector,
|
|
962
|
+
onRender: renderCanvas,
|
|
963
|
+
onHome: () => panzoom.home(),
|
|
899
964
|
});
|
|
900
|
-
|
|
901
965
|
initMap({
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
966
|
+
host: dom.canvasContent,
|
|
967
|
+
toggleBtn: dom.mmToggleBtn,
|
|
968
|
+
onSyncInspector: syncInspector,
|
|
969
|
+
onRenderSidebar: renderSidebar,
|
|
970
|
+
onOpenFile: openFile,
|
|
971
|
+
onOpenFolder: (folder) => {
|
|
972
|
+
state.folder = folder;
|
|
973
|
+
state.detail = null;
|
|
974
|
+
setView('files');
|
|
975
|
+
},
|
|
976
|
+
onToast: toast,
|
|
977
|
+
onFit: () => requestAnimationFrame(() => panzoom.fit()),
|
|
914
978
|
});
|
|
915
|
-
|
|
916
979
|
function repoOverview() {
|
|
917
|
-
|
|
980
|
+
return explainOverview(state.scan, state.facts, state.manifest).replace(/[*`]/g, '');
|
|
918
981
|
}
|
|
919
|
-
|
|
920
982
|
async function askAI() {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
983
|
+
if (!llm.isConfigured()) {
|
|
984
|
+
openSettings();
|
|
985
|
+
toast('Add an endpoint and model first.');
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (!state.selected)
|
|
989
|
+
return;
|
|
990
|
+
const settings = llm.getSettings();
|
|
991
|
+
const ctx = await gatherFileContext(state.selected);
|
|
992
|
+
const messages = llm.fileMessages({
|
|
993
|
+
repoName: state.scan.name,
|
|
994
|
+
overview: repoOverview(),
|
|
995
|
+
...ctx,
|
|
996
|
+
});
|
|
997
|
+
const writer = inspector.explainStreamWriter();
|
|
998
|
+
try {
|
|
999
|
+
// Reasoning models (like the bundled Nemotron default) think before they
|
|
1000
|
+
// write, so the budget needs room for both the thinking and the answer.
|
|
1001
|
+
for await (const delta of streamExplain({ ...settings, messages, maxTokens: 1600 })) {
|
|
1002
|
+
writer.push(delta);
|
|
1003
|
+
}
|
|
1004
|
+
writer.finish(`Written by ${settings.model}, from the real source and the import graph.`);
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
writer.fail(err.message + ' Check the API key drawer, top right.');
|
|
1008
|
+
}
|
|
947
1009
|
}
|
|
948
|
-
|
|
949
1010
|
// Free-form questions. The selected file's context rides along when there is
|
|
950
1011
|
// one; otherwise the model reasons from the repo overview alone.
|
|
951
1012
|
async function askCustom() {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1013
|
+
const question = dom.askInput.value.trim();
|
|
1014
|
+
if (!question)
|
|
1015
|
+
return toast('Type a question first.');
|
|
1016
|
+
if (!state.scan)
|
|
1017
|
+
return;
|
|
1018
|
+
if (!llm.isConfigured()) {
|
|
1019
|
+
openSettings();
|
|
1020
|
+
toast('Add an endpoint and model first.');
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const settings = llm.getSettings();
|
|
1024
|
+
const file = state.selected ? await gatherFileContext(state.selected) : null;
|
|
1025
|
+
const messages = llm.questionMessages({
|
|
1026
|
+
repoName: state.scan.name,
|
|
1027
|
+
overview: repoOverview(),
|
|
1028
|
+
question,
|
|
1029
|
+
file,
|
|
1030
|
+
});
|
|
1031
|
+
dom.askBtn.disabled = true;
|
|
1032
|
+
dom.askBtn.textContent = '…';
|
|
1033
|
+
const writer = inspector.explainStreamWriter(`<p class="ask-q">“${escapeHtml(question)}”</p>`);
|
|
1034
|
+
try {
|
|
1035
|
+
for await (const delta of streamExplain({ ...settings, messages, maxTokens: 1600 })) {
|
|
1036
|
+
writer.push(delta);
|
|
1037
|
+
}
|
|
1038
|
+
writer.finish(`Answered by ${settings.model}.`);
|
|
1039
|
+
}
|
|
1040
|
+
catch (err) {
|
|
1041
|
+
writer.fail(err.message + ' Check the API key drawer, top right.');
|
|
1042
|
+
}
|
|
1043
|
+
finally {
|
|
1044
|
+
dom.askBtn.disabled = false;
|
|
1045
|
+
dom.askBtn.textContent = 'Ask';
|
|
1046
|
+
}
|
|
984
1047
|
}
|
|
985
|
-
|
|
986
1048
|
dom.askBtn.addEventListener('click', askCustom);
|
|
987
1049
|
dom.askInput.addEventListener('keydown', (event) => {
|
|
988
|
-
|
|
1050
|
+
if (event.key === 'Enter')
|
|
1051
|
+
askCustom();
|
|
989
1052
|
});
|
|
990
|
-
|
|
991
1053
|
// ---------------------------------------------------------- settings bits --
|
|
992
|
-
|
|
993
1054
|
function openSettings() {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1055
|
+
// Same reason the MCP drawer closes settings: one scrim at a time.
|
|
1056
|
+
if (!dom.mcpDrawer.hidden)
|
|
1057
|
+
closeMcp();
|
|
1058
|
+
serverDrawer.close();
|
|
1059
|
+
const s = llm.getSettings();
|
|
1060
|
+
dom.setBaseUrl.value = s.baseUrl;
|
|
1061
|
+
dom.setApiKey.value = s.apiKey;
|
|
1062
|
+
dom.setModel.value = s.model;
|
|
1063
|
+
dom.settingsStatus.textContent = '';
|
|
1064
|
+
dom.settingsDrawer.hidden = false;
|
|
1065
|
+
dom.drawerScrim.hidden = false;
|
|
1004
1066
|
}
|
|
1005
|
-
|
|
1006
1067
|
function closeSettings() {
|
|
1007
|
-
|
|
1008
|
-
|
|
1068
|
+
dom.settingsDrawer.hidden = true;
|
|
1069
|
+
dom.drawerScrim.hidden = true;
|
|
1009
1070
|
}
|
|
1010
|
-
|
|
1011
1071
|
dom.settingsBtn.addEventListener('click', openSettings);
|
|
1012
1072
|
dom.drawerClose.addEventListener('click', closeSettings);
|
|
1013
|
-
|
|
1014
1073
|
// ------------------------------------------------------------- server bits --
|
|
1015
|
-
|
|
1016
1074
|
// The third drawer. Its content and round-trips live in js/serverSettings.js;
|
|
1017
1075
|
// what stays here is the choreography it shares with the other two drawers —
|
|
1018
1076
|
// one scrim at a time, Escape to dismiss.
|
|
1019
1077
|
const serverDrawer = createServerDrawer({ toast });
|
|
1020
|
-
|
|
1021
1078
|
function openServer() {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1079
|
+
closeSettings();
|
|
1080
|
+
if (!dom.mcpDrawer.hidden)
|
|
1081
|
+
closeMcp();
|
|
1082
|
+
serverDrawer.open();
|
|
1025
1083
|
}
|
|
1026
|
-
|
|
1027
1084
|
dom.serverBtn.addEventListener('click', () => (serverDrawer.isOpen() ? serverDrawer.close() : openServer()));
|
|
1028
|
-
|
|
1029
1085
|
// ------------------------------------------------------------------- mcp --
|
|
1030
|
-
|
|
1031
1086
|
// The topbar button, the drawer, and the polling behind them.
|
|
1032
1087
|
//
|
|
1033
1088
|
// The shape is deliberately the same as the settings drawer above — one scrim,
|
|
@@ -1036,304 +1091,297 @@ dom.serverBtn.addEventListener('click', () => (serverDrawer.isOpen() ? serverDra
|
|
|
1036
1091
|
// a live status, two actions, and a config block, so it polls. Polling continues
|
|
1037
1092
|
// while closed, slowly, because the dot on the button has to stay truthful when
|
|
1038
1093
|
// the drawer is not there to explain it.
|
|
1039
|
-
|
|
1040
1094
|
let mcpPoll = null;
|
|
1041
1095
|
let mcpBusy = false;
|
|
1042
1096
|
let mcpCommand = null; // fetched once; the command does not change while it runs
|
|
1043
|
-
|
|
1044
1097
|
// The dot's three states, and the copy that goes with each. `starting` is
|
|
1045
1098
|
// separate from `running` because the handshake takes a beat, and claiming
|
|
1046
1099
|
// success before it is known would be a lie the person acts on.
|
|
1047
1100
|
function mcpDotClass(state) {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1101
|
+
if (state === 'running')
|
|
1102
|
+
return 'is-running';
|
|
1103
|
+
if (state === 'starting' || state === 'stopping')
|
|
1104
|
+
return 'is-starting';
|
|
1105
|
+
if (state === 'error' || state === 'unavailable')
|
|
1106
|
+
return 'is-error';
|
|
1107
|
+
return '';
|
|
1052
1108
|
}
|
|
1053
|
-
|
|
1054
1109
|
function mcpStateLabel(data) {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1110
|
+
switch (data.state) {
|
|
1111
|
+
case 'running': return `Running · ${data.toolCount} tools`;
|
|
1112
|
+
case 'starting': return 'Starting…';
|
|
1113
|
+
case 'stopping': return 'Stopping…';
|
|
1114
|
+
case 'error': return 'Failed to start';
|
|
1115
|
+
case 'unavailable': return 'Not available';
|
|
1116
|
+
default: return 'Stopped';
|
|
1117
|
+
}
|
|
1063
1118
|
}
|
|
1064
|
-
|
|
1065
1119
|
function formatUptime(ms) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1120
|
+
if (!ms && ms !== 0)
|
|
1121
|
+
return '0s';
|
|
1122
|
+
const s = Math.floor(ms / 1000);
|
|
1123
|
+
if (s < 60)
|
|
1124
|
+
return `${s}s`;
|
|
1125
|
+
const m = Math.floor(s / 60);
|
|
1126
|
+
if (m < 60)
|
|
1127
|
+
return `${m}m ${s % 60}s`;
|
|
1128
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
1072
1129
|
}
|
|
1073
|
-
|
|
1074
1130
|
function renderMcpStatus(data) {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
if (data.running) {
|
|
1115
|
-
dom.mcpToolsHint.textContent = `${data.toolCount} tools: ${(data.tools || []).join(', ')}`;
|
|
1116
|
-
}
|
|
1131
|
+
if (!data) {
|
|
1132
|
+
// No server answered. The button is still there — it always is — but it says
|
|
1133
|
+
// so rather than showing a confident "stopped" for a process we cannot see.
|
|
1134
|
+
dom.mcpDot.className = 'mcp-dot is-error';
|
|
1135
|
+
dom.mcpBtnLabel.textContent = 'MCP';
|
|
1136
|
+
dom.mcpPanelDot.className = 'mcp-dot mcp-dot-lg is-error';
|
|
1137
|
+
dom.mcpPanelState.textContent = 'The server did not answer.';
|
|
1138
|
+
dom.mcpStart.disabled = true;
|
|
1139
|
+
dom.mcpStop.disabled = true;
|
|
1140
|
+
dom.mcpStatus.textContent = 'Is the Onboarder server running?';
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const cls = mcpDotClass(data.state);
|
|
1144
|
+
dom.mcpDot.className = 'mcp-dot ' + cls;
|
|
1145
|
+
dom.mcpPanelDot.className = 'mcp-dot mcp-dot-lg ' + cls;
|
|
1146
|
+
dom.mcpBtnLabel.textContent = data.running ? 'MCP on' : 'MCP';
|
|
1147
|
+
dom.mcpBtn.title = data.running
|
|
1148
|
+
? `MCP server running · ${data.toolCount} tools · pid ${data.pid}`
|
|
1149
|
+
: 'Start the MCP server so AI agents can read this repository';
|
|
1150
|
+
dom.mcpPanelState.textContent = mcpStateLabel(data);
|
|
1151
|
+
// Both buttons stay visible and swap roles through `disabled`, so the panel
|
|
1152
|
+
// does not reflow as the state changes underneath it.
|
|
1153
|
+
const busy = data.state === 'starting' || data.state === 'stopping';
|
|
1154
|
+
dom.mcpStart.disabled = busy || data.state === 'unavailable' || data.running;
|
|
1155
|
+
dom.mcpStop.disabled = busy || data.state === 'unavailable' || !data.running;
|
|
1156
|
+
const bits = [];
|
|
1157
|
+
if (data.running) {
|
|
1158
|
+
bits.push(`pid ${data.pid}`);
|
|
1159
|
+
bits.push(`up ${formatUptime(data.uptimeMs)}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (data.lastError)
|
|
1162
|
+
bits.push(data.lastError);
|
|
1163
|
+
dom.mcpStatus.textContent = bits.join(' · ');
|
|
1164
|
+
// The config only appears once there is a server to configure against. Shown
|
|
1165
|
+
// earlier, it would be a command for a process that is not running.
|
|
1166
|
+
dom.mcpConfig.hidden = !data.running;
|
|
1167
|
+
if (data.running) {
|
|
1168
|
+
dom.mcpToolsHint.textContent = `${data.toolCount} tools: ${(data.tools || []).join(', ')}`;
|
|
1169
|
+
}
|
|
1117
1170
|
}
|
|
1118
|
-
|
|
1119
1171
|
function scheduleMcpPoll(ms) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1172
|
+
clearTimeout(mcpPoll);
|
|
1173
|
+
mcpPoll = setTimeout(pollMcp, ms);
|
|
1122
1174
|
}
|
|
1123
|
-
|
|
1124
1175
|
async function pollMcp() {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1176
|
+
renderMcpStatus(await fetchMcpStatus());
|
|
1177
|
+
// Closed: a slow heartbeat, so the dot is right when the person comes back
|
|
1178
|
+
// without the tab asking the server anything it does not need to answer.
|
|
1179
|
+
scheduleMcpPoll(dom.mcpDrawer.hidden ? 8000 : 2500);
|
|
1129
1180
|
}
|
|
1130
|
-
|
|
1131
1181
|
async function loadMcpCommand() {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1182
|
+
if (mcpCommand)
|
|
1183
|
+
return mcpCommand;
|
|
1184
|
+
mcpCommand = await fetchMcpCommand();
|
|
1185
|
+
if (mcpCommand) {
|
|
1186
|
+
dom.mcpCommandInput.value = mcpCommand.command;
|
|
1187
|
+
renderMcpConfigBlock();
|
|
1188
|
+
}
|
|
1189
|
+
return mcpCommand;
|
|
1139
1190
|
}
|
|
1140
|
-
|
|
1141
1191
|
async function openMcp() {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1192
|
+
// Two drawers over one page would stack their scrims, and the top one would win
|
|
1193
|
+
// every click. Closing settings first keeps the invariant the Escape handler
|
|
1194
|
+
// assumes, and matches what the person was doing — switching panels.
|
|
1195
|
+
closeSettings();
|
|
1196
|
+
serverDrawer.close();
|
|
1197
|
+
dom.mcpDrawer.hidden = false;
|
|
1198
|
+
dom.mcpScrim.hidden = false;
|
|
1199
|
+
await pollMcp();
|
|
1200
|
+
await loadMcpCommand();
|
|
1201
|
+
scheduleMcpPoll(2500);
|
|
1152
1202
|
}
|
|
1153
|
-
|
|
1154
1203
|
function closeMcp() {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1204
|
+
dom.mcpDrawer.hidden = true;
|
|
1205
|
+
dom.mcpScrim.hidden = true;
|
|
1206
|
+
scheduleMcpPoll(8000);
|
|
1158
1207
|
}
|
|
1159
|
-
|
|
1160
1208
|
// The two config shapes a client is most likely to want: JSON for the harnesses
|
|
1161
1209
|
// that read a config file, TOML for the ones that do not. Both are built from
|
|
1162
1210
|
// what the server reported, so neither can drift from the command that actually
|
|
1163
1211
|
// starts it.
|
|
1164
1212
|
function renderMcpConfigBlock() {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1213
|
+
const ex = mcpCommand?.examples?.[0];
|
|
1214
|
+
if (!ex)
|
|
1215
|
+
return;
|
|
1216
|
+
const command = shortCommand(ex.command);
|
|
1217
|
+
const json = { mcpServers: { onboarder: { command, args: ex.args } } };
|
|
1218
|
+
const toml = [
|
|
1219
|
+
'[mcp_servers.onboarder]',
|
|
1220
|
+
`command = ${JSON.stringify(command)}`,
|
|
1221
|
+
`args = [${ex.args.map((a) => JSON.stringify(a)).join(', ')}]`,
|
|
1222
|
+
].join('\n');
|
|
1223
|
+
dom.mcpConfigBlock.textContent = dom.mcpTabToml.classList.contains('is-on')
|
|
1224
|
+
? toml
|
|
1225
|
+
: JSON.stringify(json, null, 2);
|
|
1177
1226
|
}
|
|
1178
|
-
|
|
1179
1227
|
// The server reports its own `process.execPath` so the config works even where
|
|
1180
1228
|
// the harness's PATH has no node. But an absolute path is noise in a snippet
|
|
1181
1229
|
// people read, so the common case is shown short and the input above keeps the
|
|
1182
1230
|
// exact one.
|
|
1183
1231
|
function shortCommand(cmd) {
|
|
1184
|
-
|
|
1232
|
+
return typeof cmd === 'string' && cmd.endsWith('/node') ? 'node' : cmd;
|
|
1185
1233
|
}
|
|
1186
|
-
|
|
1187
1234
|
function pickMcpTab(toml) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1235
|
+
dom.mcpTabJson.classList.toggle('is-on', !toml);
|
|
1236
|
+
dom.mcpTabToml.classList.toggle('is-on', toml);
|
|
1237
|
+
dom.mcpTabJson.setAttribute('aria-selected', String(!toml));
|
|
1238
|
+
dom.mcpTabToml.setAttribute('aria-selected', String(toml));
|
|
1239
|
+
renderMcpConfigBlock();
|
|
1193
1240
|
}
|
|
1194
|
-
|
|
1195
1241
|
dom.mcpTabJson.addEventListener('click', () => pickMcpTab(false));
|
|
1196
1242
|
dom.mcpTabToml.addEventListener('click', () => pickMcpTab(true));
|
|
1197
1243
|
dom.mcpBtn.addEventListener('click', () => (dom.mcpDrawer.hidden ? openMcp() : closeMcp()));
|
|
1198
1244
|
dom.mcpClose.addEventListener('click', closeMcp);
|
|
1199
1245
|
dom.mcpScrim.addEventListener('click', closeMcp);
|
|
1200
|
-
|
|
1201
1246
|
dom.mcpCopy.addEventListener('click', async () => {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1247
|
+
try {
|
|
1248
|
+
await navigator.clipboard.writeText(dom.mcpConfigBlock.textContent);
|
|
1249
|
+
toast('Config copied');
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
1252
|
+
// Clipboard access can be refused outright; selecting the text is the one
|
|
1253
|
+
// fallback that always works, so the person can copy it themselves.
|
|
1254
|
+
dom.mcpCommandInput.select();
|
|
1255
|
+
}
|
|
1210
1256
|
});
|
|
1211
|
-
|
|
1212
1257
|
async function setMcpState(action) {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1258
|
+
if (mcpBusy)
|
|
1259
|
+
return;
|
|
1260
|
+
mcpBusy = true;
|
|
1261
|
+
// Optimistic: the dot goes amber immediately rather than after a round-trip
|
|
1262
|
+
// during which the button would still read "stopped" and invite a second click.
|
|
1263
|
+
renderMcpStatus({ state: action === 'start' ? 'starting' : 'stopping', running: action === 'stop', toolCount: 0, tools: [] });
|
|
1264
|
+
try {
|
|
1265
|
+
const data = action === 'start' ? await startMcpServer() : await stopMcpServer();
|
|
1266
|
+
renderMcpStatus(data);
|
|
1267
|
+
if (action === 'start')
|
|
1268
|
+
await loadMcpCommand();
|
|
1269
|
+
}
|
|
1270
|
+
catch (err) {
|
|
1271
|
+
renderMcpStatus({ state: 'error', running: action === 'stop', lastError: err.message, tools: [] });
|
|
1272
|
+
toast(err.message);
|
|
1273
|
+
}
|
|
1274
|
+
finally {
|
|
1275
|
+
mcpBusy = false;
|
|
1276
|
+
scheduleMcpPoll(1500);
|
|
1277
|
+
}
|
|
1229
1278
|
}
|
|
1230
|
-
|
|
1231
1279
|
dom.mcpStart.addEventListener('click', () => setMcpState('start'));
|
|
1232
1280
|
dom.mcpStop.addEventListener('click', () => setMcpState('stop'));
|
|
1233
|
-
|
|
1234
1281
|
// One heartbeat at load. This first fetch is also what decides whether the button
|
|
1235
1282
|
// already reads "MCP on" for a server that was left running from before.
|
|
1236
1283
|
scheduleMcpPoll(0);
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
1284
|
dom.drawerScrim.addEventListener('click', closeSettings);
|
|
1240
|
-
|
|
1241
1285
|
dom.saveSettings.addEventListener('click', () => {
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1286
|
+
llm.saveSettings({ baseUrl: dom.setBaseUrl.value, apiKey: dom.setApiKey.value, model: dom.setModel.value });
|
|
1287
|
+
dom.settingsStatus.className = 'drawer-status ok';
|
|
1288
|
+
dom.settingsStatus.textContent = 'Saved in this browser.';
|
|
1245
1289
|
});
|
|
1246
|
-
|
|
1247
1290
|
dom.testSettings.addEventListener('click', async () => {
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1291
|
+
const settings = {
|
|
1292
|
+
baseUrl: dom.setBaseUrl.value.trim().replace(/\/+$/, ''),
|
|
1293
|
+
apiKey: dom.setApiKey.value,
|
|
1294
|
+
model: dom.setModel.value.trim(),
|
|
1295
|
+
};
|
|
1296
|
+
if (!settings.baseUrl || !settings.model) {
|
|
1297
|
+
dom.settingsStatus.className = 'drawer-status err';
|
|
1298
|
+
dom.settingsStatus.textContent = 'Base URL and model are both needed.';
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
dom.settingsStatus.className = 'drawer-status';
|
|
1302
|
+
dom.settingsStatus.textContent = 'Asking the endpoint…';
|
|
1303
|
+
try {
|
|
1304
|
+
let heard = '';
|
|
1305
|
+
for await (const delta of streamExplain({
|
|
1306
|
+
...settings,
|
|
1307
|
+
maxTokens: 64,
|
|
1308
|
+
messages: [{ role: 'user', content: 'Reply with the single word: ready' }],
|
|
1309
|
+
})) {
|
|
1310
|
+
heard += delta;
|
|
1311
|
+
}
|
|
1312
|
+
dom.settingsStatus.className = 'drawer-status ok';
|
|
1313
|
+
dom.settingsStatus.textContent = heard
|
|
1314
|
+
? `Connected — ${settings.model} answered.`
|
|
1315
|
+
: `Connected. It answered with silence, but that is a thinking model spending its whole token budget on thought — explanations use a bigger one.`;
|
|
1316
|
+
}
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
dom.settingsStatus.className = 'drawer-status err';
|
|
1319
|
+
dom.settingsStatus.textContent = err.message;
|
|
1268
1320
|
}
|
|
1269
|
-
dom.settingsStatus.className = 'drawer-status ok';
|
|
1270
|
-
dom.settingsStatus.textContent = heard
|
|
1271
|
-
? `Connected — ${settings.model} answered.`
|
|
1272
|
-
: `Connected. It answered with silence, but that is a thinking model spending its whole token budget on thought — explanations use a bigger one.`;
|
|
1273
|
-
} catch (err) {
|
|
1274
|
-
dom.settingsStatus.className = 'drawer-status err';
|
|
1275
|
-
dom.settingsStatus.textContent = err.message;
|
|
1276
|
-
}
|
|
1277
1321
|
});
|
|
1278
|
-
|
|
1279
1322
|
// ------------------------------------------------------------------- misc --
|
|
1280
|
-
|
|
1281
1323
|
let toastTimer = null;
|
|
1282
1324
|
function toast(message) {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1325
|
+
dom.toast.textContent = message;
|
|
1326
|
+
dom.toast.hidden = false;
|
|
1327
|
+
clearTimeout(toastTimer);
|
|
1328
|
+
toastTimer = setTimeout(() => (dom.toast.hidden = true), 2600);
|
|
1287
1329
|
}
|
|
1288
|
-
|
|
1289
1330
|
document.addEventListener('keydown', (event) => {
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
if (
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
if (event.key === '
|
|
1311
|
-
|
|
1331
|
+
if (eventElement(event)?.matches('input, textarea, select'))
|
|
1332
|
+
return;
|
|
1333
|
+
// Escape closes whichever drawer is open. The three are kept mutually
|
|
1334
|
+
// exclusive when they open, so at most one is ever showing — but checking
|
|
1335
|
+
// each here means the behavior does not depend on that invariant holding.
|
|
1336
|
+
if (event.key === 'Escape') {
|
|
1337
|
+
if (!dom.mcpDrawer.hidden)
|
|
1338
|
+
return closeMcp();
|
|
1339
|
+
if (serverDrawer.isOpen())
|
|
1340
|
+
return serverDrawer.close();
|
|
1341
|
+
return closeSettings();
|
|
1342
|
+
}
|
|
1343
|
+
if (!state.scan)
|
|
1344
|
+
return;
|
|
1345
|
+
if (event.key === '1')
|
|
1346
|
+
setView(state.lastExplorer || 'map');
|
|
1347
|
+
if (event.key === '2')
|
|
1348
|
+
setView('code');
|
|
1349
|
+
if (event.key === '3')
|
|
1350
|
+
setView('docs');
|
|
1351
|
+
if (event.key === '4')
|
|
1352
|
+
setView('about');
|
|
1353
|
+
if (event.key === '/') {
|
|
1354
|
+
event.preventDefault();
|
|
1355
|
+
dom.treeFilter.focus();
|
|
1356
|
+
}
|
|
1357
|
+
if (state.view === 'tour') {
|
|
1358
|
+
if (event.key === ']')
|
|
1359
|
+
dom.tourNext.click();
|
|
1360
|
+
if (event.key === '[')
|
|
1361
|
+
dom.tourPrev.click();
|
|
1362
|
+
}
|
|
1312
1363
|
});
|
|
1313
|
-
|
|
1314
1364
|
// ------------------------------------------------------------------ theme --
|
|
1315
|
-
|
|
1316
1365
|
const THEME_KEY = 'onboarder.theme';
|
|
1317
|
-
|
|
1318
1366
|
function getTheme() {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1367
|
+
const saved = localStorage.getItem(THEME_KEY);
|
|
1368
|
+
if (saved === 'dark' || saved === 'light')
|
|
1369
|
+
return saved;
|
|
1370
|
+
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
1322
1371
|
}
|
|
1323
|
-
|
|
1324
1372
|
function applyTheme(theme) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1373
|
+
document.documentElement.dataset.theme = theme;
|
|
1374
|
+
setDiagramTheme(theme);
|
|
1375
|
+
setViewerTheme(theme);
|
|
1376
|
+
dom.themeBtn.textContent = theme === 'dark' ? 'Light' : 'Dark';
|
|
1377
|
+
dom.themeBtn.title = theme === 'dark' ? 'Switch to the light theme' : 'Switch to the dark theme';
|
|
1330
1378
|
}
|
|
1331
|
-
|
|
1332
1379
|
dom.themeBtn.addEventListener('click', () => {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1380
|
+
const next = getTheme() === 'dark' ? 'light' : 'dark';
|
|
1381
|
+
localStorage.setItem(THEME_KEY, next);
|
|
1382
|
+
applyTheme(next);
|
|
1383
|
+
if (state.scan && (isExplorerView(state.view) || state.view === 'graph' || state.view === 'heatmap'))
|
|
1384
|
+
renderCanvas(); // Only canvas/diagram views need a theme redraw.
|
|
1337
1385
|
});
|
|
1338
|
-
|
|
1339
1386
|
applyTheme(getTheme());
|
|
1387
|
+
//# sourceMappingURL=app.js.map
|