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