@agenthippo-ai/agenthippo 0.1.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/LICENSE +21 -0
- package/bin/agenthippo.js +2 -0
- package/out/agentpack/github.js +547 -0
- package/out/agentpack/index.js +66 -0
- package/out/agentpack/install.js +297 -0
- package/out/agentpack/manager.js +441 -0
- package/out/agentpack/manifest.js +135 -0
- package/out/agentpack/paths.js +475 -0
- package/out/agentpack/publish.js +248 -0
- package/out/agentpack/stores.js +554 -0
- package/out/agentpack/types.js +5 -0
- package/out/analytics/dashboard/artifactManager.js +439 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +159 -0
- package/out/analytics/dashboard/fleetOrchestrator.js +142 -0
- package/out/analytics/dashboard/fleetRegistry.js +79 -0
- package/out/analytics/dashboard/fleetRowDefaults.js +87 -0
- package/out/analytics/dashboard/fleetScriptAdapter.js +146 -0
- package/out/analytics/dashboard/fleetStatusUtils.js +30 -0
- package/out/analytics/dashboard/fleetTitleUtils.js +68 -0
- package/out/analytics/dashboard/fleetTypes.js +29 -0
- package/out/analytics/dashboard/index.js +17 -0
- package/out/analytics/dashboard/indexingService.js +422 -0
- package/out/analytics/dashboard/localFleetAdapter.js +222 -0
- package/out/analytics/dashboard/performanceMonitor.js +76 -0
- package/out/analytics/dashboard/processManager.js +209 -0
- package/out/analytics/dashboard/provider.js +3885 -0
- package/out/analytics/dashboard/queryRunner.js +994 -0
- package/out/analytics/dashboard/refreshPolicy.js +16 -0
- package/out/analytics/dashboard/scheduledFleetFingerprint.js +22 -0
- package/out/analytics/dashboard/scheduledFleetStorage.js +241 -0
- package/out/analytics/dashboard/scheduledFleetUtils.js +22 -0
- package/out/analytics/dashboard/sessionEventFleetStats.js +118 -0
- package/out/analytics/dashboard/sessionJsonlFleet.js +204 -0
- package/out/analytics/dashboard/teamViewDiscovery.js +106 -0
- package/out/analytics/dashboard/viewsConfigMerge.js +221 -0
- package/out/analytics/dashboard/webview/analytics-panel.html +155 -0
- package/out/analytics/dashboard/webview/codicon.ttf +0 -0
- package/out/analytics/dashboard/webview/index.html +78 -0
- package/out/analytics/dashboard/webview/panel.js +2295 -0
- package/out/analytics/dashboard/webview/runtime-bridge.js +82 -0
- package/out/analytics/dashboard/webview/styles.css +1009 -0
- package/out/analytics/lifecycle.js +43 -0
- package/out/analytics/service.js +1279 -0
- package/out/analytics/sessionEvents.js +231 -0
- package/out/auth/bootstrapApply.js +72 -0
- package/out/auth/brokerClient.js +126 -0
- package/out/auth/callbackUri.js +59 -0
- package/out/auth/callbackUriParse.js +59 -0
- package/out/auth/githubStoreGate.js +70 -0
- package/out/auth/loopbackAuthServer.js +165 -0
- package/out/auth/pkce.js +9 -0
- package/out/auth/provider.js +239 -0
- package/out/auth/sessionStore.js +193 -0
- package/out/auth/signInFlow.js +182 -0
- package/out/auth/types.js +12 -0
- package/out/auth-broker/config.js +62 -0
- package/out/auth-broker/jwt.js +105 -0
- package/out/auth-broker/pkce.js +57 -0
- package/out/auth-broker/server.js +539 -0
- package/out/auth-broker/store.js +273 -0
- package/out/base-prompts/gateway/openclaw-starter.json5 +144 -0
- package/out/base-prompts/gateway/slack-socket-mode-minimal.manifest.json +44 -0
- package/out/base-prompts/mcp-servers/.mcp.json +13 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/README.md +142 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/mcp.json +13 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/package-lock.json +1192 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/package.json +23 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/run-with-env.sh +12 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/sdk-test.js +102 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/simple-test.js +137 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-client.js +118 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-server.js +209 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/src/client.ts +431 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/src/index.ts +270 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/tsconfig.json +18 -0
- package/out/base-prompts/rules/AGENTS.md +204 -0
- package/out/base-prompts/skills/agent-browser/SKILL.md +102 -0
- package/out/base-prompts/skills/agent-builder/SKILL.md +73 -0
- package/out/base-prompts/skills/agent-builder/references/agent-pack.md +63 -0
- package/out/base-prompts/skills/agent-builder/references/crewai.md +66 -0
- package/out/base-prompts/skills/agent-builder/references/langchain.md +60 -0
- package/out/base-prompts/skills/agent-builder/references/raw-sdk.md +73 -0
- package/out/base-prompts/skills/agent-engine-creator/SKILL.md +586 -0
- package/out/base-prompts/skills/agent-engine-installer/SKILL.md +106 -0
- package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.mjs +259 -0
- package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.ps1 +189 -0
- package/out/base-prompts/skills/agenthippo-cli/SKILL.md +69 -0
- package/out/base-prompts/skills/agenthippo-cli/scripts/install.sh +63 -0
- package/out/base-prompts/skills/agentpack-creator/SKILL.md +375 -0
- package/out/base-prompts/skills/agentpack-creator/references/agent-templates.md +261 -0
- package/out/base-prompts/skills/agentpack-creator/references/agents-md-guide.md +195 -0
- package/out/base-prompts/skills/agentpack-creator/scripts/init_pack.py +391 -0
- package/out/base-prompts/skills/agentpack-creator/scripts/init_pack_test.py +294 -0
- package/out/base-prompts/skills/agentpack-testbench/SKILL.md +135 -0
- package/out/base-prompts/skills/algorithmic-art/LICENSE.txt +202 -0
- package/out/base-prompts/skills/algorithmic-art/SKILL.md +405 -0
- package/out/base-prompts/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/out/base-prompts/skills/algorithmic-art/templates/viewer.html +599 -0
- package/out/base-prompts/skills/debug-agent/SKILL.md +83 -0
- package/out/base-prompts/skills/desktop-automation/SKILL.md +166 -0
- package/out/base-prompts/skills/desktop-automation/references/agenthippo-ui-recipes.md +441 -0
- package/out/base-prompts/skills/desktop-automation/scripts/windows-desktop-uia.psm1 +280 -0
- package/out/base-prompts/skills/docx/LICENSE.txt +30 -0
- package/out/base-prompts/skills/docx/SKILL.md +197 -0
- package/out/base-prompts/skills/docx/docx-js.md +350 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/mce/mc.xsd +75 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/pack.py +159 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/unpack.py +29 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validate.py +69 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/__init__.py +15 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/base.py +951 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/docx.py +274 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/pptx.py +315 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/redlining.py +279 -0
- package/out/base-prompts/skills/docx/ooxml.md +610 -0
- package/out/base-prompts/skills/docx/scripts/__init__.py +1 -0
- package/out/base-prompts/skills/docx/scripts/document.py +1276 -0
- package/out/base-prompts/skills/docx/scripts/templates/comments.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/people.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/utilities.py +374 -0
- package/out/base-prompts/skills/gateway-outbound/SKILL.md +136 -0
- package/out/base-prompts/skills/mcp-builder/LICENSE.txt +202 -0
- package/out/base-prompts/skills/mcp-builder/SKILL.md +328 -0
- package/out/base-prompts/skills/mcp-builder/reference/evaluation.md +602 -0
- package/out/base-prompts/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/out/base-prompts/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/out/base-prompts/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/out/base-prompts/skills/mcp-builder/scripts/connections.py +151 -0
- package/out/base-prompts/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/out/base-prompts/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/out/base-prompts/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/out/base-prompts/skills/openclaw/SKILL.md +406 -0
- package/out/base-prompts/skills/openclaw/scripts/run-smoke.sh +61 -0
- package/out/base-prompts/skills/pdf/LICENSE.txt +30 -0
- package/out/base-prompts/skills/pdf/SKILL.md +294 -0
- package/out/base-prompts/skills/pdf/forms.md +205 -0
- package/out/base-prompts/skills/pdf/reference.md +612 -0
- package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes.py +70 -0
- package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/out/base-prompts/skills/pdf/scripts/check_fillable_fields.py +12 -0
- package/out/base-prompts/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/out/base-prompts/skills/pdf/scripts/create_validation_image.py +41 -0
- package/out/base-prompts/skills/pdf/scripts/extract_form_field_info.py +152 -0
- package/out/base-prompts/skills/pdf/scripts/fill_fillable_fields.py +114 -0
- package/out/base-prompts/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/out/base-prompts/skills/pptx/LICENSE.txt +30 -0
- package/out/base-prompts/skills/pptx/SKILL.md +484 -0
- package/out/base-prompts/skills/pptx/html2pptx.md +625 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/mce/mc.xsd +75 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/pack.py +159 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/unpack.py +29 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validate.py +69 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/base.py +951 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/out/base-prompts/skills/pptx/ooxml.md +427 -0
- package/out/base-prompts/skills/pptx/scripts/html2pptx.js +984 -0
- package/out/base-prompts/skills/pptx/scripts/inventory.py +1020 -0
- package/out/base-prompts/skills/pptx/scripts/rearrange.py +231 -0
- package/out/base-prompts/skills/pptx/scripts/replace.py +385 -0
- package/out/base-prompts/skills/pptx/scripts/thumbnail.py +450 -0
- package/out/base-prompts/skills/prompt-lab/SKILL.md +149 -0
- package/out/base-prompts/skills/prompt-lab/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/skill-creator/LICENSE.txt +202 -0
- package/out/base-prompts/skills/skill-creator/SKILL.md +371 -0
- package/out/base-prompts/skills/skill-creator/references/output-patterns.md +82 -0
- package/out/base-prompts/skills/skill-creator/references/workflows.md +28 -0
- package/out/base-prompts/skills/skill-creator/scripts/init_skill.py +303 -0
- package/out/base-prompts/skills/skill-creator/scripts/package_skill.py +110 -0
- package/out/base-prompts/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/out/base-prompts/skills/skill-installer/SKILL.md +251 -0
- package/out/base-prompts/skills/skill-installer/references/store-api.md +219 -0
- package/out/base-prompts/skills/spotlight-dashboard/SKILL.md +83 -0
- package/out/base-prompts/skills/spotlight-insights/SKILL.md +101 -0
- package/out/base-prompts/skills/spotlight-optimize/SKILL.md +93 -0
- package/out/base-prompts/skills/spotlight-optimize/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/spotlight-query/SKILL.md +314 -0
- package/out/base-prompts/skills/spotlight-query/references/otel-schema.md +163 -0
- package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.test.sh +744 -0
- package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/sessions/sess-test-1.jsonl +14 -0
- package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/.duckdb-index.json +7 -0
- package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/sample.jsonl +5 -0
- package/out/base-prompts/skills/spotlight-report/SKILL.md +86 -0
- package/out/base-prompts/skills/spotlight-visualize/SKILL.md +92 -0
- package/out/base-prompts/skills/spotlight-visualize/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/xlsx/LICENSE.txt +30 -0
- package/out/base-prompts/skills/xlsx/SKILL.md +289 -0
- package/out/base-prompts/skills/xlsx/recalc.py +178 -0
- package/out/branding.js +23 -0
- package/out/cli/commands/anywhere.js +66 -0
- package/out/cli/commands/ask.js +243 -0
- package/out/cli/commands/auth-broker.js +24 -0
- package/out/cli/commands/auth.js +219 -0
- package/out/cli/commands/chat.js +354 -0
- package/out/cli/commands/deploy.js +223 -0
- package/out/cli/commands/gateway.js +160 -0
- package/out/cli/commands/pack.js +220 -0
- package/out/cli/commands/probe-custom-engine.js +82 -0
- package/out/cli/commands/serve.js +583 -0
- package/out/cli/commands/store.js +919 -0
- package/out/cli/credential-context.js +113 -0
- package/out/cli/deploy-init.js +2087 -0
- package/out/cli/deps.js +238 -0
- package/out/cli/index.js +390 -0
- package/out/cli/pack-loader.js +376 -0
- package/out/cli/run-custom-engine-runner.js +318 -0
- package/out/cli/runtime.js +409 -0
- package/out/cli/serve-hardening.js +325 -0
- package/out/cli/state-sync.js +135 -0
- package/out/cli/utils.js +220 -0
- package/out/cli/vscode-shim.js +86 -0
- package/out/engines/base.js +159 -0
- package/out/engines/claude.js +1215 -0
- package/out/engines/codex.js +759 -0
- package/out/engines/custom.js +632 -0
- package/out/engines/customEngineIpc.js +95 -0
- package/out/engines/customEngineRunnerEmbed.js +107 -0
- package/out/engines/customRegistry.js +274 -0
- package/out/engines/embeddedCustomEngineRunner.generated.js +9 -0
- package/out/engines/engineApiKey.js +138 -0
- package/out/engines/factory.js +45 -0
- package/out/engines/install.js +372 -0
- package/out/engines/persistentEngineWorker.js +320 -0
- package/out/engines/publish.js +341 -0
- package/out/engines/types.js +17 -0
- package/out/examples/run-headless-repl.js +291 -0
- package/out/examples/run-headless.js +187 -0
- package/out/extension.js +4472 -0
- package/out/gateway/bridge.js +805 -0
- package/out/gateway/cli-agent-id.js +36 -0
- package/out/gateway/client.js +303 -0
- package/out/gateway/client.test.js +395 -0
- package/out/gateway/gateway-scope-errors.js +14 -0
- package/out/gateway/gatewayBackup.js +76 -0
- package/out/gateway/gatewayRouteIndex.js +371 -0
- package/out/gateway/gatewaySessionStore.js +243 -0
- package/out/gateway/index.js +31 -0
- package/out/gateway/lifecycle.js +3056 -0
- package/out/gateway/openclaw-channel-routing.js +317 -0
- package/out/gateway/outboundHandles.js +373 -0
- package/out/gateway/outboundPaths.js +72 -0
- package/out/gateway/outboundSend.js +254 -0
- package/out/gateway/protocol.js +48 -0
- package/out/gateway/resetMappings.js +52 -0
- package/out/gateway/service.js +1244 -0
- package/out/gateway/sessionRouting.js +77 -0
- package/out/headless/index.js +505 -0
- package/out/headless/sync.js +473 -0
- package/out/mcp/publish.js +143 -0
- package/out/mcp.js +597 -0
- package/out/permissions.js +108 -0
- package/out/permissions.mapping.yaml +68 -0
- package/out/registry/index.js +332 -0
- package/out/remote/handleRemoteChat.js +122 -0
- package/out/remote/remotePackStatusBar.js +103 -0
- package/out/remote/serveClient.js +216 -0
- package/out/remoteSkills.js +191 -0
- package/out/runtime/index.js +32 -0
- package/out/runtime/interfaces/config-provider.js +32 -0
- package/out/runtime/interfaces/edit-tracker.js +22 -0
- package/out/runtime/interfaces/index.js +29 -0
- package/out/runtime/interfaces/logger.js +40 -0
- package/out/runtime/interfaces/permission-handler.js +66 -0
- package/out/runtime/interfaces/session-store.js +342 -0
- package/out/runtime/vscode/index.js +20 -0
- package/out/runtime/vscode/vscode-config-provider.js +77 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +120 -0
- package/out/runtime/vscode/vscode-logger.js +30 -0
- package/out/runtime/vscode/vscode-permission-handler.js +72 -0
- package/out/runtime/vscode/vscode-session-store.js +22 -0
- package/out/scripts/agentide-litellm.ps1 +501 -0
- package/out/scripts/agentide-litellm.sh +700 -0
- package/out/scripts/agentide_litellm_hooks_v1.py +604 -0
- package/out/scripts/analytics/agent-fleet-widget.js +954 -0
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +160 -0
- package/out/scripts/analytics/default-seed-scripts.json +12 -0
- package/out/scripts/analytics/duckdbLocator.js +29 -0
- package/out/scripts/analytics/fleet-local-adapter.js +246 -0
- package/out/scripts/analytics/fleet-workflow.sh +263 -0
- package/out/scripts/analytics/generate-default-charts.py +685 -0
- package/out/scripts/analytics/hotspot-analysis-widget.js +909 -0
- package/out/scripts/analytics/index-traces.py +1394 -0
- package/out/scripts/analytics/install-analytics.sh +286 -0
- package/out/scripts/analytics/live-fleet-4-collector.cjs +315 -0
- package/out/scripts/analytics/live-fleet-4-daemon.mjs +134 -0
- package/out/scripts/analytics/live-fleet-4-widget.cjs +433 -0
- package/out/scripts/analytics/otel-collector.yaml +55 -0
- package/out/scripts/analytics/query-analytics.ps1 +396 -0
- package/out/scripts/analytics/query-analytics.sh +2551 -0
- package/out/scripts/analytics/session-comparison-widget.js +1468 -0
- package/out/scripts/analytics/session-timeline-widget.js +2137 -0
- package/out/scripts/analytics/setup-analytics.ps1 +432 -0
- package/out/scripts/analytics/setup-analytics.sh +854 -0
- package/out/scripts/analytics/start-analytics.ps1 +169 -0
- package/out/scripts/analytics/start-analytics.sh +323 -0
- package/out/scripts/analytics/token-cost-flamegraph-widget.js +954 -0
- package/out/scripts/analytics/token-cost-trend-widget.js +1062 -0
- package/out/scripts/analytics/workflows-collector.cjs +315 -0
- package/out/scripts/analytics/workflows-daemon.mjs +186 -0
- package/out/scripts/analytics/workflows-widget.cjs +433 -0
- package/out/scripts/analytics/wow-model-widget.js +191 -0
- package/out/scripts/gateway/Dockerfile.serve +45 -0
- package/out/scripts/gateway/docker-serve.sh +248 -0
- package/out/scripts/gateway/setup-gateway-server.ps1 +953 -0
- package/out/scripts/gateway/setup-gateway-server.sh +935 -0
- package/out/scripts/gateway/setup-gateway.ps1 +1813 -0
- package/out/scripts/gateway/setup-gateway.sh +1541 -0
- package/out/scripts/setup-cli.ps1 +86 -0
- package/out/scripts/setup-cli.sh +117 -0
- package/out/scripts/tools-manifest.json +83 -0
- package/out/scripts/vendor-tools.mjs +234 -0
- package/out/session.js +272 -0
- package/out/shared/agentEngines.js +170 -0
- package/out/shared/index.js +109 -0
- package/out/shared/models.js +235 -0
- package/out/shared/providers.js +202 -0
- package/out/skills/publish.js +178 -0
- package/out/spotlight/install.js +339 -0
- package/out/spotlight/publish.js +544 -0
- package/out/spotlight/yaml.js +31 -0
- package/out/store/github/setup.js +235 -0
- package/out/store/github/templates/build-index.ts +339 -0
- package/out/store/github/templates/build-index.yml +97 -0
- package/out/store/github/templates/package.json +11 -0
- package/out/store/index.js +755 -0
- package/out/store/oramaIndex.js +884 -0
- package/out/store/shared.js +241 -0
- package/out/sync.js +1827 -0
- package/out/terminalLogger.js +358 -0
- package/out/test/agentEnvFile.test.js +97 -0
- package/out/test/agentYaml.test.js +373 -0
- package/out/test/agentpack.test.js +639 -0
- package/out/test/analytics-lifecycle.test.js +174 -0
- package/out/test/analytics.test.js +524 -0
- package/out/test/auth-bootstrapApply.test.js +102 -0
- package/out/test/auth-broker.test.js +434 -0
- package/out/test/auth-callback.test.js +62 -0
- package/out/test/branding.test.js +229 -0
- package/out/test/cli-agent-id.test.js +55 -0
- package/out/test/credential-context.test.js +105 -0
- package/out/test/customEngineInstall.test.js +100 -0
- package/out/test/customRegistry.test.js +122 -0
- package/out/test/deploy-init.test.js +532 -0
- package/out/test/deps-auto-install.test.js +81 -0
- package/out/test/engine-integration.test.js +297 -0
- package/out/test/enginePaths.test.js +195 -0
- package/out/test/enginePublish.test.js +82 -0
- package/out/test/fleetDisplayUtils.test.js +96 -0
- package/out/test/gateway-client.test.js +381 -0
- package/out/test/gateway-lifecycle.test.js +608 -0
- package/out/test/gateway-outbound.test.js +531 -0
- package/out/test/gateway-reset-mappings.test.js +221 -0
- package/out/test/gateway-route-index.test.js +235 -0
- package/out/test/gateway-session-routing.test.js +47 -0
- package/out/test/gateway-session-store.test.js +216 -0
- package/out/test/github.test.js +232 -0
- package/out/test/headless-sync.test.js +279 -0
- package/out/test/helpers/manifest-fixtures.js +27 -0
- package/out/test/mcp-obo.test.js +82 -0
- package/out/test/mcp.test.js +307 -0
- package/out/test/openclaw-channel-routing.test.js +191 -0
- package/out/test/pathSafety.test.js +63 -0
- package/out/test/permissions.test.js +175 -0
- package/out/test/persistent-engine-worker.test.js +220 -0
- package/out/test/refreshPolicy.test.js +60 -0
- package/out/test/registry.test.js +351 -0
- package/out/test/runtime.test.js +349 -0
- package/out/test/scheduledFleetStorage.test.js +72 -0
- package/out/test/serve-hardening.test.js +192 -0
- package/out/test/serveClient.test.js +173 -0
- package/out/test/sessionJsonlFleet.test.js +78 -0
- package/out/test/shared.test.js +173 -0
- package/out/test/skills-mcp.test.js +299 -0
- package/out/test/spotlightPublish.test.js +299 -0
- package/out/test/spotlightYaml.test.js +58 -0
- package/out/test/state-sync-peer.test.js +74 -0
- package/out/test/store.test.js +331 -0
- package/out/test/stores.test.js +504 -0
- package/out/test/sync-switch.test.js +251 -0
- package/out/test/teamViewDiscovery.test.js +90 -0
- package/out/test/toolPath.test.js +120 -0
- package/out/test/utils.test.js +308 -0
- package/out/test/viewsConfigMerge.test.js +180 -0
- package/out/test/windows-command-line.test.js +75 -0
- package/out/utils/agentEnvFile.js +131 -0
- package/out/utils/binary.js +175 -0
- package/out/utils/index.js +37 -0
- package/out/utils/network.js +74 -0
- package/out/utils/pathSafety.js +60 -0
- package/out/utils/paths.js +116 -0
- package/out/utils/pid.js +180 -0
- package/out/utils/pythonRunner.js +258 -0
- package/out/utils/resolveNodeBinary.js +168 -0
- package/out/utils/toolPath.js +83 -0
- package/out/utils/windowsCommandLine.js +29 -0
- package/out/utils.js +915 -0
- package/out/workspaceFolders.js +59 -0
- package/package.json +41 -0
|
@@ -0,0 +1,3885 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.AnalyticsDashboardProvider = exports.shouldRunScriptsOnNavigation = void 0;
|
|
40
|
+
const vscode = __importStar(require("vscode"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
const fs_1 = require("fs");
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const util_1 = require("util");
|
|
46
|
+
const pythonRunner_1 = require("../../utils/pythonRunner");
|
|
47
|
+
const toolPath_1 = require("../../utils/toolPath");
|
|
48
|
+
const index_1 = require("../../utils/index");
|
|
49
|
+
const fleetOrchestrator_1 = require("./fleetOrchestrator");
|
|
50
|
+
const fleetDisplayUtils_1 = require("./fleetDisplayUtils");
|
|
51
|
+
const fleetRowDefaults_1 = require("./fleetRowDefaults");
|
|
52
|
+
const fleetTypes_1 = require("./fleetTypes");
|
|
53
|
+
const viewsConfigMerge_1 = require("./viewsConfigMerge");
|
|
54
|
+
const teamViewDiscovery_1 = require("./teamViewDiscovery");
|
|
55
|
+
const pathSafety_1 = require("../../utils/pathSafety");
|
|
56
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
57
|
+
var viewsConfigMerge_2 = require("./viewsConfigMerge");
|
|
58
|
+
Object.defineProperty(exports, "shouldRunScriptsOnNavigation", { enumerable: true, get: function () { return viewsConfigMerge_2.shouldRunScriptsOnNavigation; } });
|
|
59
|
+
function postToWebview(webview, message) {
|
|
60
|
+
if (!webview) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
void webview.postMessage(message);
|
|
64
|
+
}
|
|
65
|
+
// -----------------------------------------------------------------------------
|
|
66
|
+
// Constants
|
|
67
|
+
// -----------------------------------------------------------------------------
|
|
68
|
+
const FILTER_AUTOGEN_DEBOUNCE_MS = 700;
|
|
69
|
+
const FILTER_AUTOGEN_COOLDOWN_MS = 12_000;
|
|
70
|
+
const MAX_FILTER_AUTOGEN_HISTORY = 100;
|
|
71
|
+
const DEFAULT_SEED_REGISTRY_FILENAME = 'default-seed-scripts.json';
|
|
72
|
+
// Fleet widget excluded so Global/Workspace default scripts stay distinct; Fleet view has scripts: ['agent-fleet-widget.js'] only.
|
|
73
|
+
const FALLBACK_DEFAULT_SEED_SCRIPT_NAMES = [
|
|
74
|
+
'generate-default-charts.py',
|
|
75
|
+
'duckdbLocator.js',
|
|
76
|
+
'wow-model-widget.js',
|
|
77
|
+
'session-timeline-widget.js',
|
|
78
|
+
'session-comparison-widget.js',
|
|
79
|
+
'hotspot-analysis-widget.js',
|
|
80
|
+
'token-cost-flamegraph-widget.js',
|
|
81
|
+
];
|
|
82
|
+
const USER_SCRIPT_MAX_CONCURRENCY = 2;
|
|
83
|
+
const WIDGET_RUN_LOG_RETENTION_COUNT = 20;
|
|
84
|
+
/** Debounce views.yaml watcher reloads (global + workspace). */
|
|
85
|
+
const VIEWS_CONFIG_DEBOUNCE_MS = 400;
|
|
86
|
+
// -----------------------------------------------------------------------------
|
|
87
|
+
// Provider Implementation
|
|
88
|
+
// -----------------------------------------------------------------------------
|
|
89
|
+
class AnalyticsDashboardProvider {
|
|
90
|
+
extensionContext;
|
|
91
|
+
queryRunner;
|
|
92
|
+
artifactManager;
|
|
93
|
+
outputChannel;
|
|
94
|
+
getIndexingService;
|
|
95
|
+
viewIdSuffix;
|
|
96
|
+
static viewType = 'agentide.analyticsView';
|
|
97
|
+
webviewView;
|
|
98
|
+
artifactWatcher;
|
|
99
|
+
viewsConfigWatcher;
|
|
100
|
+
viewsConfigDebounceTimeout;
|
|
101
|
+
generationAbortController;
|
|
102
|
+
isBatchingArtifactUpdates = false;
|
|
103
|
+
pendingArtifactRefresh = false;
|
|
104
|
+
artifactRefreshQueue = Promise.resolve();
|
|
105
|
+
pendingFilterAutoGenerateTimer;
|
|
106
|
+
isRefreshInProgress = false;
|
|
107
|
+
hasPendingRefreshRequest = false;
|
|
108
|
+
activeDuckDbScriptCount = 0;
|
|
109
|
+
duckDbScriptWaiters = [];
|
|
110
|
+
duckDbHeavyScriptNames = new Set([
|
|
111
|
+
'generate-default-charts.py',
|
|
112
|
+
'wow-model-widget.js',
|
|
113
|
+
'session-timeline-widget.js',
|
|
114
|
+
'session-comparison-widget.js',
|
|
115
|
+
'agent-fleet-widget.js',
|
|
116
|
+
]);
|
|
117
|
+
static FLEET_EXPORT_FILENAME = 'fleet.json';
|
|
118
|
+
/** @deprecated Legacy export name; read fallback only. */
|
|
119
|
+
static LEGACY_FLEET_EXPORT_FILENAME = 'agent-fleet.json';
|
|
120
|
+
static DEFAULT_GALLERY_IGNORE = ['.json', '.csv'];
|
|
121
|
+
fleetRefreshVersion = 0;
|
|
122
|
+
autoGenerationAttemptByFilter = new Map();
|
|
123
|
+
htmlPreviewPanels = new Map();
|
|
124
|
+
viewsConfig = this.createDefaultViewsConfig();
|
|
125
|
+
agentFilterId;
|
|
126
|
+
disposables = [];
|
|
127
|
+
/** Serializes create/duplicate/delete view so rapid UI clicks cannot race views.yaml. */
|
|
128
|
+
viewConfigMutationQueue = Promise.resolve();
|
|
129
|
+
constructor(extensionContext, queryRunner, artifactManager, outputChannel, getIndexingService,
|
|
130
|
+
/** When 'team', uses a separate views config and defaults to Fleet view (for Agent Team sidebar). */
|
|
131
|
+
viewIdSuffix) {
|
|
132
|
+
this.extensionContext = extensionContext;
|
|
133
|
+
this.queryRunner = queryRunner;
|
|
134
|
+
this.artifactManager = artifactManager;
|
|
135
|
+
this.outputChannel = outputChannel;
|
|
136
|
+
this.getIndexingService = getIndexingService;
|
|
137
|
+
this.viewIdSuffix = viewIdSuffix;
|
|
138
|
+
}
|
|
139
|
+
indexingService() {
|
|
140
|
+
return this.getIndexingService?.();
|
|
141
|
+
}
|
|
142
|
+
// -------------------------------------------------------------------------
|
|
143
|
+
// WebviewViewProvider Implementation
|
|
144
|
+
// -------------------------------------------------------------------------
|
|
145
|
+
resolveWebviewView(webviewView, _context, _token) {
|
|
146
|
+
this.webviewView = webviewView;
|
|
147
|
+
// Configure webview options
|
|
148
|
+
this.syncArtifactContext();
|
|
149
|
+
webviewView.webview.options = {
|
|
150
|
+
enableScripts: true,
|
|
151
|
+
localResourceRoots: [
|
|
152
|
+
this.extensionContext.extensionUri,
|
|
153
|
+
// Workspace-local and home-global analytics roots (store-installed global views).
|
|
154
|
+
vscode.Uri.file(this.artifactManager.getAnalyticsRoot()),
|
|
155
|
+
vscode.Uri.file(this.artifactManager.getGlobalAnalyticsRoot()),
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
// Set HTML content
|
|
159
|
+
webviewView.webview.html = this.getHtmlContent(webviewView.webview);
|
|
160
|
+
// Handle messages from webview
|
|
161
|
+
this.disposables.push(webviewView.webview.onDidReceiveMessage(this.handleMessage.bind(this)));
|
|
162
|
+
// Start watching artifacts for live updates (always read gallery ignore patterns from disabled-charts.yaml)
|
|
163
|
+
this.artifactWatcher = this.artifactManager.watchForChanges((artifacts) => {
|
|
164
|
+
if (this.isBatchingArtifactUpdates) {
|
|
165
|
+
this.pendingArtifactRefresh = true;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.loadDisabledChartsConfig().then((disabledConfig) => {
|
|
169
|
+
this.sendArtifactsToWebview(artifacts, disabledConfig.galleryIgnore);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
this.disposables.push(this.artifactWatcher);
|
|
173
|
+
// Watch views config (global home + workspace) so store installs and edits appear immediately.
|
|
174
|
+
const scheduleViewsConfigReload = () => {
|
|
175
|
+
if (this.viewsConfigDebounceTimeout) {
|
|
176
|
+
clearTimeout(this.viewsConfigDebounceTimeout);
|
|
177
|
+
}
|
|
178
|
+
this.viewsConfigDebounceTimeout = setTimeout(() => {
|
|
179
|
+
void this.handleViewsConfigChanged();
|
|
180
|
+
}, VIEWS_CONFIG_DEBOUNCE_MS);
|
|
181
|
+
};
|
|
182
|
+
for (const configPath of this.getViewsConfigPathsToWatch()) {
|
|
183
|
+
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(path.dirname(configPath), path.basename(configPath)));
|
|
184
|
+
this.disposables.push(watcher, watcher.onDidChange(scheduleViewsConfigReload), watcher.onDidCreate(scheduleViewsConfigReload), watcher.onDidDelete(scheduleViewsConfigReload));
|
|
185
|
+
}
|
|
186
|
+
// Cleanup old artifacts
|
|
187
|
+
const retentionDays = vscode.workspace.getConfiguration('agentide.analytics')
|
|
188
|
+
.get('artifactRetentionDays', 30);
|
|
189
|
+
this.artifactManager.cleanup(retentionDays).then(result => {
|
|
190
|
+
if (result.deletedCount > 0) {
|
|
191
|
+
this.outputChannel.appendLine(`[Dashboard] Cleaned up ${result.deletedCount} old artifacts (${(result.freedBytes / 1024).toFixed(1)} KB)`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
// Handle visibility changes
|
|
195
|
+
this.disposables.push(webviewView.onDidChangeVisibility(() => {
|
|
196
|
+
if (webviewView.visible) {
|
|
197
|
+
this.onPanelBecameVisible();
|
|
198
|
+
}
|
|
199
|
+
}));
|
|
200
|
+
// Initial load
|
|
201
|
+
this.onPanelBecameVisible();
|
|
202
|
+
}
|
|
203
|
+
// -------------------------------------------------------------------------
|
|
204
|
+
// Panel Lifecycle
|
|
205
|
+
// -------------------------------------------------------------------------
|
|
206
|
+
resolveWorkspaceRootPath() {
|
|
207
|
+
const activeEditorUri = vscode.window.activeTextEditor?.document.uri;
|
|
208
|
+
const activeFolder = activeEditorUri
|
|
209
|
+
? vscode.workspace.getWorkspaceFolder(activeEditorUri)
|
|
210
|
+
: undefined;
|
|
211
|
+
const folder = activeFolder ?? vscode.workspace.workspaceFolders?.[0];
|
|
212
|
+
return folder?.uri.fsPath;
|
|
213
|
+
}
|
|
214
|
+
resolveWorkspaceProjectId() {
|
|
215
|
+
const folderPath = this.resolveWorkspaceRootPath();
|
|
216
|
+
if (folderPath) {
|
|
217
|
+
const projectId = path.basename(folderPath).trim();
|
|
218
|
+
if (projectId) {
|
|
219
|
+
return projectId;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const activeEditorUri = vscode.window.activeTextEditor?.document.uri;
|
|
223
|
+
const activeFolder = activeEditorUri ? vscode.workspace.getWorkspaceFolder(activeEditorUri) : undefined;
|
|
224
|
+
const folder = activeFolder ?? vscode.workspace.workspaceFolders?.[0];
|
|
225
|
+
if (folder?.uri.fsPath) {
|
|
226
|
+
const projectId = path.basename(folder.uri.fsPath).trim();
|
|
227
|
+
if (projectId) {
|
|
228
|
+
return projectId;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const workspaceName = vscode.workspace.name?.trim();
|
|
232
|
+
return workspaceName || undefined;
|
|
233
|
+
}
|
|
234
|
+
syncArtifactContext() {
|
|
235
|
+
this.artifactManager.setWorkspaceRoot(this.resolveWorkspaceRootPath());
|
|
236
|
+
const filters = this.getActiveFilters();
|
|
237
|
+
this.artifactManager.setViewContext({
|
|
238
|
+
scope: filters.scope === 'workspace' ? 'workspace' : 'global',
|
|
239
|
+
projectId: filters.projectId,
|
|
240
|
+
agentId: filters.agentId,
|
|
241
|
+
viewId: this.getActiveViewId(),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async withArtifactViewContext(view, fn) {
|
|
245
|
+
this.artifactManager.setWorkspaceRoot(this.resolveWorkspaceRootPath());
|
|
246
|
+
const activeView = this.getActiveView();
|
|
247
|
+
const previousContext = this.getViewContext(activeView);
|
|
248
|
+
this.artifactManager.setViewContext(this.getViewContext(view));
|
|
249
|
+
try {
|
|
250
|
+
return await fn();
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
this.artifactManager.setViewContext(previousContext);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
runViewConfigMutation(fn) {
|
|
257
|
+
const run = this.viewConfigMutationQueue.then(fn, fn);
|
|
258
|
+
this.viewConfigMutationQueue = run.then(() => undefined, () => undefined);
|
|
259
|
+
return run;
|
|
260
|
+
}
|
|
261
|
+
/** Fleet HTML widget; only the Fleet view should run it by default. */
|
|
262
|
+
static FLEET_WIDGET_BASENAME = 'agent-fleet-widget.js';
|
|
263
|
+
stripFleetWidgetFromGlobalWorkspace(viewId, scripts) {
|
|
264
|
+
const id = this.normalizeViewId(viewId);
|
|
265
|
+
if (id !== 'global' && id !== 'workspace') {
|
|
266
|
+
return scripts;
|
|
267
|
+
}
|
|
268
|
+
return scripts.filter(s => this.normalizeBasename(s) !== AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME);
|
|
269
|
+
}
|
|
270
|
+
normalizeViewId(raw, fallback = 'global') {
|
|
271
|
+
return (0, viewsConfigMerge_1.normalizeDashboardViewId)(raw, fallback);
|
|
272
|
+
}
|
|
273
|
+
getViewsConfigFilename() {
|
|
274
|
+
return this.viewIdSuffix ? `views-${this.viewIdSuffix}.yaml` : 'views.yaml';
|
|
275
|
+
}
|
|
276
|
+
getGlobalViewsConfigPath() {
|
|
277
|
+
return path.join(this.artifactManager.getGlobalAnalyticsRoot(), 'panel', this.getViewsConfigFilename());
|
|
278
|
+
}
|
|
279
|
+
getWorkspaceViewsConfigPath() {
|
|
280
|
+
const workspaceRoot = this.resolveWorkspaceRootPath();
|
|
281
|
+
if (!workspaceRoot) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
return path.join(workspaceRoot, '.agent-hippo', 'analytics', 'panel', this.getViewsConfigFilename());
|
|
285
|
+
}
|
|
286
|
+
async readPanelConfigLayers(configFilename = this.getViewsConfigFilename()) {
|
|
287
|
+
const globalPath = path.join(this.artifactManager.getGlobalAnalyticsRoot(), 'panel', configFilename);
|
|
288
|
+
const workspaceRoot = this.resolveWorkspaceRootPath();
|
|
289
|
+
const workspacePath = workspaceRoot
|
|
290
|
+
? path.join(workspaceRoot, '.agent-hippo', 'analytics', 'panel', configFilename)
|
|
291
|
+
: undefined;
|
|
292
|
+
return Promise.all([
|
|
293
|
+
this.readViewsConfigFile(globalPath),
|
|
294
|
+
workspacePath ? this.readViewsConfigFile(workspacePath) : Promise.resolve(undefined),
|
|
295
|
+
]);
|
|
296
|
+
}
|
|
297
|
+
getViewsConfigPathsToWatch() {
|
|
298
|
+
const paths = [this.getGlobalViewsConfigPath()];
|
|
299
|
+
const workspacePath = this.getWorkspaceViewsConfigPath();
|
|
300
|
+
if (workspacePath && workspacePath !== paths[0]) {
|
|
301
|
+
paths.push(workspacePath);
|
|
302
|
+
}
|
|
303
|
+
return paths;
|
|
304
|
+
}
|
|
305
|
+
/** Workspace and home analytics roots (store-installed global views live under home). */
|
|
306
|
+
getAllowedAnalyticsRoots() {
|
|
307
|
+
const roots = [
|
|
308
|
+
path.resolve(this.artifactManager.getGlobalAnalyticsRoot()),
|
|
309
|
+
path.resolve(this.artifactManager.getAnalyticsRoot()),
|
|
310
|
+
];
|
|
311
|
+
return [...new Set(roots)];
|
|
312
|
+
}
|
|
313
|
+
isPathWithinAllowedAnalyticsRoots(filePath) {
|
|
314
|
+
return (0, pathSafety_1.isPathUnderAnyRoot)(this.getAllowedAnalyticsRoots(), filePath);
|
|
315
|
+
}
|
|
316
|
+
isTeamSurface() {
|
|
317
|
+
return this.viewIdSuffix === 'team';
|
|
318
|
+
}
|
|
319
|
+
getSurfaceMode() {
|
|
320
|
+
return this.isTeamSurface() ? 'team' : 'spotlight';
|
|
321
|
+
}
|
|
322
|
+
async hydrateView(view, policy) {
|
|
323
|
+
const panelPolicy = policy ?? view;
|
|
324
|
+
const metadata = await this.readViewMetadata(view.id, view.scope);
|
|
325
|
+
let resolved;
|
|
326
|
+
if (metadata) {
|
|
327
|
+
const id = this.normalizeViewId(metadata.id, view.id);
|
|
328
|
+
let scripts = this.dedupeBasenames(metadata.scripts.length ? metadata.scripts : view.scripts);
|
|
329
|
+
scripts = id === 'fleet'
|
|
330
|
+
? [AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME]
|
|
331
|
+
: this.stripFleetWidgetFromGlobalWorkspace(id, scripts);
|
|
332
|
+
resolved = {
|
|
333
|
+
id,
|
|
334
|
+
name: metadata.name,
|
|
335
|
+
scope: metadata.scope,
|
|
336
|
+
scripts,
|
|
337
|
+
system: view.system === true || metadata.system === true,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const base = view.id === 'fleet'
|
|
342
|
+
? { ...view, scripts: [AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME] }
|
|
343
|
+
: view;
|
|
344
|
+
resolved = {
|
|
345
|
+
...base,
|
|
346
|
+
scripts: this.stripFleetWidgetFromGlobalWorkspace(base.id, base.scripts),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
...resolved,
|
|
351
|
+
refreshOnNavigate: panelPolicy.refreshOnNavigate,
|
|
352
|
+
layout: panelPolicy.layout,
|
|
353
|
+
preserveFleetIframe: panelPolicy.preserveFleetIframe,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/** Agent Team: built-in Fleet plus views tagged `team` or `fleet` in view.yaml. */
|
|
357
|
+
async buildTeamViewsConfig(preferredActiveId, spotlightPanel) {
|
|
358
|
+
const policyById = new Map(spotlightPanel.views.map(view => [view.id, view]));
|
|
359
|
+
const discovered = await (0, teamViewDiscovery_1.discoverTeamTaggedViews)(this.getAllowedAnalyticsRoots());
|
|
360
|
+
const views = [
|
|
361
|
+
{
|
|
362
|
+
id: 'fleet',
|
|
363
|
+
name: 'Fleet',
|
|
364
|
+
scope: 'global',
|
|
365
|
+
scripts: [AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME],
|
|
366
|
+
system: true,
|
|
367
|
+
},
|
|
368
|
+
// Workflows is a built-in team view (like Fleet): adding it here keeps it
|
|
369
|
+
// stable across reloads, unlike a postViewsState injection which flaps on
|
|
370
|
+
// this surface because buildTeamViewsConfig rebuilds the list each time.
|
|
371
|
+
(0, viewsConfigMerge_1.createDefaultWorkflowsView)(),
|
|
372
|
+
];
|
|
373
|
+
for (const record of discovered) {
|
|
374
|
+
const policy = policyById.get(record.id);
|
|
375
|
+
views.push(await this.hydrateView({
|
|
376
|
+
id: record.id,
|
|
377
|
+
name: record.name,
|
|
378
|
+
scope: record.scope,
|
|
379
|
+
scripts: policy?.scripts ?? [],
|
|
380
|
+
system: false,
|
|
381
|
+
}, policy));
|
|
382
|
+
}
|
|
383
|
+
return this.finalizeTeamViewsConfig(views, preferredActiveId);
|
|
384
|
+
}
|
|
385
|
+
async loadTeamViewsConfig() {
|
|
386
|
+
const [[globalLoaded, workspaceLoaded], [spotlightGlobal, spotlightWorkspace]] = await Promise.all([
|
|
387
|
+
this.readPanelConfigLayers(),
|
|
388
|
+
this.readPanelConfigLayers('views.yaml'),
|
|
389
|
+
]);
|
|
390
|
+
const spotlightPanel = this.mergeViewsConfigs(spotlightGlobal, spotlightWorkspace);
|
|
391
|
+
const preferredActiveId = this.normalizeViewId(workspaceLoaded?.activeViewId ?? globalLoaded?.activeViewId, 'fleet');
|
|
392
|
+
return this.buildTeamViewsConfig(preferredActiveId, spotlightPanel);
|
|
393
|
+
}
|
|
394
|
+
/** Agent Team picker: fleet plus discovered views only (no Spotlight global/workspace stubs). */
|
|
395
|
+
finalizeTeamViewsConfig(views, preferredActiveId) {
|
|
396
|
+
const normalizedViews = views.map(view => ({
|
|
397
|
+
...view,
|
|
398
|
+
id: this.normalizeViewId(view.id, 'fleet'),
|
|
399
|
+
scripts: view.id === 'fleet'
|
|
400
|
+
? [AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME]
|
|
401
|
+
: this.stripFleetWidgetFromGlobalWorkspace(view.id, this.dedupeBasenames(view.scripts)),
|
|
402
|
+
}));
|
|
403
|
+
if (!normalizedViews.some(view => view.id === 'fleet')) {
|
|
404
|
+
normalizedViews.unshift({
|
|
405
|
+
id: 'fleet',
|
|
406
|
+
name: 'Fleet',
|
|
407
|
+
scope: 'global',
|
|
408
|
+
scripts: [AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME],
|
|
409
|
+
system: true,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
version: 1,
|
|
414
|
+
activeViewId: this.resolvePersistedActiveId(normalizedViews, preferredActiveId, 'fleet'),
|
|
415
|
+
views: normalizedViews,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
getViewMetadataPath(viewId, scope) {
|
|
419
|
+
const context = {
|
|
420
|
+
scope,
|
|
421
|
+
projectId: this.resolveWorkspaceProjectId(),
|
|
422
|
+
agentId: this.agentFilterId,
|
|
423
|
+
viewId,
|
|
424
|
+
};
|
|
425
|
+
return path.join(this.artifactManager.resolveViewRoot(context), 'view.yaml');
|
|
426
|
+
}
|
|
427
|
+
async readViewMetadata(viewId, scope) {
|
|
428
|
+
const metadataPath = this.getViewMetadataPath(viewId, scope);
|
|
429
|
+
try {
|
|
430
|
+
const raw = await fs_1.promises.readFile(metadataPath, 'utf-8');
|
|
431
|
+
const yaml = require('js-yaml');
|
|
432
|
+
const parsed = yaml.load(raw);
|
|
433
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
const now = new Date().toISOString();
|
|
437
|
+
const versionField = parsed.version;
|
|
438
|
+
const version = typeof versionField === 'string' && versionField.trim()
|
|
439
|
+
? versionField.trim()
|
|
440
|
+
: typeof versionField === 'number'
|
|
441
|
+
? versionField
|
|
442
|
+
: 1;
|
|
443
|
+
return {
|
|
444
|
+
version,
|
|
445
|
+
id: this.normalizeViewId(typeof parsed.id === 'string' ? parsed.id : viewId, viewId),
|
|
446
|
+
name: typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : viewId,
|
|
447
|
+
scope: parsed.scope === 'workspace' ? 'workspace' : 'global',
|
|
448
|
+
projectId: typeof parsed.projectId === 'string' ? parsed.projectId : '',
|
|
449
|
+
agentId: typeof parsed.agentId === 'string' ? parsed.agentId : '',
|
|
450
|
+
createdAt: typeof parsed.createdAt === 'string' && parsed.createdAt.trim() ? parsed.createdAt : now,
|
|
451
|
+
updatedAt: typeof parsed.updatedAt === 'string' && parsed.updatedAt.trim() ? parsed.updatedAt : now,
|
|
452
|
+
scripts: this.dedupeBasenames(this.parseStringArray(parsed.scripts)),
|
|
453
|
+
system: parsed.system === true,
|
|
454
|
+
description: typeof parsed.description === 'string' ? parsed.description.trim() : undefined,
|
|
455
|
+
author: typeof parsed.author === 'string' ? parsed.author.trim() : undefined,
|
|
456
|
+
tags: Array.isArray(parsed.tags)
|
|
457
|
+
? parsed.tags.filter((tag) => typeof tag === 'string')
|
|
458
|
+
: undefined,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async writeViewMetadata(view, createdAt) {
|
|
466
|
+
const metadataPath = this.getViewMetadataPath(view.id, view.scope);
|
|
467
|
+
await fs_1.promises.mkdir(path.dirname(metadataPath), { recursive: true });
|
|
468
|
+
const now = new Date().toISOString();
|
|
469
|
+
const existing = await this.readViewMetadata(view.id, view.scope);
|
|
470
|
+
// Agent Team / Fleet view: persist only agent-fleet-widget.js by default.
|
|
471
|
+
const scripts = view.id === 'fleet' ? ['agent-fleet-widget.js'] : this.dedupeBasenames(view.scripts);
|
|
472
|
+
const payload = {
|
|
473
|
+
version: existing?.version ?? '1.0.0',
|
|
474
|
+
id: view.id,
|
|
475
|
+
name: view.name,
|
|
476
|
+
scope: view.scope,
|
|
477
|
+
projectId: this.resolveWorkspaceProjectId() ?? '',
|
|
478
|
+
agentId: this.agentFilterId ?? '',
|
|
479
|
+
createdAt: createdAt ?? existing?.createdAt ?? now,
|
|
480
|
+
updatedAt: now,
|
|
481
|
+
scripts,
|
|
482
|
+
system: view.system === true,
|
|
483
|
+
...(existing?.description ? { description: existing.description } : {}),
|
|
484
|
+
...(existing?.author ? { author: existing.author } : {}),
|
|
485
|
+
...(existing?.tags?.length ? { tags: existing.tags } : {}),
|
|
486
|
+
};
|
|
487
|
+
const yaml = require('js-yaml');
|
|
488
|
+
const content = yaml.dump(payload, { lineWidth: 120, noRefs: true });
|
|
489
|
+
await fs_1.promises.writeFile(metadataPath, content, 'utf-8');
|
|
490
|
+
}
|
|
491
|
+
getViewContext(view) {
|
|
492
|
+
return {
|
|
493
|
+
scope: view.scope,
|
|
494
|
+
projectId: this.resolveWorkspaceProjectId(),
|
|
495
|
+
agentId: this.agentFilterId,
|
|
496
|
+
viewId: view.id,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
getViewScriptsDir(view) {
|
|
500
|
+
return path.join(this.artifactManager.resolveViewRoot(this.getViewContext(view)), 'scripts');
|
|
501
|
+
}
|
|
502
|
+
async ensureViewOwnedScripts(view) {
|
|
503
|
+
// Seed the run scripts plus any declared sidecars (e.g. the Workflows collector/daemon
|
|
504
|
+
// the widget spawns) — driven by view config, not the view id.
|
|
505
|
+
const expectedScripts = this.dedupeBasenames([...view.scripts, ...(view.bundledScripts ?? [])]);
|
|
506
|
+
if (expectedScripts.length === 0) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const scriptsDir = this.getViewScriptsDir(view);
|
|
510
|
+
await fs_1.promises.mkdir(scriptsDir, { recursive: true });
|
|
511
|
+
for (const scriptName of expectedScripts) {
|
|
512
|
+
const destination = path.join(scriptsDir, scriptName);
|
|
513
|
+
try {
|
|
514
|
+
await fs_1.promises.access(destination);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Missing destination; seed below when a bundled script exists.
|
|
519
|
+
}
|
|
520
|
+
const source = await this.findBundledAnalyticsScript(scriptName);
|
|
521
|
+
if (!source) {
|
|
522
|
+
this.outputChannel.appendLine(`[Dashboard] Could not seed script "${scriptName}" for view "${view.id}" (bundled script not found)`);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
await fs_1.promises.copyFile(source, destination);
|
|
527
|
+
this.outputChannel.appendLine(`[Dashboard] Seeded missing view script "${scriptName}" for view "${view.id}"`);
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
this.outputChannel.appendLine(`[Dashboard] Failed seeding script "${scriptName}" for view "${view.id}": ${err}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async ensureViewOwnedDirectories(view) {
|
|
535
|
+
const viewRoot = this.artifactManager.resolveViewRoot(this.getViewContext(view));
|
|
536
|
+
for (const dir of [
|
|
537
|
+
path.join(viewRoot, 'scripts'),
|
|
538
|
+
path.join(viewRoot, 'charts'),
|
|
539
|
+
path.join(viewRoot, 'reports'),
|
|
540
|
+
path.join(viewRoot, 'exports'),
|
|
541
|
+
path.join(viewRoot, 'images'),
|
|
542
|
+
path.join(viewRoot, 'assets'),
|
|
543
|
+
]) {
|
|
544
|
+
await fs_1.promises.mkdir(dir, { recursive: true });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async ensureViewsFilesystemState() {
|
|
548
|
+
for (const view of this.viewsConfig.views) {
|
|
549
|
+
await this.ensureViewOwnedDirectories(view);
|
|
550
|
+
await this.ensureViewOwnedScripts(view);
|
|
551
|
+
await this.writeViewMetadata(view);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async copyViewFiles(sourceView, targetView, folders) {
|
|
555
|
+
const sourceRoot = this.artifactManager.resolveViewRoot({
|
|
556
|
+
scope: sourceView.scope,
|
|
557
|
+
projectId: this.resolveWorkspaceProjectId(),
|
|
558
|
+
agentId: this.agentFilterId,
|
|
559
|
+
viewId: sourceView.id,
|
|
560
|
+
});
|
|
561
|
+
const destinationRoot = this.artifactManager.resolveViewRoot({
|
|
562
|
+
scope: targetView.scope,
|
|
563
|
+
projectId: this.resolveWorkspaceProjectId(),
|
|
564
|
+
agentId: this.agentFilterId,
|
|
565
|
+
viewId: targetView.id,
|
|
566
|
+
});
|
|
567
|
+
for (const folder of folders) {
|
|
568
|
+
const sourceDir = path.join(sourceRoot, folder);
|
|
569
|
+
const destinationDir = path.join(destinationRoot, folder);
|
|
570
|
+
await fs_1.promises.mkdir(destinationDir, { recursive: true });
|
|
571
|
+
let entries = [];
|
|
572
|
+
try {
|
|
573
|
+
entries = await fs_1.promises.readdir(sourceDir);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
for (const entry of entries) {
|
|
579
|
+
const sourcePath = path.join(sourceDir, entry);
|
|
580
|
+
const destinationPath = path.join(destinationDir, entry);
|
|
581
|
+
try {
|
|
582
|
+
const stat = await fs_1.promises.stat(sourcePath);
|
|
583
|
+
if (!stat.isFile()) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
await fs_1.promises.copyFile(sourcePath, destinationPath);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// Best effort copy.
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
createDefaultViewsConfig() {
|
|
595
|
+
const defaultActiveViewId = this.viewIdSuffix === 'team' ? 'fleet' : 'global';
|
|
596
|
+
return {
|
|
597
|
+
version: 1,
|
|
598
|
+
activeViewId: defaultActiveViewId,
|
|
599
|
+
views: [
|
|
600
|
+
{ id: 'global', name: 'Global', scope: 'global', scripts: [], system: true },
|
|
601
|
+
{ id: 'workspace', name: 'Workspace', scope: 'workspace', scripts: [], system: true },
|
|
602
|
+
{ id: 'fleet', name: 'Fleet', scope: 'global', scripts: ['agent-fleet-widget.js'], system: true },
|
|
603
|
+
(0, viewsConfigMerge_1.createDefaultWorkflowsView)(),
|
|
604
|
+
],
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
normalizeViewsConfig(raw, options) {
|
|
608
|
+
const parsed = raw && typeof raw === 'object' ? raw : {};
|
|
609
|
+
const rawViews = Array.isArray(parsed.views) ? parsed.views : [];
|
|
610
|
+
const views = [];
|
|
611
|
+
const seen = new Set();
|
|
612
|
+
for (const item of rawViews) {
|
|
613
|
+
if (!item || typeof item !== 'object') {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const candidate = item;
|
|
617
|
+
const id = this.normalizeViewId(typeof candidate.id === 'string' ? candidate.id : undefined, 'view');
|
|
618
|
+
if (!id || seen.has(id)) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
seen.add(id);
|
|
622
|
+
const scope = candidate.scope === 'workspace' ? 'workspace' : 'global';
|
|
623
|
+
const name = typeof candidate.name === 'string' && candidate.name.trim() ? candidate.name.trim() : id;
|
|
624
|
+
const scripts = this.stripFleetWidgetFromGlobalWorkspace(id, this.dedupeBasenames(this.parseStringArray(candidate.scripts)));
|
|
625
|
+
const bundledScripts = this.dedupeBasenames(this.parseStringArray(candidate.bundledScripts));
|
|
626
|
+
const system = candidate.system === true;
|
|
627
|
+
const refreshOnNavigate = candidate.refreshOnNavigate === 'always' || candidate.refreshOnNavigate === 'when-empty'
|
|
628
|
+
? candidate.refreshOnNavigate
|
|
629
|
+
: undefined;
|
|
630
|
+
const layout = candidate.layout === 'fleet-dashboard' || candidate.layout === 'gallery'
|
|
631
|
+
? candidate.layout
|
|
632
|
+
: undefined;
|
|
633
|
+
const preserveFleetIframe = candidate.preserveFleetIframe === true
|
|
634
|
+
? true
|
|
635
|
+
: candidate.preserveFleetIframe === false
|
|
636
|
+
? false
|
|
637
|
+
: undefined;
|
|
638
|
+
views.push({
|
|
639
|
+
id, name, scope, scripts,
|
|
640
|
+
bundledScripts: bundledScripts.length ? bundledScripts : undefined,
|
|
641
|
+
system, refreshOnNavigate, layout, preserveFleetIframe,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (!views.some(view => view.id === 'global')) {
|
|
645
|
+
views.unshift({ id: 'global', name: 'Global', scope: 'global', scripts: [], system: true });
|
|
646
|
+
}
|
|
647
|
+
if (!views.some(view => view.id === 'workspace')) {
|
|
648
|
+
views.push({ id: 'workspace', name: 'Workspace', scope: 'workspace', scripts: [], system: true });
|
|
649
|
+
}
|
|
650
|
+
if (!views.some(view => view.id === 'fleet')) {
|
|
651
|
+
views.push({ id: 'fleet', name: 'Fleet', scope: 'global', scripts: ['agent-fleet-widget.js'], system: true });
|
|
652
|
+
}
|
|
653
|
+
if (!views.some(view => view.id === viewsConfigMerge_1.WORKFLOWS_VIEW_ID)) {
|
|
654
|
+
views.push((0, viewsConfigMerge_1.createDefaultWorkflowsView)());
|
|
655
|
+
}
|
|
656
|
+
const defaultConfig = this.createDefaultViewsConfig();
|
|
657
|
+
const rawActive = typeof parsed.activeViewId === 'string' ? parsed.activeViewId : defaultConfig.activeViewId;
|
|
658
|
+
const activeViewId = this.normalizeViewId(rawActive, defaultConfig.activeViewId);
|
|
659
|
+
const hasActive = views.some(view => view.id === activeViewId);
|
|
660
|
+
return {
|
|
661
|
+
version: 1,
|
|
662
|
+
activeViewId: hasActive || options?.allowOrphanActiveId ? activeViewId : defaultConfig.activeViewId,
|
|
663
|
+
views,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
async readViewsConfigFile(configPath) {
|
|
667
|
+
try {
|
|
668
|
+
const raw = await fs_1.promises.readFile(configPath, 'utf-8');
|
|
669
|
+
const yaml = require('js-yaml');
|
|
670
|
+
return this.normalizeViewsConfig(yaml.load(raw), { allowOrphanActiveId: true });
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
mergeViewsConfigs(globalConfig, workspaceConfig) {
|
|
677
|
+
const defaults = this.createDefaultViewsConfig();
|
|
678
|
+
const merged = (0, viewsConfigMerge_1.mergeDashboardViewsConfigs)(globalConfig, workspaceConfig, defaults);
|
|
679
|
+
return this.normalizeViewsConfig({
|
|
680
|
+
version: 1,
|
|
681
|
+
activeViewId: merged.activeViewId,
|
|
682
|
+
views: merged.views,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
async handleViewsConfigChanged() {
|
|
686
|
+
const previousActiveViewId = this.getActiveViewId();
|
|
687
|
+
await this.loadViewsConfig();
|
|
688
|
+
if (!this.webviewView?.visible) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
await this.postViewsState();
|
|
692
|
+
await this.postPanelState('default');
|
|
693
|
+
if (this.getActiveViewId() !== previousActiveViewId) {
|
|
694
|
+
await this.refreshAfterViewChange();
|
|
695
|
+
await this.refreshChartsForActiveViewAfterSwitch();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async loadViewsConfig() {
|
|
699
|
+
if (this.isTeamSurface()) {
|
|
700
|
+
const teamConfigPath = this.getGlobalViewsConfigPath();
|
|
701
|
+
let needsPersist = false;
|
|
702
|
+
try {
|
|
703
|
+
await fs_1.promises.access(teamConfigPath);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
needsPersist = true;
|
|
707
|
+
}
|
|
708
|
+
this.viewsConfig = await this.loadTeamViewsConfig();
|
|
709
|
+
if (needsPersist) {
|
|
710
|
+
await this.saveViewsConfig(this.viewsConfig);
|
|
711
|
+
}
|
|
712
|
+
return this.viewsConfig;
|
|
713
|
+
}
|
|
714
|
+
const workspacePath = this.getWorkspaceViewsConfigPath();
|
|
715
|
+
const [globalLoaded, workspaceLoaded] = await this.readPanelConfigLayers();
|
|
716
|
+
let usedFallback = !globalLoaded && !workspaceLoaded;
|
|
717
|
+
let loaded = this.mergeViewsConfigs(globalLoaded, workspaceLoaded);
|
|
718
|
+
if (usedFallback) {
|
|
719
|
+
const fallback = this.createDefaultViewsConfig();
|
|
720
|
+
const allScripts = await this.artifactManager.listScripts({ disabledScripts: [] });
|
|
721
|
+
const allScriptNames = this.dedupeBasenames(allScripts.map(script => script.filename));
|
|
722
|
+
const fleetOnly = AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME;
|
|
723
|
+
loaded = {
|
|
724
|
+
...fallback,
|
|
725
|
+
views: fallback.views.map(view => ({
|
|
726
|
+
...view,
|
|
727
|
+
scripts: view.scripts.length
|
|
728
|
+
? view.scripts
|
|
729
|
+
: allScriptNames.filter((name) => view.id === 'fleet'
|
|
730
|
+
? this.normalizeBasename(name) === fleetOnly
|
|
731
|
+
: this.normalizeBasename(name) !== fleetOnly),
|
|
732
|
+
})),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const hydratedViews = [];
|
|
736
|
+
for (const view of loaded.views) {
|
|
737
|
+
hydratedViews.push(await this.hydrateView(view));
|
|
738
|
+
}
|
|
739
|
+
const normalized = this.normalizeViewsConfig({
|
|
740
|
+
version: 1,
|
|
741
|
+
activeViewId: loaded.activeViewId,
|
|
742
|
+
views: hydratedViews,
|
|
743
|
+
});
|
|
744
|
+
this.viewsConfig = normalized;
|
|
745
|
+
const shouldPersist = usedFallback
|
|
746
|
+
|| this.shouldHealWorkspaceActiveViewId(workspaceLoaded, workspacePath, loaded.activeViewId);
|
|
747
|
+
if (shouldPersist) {
|
|
748
|
+
await this.saveViewsConfig(this.viewsConfig);
|
|
749
|
+
}
|
|
750
|
+
return this.viewsConfig;
|
|
751
|
+
}
|
|
752
|
+
resolvePersistedActiveId(views, preferredId, fallbackId) {
|
|
753
|
+
const normalized = this.normalizeViewId(preferredId, fallbackId);
|
|
754
|
+
return views.some(view => view.id === normalized) ? normalized : fallbackId;
|
|
755
|
+
}
|
|
756
|
+
/** Rewrite workspace views.yaml when merge healed an orphan workspace activeViewId. */
|
|
757
|
+
shouldHealWorkspaceActiveViewId(workspaceLoaded, workspacePath, resolvedActiveViewId) {
|
|
758
|
+
if (!workspacePath || !workspaceLoaded) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
const stored = this.normalizeViewId(workspaceLoaded.activeViewId, '');
|
|
762
|
+
const resolved = this.normalizeViewId(resolvedActiveViewId, '');
|
|
763
|
+
return stored.length > 0 && stored !== resolved;
|
|
764
|
+
}
|
|
765
|
+
async saveViewsConfig(config) {
|
|
766
|
+
const normalized = this.normalizeViewsConfig(config);
|
|
767
|
+
const yaml = require('js-yaml');
|
|
768
|
+
if (this.isTeamSurface()) {
|
|
769
|
+
const defaultActive = 'fleet';
|
|
770
|
+
const activeViewId = this.resolvePersistedActiveId(normalized.views, normalized.activeViewId, defaultActive);
|
|
771
|
+
const teamPersist = {
|
|
772
|
+
version: 1,
|
|
773
|
+
activeViewId,
|
|
774
|
+
views: [{
|
|
775
|
+
id: 'fleet',
|
|
776
|
+
name: 'Fleet',
|
|
777
|
+
scope: 'global',
|
|
778
|
+
scripts: [AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME],
|
|
779
|
+
system: true,
|
|
780
|
+
}],
|
|
781
|
+
};
|
|
782
|
+
const globalPath = this.getGlobalViewsConfigPath();
|
|
783
|
+
await fs_1.promises.mkdir(path.dirname(globalPath), { recursive: true });
|
|
784
|
+
await fs_1.promises.writeFile(globalPath, yaml.dump(teamPersist, { lineWidth: 120, noRefs: true }), 'utf-8');
|
|
785
|
+
const workspacePath = this.getWorkspaceViewsConfigPath();
|
|
786
|
+
if (workspacePath) {
|
|
787
|
+
await fs_1.promises.mkdir(path.dirname(workspacePath), { recursive: true });
|
|
788
|
+
await fs_1.promises.writeFile(workspacePath, yaml.dump({ version: 1, activeViewId, views: [] }, { lineWidth: 120, noRefs: true }), 'utf-8');
|
|
789
|
+
}
|
|
790
|
+
this.viewsConfig = normalized;
|
|
791
|
+
await this.ensureViewsFilesystemState();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const globalViews = normalized.views.filter(view => view.scope !== 'workspace');
|
|
795
|
+
const workspaceViews = normalized.views.filter(view => view.scope === 'workspace');
|
|
796
|
+
const defaultActive = 'global';
|
|
797
|
+
const unifiedActive = this.normalizeViewId(normalized.activeViewId, defaultActive);
|
|
798
|
+
const activeViewId = normalized.views.some(view => view.id === unifiedActive)
|
|
799
|
+
? unifiedActive
|
|
800
|
+
: defaultActive;
|
|
801
|
+
const globalPath = this.getGlobalViewsConfigPath();
|
|
802
|
+
const workspacePath = this.getWorkspaceViewsConfigPath();
|
|
803
|
+
const [existingGlobal] = await Promise.all([
|
|
804
|
+
this.readViewsConfigFile(globalPath),
|
|
805
|
+
workspacePath ? this.readViewsConfigFile(workspacePath) : Promise.resolve(undefined),
|
|
806
|
+
]);
|
|
807
|
+
// Per-repo selection lives in workspace views.yaml when a folder is open; global activeViewId is home-only.
|
|
808
|
+
const globalActiveId = workspacePath
|
|
809
|
+
? this.resolvePersistedActiveId(globalViews, existingGlobal?.activeViewId ?? defaultActive, defaultActive)
|
|
810
|
+
: activeViewId;
|
|
811
|
+
const globalConfig = {
|
|
812
|
+
version: 1,
|
|
813
|
+
activeViewId: globalActiveId,
|
|
814
|
+
views: globalViews,
|
|
815
|
+
};
|
|
816
|
+
await fs_1.promises.mkdir(path.dirname(globalPath), { recursive: true });
|
|
817
|
+
await fs_1.promises.writeFile(globalPath, yaml.dump(globalConfig, { lineWidth: 120, noRefs: true }), 'utf-8');
|
|
818
|
+
if (workspacePath) {
|
|
819
|
+
const workspaceConfig = {
|
|
820
|
+
version: 1,
|
|
821
|
+
activeViewId,
|
|
822
|
+
views: workspaceViews,
|
|
823
|
+
};
|
|
824
|
+
await fs_1.promises.mkdir(path.dirname(workspacePath), { recursive: true });
|
|
825
|
+
await fs_1.promises.writeFile(workspacePath, yaml.dump(workspaceConfig, { lineWidth: 120, noRefs: true }), 'utf-8');
|
|
826
|
+
}
|
|
827
|
+
this.viewsConfig = normalized;
|
|
828
|
+
await this.ensureViewsFilesystemState();
|
|
829
|
+
}
|
|
830
|
+
getActiveViewId() {
|
|
831
|
+
const fallback = this.isTeamSurface() ? 'fleet' : 'global';
|
|
832
|
+
const activeId = this.normalizeViewId(this.viewsConfig.activeViewId, fallback);
|
|
833
|
+
if (this.viewsConfig.views.some(view => view.id === activeId)) {
|
|
834
|
+
return activeId;
|
|
835
|
+
}
|
|
836
|
+
return fallback;
|
|
837
|
+
}
|
|
838
|
+
getActiveView() {
|
|
839
|
+
const activeId = this.getActiveViewId();
|
|
840
|
+
return this.viewsConfig.views.find(view => view.id === activeId) ?? this.createDefaultViewsConfig().views[0];
|
|
841
|
+
}
|
|
842
|
+
findViewById(viewId) {
|
|
843
|
+
const normalized = this.normalizeViewId(viewId);
|
|
844
|
+
return this.viewsConfig.views.find(view => view.id === normalized);
|
|
845
|
+
}
|
|
846
|
+
async postViewsState() {
|
|
847
|
+
if (!this.viewsConfig.views.some(v => v.id === 'fleet')) {
|
|
848
|
+
const fleetView = { id: 'fleet', name: 'Fleet', scope: 'global', scripts: ['agent-fleet-widget.js'], system: true };
|
|
849
|
+
this.viewsConfig = {
|
|
850
|
+
...this.viewsConfig,
|
|
851
|
+
views: [...this.viewsConfig.views, fleetView],
|
|
852
|
+
};
|
|
853
|
+
await this.ensureViewOwnedDirectories(fleetView);
|
|
854
|
+
await this.writeViewMetadata(fleetView);
|
|
855
|
+
await this.saveViewsConfig(this.viewsConfig);
|
|
856
|
+
}
|
|
857
|
+
// Spotlight-only safety net. The team surface already owns Workflows as a built-in
|
|
858
|
+
// in buildTeamViewsConfig; injecting it here too would flap the picker (it rebuilds
|
|
859
|
+
// the list each reload) and pollute the shared team config on disk.
|
|
860
|
+
if (!this.isTeamSurface() && !this.viewsConfig.views.some(v => v.id === viewsConfigMerge_1.WORKFLOWS_VIEW_ID)) {
|
|
861
|
+
const workflowsView = (0, viewsConfigMerge_1.createDefaultWorkflowsView)();
|
|
862
|
+
this.viewsConfig = {
|
|
863
|
+
...this.viewsConfig,
|
|
864
|
+
views: [...this.viewsConfig.views, workflowsView],
|
|
865
|
+
};
|
|
866
|
+
await this.ensureViewOwnedDirectories(workflowsView);
|
|
867
|
+
await this.ensureViewOwnedScripts(workflowsView);
|
|
868
|
+
await this.writeViewMetadata(workflowsView);
|
|
869
|
+
await this.saveViewsConfig(this.viewsConfig);
|
|
870
|
+
}
|
|
871
|
+
const activeViewId = this.getActiveViewId();
|
|
872
|
+
const views = this.viewsConfig.views.map(view => ({
|
|
873
|
+
id: view.id,
|
|
874
|
+
name: view.name,
|
|
875
|
+
scope: view.scope,
|
|
876
|
+
system: view.system === true,
|
|
877
|
+
layout: (0, viewsConfigMerge_1.resolveGalleryLayout)(view),
|
|
878
|
+
refreshOnNavigate: (0, viewsConfigMerge_1.resolveRefreshOnNavigate)(view),
|
|
879
|
+
preserveFleetIframe: (0, viewsConfigMerge_1.resolvePreserveFleetIframe)(view),
|
|
880
|
+
}));
|
|
881
|
+
postToWebview(this.webviewView?.webview, {
|
|
882
|
+
type: 'viewsLoaded',
|
|
883
|
+
activeViewId,
|
|
884
|
+
views,
|
|
885
|
+
surfaceMode: this.getSurfaceMode(),
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
async loadAndPostViewsState() {
|
|
889
|
+
this.viewsConfig = await this.loadViewsConfig();
|
|
890
|
+
await this.postViewsState();
|
|
891
|
+
}
|
|
892
|
+
/** Reload views.yaml from disk (global + workspace) and refresh the webview dropdown. */
|
|
893
|
+
async refreshViewsFromDisk() {
|
|
894
|
+
await this.loadAndPostViewsState();
|
|
895
|
+
}
|
|
896
|
+
async setActiveView(viewId) {
|
|
897
|
+
const normalized = this.normalizeViewId(viewId);
|
|
898
|
+
if (!this.viewsConfig.views.some(view => view.id === normalized)) {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
if (this.viewsConfig.activeViewId === normalized) {
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
this.viewsConfig = {
|
|
905
|
+
...this.viewsConfig,
|
|
906
|
+
activeViewId: normalized,
|
|
907
|
+
};
|
|
908
|
+
await this.saveViewsConfig(this.viewsConfig);
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Switch the dashboard to the given view and persist. Caller should focus the panel after.
|
|
913
|
+
* Used by "Open Agent Fleet" to open the dashboard with the Fleet view active.
|
|
914
|
+
*/
|
|
915
|
+
async openWithView(viewId) {
|
|
916
|
+
await this.loadViewsConfig();
|
|
917
|
+
const switched = await this.setActiveView(viewId);
|
|
918
|
+
if (!this.webviewView?.visible) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
await this.postViewsState();
|
|
922
|
+
if (switched) {
|
|
923
|
+
this.syncArtifactContext();
|
|
924
|
+
await this.refreshAfterViewChange();
|
|
925
|
+
await this.refreshChartsForActiveViewAfterSwitch();
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
allocateViewId(name, fallback = 'view') {
|
|
929
|
+
const baseId = this.normalizeViewId(name, fallback);
|
|
930
|
+
let candidateId = baseId;
|
|
931
|
+
let suffix = 2;
|
|
932
|
+
while (this.viewsConfig.views.some(view => view.id === candidateId)) {
|
|
933
|
+
candidateId = `${baseId}-${suffix++}`;
|
|
934
|
+
}
|
|
935
|
+
return candidateId;
|
|
936
|
+
}
|
|
937
|
+
async createView(name) {
|
|
938
|
+
const trimmed = name.trim();
|
|
939
|
+
if (!trimmed) {
|
|
940
|
+
return undefined;
|
|
941
|
+
}
|
|
942
|
+
const candidateId = this.allocateViewId(trimmed, 'view');
|
|
943
|
+
const sourceView = this.getActiveView();
|
|
944
|
+
const newView = {
|
|
945
|
+
id: candidateId,
|
|
946
|
+
name: trimmed,
|
|
947
|
+
scope: sourceView.scope,
|
|
948
|
+
scripts: [],
|
|
949
|
+
system: false,
|
|
950
|
+
};
|
|
951
|
+
await this.ensureViewOwnedDirectories(newView);
|
|
952
|
+
await this.copyViewFiles(sourceView, newView, ['assets', 'images']);
|
|
953
|
+
this.viewsConfig = {
|
|
954
|
+
...this.viewsConfig,
|
|
955
|
+
activeViewId: newView.id,
|
|
956
|
+
views: [...this.viewsConfig.views, newView],
|
|
957
|
+
};
|
|
958
|
+
await this.saveViewsConfig(this.viewsConfig);
|
|
959
|
+
return newView;
|
|
960
|
+
}
|
|
961
|
+
async renameView(viewId, name) {
|
|
962
|
+
const target = this.findViewById(viewId);
|
|
963
|
+
if (!target || target.system) {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
const trimmed = name.trim();
|
|
967
|
+
if (!trimmed) {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
this.viewsConfig = {
|
|
971
|
+
...this.viewsConfig,
|
|
972
|
+
views: this.viewsConfig.views.map(view => view.id === target.id ? { ...view, name: trimmed } : view),
|
|
973
|
+
};
|
|
974
|
+
await this.saveViewsConfig(this.viewsConfig);
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
async duplicateView(viewId) {
|
|
978
|
+
const source = this.findViewById(viewId);
|
|
979
|
+
if (!source) {
|
|
980
|
+
return undefined;
|
|
981
|
+
}
|
|
982
|
+
const copyBaseName = `${source.name} Copy`;
|
|
983
|
+
const candidateId = this.allocateViewId(copyBaseName, 'view');
|
|
984
|
+
const copyDisplayName = (0, viewsConfigMerge_1.formatDisplayNameForAllocatedViewId)(copyBaseName, candidateId, (raw, fallback) => this.normalizeViewId(raw, fallback));
|
|
985
|
+
const scripts = await this.resolveScriptBasenamesForDuplicate(source);
|
|
986
|
+
const duplicate = {
|
|
987
|
+
id: candidateId,
|
|
988
|
+
name: copyDisplayName,
|
|
989
|
+
scope: source.scope,
|
|
990
|
+
scripts,
|
|
991
|
+
system: false,
|
|
992
|
+
};
|
|
993
|
+
await this.ensureViewOwnedDirectories(duplicate);
|
|
994
|
+
await this.copyViewFiles(source, duplicate, ['scripts', 'assets', 'images', 'charts', 'reports', 'exports']);
|
|
995
|
+
this.viewsConfig = {
|
|
996
|
+
...this.viewsConfig,
|
|
997
|
+
activeViewId: duplicate.id,
|
|
998
|
+
views: [...this.viewsConfig.views, duplicate],
|
|
999
|
+
};
|
|
1000
|
+
await this.saveViewsConfig(this.viewsConfig);
|
|
1001
|
+
return duplicate;
|
|
1002
|
+
}
|
|
1003
|
+
async deleteView(viewId) {
|
|
1004
|
+
const target = this.findViewById(viewId);
|
|
1005
|
+
if (!target || target.system) {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
const nextActiveViewId = this.getActiveViewId() === target.id ? 'global' : this.getActiveViewId();
|
|
1009
|
+
this.viewsConfig = {
|
|
1010
|
+
...this.viewsConfig,
|
|
1011
|
+
activeViewId: nextActiveViewId,
|
|
1012
|
+
views: this.viewsConfig.views.filter(view => view.id !== target.id),
|
|
1013
|
+
};
|
|
1014
|
+
const viewRoot = this.artifactManager.resolveViewRoot({
|
|
1015
|
+
scope: target.scope,
|
|
1016
|
+
projectId: this.resolveWorkspaceProjectId(),
|
|
1017
|
+
agentId: this.agentFilterId,
|
|
1018
|
+
viewId: target.id,
|
|
1019
|
+
});
|
|
1020
|
+
await fs_1.promises.rm(viewRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
1021
|
+
const panelStatePath = path.join(this.artifactManager.getAnalyticsRoot(), 'panel-state', `default--${target.id}.json`);
|
|
1022
|
+
await fs_1.promises.rm(panelStatePath, { force: true }).catch(() => undefined);
|
|
1023
|
+
await this.saveViewsConfig(this.viewsConfig);
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
async editViewScripts(viewId) {
|
|
1027
|
+
const view = viewId ? this.findViewById(viewId) : this.getActiveView();
|
|
1028
|
+
if (!view) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
const scriptRoot = path.join(this.artifactManager.resolveViewRoot({
|
|
1032
|
+
scope: view.scope,
|
|
1033
|
+
projectId: this.resolveWorkspaceProjectId(),
|
|
1034
|
+
agentId: this.agentFilterId,
|
|
1035
|
+
viewId: view.id,
|
|
1036
|
+
}), 'scripts');
|
|
1037
|
+
await fs_1.promises.mkdir(scriptRoot, { recursive: true });
|
|
1038
|
+
await vscode.commands.executeCommand('revealInExplorer', vscode.Uri.file(scriptRoot));
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
async refreshAllViews() {
|
|
1042
|
+
// Spotlight "refresh" should never change the user's currently-selected view.
|
|
1043
|
+
// Refresh only the active view to avoid temporarily switching through other
|
|
1044
|
+
// system views (notably `fleet`) during the refresh cycle.
|
|
1045
|
+
const activeViewId = this.getActiveViewId();
|
|
1046
|
+
await this.refreshCharts({ viewId: activeViewId });
|
|
1047
|
+
}
|
|
1048
|
+
getRefreshContext(options) {
|
|
1049
|
+
const requestedViewId = options?.viewId ? this.normalizeViewId(options.viewId) : this.getActiveViewId();
|
|
1050
|
+
const view = this.viewsConfig.views.find(item => item.id === requestedViewId) ?? this.getActiveView();
|
|
1051
|
+
return {
|
|
1052
|
+
viewId: view.id,
|
|
1053
|
+
viewName: view.name,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
postRefreshStatus(type, options) {
|
|
1057
|
+
const refreshContext = this.getRefreshContext(options);
|
|
1058
|
+
postToWebview(this.webviewView?.webview, {
|
|
1059
|
+
type,
|
|
1060
|
+
viewId: refreshContext.viewId,
|
|
1061
|
+
viewName: refreshContext.viewName,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
async getScriptsForView(options) {
|
|
1065
|
+
const viewId = options?.viewId ? this.normalizeViewId(options.viewId) : this.getActiveViewId();
|
|
1066
|
+
const view = this.viewsConfig.views.find(item => item.id === viewId) ?? this.getActiveView();
|
|
1067
|
+
await this.ensureViewOwnedScripts(view);
|
|
1068
|
+
return this.withArtifactViewContext(view, async () => {
|
|
1069
|
+
const allScripts = await this.artifactManager.listScripts({ disabledScripts: [] });
|
|
1070
|
+
return this.resolveScriptsForView(view, allScripts);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
resolveScriptsForView(view, allScripts) {
|
|
1074
|
+
if (!view.scripts.length) {
|
|
1075
|
+
// System views (e.g. Global, Workspace) with no scripts → use all scripts except the Fleet-only widget.
|
|
1076
|
+
// Fleet view with no scripts → only agent-fleet-widget.js. User-created "Add view" → empty canvas.
|
|
1077
|
+
if (!view.system) {
|
|
1078
|
+
return { view, scripts: [] };
|
|
1079
|
+
}
|
|
1080
|
+
if (view.id === 'fleet') {
|
|
1081
|
+
return {
|
|
1082
|
+
view,
|
|
1083
|
+
scripts: allScripts.filter(s => this.normalizeBasename(s.filename) === AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME),
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
view,
|
|
1088
|
+
scripts: allScripts.filter(s => this.normalizeBasename(s.filename) !== AnalyticsDashboardProvider.FLEET_WIDGET_BASENAME),
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const selectedBasenames = new Set(view.scripts.map(name => this.normalizeBasename(name)));
|
|
1092
|
+
return {
|
|
1093
|
+
view,
|
|
1094
|
+
scripts: allScripts.filter(script => selectedBasenames.has(this.normalizeBasename(script.filename))),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
async resolveScriptBasenamesForDuplicate(source) {
|
|
1098
|
+
if (source.scripts.length > 0) {
|
|
1099
|
+
return this.dedupeBasenames(source.scripts);
|
|
1100
|
+
}
|
|
1101
|
+
const selection = await this.withArtifactViewContext(source, async () => {
|
|
1102
|
+
const allScripts = await this.artifactManager.listScripts({ disabledScripts: [] });
|
|
1103
|
+
return this.resolveScriptsForView(source, allScripts);
|
|
1104
|
+
});
|
|
1105
|
+
return this.dedupeBasenames(selection.scripts.map(script => script.filename));
|
|
1106
|
+
}
|
|
1107
|
+
getScopedPanelId(panelId) {
|
|
1108
|
+
const normalizedPanelId = this.normalizePanelStateId(panelId);
|
|
1109
|
+
const viewId = this.getActiveViewId();
|
|
1110
|
+
return `${normalizedPanelId}--${viewId}`;
|
|
1111
|
+
}
|
|
1112
|
+
async refreshDashboardData() {
|
|
1113
|
+
this.queryRunner.invalidateCache();
|
|
1114
|
+
void this.fetchDashboardBootstrapData().then(bootstrap => {
|
|
1115
|
+
void this.postFilterState(bootstrap?.agentIds);
|
|
1116
|
+
this.triggerSummaryCardsUpdate(bootstrap?.summary);
|
|
1117
|
+
});
|
|
1118
|
+
const refreshedBootstrap = await this.fetchDashboardBootstrapData();
|
|
1119
|
+
await this.postFilterState(refreshedBootstrap?.agentIds);
|
|
1120
|
+
this.triggerSummaryCardsUpdate(refreshedBootstrap?.summary);
|
|
1121
|
+
await this.updateDataFreshnessHint(refreshedBootstrap
|
|
1122
|
+
? { hasData: refreshedBootstrap.hasData, dataHash: refreshedBootstrap.dataHash }
|
|
1123
|
+
: undefined);
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Reload gallery and summary after the active view or filters change.
|
|
1127
|
+
* Policy: artifact-first — always load from disk here; script execution is gated separately
|
|
1128
|
+
* (see PLAN/spotlight-refresh-policy.md and maybeRefreshChartsAfterViewSwitch).
|
|
1129
|
+
*/
|
|
1130
|
+
async refreshAfterViewChange() {
|
|
1131
|
+
this.queryRunner.invalidateCache();
|
|
1132
|
+
const bootstrapPromise = this.fetchDashboardBootstrapData();
|
|
1133
|
+
await this.loadAndSendArtifacts();
|
|
1134
|
+
this.scheduleAutoGenerateForCurrentFilter();
|
|
1135
|
+
const bootstrap = await bootstrapPromise;
|
|
1136
|
+
await this.postFilterState(bootstrap?.agentIds);
|
|
1137
|
+
this.triggerSummaryCardsUpdate(bootstrap?.summary);
|
|
1138
|
+
await this.updateDataFreshnessHint(bootstrap ? { hasData: bootstrap.hasData, dataHash: bootstrap.dataHash } : undefined);
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Whether navigation should run widget scripts (view switch after load).
|
|
1142
|
+
* Fleet always runs; otherwise run only when the view has no chart artifacts.
|
|
1143
|
+
*/
|
|
1144
|
+
async shouldRunScriptsOnNavigation() {
|
|
1145
|
+
this.syncArtifactContext();
|
|
1146
|
+
const artifacts = await this.artifactManager.listArtifacts();
|
|
1147
|
+
return (0, viewsConfigMerge_1.shouldRunScriptsOnNavigation)(this.getActiveView(), artifacts.map(artifact => artifact.type));
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Run widget scripts after view switch when policy allows (compute when empty; fleet always).
|
|
1151
|
+
*/
|
|
1152
|
+
async refreshChartsForActiveViewAfterSwitch() {
|
|
1153
|
+
try {
|
|
1154
|
+
if (!(await this.shouldRunScriptsOnNavigation())) {
|
|
1155
|
+
this.outputChannel.appendLine('[Dashboard] View switch: load only (chart artifacts present; use Refresh to re-run scripts)');
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
await this.refreshCharts({ viewId: this.getActiveViewId() });
|
|
1159
|
+
}
|
|
1160
|
+
catch (err) {
|
|
1161
|
+
this.outputChannel.appendLine(`[Dashboard] View-switch chart refresh failed: ${err}`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
getActiveFilters() {
|
|
1165
|
+
const activeView = this.getActiveView();
|
|
1166
|
+
return {
|
|
1167
|
+
scope: activeView.scope,
|
|
1168
|
+
projectId: this.resolveWorkspaceProjectId(),
|
|
1169
|
+
agentId: this.agentFilterId,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
async fetchDashboardBootstrapData() {
|
|
1173
|
+
try {
|
|
1174
|
+
return await this.queryRunner.getDashboardBootstrap(this.getActiveFilters());
|
|
1175
|
+
}
|
|
1176
|
+
catch (err) {
|
|
1177
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1178
|
+
this.outputChannel.appendLine(`[Dashboard] Could not load dashboard bootstrap data: ${message}`);
|
|
1179
|
+
if (message.toLowerCase().includes('indexed analytics db')) {
|
|
1180
|
+
this.webviewView?.webview.postMessage({
|
|
1181
|
+
type: 'notifyIndexing',
|
|
1182
|
+
message: 'Spotlight index unavailable. Re-index traces (Setup Spotlight) and refresh.',
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
return undefined;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
triggerSummaryCardsUpdate(summaryOverride) {
|
|
1189
|
+
void this.sendSummaryCards(summaryOverride);
|
|
1190
|
+
}
|
|
1191
|
+
async postFilterState(availableAgentIdsOverride) {
|
|
1192
|
+
const filters = this.getActiveFilters();
|
|
1193
|
+
let availableAgentIds = [];
|
|
1194
|
+
if (availableAgentIdsOverride) {
|
|
1195
|
+
availableAgentIds = [...availableAgentIdsOverride];
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
try {
|
|
1199
|
+
availableAgentIds = await this.queryRunner.getAgentIds({
|
|
1200
|
+
scope: filters.scope,
|
|
1201
|
+
projectId: filters.projectId,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
catch (err) {
|
|
1205
|
+
this.outputChannel.appendLine(`[Dashboard] Could not load agent filters: ${err}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (this.agentFilterId && !availableAgentIds.includes(this.agentFilterId)) {
|
|
1209
|
+
this.agentFilterId = undefined;
|
|
1210
|
+
}
|
|
1211
|
+
postToWebview(this.webviewView?.webview, {
|
|
1212
|
+
type: 'filtersUpdated',
|
|
1213
|
+
scope: filters.scope,
|
|
1214
|
+
projectId: filters.projectId ?? '',
|
|
1215
|
+
agentId: this.agentFilterId ?? '',
|
|
1216
|
+
agentIds: availableAgentIds,
|
|
1217
|
+
activeViewId: this.getActiveViewId(),
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
resolveIndexScriptPathForScripts() {
|
|
1221
|
+
const candidate = path.resolve(__dirname, '../../scripts/analytics/index-traces.py');
|
|
1222
|
+
return (0, fs_1.existsSync)(candidate) ? candidate : '';
|
|
1223
|
+
}
|
|
1224
|
+
buildScriptEnvironment(outputDir, disabledCharts = []) {
|
|
1225
|
+
this.syncArtifactContext();
|
|
1226
|
+
const filters = this.getActiveFilters();
|
|
1227
|
+
const projectIdForScope = filters.scope === 'workspace' ? (filters.projectId ?? '') : '';
|
|
1228
|
+
const workspaceRoot = this.resolveWorkspaceRootPath() ?? '';
|
|
1229
|
+
const tracesDir = path.join((0, index_1.getAgentideHome)(), 'analytics', 'traces');
|
|
1230
|
+
const indexingService = this.indexingService();
|
|
1231
|
+
const indexedPath = indexingService?.getIndexPath().trim() ?? '';
|
|
1232
|
+
const indexDbPath = indexedPath || path.join(tracesDir, 'analytics.duckdb');
|
|
1233
|
+
const isIndexed = indexedPath.length > 0;
|
|
1234
|
+
const indexScriptPath = this.resolveIndexScriptPathForScripts();
|
|
1235
|
+
const analyticsConfig = vscode.workspace.getConfiguration('agentide.analytics');
|
|
1236
|
+
const duckdbMemoryLimitMb = Math.max(64, analyticsConfig.get('duckdb.memoryLimitMb', 1024) ?? 1024);
|
|
1237
|
+
const duckdbThreads = Math.max(1, analyticsConfig.get('duckdb.threads', 2) ?? 2);
|
|
1238
|
+
const duckdbQueryTimeoutSeconds = Math.max(5, analyticsConfig.get('duckdb.queryTimeoutSeconds', 30) ?? 30);
|
|
1239
|
+
const duckdbMaxResultRows = Math.max(100, analyticsConfig.get('duckdb.maxResultRows', 5000) ?? 5000);
|
|
1240
|
+
const duckdbTempDirectory = (analyticsConfig.get('duckdb.tempDirectory', '') ?? '').trim();
|
|
1241
|
+
return {
|
|
1242
|
+
...process.env,
|
|
1243
|
+
// Augment PATH so uv/python/pip are found even when VS Code is
|
|
1244
|
+
// launched from the Dock (which inherits a minimal PATH).
|
|
1245
|
+
PATH: (0, toolPath_1.augmentPathForTools)(process.env.PATH ?? ''),
|
|
1246
|
+
AGENTIDE_TRACES_DIR: tracesDir,
|
|
1247
|
+
AGENTIDE_CHART_DIR: this.artifactManager.getChartDir(),
|
|
1248
|
+
AGENTIDE_REPORT_DIR: this.artifactManager.getReportDir(),
|
|
1249
|
+
AGENTIDE_EXPORT_DIR: this.artifactManager.getExportDir(),
|
|
1250
|
+
AGENTIDE_IMAGE_DIR: this.artifactManager.getImageDir(),
|
|
1251
|
+
AGENTIDE_ASSET_DIR: this.artifactManager.getAssetDir(),
|
|
1252
|
+
AGENTIDE_VIEW_ROOT: this.artifactManager.getViewRoot(),
|
|
1253
|
+
AGENTIDE_VIEW_ID: this.getActiveViewId(),
|
|
1254
|
+
AGENTIDE_OUTPUT_DIR: outputDir,
|
|
1255
|
+
AGENTIDE_ANALYTICS_SCOPE: filters.scope ?? 'global',
|
|
1256
|
+
AGENTIDE_ANALYTICS_PROJECT_ID: projectIdForScope,
|
|
1257
|
+
AGENTIDE_ANALYTICS_AGENT_ID: filters.agentId ?? '',
|
|
1258
|
+
AGENTIDE_WORKSPACE_ROOT: workspaceRoot,
|
|
1259
|
+
AGENTIDE_DISABLED_CHARTS: disabledCharts.join(','),
|
|
1260
|
+
// Pass indexing status to Python script for fallback logic
|
|
1261
|
+
AGENTIDE_INDEX_DB_PATH: indexDbPath,
|
|
1262
|
+
AGENTIDE_USE_DUCKDB: isIndexed ? 'true' : 'false',
|
|
1263
|
+
AGENTIDE_INDEX_SCRIPT_PATH: indexScriptPath,
|
|
1264
|
+
AGENTIDE_DUCKDB_MEMORY_LIMIT_MB: String(duckdbMemoryLimitMb),
|
|
1265
|
+
AGENTIDE_DUCKDB_THREADS: String(duckdbThreads),
|
|
1266
|
+
AGENTIDE_DUCKDB_QUERY_TIMEOUT_SECONDS: String(duckdbQueryTimeoutSeconds),
|
|
1267
|
+
AGENTIDE_DUCKDB_MAX_RESULT_ROWS: String(duckdbMaxResultRows),
|
|
1268
|
+
AGENTIDE_DUCKDB_TEMP_DIRECTORY: duckdbTempDirectory,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
parseStringArray(raw) {
|
|
1272
|
+
if (!Array.isArray(raw)) {
|
|
1273
|
+
return [];
|
|
1274
|
+
}
|
|
1275
|
+
return raw
|
|
1276
|
+
.filter((entry) => typeof entry === 'string')
|
|
1277
|
+
.map(entry => entry.trim())
|
|
1278
|
+
.filter(entry => entry.length > 0);
|
|
1279
|
+
}
|
|
1280
|
+
normalizeBasename(value) {
|
|
1281
|
+
return path.basename(value.trim()).toLowerCase();
|
|
1282
|
+
}
|
|
1283
|
+
dedupeBasenames(values) {
|
|
1284
|
+
const seen = new Set();
|
|
1285
|
+
const deduped = [];
|
|
1286
|
+
for (const value of values) {
|
|
1287
|
+
const trimmed = value.trim();
|
|
1288
|
+
if (!trimmed) {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const basename = path.basename(trimmed);
|
|
1292
|
+
const key = basename.toLowerCase();
|
|
1293
|
+
if (!key || seen.has(key)) {
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
seen.add(key);
|
|
1297
|
+
deduped.push(basename);
|
|
1298
|
+
}
|
|
1299
|
+
return deduped;
|
|
1300
|
+
}
|
|
1301
|
+
dedupeCaseInsensitive(values) {
|
|
1302
|
+
const seen = new Set();
|
|
1303
|
+
const deduped = [];
|
|
1304
|
+
for (const value of values) {
|
|
1305
|
+
const trimmed = value.trim();
|
|
1306
|
+
if (!trimmed) {
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
const key = trimmed.toLowerCase();
|
|
1310
|
+
if (seen.has(key)) {
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
seen.add(key);
|
|
1314
|
+
deduped.push(trimmed);
|
|
1315
|
+
}
|
|
1316
|
+
return deduped;
|
|
1317
|
+
}
|
|
1318
|
+
parseDisabledScriptsByView(raw) {
|
|
1319
|
+
if (Array.isArray(raw)) {
|
|
1320
|
+
const allScripts = this.dedupeBasenames(this.parseStringArray(raw));
|
|
1321
|
+
return allScripts.length > 0 ? { all: allScripts } : {};
|
|
1322
|
+
}
|
|
1323
|
+
if (!raw || typeof raw !== 'object') {
|
|
1324
|
+
return {};
|
|
1325
|
+
}
|
|
1326
|
+
const scriptsByView = {};
|
|
1327
|
+
for (const [rawViewId, rawScripts] of Object.entries(raw)) {
|
|
1328
|
+
const normalizedViewId = this.normalizeViewId(rawViewId, 'all');
|
|
1329
|
+
const scripts = this.dedupeBasenames(this.parseStringArray(rawScripts));
|
|
1330
|
+
if (scripts.length > 0) {
|
|
1331
|
+
scriptsByView[normalizedViewId] = scripts;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return scriptsByView;
|
|
1335
|
+
}
|
|
1336
|
+
mergeDisabledScriptsByView(primary, fallback) {
|
|
1337
|
+
const merged = {};
|
|
1338
|
+
for (const [viewId, scripts] of Object.entries(fallback)) {
|
|
1339
|
+
merged[viewId] = this.dedupeBasenames(scripts);
|
|
1340
|
+
}
|
|
1341
|
+
for (const [viewId, scripts] of Object.entries(primary)) {
|
|
1342
|
+
merged[viewId] = this.dedupeBasenames([...(merged[viewId] ?? []), ...scripts]);
|
|
1343
|
+
}
|
|
1344
|
+
return merged;
|
|
1345
|
+
}
|
|
1346
|
+
getDisabledScriptsForView(config, viewId) {
|
|
1347
|
+
const normalizedViewId = this.normalizeViewId(viewId, 'global');
|
|
1348
|
+
return this.dedupeBasenames([
|
|
1349
|
+
...(config.scriptsByView.all ?? []),
|
|
1350
|
+
...(config.scriptsByView[normalizedViewId] ?? []),
|
|
1351
|
+
]);
|
|
1352
|
+
}
|
|
1353
|
+
async loadDisabledChartsConfig() {
|
|
1354
|
+
const configPath = this.getDisabledChartsConfigPath();
|
|
1355
|
+
try {
|
|
1356
|
+
const raw = await fs_1.promises.readFile(configPath, 'utf-8');
|
|
1357
|
+
const yaml = require('js-yaml');
|
|
1358
|
+
const parsed = yaml.load(raw);
|
|
1359
|
+
const disabled = parsed?.disabled && typeof parsed.disabled === 'object'
|
|
1360
|
+
? parsed.disabled
|
|
1361
|
+
: {};
|
|
1362
|
+
const scriptsByView = this.mergeDisabledScriptsByView(this.parseDisabledScriptsByView(parsed?.scripts), this.parseDisabledScriptsByView(disabled.scripts));
|
|
1363
|
+
const hasGalleryIgnoreSetting = Boolean((parsed && (Object.prototype.hasOwnProperty.call(parsed, 'galleryIgnore') || Object.prototype.hasOwnProperty.call(parsed, 'gallery_ignore')))
|
|
1364
|
+
|| (Object.prototype.hasOwnProperty.call(disabled, 'galleryIgnore') || Object.prototype.hasOwnProperty.call(disabled, 'gallery_ignore')));
|
|
1365
|
+
const galleryIgnore = this.dedupeCaseInsensitive([
|
|
1366
|
+
...this.parseStringArray(parsed?.galleryIgnore),
|
|
1367
|
+
...this.parseStringArray(parsed?.gallery_ignore),
|
|
1368
|
+
...this.parseStringArray(disabled.galleryIgnore),
|
|
1369
|
+
...this.parseStringArray(disabled.gallery_ignore),
|
|
1370
|
+
]);
|
|
1371
|
+
return {
|
|
1372
|
+
scriptsByView,
|
|
1373
|
+
charts: this.dedupeBasenames([
|
|
1374
|
+
...this.parseStringArray(parsed?.charts),
|
|
1375
|
+
...this.parseStringArray(disabled.charts),
|
|
1376
|
+
]),
|
|
1377
|
+
galleryIgnore: hasGalleryIgnoreSetting
|
|
1378
|
+
? galleryIgnore
|
|
1379
|
+
: [...AnalyticsDashboardProvider.DEFAULT_GALLERY_IGNORE],
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
catch {
|
|
1383
|
+
return {
|
|
1384
|
+
scriptsByView: {},
|
|
1385
|
+
charts: [],
|
|
1386
|
+
galleryIgnore: [...AnalyticsDashboardProvider.DEFAULT_GALLERY_IGNORE],
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
async postDisabledChartsState(disabledConfig) {
|
|
1391
|
+
const resolvedConfig = disabledConfig ?? await this.loadDisabledChartsConfig();
|
|
1392
|
+
this.webviewView?.webview.postMessage({
|
|
1393
|
+
type: 'disabledChartsUpdated',
|
|
1394
|
+
charts: resolvedConfig.charts,
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
async saveDisabledChartsConfig(config) {
|
|
1398
|
+
const configPath = this.getDisabledChartsConfigPath();
|
|
1399
|
+
await fs_1.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
1400
|
+
const scriptsByView = {};
|
|
1401
|
+
for (const [rawViewId, rawScripts] of Object.entries(config.scriptsByView)) {
|
|
1402
|
+
const viewId = this.normalizeViewId(rawViewId, 'all');
|
|
1403
|
+
const scripts = this.dedupeBasenames(rawScripts);
|
|
1404
|
+
if (scripts.length > 0) {
|
|
1405
|
+
scriptsByView[viewId] = scripts;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
const yaml = require('js-yaml');
|
|
1409
|
+
const content = yaml.dump({
|
|
1410
|
+
scripts: scriptsByView,
|
|
1411
|
+
charts: this.dedupeBasenames(config.charts),
|
|
1412
|
+
galleryIgnore: this.dedupeCaseInsensitive(config.galleryIgnore),
|
|
1413
|
+
}, { lineWidth: 120, noRefs: true });
|
|
1414
|
+
await fs_1.promises.writeFile(configPath, content, 'utf-8');
|
|
1415
|
+
}
|
|
1416
|
+
isPathWithinDir(pathValue, dirPath) {
|
|
1417
|
+
return (0, pathSafety_1.isPathUnderRoot)(dirPath, pathValue);
|
|
1418
|
+
}
|
|
1419
|
+
async disableChartByFilename(filename) {
|
|
1420
|
+
const normalizedFilename = path.basename(filename.trim());
|
|
1421
|
+
if (!normalizedFilename) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const disabledConfig = await this.loadDisabledChartsConfig();
|
|
1425
|
+
const existing = new Set(disabledConfig.charts.map(name => this.normalizeBasename(name)));
|
|
1426
|
+
const key = this.normalizeBasename(normalizedFilename);
|
|
1427
|
+
if (!key || existing.has(key)) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
await this.saveDisabledChartsConfig({
|
|
1431
|
+
scriptsByView: disabledConfig.scriptsByView,
|
|
1432
|
+
charts: [...disabledConfig.charts, normalizedFilename],
|
|
1433
|
+
galleryIgnore: disabledConfig.galleryIgnore,
|
|
1434
|
+
});
|
|
1435
|
+
await this.postDisabledChartsState();
|
|
1436
|
+
}
|
|
1437
|
+
normalizePanelStateId(panelId) {
|
|
1438
|
+
const normalized = (panelId ?? 'default')
|
|
1439
|
+
.trim()
|
|
1440
|
+
.toLowerCase()
|
|
1441
|
+
.replace(/[^a-z0-9._-]+/g, '_')
|
|
1442
|
+
.replace(/^_+|_+$/g, '');
|
|
1443
|
+
return normalized || 'default';
|
|
1444
|
+
}
|
|
1445
|
+
getPanelStatePath(panelId) {
|
|
1446
|
+
const scopedPanelId = this.getScopedPanelId(panelId);
|
|
1447
|
+
return path.join(this.artifactManager.getAnalyticsRoot(), 'panel-state', `${scopedPanelId}.json`);
|
|
1448
|
+
}
|
|
1449
|
+
async loadPanelState(panelId) {
|
|
1450
|
+
const statePath = this.getPanelStatePath(panelId);
|
|
1451
|
+
try {
|
|
1452
|
+
const raw = await fs_1.promises.readFile(statePath, 'utf-8');
|
|
1453
|
+
return JSON.parse(raw);
|
|
1454
|
+
}
|
|
1455
|
+
catch {
|
|
1456
|
+
return {};
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
async savePanelState(panelId, state) {
|
|
1460
|
+
const statePath = this.getPanelStatePath(panelId);
|
|
1461
|
+
await fs_1.promises.mkdir(path.dirname(statePath), { recursive: true });
|
|
1462
|
+
const payload = state && typeof state === 'object' ? state : {};
|
|
1463
|
+
await fs_1.promises.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
1464
|
+
}
|
|
1465
|
+
async postPanelState(panelId) {
|
|
1466
|
+
const normalizedPanelId = this.normalizePanelStateId(panelId);
|
|
1467
|
+
const state = await this.loadPanelState(panelId);
|
|
1468
|
+
const fleetInterval = Math.max(0, vscode.workspace.getConfiguration('agentide.analytics').get('fleetAutoRefreshIntervalSeconds', 0));
|
|
1469
|
+
this.webviewView?.webview.postMessage({
|
|
1470
|
+
type: 'panelStateLoaded',
|
|
1471
|
+
panelId: normalizedPanelId,
|
|
1472
|
+
state,
|
|
1473
|
+
fleetAutoRefreshIntervalSeconds: fleetInterval,
|
|
1474
|
+
surfaceMode: this.getSurfaceMode(),
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
async updateDataFreshnessHint(dataHint) {
|
|
1478
|
+
const filters = this.getActiveFilters();
|
|
1479
|
+
const hasData = dataHint?.hasData ?? await this.queryRunner.hasData(filters);
|
|
1480
|
+
if (!hasData) {
|
|
1481
|
+
this.webviewView?.webview.postMessage({ type: 'dataFresh' });
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const manifest = await this.artifactManager.getManifest();
|
|
1485
|
+
const currentHash = dataHint?.dataHash ?? await this.queryRunner.getDataHash(filters);
|
|
1486
|
+
if (!manifest) {
|
|
1487
|
+
// No manifest yet; don't show stale hint until first generation.
|
|
1488
|
+
this.webviewView?.webview.postMessage({ type: 'dataFresh' });
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
if (manifest.dataHash !== currentHash) {
|
|
1492
|
+
this.webviewView?.webview.postMessage({
|
|
1493
|
+
type: 'dataStale',
|
|
1494
|
+
generatedAt: manifest.generatedAt,
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
this.webviewView?.webview.postMessage({ type: 'dataFresh' });
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/** Panel visible: load artifacts, then apply refresh policy (see PLAN/spotlight-refresh-policy.md). */
|
|
1502
|
+
async onPanelBecameVisible() {
|
|
1503
|
+
await this.loadAndPostViewsState();
|
|
1504
|
+
this.syncArtifactContext();
|
|
1505
|
+
await this.postPanelState('default');
|
|
1506
|
+
await this.seedWorkspaceScripts();
|
|
1507
|
+
const bootstrapPromise = this.fetchDashboardBootstrapData();
|
|
1508
|
+
// Load existing artifacts immediately; don't block chart refresh on summary queries.
|
|
1509
|
+
await this.loadAndSendArtifacts();
|
|
1510
|
+
await this.postPanelState('default');
|
|
1511
|
+
await this.ensureChartsForCurrentFilterIfNeeded({ trigger: 'panel-open' });
|
|
1512
|
+
await this.autoRefreshDefaultCharts();
|
|
1513
|
+
const bootstrap = await bootstrapPromise;
|
|
1514
|
+
await this.postFilterState(bootstrap?.agentIds);
|
|
1515
|
+
this.triggerSummaryCardsUpdate(bootstrap?.summary);
|
|
1516
|
+
await this.updateDataFreshnessHint(bootstrap ? { hasData: bootstrap.hasData, dataHash: bootstrap.dataHash } : undefined);
|
|
1517
|
+
}
|
|
1518
|
+
getFilterCacheKey(filters = this.getActiveFilters()) {
|
|
1519
|
+
const scope = filters.scope === 'workspace' ? 'workspace' : 'global';
|
|
1520
|
+
const projectId = filters.scope === 'workspace' ? (filters.projectId ?? '') : '';
|
|
1521
|
+
const agentId = filters.agentId ?? '';
|
|
1522
|
+
return `${scope}::${projectId}::${agentId}`;
|
|
1523
|
+
}
|
|
1524
|
+
pruneAutoGenerationHistory() {
|
|
1525
|
+
if (this.autoGenerationAttemptByFilter.size <= MAX_FILTER_AUTOGEN_HISTORY) {
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const entries = [...this.autoGenerationAttemptByFilter.entries()]
|
|
1529
|
+
.sort((a, b) => a[1] - b[1]);
|
|
1530
|
+
const removeCount = this.autoGenerationAttemptByFilter.size - MAX_FILTER_AUTOGEN_HISTORY;
|
|
1531
|
+
for (let i = 0; i < removeCount; i++) {
|
|
1532
|
+
this.autoGenerationAttemptByFilter.delete(entries[i][0]);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
scheduleAutoGenerateForCurrentFilter() {
|
|
1536
|
+
if (this.pendingFilterAutoGenerateTimer) {
|
|
1537
|
+
clearTimeout(this.pendingFilterAutoGenerateTimer);
|
|
1538
|
+
}
|
|
1539
|
+
const scheduledFilterKey = this.getFilterCacheKey();
|
|
1540
|
+
this.pendingFilterAutoGenerateTimer = setTimeout(() => {
|
|
1541
|
+
void this.ensureChartsForCurrentFilterIfNeeded({
|
|
1542
|
+
trigger: 'filter-switch',
|
|
1543
|
+
expectedFilterKey: scheduledFilterKey,
|
|
1544
|
+
respectCooldown: true,
|
|
1545
|
+
});
|
|
1546
|
+
}, FILTER_AUTOGEN_DEBOUNCE_MS);
|
|
1547
|
+
}
|
|
1548
|
+
async ensureChartsForCurrentFilterIfNeeded(options) {
|
|
1549
|
+
this.syncArtifactContext();
|
|
1550
|
+
const filters = this.getActiveFilters();
|
|
1551
|
+
const filterKey = this.getFilterCacheKey(filters);
|
|
1552
|
+
if (options?.expectedFilterKey && options.expectedFilterKey !== filterKey) {
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if (this.isRefreshInProgress) {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
if (options?.respectCooldown) {
|
|
1559
|
+
const lastAttempt = this.autoGenerationAttemptByFilter.get(filterKey) ?? 0;
|
|
1560
|
+
if (Date.now() - lastAttempt < FILTER_AUTOGEN_COOLDOWN_MS) {
|
|
1561
|
+
this.outputChannel.appendLine('[Dashboard] Auto-generation skipped (cooldown active)');
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const artifacts = await this.artifactManager.listArtifacts();
|
|
1566
|
+
const hasCharts = artifacts.some(artifact => artifact.type === 'chart');
|
|
1567
|
+
if (hasCharts) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const hasData = options?.hasDataHint ?? await this.queryRunner.hasData(filters);
|
|
1571
|
+
if (!hasData) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
this.autoGenerationAttemptByFilter.set(filterKey, Date.now());
|
|
1575
|
+
this.pruneAutoGenerationHistory();
|
|
1576
|
+
this.outputChannel.appendLine('[Dashboard] No cached charts for selected filters; auto-generating');
|
|
1577
|
+
await this.refreshCharts();
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Panel-open chart bootstrap: compute when empty; hint when stale (no silent regen).
|
|
1581
|
+
* Aligns with PLAN/spotlight-refresh-policy.md — no manifest → refreshCharts;
|
|
1582
|
+
* stale hash → dataStale; fresh → dataFresh.
|
|
1583
|
+
*/
|
|
1584
|
+
async autoRefreshDefaultCharts(options) {
|
|
1585
|
+
const config = vscode.workspace.getConfiguration('agentide.analytics');
|
|
1586
|
+
const autoGenerate = config.get('autoGenerateDefaults', true);
|
|
1587
|
+
if (!autoGenerate) {
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
const filters = this.getActiveFilters();
|
|
1591
|
+
const hasData = options?.hasDataHint ?? await this.queryRunner.hasData(filters);
|
|
1592
|
+
if (!hasData) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const manifest = await this.artifactManager.getManifest();
|
|
1596
|
+
const currentHash = options?.dataHashHint ?? await this.queryRunner.getDataHash(filters);
|
|
1597
|
+
if (!manifest) {
|
|
1598
|
+
// No manifest = first time; run the full script set (seeds + runs all
|
|
1599
|
+
// widget scripts, not just generate-default-charts.py).
|
|
1600
|
+
await this.refreshCharts();
|
|
1601
|
+
}
|
|
1602
|
+
else if (manifest.dataHash !== currentHash) {
|
|
1603
|
+
// Data changed since last generation - notify user, don't auto-regenerate
|
|
1604
|
+
this.webviewView?.webview.postMessage({
|
|
1605
|
+
type: 'dataStale',
|
|
1606
|
+
generatedAt: manifest.generatedAt,
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
else {
|
|
1610
|
+
this.webviewView?.webview.postMessage({ type: 'dataFresh' });
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
// -------------------------------------------------------------------------
|
|
1614
|
+
// Default Chart Generation (script-based)
|
|
1615
|
+
// -------------------------------------------------------------------------
|
|
1616
|
+
/**
|
|
1617
|
+
* Run the bundled Python script to generate default dashboard charts.
|
|
1618
|
+
* This is fast and reliable - no agent/LLM needed.
|
|
1619
|
+
*/
|
|
1620
|
+
async runDefaultChartScript(options) {
|
|
1621
|
+
this.syncArtifactContext();
|
|
1622
|
+
const preserveExistingContent = options?.preserveExistingContent ?? false;
|
|
1623
|
+
let refreshOutcome = 'finished';
|
|
1624
|
+
let refreshError = '';
|
|
1625
|
+
// Cancel any in-flight chart generation before starting a new one.
|
|
1626
|
+
// This sends SIGTERM to the Python/DuckDB subprocess via the AbortSignal,
|
|
1627
|
+
// preventing stale processes from piling up when users click refresh rapidly.
|
|
1628
|
+
this.generationAbortController?.abort();
|
|
1629
|
+
this.generationAbortController = new AbortController();
|
|
1630
|
+
// Notify if indexing is in progress
|
|
1631
|
+
const indexingService = this.indexingService();
|
|
1632
|
+
if (indexingService && indexingService.isIndexing()) {
|
|
1633
|
+
this.outputChannel.appendLine('[Dashboard] Background DuckDB indexing in progress; using JSONL fallback for charts');
|
|
1634
|
+
if (!preserveExistingContent) {
|
|
1635
|
+
this.webviewView?.webview.postMessage({
|
|
1636
|
+
type: 'notifyIndexing',
|
|
1637
|
+
message: 'DuckDB index is being built in background. Charts will use JSONL for now (slower).',
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (preserveExistingContent) {
|
|
1642
|
+
this.startSmoothRefresh({ batchArtifactUpdates: true });
|
|
1643
|
+
}
|
|
1644
|
+
else {
|
|
1645
|
+
this.webviewView?.webview.postMessage({ type: 'generatingStarted' });
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
const tracesDir = path.join((0, index_1.getAgentideHome)(), 'analytics', 'traces');
|
|
1649
|
+
const outputDir = (await this.artifactManager.ensureArtifactDir()).fsPath;
|
|
1650
|
+
const disabledConfig = await this.loadDisabledChartsConfig();
|
|
1651
|
+
const scriptEnv = this.buildScriptEnvironment(outputDir, disabledConfig.charts);
|
|
1652
|
+
// Find the chart generation script
|
|
1653
|
+
const scriptPath = await this.findChartScript();
|
|
1654
|
+
if (!scriptPath) {
|
|
1655
|
+
throw new Error('Chart generation script not found');
|
|
1656
|
+
}
|
|
1657
|
+
this.outputChannel.appendLine(`[Dashboard] Running default chart script: ${scriptPath}`);
|
|
1658
|
+
this.outputChannel.appendLine(`[Dashboard] Traces: ${tracesDir}, Output: ${outputDir}`);
|
|
1659
|
+
// Run with dependency management via shared Python runner.
|
|
1660
|
+
const deps = await this.readPythonDeps();
|
|
1661
|
+
let result;
|
|
1662
|
+
try {
|
|
1663
|
+
const runResult = await (0, pythonRunner_1.runPythonWithDeps)({
|
|
1664
|
+
scriptPath,
|
|
1665
|
+
args: [tracesDir, outputDir],
|
|
1666
|
+
deps,
|
|
1667
|
+
analyticsRoot: this.artifactManager.getAnalyticsRoot(),
|
|
1668
|
+
requirementsPath: path.join(this.artifactManager.getScriptDir(), 'requirements.txt'),
|
|
1669
|
+
execOptions: {
|
|
1670
|
+
timeout: 60_000,
|
|
1671
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
1672
|
+
env: scriptEnv,
|
|
1673
|
+
signal: this.generationAbortController?.signal,
|
|
1674
|
+
},
|
|
1675
|
+
log: (message) => this.outputChannel.appendLine(message),
|
|
1676
|
+
});
|
|
1677
|
+
result = { stdout: runResult.stdout, stderr: runResult.stderr };
|
|
1678
|
+
}
|
|
1679
|
+
catch (err) {
|
|
1680
|
+
// generate-default-charts.py uses exit code 2 for "no data" but still
|
|
1681
|
+
// prints JSON to stdout. Recover that stdout so we can handle no_data cleanly.
|
|
1682
|
+
const stdout = err.stdout;
|
|
1683
|
+
const stderr = err.stderr;
|
|
1684
|
+
if (typeof stdout === 'string' && stdout.trim().startsWith('{')) {
|
|
1685
|
+
result = {
|
|
1686
|
+
stdout,
|
|
1687
|
+
stderr: typeof stderr === 'string' ? stderr : '',
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
throw err;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (result.stderr) {
|
|
1695
|
+
this.outputChannel.appendLine(`[Dashboard] Chart script stderr: ${result.stderr}`);
|
|
1696
|
+
}
|
|
1697
|
+
// Parse result JSON
|
|
1698
|
+
let scriptResult;
|
|
1699
|
+
try {
|
|
1700
|
+
scriptResult = JSON.parse(result.stdout);
|
|
1701
|
+
}
|
|
1702
|
+
catch {
|
|
1703
|
+
throw new Error(`Script output not valid JSON: ${result.stdout.slice(0, 200)}`);
|
|
1704
|
+
}
|
|
1705
|
+
if (scriptResult.status === 'no_data') {
|
|
1706
|
+
this.outputChannel.appendLine('[Dashboard] No trace data available');
|
|
1707
|
+
if (!preserveExistingContent) {
|
|
1708
|
+
this.webviewView?.webview.postMessage({ type: 'generatingCancelled' });
|
|
1709
|
+
}
|
|
1710
|
+
else {
|
|
1711
|
+
refreshOutcome = 'noData';
|
|
1712
|
+
}
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (scriptResult.status !== 'ok') {
|
|
1716
|
+
throw new Error(scriptResult.error || 'Chart generation failed');
|
|
1717
|
+
}
|
|
1718
|
+
// Build and save manifest
|
|
1719
|
+
const dataHash = await this.queryRunner.getDataHash(this.getActiveFilters());
|
|
1720
|
+
const manifestEntries = (scriptResult.charts ?? []).map(c => ({
|
|
1721
|
+
filename: path.basename(c.file),
|
|
1722
|
+
path: path.relative(outputDir, c.file),
|
|
1723
|
+
type: 'chart',
|
|
1724
|
+
source: 'default',
|
|
1725
|
+
title: c.title,
|
|
1726
|
+
createdAt: new Date().toISOString(),
|
|
1727
|
+
generator: 'generate-default-charts.py',
|
|
1728
|
+
}));
|
|
1729
|
+
const manifest = {
|
|
1730
|
+
version: 1,
|
|
1731
|
+
generatedAt: scriptResult.generated_at || new Date().toISOString(),
|
|
1732
|
+
dataHash,
|
|
1733
|
+
prompt: 'auto-generated',
|
|
1734
|
+
artifacts: manifestEntries,
|
|
1735
|
+
};
|
|
1736
|
+
await this.artifactManager.saveManifest(manifest);
|
|
1737
|
+
await this.writeFleetExportForCurrentView();
|
|
1738
|
+
this.outputChannel.appendLine(`[Dashboard] Generated ${manifestEntries.length} default charts`);
|
|
1739
|
+
// Reload artifacts in panel
|
|
1740
|
+
await this.loadAndSendArtifacts();
|
|
1741
|
+
}
|
|
1742
|
+
catch (err) {
|
|
1743
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
1744
|
+
if (!preserveExistingContent) {
|
|
1745
|
+
this.webviewView?.webview.postMessage({ type: 'generatingCancelled' });
|
|
1746
|
+
}
|
|
1747
|
+
else {
|
|
1748
|
+
refreshOutcome = 'failed';
|
|
1749
|
+
refreshError = 'Refresh cancelled';
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
else {
|
|
1753
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1754
|
+
this.outputChannel.appendLine(`[Dashboard] Chart generation failed: ${errorMessage}`);
|
|
1755
|
+
if (preserveExistingContent) {
|
|
1756
|
+
refreshOutcome = 'failed';
|
|
1757
|
+
refreshError = errorMessage;
|
|
1758
|
+
}
|
|
1759
|
+
else {
|
|
1760
|
+
this.webviewView?.webview.postMessage({
|
|
1761
|
+
type: 'generatingFailed',
|
|
1762
|
+
error: errorMessage,
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
finally {
|
|
1768
|
+
if (preserveExistingContent) {
|
|
1769
|
+
try {
|
|
1770
|
+
await this.finishSmoothRefresh({ notifyFinished: false });
|
|
1771
|
+
}
|
|
1772
|
+
catch (finishErr) {
|
|
1773
|
+
this.outputChannel.appendLine(`[Dashboard] Failed finishing default-refresh state: ${finishErr}`);
|
|
1774
|
+
this.webviewView?.webview.postMessage({ type: 'refreshFinished' });
|
|
1775
|
+
}
|
|
1776
|
+
if (refreshOutcome === 'finished') {
|
|
1777
|
+
this.webviewView?.webview.postMessage({ type: 'refreshFinished' });
|
|
1778
|
+
}
|
|
1779
|
+
else if (refreshOutcome === 'noData') {
|
|
1780
|
+
this.webviewView?.webview.postMessage({ type: 'refreshNoData' });
|
|
1781
|
+
}
|
|
1782
|
+
else {
|
|
1783
|
+
this.webviewView?.webview.postMessage({
|
|
1784
|
+
type: 'refreshFailed',
|
|
1785
|
+
error: refreshError || 'Refresh failed',
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
this.generationAbortController = undefined;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Find the chart generation script in bundled or dev locations.
|
|
1794
|
+
*/
|
|
1795
|
+
async findChartScript() {
|
|
1796
|
+
const candidates = this.getBundledAnalyticsScriptCandidates('generate-default-charts.py');
|
|
1797
|
+
for (const candidate of candidates) {
|
|
1798
|
+
try {
|
|
1799
|
+
await fs_1.promises.access(candidate);
|
|
1800
|
+
return candidate;
|
|
1801
|
+
}
|
|
1802
|
+
catch {
|
|
1803
|
+
// Try next
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
async findBundledAnalyticsScript(filename) {
|
|
1809
|
+
const candidates = this.getBundledAnalyticsScriptCandidates(filename);
|
|
1810
|
+
for (const candidate of candidates) {
|
|
1811
|
+
try {
|
|
1812
|
+
await fs_1.promises.access(candidate);
|
|
1813
|
+
return candidate;
|
|
1814
|
+
}
|
|
1815
|
+
catch {
|
|
1816
|
+
// Try next.
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
getBundledAnalyticsScriptCandidates(filename) {
|
|
1822
|
+
const extensionPath = this.extensionContext.extensionPath;
|
|
1823
|
+
const runtimeRelativeCandidates = [
|
|
1824
|
+
path.resolve(__dirname, '../../scripts', 'analytics', filename),
|
|
1825
|
+
path.resolve(__dirname, '../../../scripts', 'analytics', filename),
|
|
1826
|
+
path.resolve(__dirname, '../../../../extensions', 'agentide', 'scripts', 'analytics', filename),
|
|
1827
|
+
];
|
|
1828
|
+
const extensionRelativeCandidates = [
|
|
1829
|
+
path.join(extensionPath, 'out', 'scripts', 'analytics', filename),
|
|
1830
|
+
path.join(extensionPath, 'scripts', 'analytics', filename),
|
|
1831
|
+
path.join(extensionPath, '..', 'scripts', 'analytics', filename),
|
|
1832
|
+
path.join(extensionPath, '..', '..', 'scripts', 'analytics', filename),
|
|
1833
|
+
path.join(extensionPath, '..', '..', '..', 'extensions', 'agentide', 'scripts', 'analytics', filename),
|
|
1834
|
+
];
|
|
1835
|
+
return [...new Set([...runtimeRelativeCandidates, ...extensionRelativeCandidates])];
|
|
1836
|
+
}
|
|
1837
|
+
async loadBundledDefaultSeedScriptNames() {
|
|
1838
|
+
const fallbackScriptNames = [...FALLBACK_DEFAULT_SEED_SCRIPT_NAMES];
|
|
1839
|
+
const registryPath = await this.findBundledAnalyticsScript(DEFAULT_SEED_REGISTRY_FILENAME);
|
|
1840
|
+
if (!registryPath) {
|
|
1841
|
+
this.outputChannel.appendLine(`[Dashboard] Missing default seed registry: ${DEFAULT_SEED_REGISTRY_FILENAME}. Using built-in defaults.`);
|
|
1842
|
+
return fallbackScriptNames;
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
const raw = await fs_1.promises.readFile(registryPath, 'utf-8');
|
|
1846
|
+
const parsed = JSON.parse(raw);
|
|
1847
|
+
const registryScriptNames = this.parseStringArray(parsed.seedScripts)
|
|
1848
|
+
.map(entry => path.basename(entry))
|
|
1849
|
+
.filter(Boolean);
|
|
1850
|
+
if (registryScriptNames.length === 0) {
|
|
1851
|
+
this.outputChannel.appendLine(`[Dashboard] Seed registry ${registryPath} had no scripts. Using built-in defaults.`);
|
|
1852
|
+
}
|
|
1853
|
+
return this.dedupeBasenames([...registryScriptNames, ...fallbackScriptNames]);
|
|
1854
|
+
}
|
|
1855
|
+
catch (err) {
|
|
1856
|
+
this.outputChannel.appendLine(`[Dashboard] Failed reading seed registry ${registryPath}: ${err}`);
|
|
1857
|
+
return fallbackScriptNames;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
getDisabledChartsConfigPath() {
|
|
1861
|
+
return path.join(this.artifactManager.getAnalyticsRoot(), 'disabled-charts.yaml');
|
|
1862
|
+
}
|
|
1863
|
+
hasDuplicateShebangMarkers(content) {
|
|
1864
|
+
const shebangMatches = content.match(/^#!/gm);
|
|
1865
|
+
return (shebangMatches?.length ?? 0) > 1;
|
|
1866
|
+
}
|
|
1867
|
+
hasStaleDuckDbLookup(filename, content) {
|
|
1868
|
+
const targetScripts = new Set(['session-timeline-widget.js', 'session-comparison-widget.js', 'wow-model-widget.js']);
|
|
1869
|
+
if (!targetScripts.has(filename)) {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
const usesSharedLocator = content.includes(`require('./duckdbLocator')`);
|
|
1873
|
+
const hasLegacyInlineFinder = content.includes('function findDuckdb()')
|
|
1874
|
+
&& content.includes("path.join(home, '.agent-hippo', 'bin', 'duckdb')");
|
|
1875
|
+
const hasWindowsCandidate = content.includes('duckdb.exe');
|
|
1876
|
+
const hasLegacyHomeLookup = content.includes("const home = process.env.HOME || ''");
|
|
1877
|
+
// Repair stale seeded scripts that still use legacy inline lookup and miss
|
|
1878
|
+
// current Windows-safe logic and shared helper import.
|
|
1879
|
+
return !usesSharedLocator && (hasLegacyInlineFinder || hasLegacyHomeLookup || !hasWindowsCandidate);
|
|
1880
|
+
}
|
|
1881
|
+
async repairCorruptedBundledDefaultScripts(defaultScriptNames) {
|
|
1882
|
+
const scriptsDir = this.artifactManager.getScriptDir();
|
|
1883
|
+
let repairedCount = 0;
|
|
1884
|
+
for (const rawFilename of defaultScriptNames) {
|
|
1885
|
+
const filename = path.basename(rawFilename);
|
|
1886
|
+
if (!filename) {
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
const destinationPath = path.join(scriptsDir, filename);
|
|
1890
|
+
let content;
|
|
1891
|
+
try {
|
|
1892
|
+
content = await fs_1.promises.readFile(destinationPath, 'utf-8');
|
|
1893
|
+
}
|
|
1894
|
+
catch {
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
const needsShebangRepair = this.hasDuplicateShebangMarkers(content);
|
|
1898
|
+
const needsDuckdbRepair = this.hasStaleDuckDbLookup(filename, content);
|
|
1899
|
+
if (!needsShebangRepair && !needsDuckdbRepair) {
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
const sourcePath = await this.findBundledAnalyticsScript(filename);
|
|
1903
|
+
if (!sourcePath) {
|
|
1904
|
+
this.outputChannel.appendLine(`[Dashboard] Could not repair script ${filename}: source not found`);
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
try {
|
|
1911
|
+
await fs_1.promises.copyFile(sourcePath, destinationPath);
|
|
1912
|
+
repairedCount += 1;
|
|
1913
|
+
const reason = needsShebangRepair ? 'duplicate shebang markers' : 'stale DuckDB lookup';
|
|
1914
|
+
this.outputChannel.appendLine(`[Dashboard] Repaired default script ${filename} (${reason})`);
|
|
1915
|
+
}
|
|
1916
|
+
catch (err) {
|
|
1917
|
+
this.outputChannel.appendLine(`[Dashboard] Failed repairing default script ${filename}: ${err}`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return repairedCount;
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Bundled DuckDB/chart scripts belong on Workspace-style views, not custom views
|
|
1924
|
+
* (e.g. live-fleet-2) that declare their own script list.
|
|
1925
|
+
*/
|
|
1926
|
+
viewNeedsBundledDefaultScripts(view, defaultScriptNames) {
|
|
1927
|
+
if (view.scripts.length === 0) {
|
|
1928
|
+
// System views with no scripts (Workspace) run every script on disk except Fleet widget.
|
|
1929
|
+
return view.system === true && view.id !== 'fleet';
|
|
1930
|
+
}
|
|
1931
|
+
const defaultBasenames = new Set(defaultScriptNames.map(name => path.basename(name).toLowerCase()));
|
|
1932
|
+
return this.dedupeBasenames(view.scripts).some(script => defaultBasenames.has(script.toLowerCase()));
|
|
1933
|
+
}
|
|
1934
|
+
async ensureBundledDefaultScriptsPresent() {
|
|
1935
|
+
const defaultScriptNames = await this.loadBundledDefaultSeedScriptNames();
|
|
1936
|
+
if (defaultScriptNames.length === 0) {
|
|
1937
|
+
this.outputChannel.appendLine('[Dashboard] No default scripts listed in seed registry');
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const activeView = this.getActiveView();
|
|
1941
|
+
if (!this.viewNeedsBundledDefaultScripts(activeView, defaultScriptNames)) {
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const existingScripts = await this.artifactManager.listScripts();
|
|
1945
|
+
const existingByBasename = new Set(existingScripts.map(script => path.basename(script.filename).toLowerCase()));
|
|
1946
|
+
const seedCandidates = [];
|
|
1947
|
+
for (const filename of defaultScriptNames) {
|
|
1948
|
+
if (existingByBasename.has(path.basename(filename).toLowerCase())) {
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
const sourcePath = await this.findBundledAnalyticsScript(filename);
|
|
1952
|
+
if (sourcePath) {
|
|
1953
|
+
seedCandidates.push({ sourcePath, filename });
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
if (seedCandidates.length > 0) {
|
|
1957
|
+
await this.artifactManager.ensureSeedScripts(seedCandidates);
|
|
1958
|
+
this.outputChannel.appendLine(`[Dashboard] Seeded ${seedCandidates.length} missing default script(s)`);
|
|
1959
|
+
}
|
|
1960
|
+
else {
|
|
1961
|
+
this.outputChannel.appendLine(`[Dashboard] No missing default scripts were resolvable from extension path: ${this.extensionContext.extensionPath}`);
|
|
1962
|
+
}
|
|
1963
|
+
const repairedCount = await this.repairCorruptedBundledDefaultScripts(defaultScriptNames);
|
|
1964
|
+
if (repairedCount > 0) {
|
|
1965
|
+
this.outputChannel.appendLine(`[Dashboard] Repaired ${repairedCount} default script(s)`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
async seedWorkspaceScripts() {
|
|
1969
|
+
// Seed requirements.txt for Python script dependencies
|
|
1970
|
+
await this.seedRequirements();
|
|
1971
|
+
// Seed disabled-charts config for script/chart opt-out.
|
|
1972
|
+
await this.seedDisabledChartsConfig();
|
|
1973
|
+
// Ensure all bundled default scripts exist; never overwrite user edits.
|
|
1974
|
+
await this.ensureBundledDefaultScriptsPresent();
|
|
1975
|
+
// Ensure widget log folder exists even before first refresh so users can
|
|
1976
|
+
// verify where per-run JSONL logs will appear.
|
|
1977
|
+
try {
|
|
1978
|
+
await fs_1.promises.mkdir(this.getWidgetRunLogsDir(), { recursive: true });
|
|
1979
|
+
}
|
|
1980
|
+
catch (err) {
|
|
1981
|
+
this.outputChannel.appendLine(`[Dashboard] Failed creating widget log directory: ${err}`);
|
|
1982
|
+
}
|
|
1983
|
+
// Seed panel files (panel.js, panel.css, panel.html) into .agent-hippo/analytics/panel/
|
|
1984
|
+
await this.seedPanelFiles();
|
|
1985
|
+
}
|
|
1986
|
+
async seedDisabledChartsConfig() {
|
|
1987
|
+
const configPath = this.getDisabledChartsConfigPath();
|
|
1988
|
+
try {
|
|
1989
|
+
await fs_1.promises.access(configPath);
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
catch {
|
|
1993
|
+
// Missing file; seed default template.
|
|
1994
|
+
}
|
|
1995
|
+
const template = [
|
|
1996
|
+
'# Disable script execution or chart outputs without deleting files.',
|
|
1997
|
+
'# scripts: per-view script filenames in analytics/views/<viewId>/scripts to skip running.',
|
|
1998
|
+
'# Use "all" to apply to every view id.',
|
|
1999
|
+
'scripts:',
|
|
2000
|
+
' all: []',
|
|
2001
|
+
' # all:',
|
|
2002
|
+
' # - wow-model-widget.js',
|
|
2003
|
+
' # fleet:',
|
|
2004
|
+
' # - agent-fleet-widget.js',
|
|
2005
|
+
'# charts: output filenames in analytics/views/.../charts to disable/hide.',
|
|
2006
|
+
'charts:',
|
|
2007
|
+
' # Hotspot analysis is opt-in by default; remove this entry to enable it.',
|
|
2008
|
+
' - hotspot-analysis-widget.html',
|
|
2009
|
+
'# galleryIgnore: suffixes or exact filenames hidden from gallery cards.',
|
|
2010
|
+
'galleryIgnore:',
|
|
2011
|
+
` - ${AnalyticsDashboardProvider.DEFAULT_GALLERY_IGNORE[0]}`,
|
|
2012
|
+
` - ${AnalyticsDashboardProvider.DEFAULT_GALLERY_IGNORE[1]}`,
|
|
2013
|
+
'',
|
|
2014
|
+
].join('\n');
|
|
2015
|
+
try {
|
|
2016
|
+
await fs_1.promises.writeFile(configPath, template, 'utf-8');
|
|
2017
|
+
}
|
|
2018
|
+
catch (err) {
|
|
2019
|
+
this.outputChannel.appendLine(`[Dashboard] Failed seeding disabled-charts.yaml: ${err}`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Seed a default requirements.txt in the scripts directory.
|
|
2024
|
+
* Users and agents can edit this file to add/remove Python dependencies.
|
|
2025
|
+
* Non-destructive: never overwrites an existing file.
|
|
2026
|
+
*/
|
|
2027
|
+
async seedRequirements() {
|
|
2028
|
+
const scriptsDir = this.artifactManager.getScriptDir();
|
|
2029
|
+
await fs_1.promises.mkdir(scriptsDir, { recursive: true });
|
|
2030
|
+
const reqPath = path.join(scriptsDir, 'requirements.txt');
|
|
2031
|
+
try {
|
|
2032
|
+
await fs_1.promises.access(reqPath);
|
|
2033
|
+
return; // Already exists, respect user edits.
|
|
2034
|
+
}
|
|
2035
|
+
catch {
|
|
2036
|
+
// Missing, seed it.
|
|
2037
|
+
}
|
|
2038
|
+
try {
|
|
2039
|
+
await fs_1.promises.writeFile(reqPath, [
|
|
2040
|
+
'# Python dependencies for analytics scripts.',
|
|
2041
|
+
'# Add packages here - they are auto-installed when scripts run.',
|
|
2042
|
+
'matplotlib',
|
|
2043
|
+
'duckdb',
|
|
2044
|
+
'',
|
|
2045
|
+
].join('\n'), 'utf-8');
|
|
2046
|
+
}
|
|
2047
|
+
catch (err) {
|
|
2048
|
+
this.outputChannel.appendLine(`[Dashboard] Failed seeding requirements.txt: ${err}`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Seed default panel files from bundled webview assets into
|
|
2053
|
+
* the user's analytics panel directory (.agent-hippo/analytics/panel/).
|
|
2054
|
+
* Non-destructive: existing files are never overwritten.
|
|
2055
|
+
*/
|
|
2056
|
+
async seedPanelFiles() {
|
|
2057
|
+
const panelDir = this.getPanelDir();
|
|
2058
|
+
await fs_1.promises.mkdir(panelDir, { recursive: true });
|
|
2059
|
+
const webviewBasePath = this.getWebviewPath();
|
|
2060
|
+
const bundledDir = path.join(this.extensionContext.extensionPath, webviewBasePath);
|
|
2061
|
+
// Bundled source → user destination filename
|
|
2062
|
+
const seedMap = [
|
|
2063
|
+
{ bundled: 'panel.js', userFile: 'panel.js' },
|
|
2064
|
+
{ bundled: 'styles.css', userFile: 'panel.css' },
|
|
2065
|
+
{ bundled: 'analytics-panel.html', userFile: 'panel.html' },
|
|
2066
|
+
];
|
|
2067
|
+
for (const { bundled, userFile } of seedMap) {
|
|
2068
|
+
const dest = path.join(panelDir, userFile);
|
|
2069
|
+
try {
|
|
2070
|
+
await fs_1.promises.access(dest);
|
|
2071
|
+
continue; // Already exists, respect user modifications.
|
|
2072
|
+
}
|
|
2073
|
+
catch {
|
|
2074
|
+
// Missing, seed it.
|
|
2075
|
+
}
|
|
2076
|
+
const source = path.join(bundledDir, bundled);
|
|
2077
|
+
try {
|
|
2078
|
+
await fs_1.promises.copyFile(source, dest);
|
|
2079
|
+
}
|
|
2080
|
+
catch (err) {
|
|
2081
|
+
this.outputChannel.appendLine(`[Dashboard] Failed seeding panel file ${userFile}: ${err}`);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
await this.patchUserPanelHtmlOnDisk();
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Get the user's panel directory under the analytics root.
|
|
2088
|
+
*/
|
|
2089
|
+
getPanelDir() {
|
|
2090
|
+
return this.artifactManager.getPanelDir();
|
|
2091
|
+
}
|
|
2092
|
+
// -------------------------------------------------------------------------
|
|
2093
|
+
// Refresh: re-run widget scripts (manual, Fleet, or "compute when empty")
|
|
2094
|
+
// Policy: PLAN/spotlight-refresh-policy.md — manual + Fleet always enter here;
|
|
2095
|
+
// navigation should gate before calling refreshCharts on view switch.
|
|
2096
|
+
// -------------------------------------------------------------------------
|
|
2097
|
+
/**
|
|
2098
|
+
* Run assigned widget scripts for the active view (or requested view when provided).
|
|
2099
|
+
* Writes artifacts and updates manifest.dataHash.
|
|
2100
|
+
*/
|
|
2101
|
+
async refreshCharts(options) {
|
|
2102
|
+
if (options?.viewId) {
|
|
2103
|
+
const switched = await this.setActiveView(options.viewId);
|
|
2104
|
+
if (!switched) {
|
|
2105
|
+
this.outputChannel.appendLine(`[Dashboard] Ignoring refresh for unknown view: ${options.viewId}`);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
this.syncArtifactContext();
|
|
2110
|
+
if (this.isRefreshInProgress) {
|
|
2111
|
+
this.hasPendingRefreshRequest = true;
|
|
2112
|
+
this.outputChannel.appendLine('[Dashboard] Refresh already running; queued one follow-up refresh');
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
const refreshContext = this.getRefreshContext(options);
|
|
2116
|
+
this.isRefreshInProgress = true;
|
|
2117
|
+
let rerunRequested = false;
|
|
2118
|
+
let logContext;
|
|
2119
|
+
let refreshOutcome = 'ok';
|
|
2120
|
+
let refreshOutcomeError = '';
|
|
2121
|
+
try {
|
|
2122
|
+
try {
|
|
2123
|
+
logContext = await this.startWidgetRunLog();
|
|
2124
|
+
}
|
|
2125
|
+
catch (logErr) {
|
|
2126
|
+
this.outputChannel.appendLine(`[Dashboard] Failed to initialize widget run log: ${logErr}`);
|
|
2127
|
+
}
|
|
2128
|
+
await this.ensureBundledDefaultScriptsPresent();
|
|
2129
|
+
const disabledConfig = await this.loadDisabledChartsConfig();
|
|
2130
|
+
const scriptSelection = await this.getScriptsForView(options);
|
|
2131
|
+
const disabledScriptNames = new Set(this.getDisabledScriptsForView(disabledConfig, scriptSelection.view.id)
|
|
2132
|
+
.map(name => this.normalizeBasename(name)));
|
|
2133
|
+
const userScripts = scriptSelection.scripts.filter(script => !disabledScriptNames.has(this.normalizeBasename(script.filename)));
|
|
2134
|
+
if (userScripts.length === 0) {
|
|
2135
|
+
this.outputChannel.appendLine(`[Dashboard] No scripts assigned to view "${scriptSelection.view.name}"`);
|
|
2136
|
+
await this.finishSmoothRefresh({ notifyFinished: false, refresh: options });
|
|
2137
|
+
postToWebview(this.webviewView?.webview, { type: 'refreshNoData', viewId: refreshContext.viewId });
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
this.startSmoothRefresh({ batchArtifactUpdates: false, refresh: options });
|
|
2141
|
+
let refreshError = null;
|
|
2142
|
+
let widgetRunResult = { totalScripts: 0, failedScripts: 0 };
|
|
2143
|
+
try {
|
|
2144
|
+
const hasFleetScript = userScripts.some(s => this.normalizeBasename(s.filename) === 'agent-fleet-widget.js');
|
|
2145
|
+
if (hasFleetScript) {
|
|
2146
|
+
// Use latest DuckDB data (avoid 30s cache) and request a quick re-index so next refresh is fresher
|
|
2147
|
+
this.queryRunner.invalidateCache();
|
|
2148
|
+
this.indexingService()?.scheduleIndexing(true, { debounceMs: 500 });
|
|
2149
|
+
const activeView = this.getActiveView();
|
|
2150
|
+
await this.ensureViewOwnedDirectories(activeView);
|
|
2151
|
+
await this.writeFleetExportForCurrentView();
|
|
2152
|
+
}
|
|
2153
|
+
this.outputChannel.appendLine(`[Dashboard] Refreshing view "${scriptSelection.view.name}" via ${userScripts.length} script(s)`);
|
|
2154
|
+
widgetRunResult = await this.runUserScripts(userScripts, disabledConfig.charts, logContext);
|
|
2155
|
+
if (widgetRunResult.failedScripts > 0 && refreshOutcome === 'ok') {
|
|
2156
|
+
refreshOutcome = 'failed';
|
|
2157
|
+
refreshOutcomeError = `${widgetRunResult.failedScripts}/${widgetRunResult.totalScripts} widget scripts failed`;
|
|
2158
|
+
}
|
|
2159
|
+
const dataHash = await this.queryRunner.getDataHash(this.getActiveFilters());
|
|
2160
|
+
const manifest = await this.artifactManager.getManifest() ?? {
|
|
2161
|
+
version: 1,
|
|
2162
|
+
generatedAt: new Date().toISOString(),
|
|
2163
|
+
dataHash,
|
|
2164
|
+
prompt: '',
|
|
2165
|
+
artifacts: [],
|
|
2166
|
+
};
|
|
2167
|
+
manifest.generatedAt = new Date().toISOString();
|
|
2168
|
+
manifest.dataHash = dataHash;
|
|
2169
|
+
await this.artifactManager.saveManifest(manifest);
|
|
2170
|
+
if (this.shouldUpdateFleetExportAfterRefresh(hasFleetScript)) {
|
|
2171
|
+
await this.writeFleetExportForCurrentView();
|
|
2172
|
+
}
|
|
2173
|
+
await this.enqueueArtifactRefresh();
|
|
2174
|
+
}
|
|
2175
|
+
catch (err) {
|
|
2176
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2177
|
+
this.outputChannel.appendLine(`[Dashboard] Refresh failed: ${errorMessage}`);
|
|
2178
|
+
refreshError = errorMessage;
|
|
2179
|
+
refreshOutcome = errorMessage.toLowerCase().includes('cancel') ? 'cancelled' : 'failed';
|
|
2180
|
+
refreshOutcomeError = errorMessage;
|
|
2181
|
+
}
|
|
2182
|
+
finally {
|
|
2183
|
+
try {
|
|
2184
|
+
await this.finishSmoothRefresh({ notifyFinished: true, refresh: options });
|
|
2185
|
+
}
|
|
2186
|
+
catch (finishErr) {
|
|
2187
|
+
this.outputChannel.appendLine(`[Dashboard] Failed finishing refresh state: ${finishErr}`);
|
|
2188
|
+
this.postRefreshStatus('refreshFinished', options);
|
|
2189
|
+
}
|
|
2190
|
+
if (refreshError !== null) {
|
|
2191
|
+
postToWebview(this.webviewView?.webview, {
|
|
2192
|
+
type: 'refreshFailed',
|
|
2193
|
+
viewId: refreshContext.viewId,
|
|
2194
|
+
error: refreshError,
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
catch (err) {
|
|
2200
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2201
|
+
this.outputChannel.appendLine(`[Dashboard] Refresh failed: ${errorMessage}`);
|
|
2202
|
+
postToWebview(this.webviewView?.webview, {
|
|
2203
|
+
type: 'refreshFailed',
|
|
2204
|
+
viewId: refreshContext.viewId,
|
|
2205
|
+
error: errorMessage,
|
|
2206
|
+
});
|
|
2207
|
+
this.postRefreshStatus('refreshFinished', options);
|
|
2208
|
+
refreshOutcome = errorMessage.toLowerCase().includes('cancel') ? 'cancelled' : 'failed';
|
|
2209
|
+
refreshOutcomeError = errorMessage;
|
|
2210
|
+
}
|
|
2211
|
+
finally {
|
|
2212
|
+
try {
|
|
2213
|
+
await this.finishWidgetRunLog(logContext, refreshOutcome, refreshOutcomeError);
|
|
2214
|
+
}
|
|
2215
|
+
catch (logErr) {
|
|
2216
|
+
this.outputChannel.appendLine(`[Dashboard] Failed finalizing widget run log: ${logErr}`);
|
|
2217
|
+
}
|
|
2218
|
+
rerunRequested = this.hasPendingRefreshRequest;
|
|
2219
|
+
this.hasPendingRefreshRequest = false;
|
|
2220
|
+
this.isRefreshInProgress = false;
|
|
2221
|
+
}
|
|
2222
|
+
if (rerunRequested) {
|
|
2223
|
+
this.outputChannel.appendLine('[Dashboard] Running queued follow-up refresh');
|
|
2224
|
+
await this.refreshCharts({ viewId: this.getActiveViewId() });
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async refreshWidgetScriptsOnly(requestedScripts) {
|
|
2228
|
+
const normalizedRequested = Array.from(new Set(requestedScripts
|
|
2229
|
+
.map(script => this.normalizeBasename(script))
|
|
2230
|
+
.filter(script => !!script)));
|
|
2231
|
+
if (normalizedRequested.length === 0) {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
const refreshContext = {
|
|
2235
|
+
viewId: this.getActiveViewId(),
|
|
2236
|
+
viewName: this.getActiveView().name,
|
|
2237
|
+
};
|
|
2238
|
+
this.startSmoothRefresh({
|
|
2239
|
+
batchArtifactUpdates: true,
|
|
2240
|
+
refresh: refreshContext,
|
|
2241
|
+
});
|
|
2242
|
+
try {
|
|
2243
|
+
this.syncArtifactContext();
|
|
2244
|
+
await this.ensureBundledDefaultScriptsPresent();
|
|
2245
|
+
const requestedSet = new Set(normalizedRequested);
|
|
2246
|
+
const isFleetWidgetRefresh = requestedSet.has('agent-fleet-widget.js') && this.getActiveViewId() === 'fleet';
|
|
2247
|
+
if (isFleetWidgetRefresh) {
|
|
2248
|
+
const activeView = this.getActiveView();
|
|
2249
|
+
await this.ensureViewOwnedDirectories(activeView);
|
|
2250
|
+
await this.writeFleetExportForCurrentView();
|
|
2251
|
+
}
|
|
2252
|
+
const scriptSelection = await this.getScriptsForView({ viewId: this.getActiveViewId() });
|
|
2253
|
+
const disabledConfig = await this.loadDisabledChartsConfig();
|
|
2254
|
+
const disabledScriptNames = new Set(this.getDisabledScriptsForView(disabledConfig, scriptSelection.view.id)
|
|
2255
|
+
.map(name => this.normalizeBasename(name)));
|
|
2256
|
+
const scriptsToRun = scriptSelection.scripts.filter(script => requestedSet.has(this.normalizeBasename(script.filename))
|
|
2257
|
+
&& !disabledScriptNames.has(this.normalizeBasename(script.filename)));
|
|
2258
|
+
if (scriptsToRun.length === 0) {
|
|
2259
|
+
await this.enqueueArtifactRefresh();
|
|
2260
|
+
await this.postViewExportSnapshot({ respondWhenMissing: true });
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
await this.runUserScripts(scriptsToRun, disabledConfig.charts);
|
|
2264
|
+
if (isFleetWidgetRefresh) {
|
|
2265
|
+
// Bump version so non-preserveFleetIframe paths reload the iframe. preserveFleetIframe
|
|
2266
|
+
// views (the default for fleet-dashboard layout) ignore this and rely on the
|
|
2267
|
+
// viewExportSnapshot postMessage from postViewExportSnapshot below — the widget's
|
|
2268
|
+
// DOM-diff renderer applies updates in-place without flicker.
|
|
2269
|
+
this.fleetRefreshVersion += 1;
|
|
2270
|
+
}
|
|
2271
|
+
await this.enqueueArtifactRefresh();
|
|
2272
|
+
await this.postViewExportSnapshot({ respondWhenMissing: true });
|
|
2273
|
+
}
|
|
2274
|
+
finally {
|
|
2275
|
+
await this.finishSmoothRefresh({
|
|
2276
|
+
notifyFinished: true,
|
|
2277
|
+
refresh: refreshContext,
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Run user-created scripts from <workspace>/.agent-hippo/analytics/views/<viewId>/scripts/.
|
|
2283
|
+
* Each script receives env vars for traces dir and output dirs.
|
|
2284
|
+
*/
|
|
2285
|
+
isDuckDbHeavyScript(script) {
|
|
2286
|
+
return this.duckDbHeavyScriptNames.has(path.basename(script.filename).toLowerCase());
|
|
2287
|
+
}
|
|
2288
|
+
getWidgetRunLogsDir() {
|
|
2289
|
+
return path.join(this.artifactManager.getAnalyticsRoot(), 'logs', 'widgets');
|
|
2290
|
+
}
|
|
2291
|
+
createWidgetRunId() {
|
|
2292
|
+
const now = new Date();
|
|
2293
|
+
const ts = now.toISOString().replace(/[:.]/g, '-');
|
|
2294
|
+
const nonce = Math.random().toString(36).slice(2, 8);
|
|
2295
|
+
return `refresh-${ts}-${nonce}`;
|
|
2296
|
+
}
|
|
2297
|
+
async appendWidgetRunLogLine(logPath, payload) {
|
|
2298
|
+
const serialized = `${JSON.stringify(payload)}\n`;
|
|
2299
|
+
await fs_1.promises.appendFile(logPath, serialized, 'utf-8');
|
|
2300
|
+
}
|
|
2301
|
+
/** Fleet export is only needed for the Fleet view or views that run agent-fleet-widget.js. */
|
|
2302
|
+
shouldUpdateFleetExportAfterRefresh(hasFleetScript) {
|
|
2303
|
+
return this.getActiveViewId() === 'fleet' || hasFleetScript;
|
|
2304
|
+
}
|
|
2305
|
+
async writeFleetExportForCurrentView() {
|
|
2306
|
+
const filters = this.getActiveFilters();
|
|
2307
|
+
const exportPath = path.join(this.artifactManager.getExportDir(), AnalyticsDashboardProvider.FLEET_EXPORT_FILENAME);
|
|
2308
|
+
const normalizeSessionsForCompare = (sessions) => {
|
|
2309
|
+
if (!Array.isArray(sessions)) {
|
|
2310
|
+
return [];
|
|
2311
|
+
}
|
|
2312
|
+
return sessions
|
|
2313
|
+
.map((s) => (s && typeof s === 'object' ? s : null))
|
|
2314
|
+
.filter((s) => Boolean(s))
|
|
2315
|
+
.slice()
|
|
2316
|
+
.sort((a, b) => {
|
|
2317
|
+
const rankDiff = (0, fleetTypes_1.rankFleetStatus)(a.status) - (0, fleetTypes_1.rankFleetStatus)(b.status);
|
|
2318
|
+
if (rankDiff !== 0) {
|
|
2319
|
+
return rankDiff;
|
|
2320
|
+
}
|
|
2321
|
+
const aStarted = Date.parse(String(a.startedAt ?? ''));
|
|
2322
|
+
const bStarted = Date.parse(String(b.startedAt ?? ''));
|
|
2323
|
+
if (Number.isFinite(aStarted) && Number.isFinite(bStarted) && aStarted !== bStarted) {
|
|
2324
|
+
return bStarted - aStarted;
|
|
2325
|
+
}
|
|
2326
|
+
return String(a.sessionId ?? '').localeCompare(String(b.sessionId ?? ''));
|
|
2327
|
+
});
|
|
2328
|
+
};
|
|
2329
|
+
const comparableFromPayload = (payload) => ({
|
|
2330
|
+
scope: payload.scope ?? 'global',
|
|
2331
|
+
projectId: payload.projectId ?? '',
|
|
2332
|
+
agentId: payload.agentId ?? '',
|
|
2333
|
+
totals: payload.totals ?? {},
|
|
2334
|
+
sessions: normalizeSessionsForCompare(payload.sessions),
|
|
2335
|
+
});
|
|
2336
|
+
const readComparable = async () => {
|
|
2337
|
+
try {
|
|
2338
|
+
const raw = await fs_1.promises.readFile(exportPath, 'utf-8');
|
|
2339
|
+
const parsed = JSON.parse(raw);
|
|
2340
|
+
return JSON.stringify(comparableFromPayload(parsed));
|
|
2341
|
+
}
|
|
2342
|
+
catch {
|
|
2343
|
+
return null;
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
const previousComparable = await readComparable();
|
|
2347
|
+
try {
|
|
2348
|
+
const result = await (0, fleetOrchestrator_1.runFleetRefresh)({ scope: filters.scope ?? 'global', projectId: filters.projectId, agentId: filters.agentId }, {
|
|
2349
|
+
queryRunner: this.queryRunner,
|
|
2350
|
+
exportPath,
|
|
2351
|
+
onError: (sourceId, err) => {
|
|
2352
|
+
this.outputChannel.appendLine(`[Dashboard] Fleet source "${sourceId}" failed: ${String(err)}`);
|
|
2353
|
+
},
|
|
2354
|
+
});
|
|
2355
|
+
if (!result.success) {
|
|
2356
|
+
this.outputChannel.appendLine(`[Dashboard] Fleet refresh completed with source errors (failed: ${result.failedSources.join(', ') || 'none'}).`);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
catch (err) {
|
|
2360
|
+
this.outputChannel.appendLine(`[Dashboard] Fleet refresh failed: ${err}`);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
const nextComparable = await readComparable();
|
|
2364
|
+
const meaningfulChange = previousComparable === null || nextComparable !== previousComparable;
|
|
2365
|
+
if (meaningfulChange && this.getActiveViewId() === 'fleet') {
|
|
2366
|
+
this.fleetRefreshVersion += 1;
|
|
2367
|
+
// Regenerate agent-fleet.html from the updated JSON so gallery mtime and iframe content stay in sync.
|
|
2368
|
+
await this.refreshFleetWidgetFromCurrentExport();
|
|
2369
|
+
// During refreshCharts, the outer flow calls enqueueArtifactRefresh after scripts; skip mid-refresh to avoid flicker.
|
|
2370
|
+
if (!this.isRefreshInProgress) {
|
|
2371
|
+
await this.enqueueArtifactRefresh();
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
async handleFleetCardTitleUpdate(sourceId, sessionIdValue, cardTitleValue) {
|
|
2376
|
+
const sessionId = String(sessionIdValue ?? '').trim();
|
|
2377
|
+
if (!sessionId) {
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
const cardTitle = String(cardTitleValue ?? '').trim();
|
|
2381
|
+
const fleetTitleLogPaths = Array.from(new Set([
|
|
2382
|
+
path.join((0, index_1.getAgentideHome)(), 'analytics', 'fleet-title-updates.log'),
|
|
2383
|
+
path.join(this.artifactManager.getAnalyticsRoot(), 'fleet-title-updates.log'),
|
|
2384
|
+
]));
|
|
2385
|
+
const logLine = async (status, err) => {
|
|
2386
|
+
const message = `${new Date().toISOString()}\tsessionId=${sessionId}\tcardTitle=${JSON.stringify(cardTitle)}\t${status}`;
|
|
2387
|
+
const extra = err !== undefined ? `\terror=${String(err)}` : '';
|
|
2388
|
+
await Promise.all(fleetTitleLogPaths.map(async (p) => {
|
|
2389
|
+
await fs_1.promises.mkdir(path.dirname(p), { recursive: true });
|
|
2390
|
+
await fs_1.promises.appendFile(p, `${message}${extra}\n`, 'utf-8').catch(() => undefined);
|
|
2391
|
+
}));
|
|
2392
|
+
};
|
|
2393
|
+
await logLine('requested');
|
|
2394
|
+
try {
|
|
2395
|
+
await this.queryRunner.updateFleetCardTitle(sourceId, sessionId, cardTitle);
|
|
2396
|
+
// Ensure widget export/html reflect the persisted title (writeFleetExport regenerates HTML when Fleet is active).
|
|
2397
|
+
await this.writeFleetExportForCurrentView();
|
|
2398
|
+
await logLine('persisted');
|
|
2399
|
+
this.outputChannel.appendLine(`[Dashboard] Fleet title edit persisted for session=${sessionId}`);
|
|
2400
|
+
}
|
|
2401
|
+
catch (err) {
|
|
2402
|
+
await logLine('failed', err);
|
|
2403
|
+
this.outputChannel.appendLine(`[Dashboard] Fleet title edit failed for session=${sessionId}: ${err}`);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
async refreshFleetWidgetFromCurrentExport() {
|
|
2407
|
+
const activeViewId = this.getActiveViewId();
|
|
2408
|
+
if (activeViewId !== 'fleet') {
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
const activeView = this.getActiveView();
|
|
2412
|
+
if (!activeView) {
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
this.syncArtifactContext();
|
|
2416
|
+
await this.ensureBundledDefaultScriptsPresent();
|
|
2417
|
+
await this.ensureViewOwnedDirectories(activeView);
|
|
2418
|
+
const scriptSelection = await this.getScriptsForView({ viewId: activeViewId });
|
|
2419
|
+
const disabledConfig = await this.loadDisabledChartsConfig();
|
|
2420
|
+
const disabledScriptNames = new Set(this.getDisabledScriptsForView(disabledConfig, scriptSelection.view.id)
|
|
2421
|
+
.map(name => this.normalizeBasename(name)));
|
|
2422
|
+
const scriptsToRun = scriptSelection.scripts.filter(script => {
|
|
2423
|
+
const basename = this.normalizeBasename(script.filename);
|
|
2424
|
+
return basename === 'agent-fleet-widget.js' && !disabledScriptNames.has(basename);
|
|
2425
|
+
});
|
|
2426
|
+
await this.runUserScripts(scriptsToRun, disabledConfig.charts);
|
|
2427
|
+
}
|
|
2428
|
+
buildFleetDisconnectPrompt(sessionId, channelType, channelTarget) {
|
|
2429
|
+
const safeSessionId = sessionId.trim();
|
|
2430
|
+
const safeChannelType = (channelType ?? '').trim() || 'unknown';
|
|
2431
|
+
const safeChannelTarget = (channelTarget ?? '').trim();
|
|
2432
|
+
return [
|
|
2433
|
+
'Disconnect this Agent Anywhere channel session.',
|
|
2434
|
+
`Session ID: ${safeSessionId}`,
|
|
2435
|
+
`Channel Type: ${safeChannelType}`,
|
|
2436
|
+
safeChannelTarget ? `Channel Target: ${safeChannelTarget}` : '',
|
|
2437
|
+
'If direct disconnect tooling is unavailable, provide the exact command/workflow to disconnect safely.',
|
|
2438
|
+
].filter(Boolean).join('\n');
|
|
2439
|
+
}
|
|
2440
|
+
async handleFleetDisconnectRequest(sessionIdValue, channelType, channelTarget) {
|
|
2441
|
+
const sessionId = sessionIdValue.trim();
|
|
2442
|
+
if (!sessionId) {
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
const choice = await vscode.window.showWarningMessage(`Disconnect channel for session ${sessionId}?`, { modal: true }, 'Disconnect');
|
|
2446
|
+
if (choice !== 'Disconnect') {
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
await this.openVsCodeChat({
|
|
2450
|
+
prompt: this.buildFleetDisconnectPrompt(sessionId, channelType, channelTarget),
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Builds the VS Code chat session resource URI for a local session id.
|
|
2455
|
+
* Matches workbench LocalChatSessionUri: vscode-chat-session://local/{base64url(sessionId)}
|
|
2456
|
+
*/
|
|
2457
|
+
buildLocalChatSessionResource(sessionId) {
|
|
2458
|
+
const base64url = Buffer.from(sessionId, 'utf-8')
|
|
2459
|
+
.toString('base64')
|
|
2460
|
+
.replace(/\+/g, '-')
|
|
2461
|
+
.replace(/\//g, '_')
|
|
2462
|
+
.replace(/=+$/, '');
|
|
2463
|
+
return `vscode-chat-session://local/${base64url}`;
|
|
2464
|
+
}
|
|
2465
|
+
async resolveFleetSessionOpenPreferences(sessionId, model, engine, agentId, taskTitle) {
|
|
2466
|
+
const row = (0, fleetRowDefaults_1.buildFleetSessionRow)('local', {
|
|
2467
|
+
sessionId,
|
|
2468
|
+
agentId,
|
|
2469
|
+
status: 'in_progress',
|
|
2470
|
+
startedAt: new Date().toISOString(),
|
|
2471
|
+
endedAt: '',
|
|
2472
|
+
taskTitle: String(taskTitle ?? '').trim() || sessionId,
|
|
2473
|
+
model: String(model ?? '').trim() || 'unknown',
|
|
2474
|
+
engine: String(engine ?? '').trim(),
|
|
2475
|
+
channelType: 'none',
|
|
2476
|
+
channelTarget: '',
|
|
2477
|
+
turns: 0,
|
|
2478
|
+
totalTokens: 0,
|
|
2479
|
+
});
|
|
2480
|
+
if (!taskTitle?.trim()) {
|
|
2481
|
+
try {
|
|
2482
|
+
const titleMap = await vscode.commands.executeCommand('agentide.resolveFleetSessionTitles', [sessionId]);
|
|
2483
|
+
const resolved = typeof titleMap?.[sessionId] === 'string' ? titleMap[sessionId].trim() : '';
|
|
2484
|
+
if (resolved) {
|
|
2485
|
+
row.taskTitle = resolved;
|
|
2486
|
+
row.cardTitle = row.cardTitle ?? resolved;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
catch {
|
|
2490
|
+
// Workbench command unavailable outside AgentHippo builds.
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
(0, fleetDisplayUtils_1.enrichFleetRowMetadata)(row);
|
|
2494
|
+
return {
|
|
2495
|
+
model: row.model,
|
|
2496
|
+
engine: row.engine,
|
|
2497
|
+
modeName: (0, fleetDisplayUtils_1.fleetEngineIdToChatModeName)(row.engine),
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Opens the actual chat session by id in this workspace when it exists;
|
|
2502
|
+
* otherwise opens chat with a prompt to investigate the session.
|
|
2503
|
+
*/
|
|
2504
|
+
async openSpotlightForSession(sessionIdValue, channelType, channelTarget, model, engine, agentId) {
|
|
2505
|
+
let sessionId = sessionIdValue.trim();
|
|
2506
|
+
if (!sessionId) {
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
// Fleet schedule cards use sessionId schedule:<id>; channelTarget stores the chat session id.
|
|
2510
|
+
if (sessionId.startsWith('schedule:')) {
|
|
2511
|
+
const linked = String(channelTarget ?? '').trim();
|
|
2512
|
+
if (linked) {
|
|
2513
|
+
sessionId = linked;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const sessionResource = this.buildLocalChatSessionResource(sessionId);
|
|
2517
|
+
const resolved = await this.resolveFleetSessionOpenPreferences(sessionId, model, engine, agentId);
|
|
2518
|
+
const preferredModel = resolved.model;
|
|
2519
|
+
const preferredEngine = resolved.engine;
|
|
2520
|
+
const packId = String(agentId ?? '').trim() || 'default';
|
|
2521
|
+
const fallbackPrompt = [
|
|
2522
|
+
'Show the session investigation for this agent session.',
|
|
2523
|
+
`Session ID: ${sessionId}`,
|
|
2524
|
+
channelType ? `Channel: ${channelType}` : '',
|
|
2525
|
+
channelTarget ? `Target: ${channelTarget}` : '',
|
|
2526
|
+
'Include: the live chat transcript, a chronological feed of the agent\'s actions/logs, and the exact prompt or payload that initiated the task.',
|
|
2527
|
+
].filter(Boolean).join('\n');
|
|
2528
|
+
try {
|
|
2529
|
+
await vscode.commands.executeCommand('workbench.action.chat.open', {
|
|
2530
|
+
sessionResource,
|
|
2531
|
+
mode: resolved.modeName ?? 'agent',
|
|
2532
|
+
});
|
|
2533
|
+
// Restore engine/model from fleet card (scheduled jobs often have no DuckDB token rows).
|
|
2534
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
2535
|
+
await new Promise(resolve => setTimeout(resolve, attempt === 0 ? 150 : 100));
|
|
2536
|
+
const chatState = await vscode.commands.executeCommand('agentide.getChatState');
|
|
2537
|
+
const activeSessionId = chatState?.sessionId?.trim() ?? '';
|
|
2538
|
+
if (activeSessionId !== sessionId) {
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
await this.applyFleetSessionChatPreferences(packId, preferredEngine, preferredModel);
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
await this.applyFleetSessionChatPreferences(packId, preferredEngine, preferredModel);
|
|
2545
|
+
// workbench.action.chat.open may silently fall back to current session when sessionResource is unknown.
|
|
2546
|
+
// Verify active session and use prompt fallback if we didn't land on the requested id.
|
|
2547
|
+
const chatState = await vscode.commands.executeCommand('agentide.getChatState');
|
|
2548
|
+
const activeSessionId = chatState?.sessionId?.trim() ?? '';
|
|
2549
|
+
if (activeSessionId === sessionId) {
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
this.outputChannel.appendLine(`[Dashboard] Could not open session ${sessionId} directly (active=${activeSessionId || 'unknown'}); falling back to investigation prompt`);
|
|
2553
|
+
await this.openVsCodeChat({ prompt: fallbackPrompt });
|
|
2554
|
+
}
|
|
2555
|
+
catch {
|
|
2556
|
+
// Session may not exist in this workspace; fall back to opening chat with investigation prompt.
|
|
2557
|
+
await this.openVsCodeChat({ prompt: fallbackPrompt });
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
async applyFleetSessionChatPreferences(packId, preferredEngine, preferredModel) {
|
|
2561
|
+
if (!preferredEngine && !preferredModel) {
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
try {
|
|
2565
|
+
await vscode.commands.executeCommand('agentide.applyPackPreferences', {
|
|
2566
|
+
packId,
|
|
2567
|
+
preferredEngine: preferredEngine || undefined,
|
|
2568
|
+
preferredModel: preferredModel || undefined,
|
|
2569
|
+
silent: true,
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
catch (err) {
|
|
2573
|
+
this.outputChannel.appendLine(`[Dashboard] Could not restore fleet session engine/model: ${err}`);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
async startWidgetRunLog() {
|
|
2577
|
+
const logsDir = this.getWidgetRunLogsDir();
|
|
2578
|
+
await fs_1.promises.mkdir(logsDir, { recursive: true });
|
|
2579
|
+
await this.trimOldWidgetRunLogs();
|
|
2580
|
+
const runId = this.createWidgetRunId();
|
|
2581
|
+
const logPath = path.join(logsDir, `${runId}.jsonl`);
|
|
2582
|
+
const startedAt = new Date().toISOString();
|
|
2583
|
+
const filters = this.getActiveFilters();
|
|
2584
|
+
await this.appendWidgetRunLogLine(logPath, {
|
|
2585
|
+
type: 'refresh_started',
|
|
2586
|
+
runId,
|
|
2587
|
+
startedAt,
|
|
2588
|
+
scope: filters.scope,
|
|
2589
|
+
projectId: filters.projectId ?? '',
|
|
2590
|
+
agentId: filters.agentId ?? '',
|
|
2591
|
+
});
|
|
2592
|
+
this.outputChannel.appendLine(`[Dashboard] Widget refresh log: ${logPath}`);
|
|
2593
|
+
return { runId, logPath, startedAt };
|
|
2594
|
+
}
|
|
2595
|
+
async trimOldWidgetRunLogs() {
|
|
2596
|
+
const logsDir = this.getWidgetRunLogsDir();
|
|
2597
|
+
let entries = [];
|
|
2598
|
+
try {
|
|
2599
|
+
const dirEntries = await fs_1.promises.readdir(logsDir, { withFileTypes: true });
|
|
2600
|
+
entries = await Promise.all(dirEntries
|
|
2601
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
2602
|
+
.map(async (entry) => {
|
|
2603
|
+
const fullPath = path.join(logsDir, entry.name);
|
|
2604
|
+
const stat = await fs_1.promises.stat(fullPath);
|
|
2605
|
+
return { name: entry.name, fullPath, mtimeMs: stat.mtimeMs };
|
|
2606
|
+
}));
|
|
2607
|
+
}
|
|
2608
|
+
catch {
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
if (entries.length <= WIDGET_RUN_LOG_RETENTION_COUNT) {
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2615
|
+
const stale = entries.slice(WIDGET_RUN_LOG_RETENTION_COUNT);
|
|
2616
|
+
await Promise.all(stale.map(entry => fs_1.promises.unlink(entry.fullPath).catch(() => undefined)));
|
|
2617
|
+
}
|
|
2618
|
+
async finishWidgetRunLog(context, status, error) {
|
|
2619
|
+
if (!context) {
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
await this.appendWidgetRunLogLine(context.logPath, {
|
|
2623
|
+
type: 'refresh_finished',
|
|
2624
|
+
runId: context.runId,
|
|
2625
|
+
status,
|
|
2626
|
+
endedAt: new Date().toISOString(),
|
|
2627
|
+
error: error ?? '',
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
async acquireDuckDbScriptSlot(script) {
|
|
2631
|
+
if (!this.isDuckDbHeavyScript(script)) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
while (this.activeDuckDbScriptCount >= 1) {
|
|
2635
|
+
await new Promise(resolve => this.duckDbScriptWaiters.push(resolve));
|
|
2636
|
+
}
|
|
2637
|
+
this.activeDuckDbScriptCount += 1;
|
|
2638
|
+
this.outputChannel.appendLine(`[Dashboard] DuckDB gate acquired: ${script.filename}`);
|
|
2639
|
+
}
|
|
2640
|
+
releaseDuckDbScriptSlot(script) {
|
|
2641
|
+
if (!this.isDuckDbHeavyScript(script)) {
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
this.activeDuckDbScriptCount = Math.max(0, this.activeDuckDbScriptCount - 1);
|
|
2645
|
+
this.outputChannel.appendLine(`[Dashboard] DuckDB gate released: ${script.filename}`);
|
|
2646
|
+
const next = this.duckDbScriptWaiters.shift();
|
|
2647
|
+
next?.();
|
|
2648
|
+
}
|
|
2649
|
+
async runUserScripts(scripts, disabledCharts, logContext) {
|
|
2650
|
+
this.syncArtifactContext();
|
|
2651
|
+
const outputDir = (await this.artifactManager.ensureArtifactDir()).fsPath;
|
|
2652
|
+
if (scripts.length === 0) {
|
|
2653
|
+
this.outputChannel.appendLine('[Dashboard] All scripts disabled; nothing to run');
|
|
2654
|
+
return { totalScripts: 0, failedScripts: 0 };
|
|
2655
|
+
}
|
|
2656
|
+
// Seed requirements.txt only when this run includes Python widgets.
|
|
2657
|
+
if (scripts.some(script => script.filename.toLowerCase().endsWith('.py'))) {
|
|
2658
|
+
await this.seedRequirements();
|
|
2659
|
+
}
|
|
2660
|
+
const env = this.buildScriptEnvironment(outputDir, disabledCharts);
|
|
2661
|
+
const maxConcurrency = Math.max(1, Math.min(USER_SCRIPT_MAX_CONCURRENCY, scripts.length));
|
|
2662
|
+
const failures = [];
|
|
2663
|
+
let nextScriptIndex = 0;
|
|
2664
|
+
const worker = async () => {
|
|
2665
|
+
while (true) {
|
|
2666
|
+
const currentIndex = nextScriptIndex++;
|
|
2667
|
+
if (currentIndex >= scripts.length) {
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
const script = scripts[currentIndex];
|
|
2671
|
+
const scriptStartedAtMs = Date.now();
|
|
2672
|
+
let acquiredDuckDbSlot = false;
|
|
2673
|
+
let duckDbGateWaitMs = 0;
|
|
2674
|
+
let scriptStatus = 'ok';
|
|
2675
|
+
let scriptError = '';
|
|
2676
|
+
let scriptStdout = '';
|
|
2677
|
+
let scriptStderr = '';
|
|
2678
|
+
try {
|
|
2679
|
+
const gateWaitStartedAtMs = Date.now();
|
|
2680
|
+
await this.acquireDuckDbScriptSlot(script);
|
|
2681
|
+
duckDbGateWaitMs = Date.now() - gateWaitStartedAtMs;
|
|
2682
|
+
acquiredDuckDbSlot = true;
|
|
2683
|
+
const executionResult = await this.executeUserScript(script, env);
|
|
2684
|
+
scriptStdout = executionResult.stdout;
|
|
2685
|
+
scriptStderr = executionResult.stderr;
|
|
2686
|
+
}
|
|
2687
|
+
catch (error) {
|
|
2688
|
+
scriptStatus = 'failed';
|
|
2689
|
+
scriptError = error instanceof Error ? error.message : String(error);
|
|
2690
|
+
failures.push({ script, error });
|
|
2691
|
+
}
|
|
2692
|
+
finally {
|
|
2693
|
+
if (acquiredDuckDbSlot) {
|
|
2694
|
+
this.releaseDuckDbScriptSlot(script);
|
|
2695
|
+
}
|
|
2696
|
+
if (logContext) {
|
|
2697
|
+
try {
|
|
2698
|
+
const finishedAt = new Date().toISOString();
|
|
2699
|
+
const durationMs = Date.now() - scriptStartedAtMs;
|
|
2700
|
+
const filters = this.getActiveFilters();
|
|
2701
|
+
await this.appendWidgetRunLogLine(logContext.logPath, {
|
|
2702
|
+
type: 'script_run',
|
|
2703
|
+
runId: logContext.runId,
|
|
2704
|
+
script: script.filename,
|
|
2705
|
+
language: script.language,
|
|
2706
|
+
status: scriptStatus,
|
|
2707
|
+
error: scriptError,
|
|
2708
|
+
stdout: scriptStdout,
|
|
2709
|
+
stderr: scriptStderr,
|
|
2710
|
+
durationMs,
|
|
2711
|
+
duckDbGateWaitMs,
|
|
2712
|
+
finishedAt,
|
|
2713
|
+
scope: filters.scope,
|
|
2714
|
+
projectId: filters.projectId ?? '',
|
|
2715
|
+
agentId: filters.agentId ?? '',
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
catch (logErr) {
|
|
2719
|
+
this.outputChannel.appendLine(`[Dashboard] Failed writing script run log: ${logErr}`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
// Queue artifact refresh after each script; during smooth refresh
|
|
2723
|
+
// this is batched and emitted once at the end to avoid HTML flicker.
|
|
2724
|
+
await this.enqueueArtifactRefresh();
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
};
|
|
2728
|
+
await Promise.all(Array.from({ length: maxConcurrency }, () => worker()));
|
|
2729
|
+
if (failures.length === scripts.length) {
|
|
2730
|
+
throw new Error('All refresh scripts failed. Check output logs for details.');
|
|
2731
|
+
}
|
|
2732
|
+
if (failures.length > 0) {
|
|
2733
|
+
this.outputChannel.appendLine(`[Dashboard] Refresh completed with ${failures.length} script failure(s)`);
|
|
2734
|
+
for (const failure of failures) {
|
|
2735
|
+
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
2736
|
+
this.outputChannel.appendLine(`[Dashboard] Script failed: ${failure.script.filename}: ${message}`);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return { totalScripts: scripts.length, failedScripts: failures.length };
|
|
2740
|
+
}
|
|
2741
|
+
isJavascriptEsmScopeError(error) {
|
|
2742
|
+
const stderr = typeof error.stderr === 'string'
|
|
2743
|
+
? error.stderr
|
|
2744
|
+
: '';
|
|
2745
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2746
|
+
const haystack = [message, stderr].join('\n').toLowerCase();
|
|
2747
|
+
return haystack.includes('require is not defined in es module scope')
|
|
2748
|
+
|| haystack.includes('module is not defined in es module scope')
|
|
2749
|
+
|| haystack.includes('exports is not defined in es module scope');
|
|
2750
|
+
}
|
|
2751
|
+
async runJavascriptWithCommonJsRetry(scriptPath, execOpts) {
|
|
2752
|
+
try {
|
|
2753
|
+
return await execFileAsync('node', [scriptPath], execOpts);
|
|
2754
|
+
}
|
|
2755
|
+
catch (err) {
|
|
2756
|
+
const extension = path.extname(scriptPath).toLowerCase();
|
|
2757
|
+
if (extension !== '.js' || !this.isJavascriptEsmScopeError(err)) {
|
|
2758
|
+
throw err;
|
|
2759
|
+
}
|
|
2760
|
+
const parsed = path.parse(scriptPath);
|
|
2761
|
+
this.outputChannel.appendLine(`[Dashboard] Retrying JavaScript script as CommonJS: ${parsed.base}`);
|
|
2762
|
+
const sandboxRoot = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), `agentide-cjs-run-${parsed.name}-`));
|
|
2763
|
+
const sandboxScriptsDir = path.join(sandboxRoot, 'scripts');
|
|
2764
|
+
const sandboxScriptPath = path.join(sandboxScriptsDir, parsed.base);
|
|
2765
|
+
try {
|
|
2766
|
+
// Copy the whole script directory into a sandbox and force CommonJS mode there.
|
|
2767
|
+
// This keeps relative requires (e.g. ./duckdbLocator) working on both Unix and Windows.
|
|
2768
|
+
await fs_1.promises.cp(parsed.dir, sandboxScriptsDir, { recursive: true });
|
|
2769
|
+
await fs_1.promises.writeFile(path.join(sandboxScriptsDir, 'package.json'), JSON.stringify({ type: 'commonjs' }), 'utf-8');
|
|
2770
|
+
return await execFileAsync('node', [sandboxScriptPath], execOpts);
|
|
2771
|
+
}
|
|
2772
|
+
finally {
|
|
2773
|
+
await fs_1.promises.rm(sandboxRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
async executeUserScript(script, env) {
|
|
2778
|
+
const scriptPath = script.uri.fsPath;
|
|
2779
|
+
this.outputChannel.appendLine(`[Dashboard] Running user script: ${script.filename}`);
|
|
2780
|
+
const execOpts = { timeout: 60_000, maxBuffer: 5 * 1024 * 1024, env };
|
|
2781
|
+
let result;
|
|
2782
|
+
switch (script.language) {
|
|
2783
|
+
case 'python':
|
|
2784
|
+
{
|
|
2785
|
+
const deps = await this.readPythonDeps();
|
|
2786
|
+
const runResult = await (0, pythonRunner_1.runPythonWithDeps)({
|
|
2787
|
+
scriptPath,
|
|
2788
|
+
args: [],
|
|
2789
|
+
deps,
|
|
2790
|
+
analyticsRoot: this.artifactManager.getAnalyticsRoot(),
|
|
2791
|
+
requirementsPath: path.join(this.artifactManager.getScriptDir(), 'requirements.txt'),
|
|
2792
|
+
execOptions: execOpts,
|
|
2793
|
+
log: (message) => this.outputChannel.appendLine(message),
|
|
2794
|
+
});
|
|
2795
|
+
result = { stdout: runResult.stdout, stderr: runResult.stderr };
|
|
2796
|
+
}
|
|
2797
|
+
break;
|
|
2798
|
+
case 'javascript':
|
|
2799
|
+
result = await this.runJavascriptWithCommonJsRetry(scriptPath, execOpts);
|
|
2800
|
+
break;
|
|
2801
|
+
case 'shell':
|
|
2802
|
+
result = await execFileAsync('bash', [scriptPath], execOpts);
|
|
2803
|
+
break;
|
|
2804
|
+
default:
|
|
2805
|
+
throw new Error(`Unsupported script language: ${script.language}`);
|
|
2806
|
+
}
|
|
2807
|
+
if (result.stderr) {
|
|
2808
|
+
this.outputChannel.appendLine(`[Dashboard] ${script.filename} stderr: ${result.stderr.slice(0, 500)}`);
|
|
2809
|
+
}
|
|
2810
|
+
if (result.stdout) {
|
|
2811
|
+
this.outputChannel.appendLine(`[Dashboard] ${script.filename} output: ${result.stdout.slice(0, 300)}`);
|
|
2812
|
+
}
|
|
2813
|
+
return result;
|
|
2814
|
+
}
|
|
2815
|
+
// -------------------------------------------------------------------------
|
|
2816
|
+
// Python Dependency Management
|
|
2817
|
+
// -------------------------------------------------------------------------
|
|
2818
|
+
/**
|
|
2819
|
+
* Run a Python script with automatic dependency resolution.
|
|
2820
|
+
* Chain: uv run --with <deps> → pip install --user + python3 → actionable error.
|
|
2821
|
+
*/
|
|
2822
|
+
/**
|
|
2823
|
+
* Read Python dependencies from .agent-hippo/analytics/views/<viewId>/scripts/requirements.txt.
|
|
2824
|
+
* Returns an empty array if the file doesn't exist.
|
|
2825
|
+
*/
|
|
2826
|
+
async readPythonDeps() {
|
|
2827
|
+
const reqFile = path.join(this.artifactManager.getScriptDir(), 'requirements.txt');
|
|
2828
|
+
try {
|
|
2829
|
+
const content = await fs_1.promises.readFile(reqFile, 'utf-8');
|
|
2830
|
+
return content
|
|
2831
|
+
.split('\n')
|
|
2832
|
+
.map(line => line.replace(/#.*$/, '').trim()) // strip comments
|
|
2833
|
+
.filter(line => line.length > 0);
|
|
2834
|
+
}
|
|
2835
|
+
catch {
|
|
2836
|
+
return []; // no requirements.txt
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
// -------------------------------------------------------------------------
|
|
2840
|
+
// Summary Cards
|
|
2841
|
+
// -------------------------------------------------------------------------
|
|
2842
|
+
async sendSummaryCards(summaryOverride) {
|
|
2843
|
+
try {
|
|
2844
|
+
const summary = summaryOverride ?? await this.queryRunner.getCostSummary(this.getActiveFilters());
|
|
2845
|
+
const cards = [
|
|
2846
|
+
{ label: 'Total Cost', value: `$${summary.totalCost.toFixed(2)}`, icon: '\u{1F4B0}' },
|
|
2847
|
+
{ label: 'Requests', value: summary.totalRequests.toLocaleString(), icon: '\u{1F4CA}' },
|
|
2848
|
+
{ label: 'Avg Latency', value: `${Math.round(summary.avgDurationMs)}ms`, icon: '\u26A1' },
|
|
2849
|
+
{ label: 'Top Model', value: this.formatModelName(summary.topModel), icon: '\u{1F916}' },
|
|
2850
|
+
];
|
|
2851
|
+
this.webviewView?.webview.postMessage({
|
|
2852
|
+
type: 'summaryCards',
|
|
2853
|
+
cards,
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
catch (err) {
|
|
2857
|
+
// Keep the previous summary cards on errors (for example during scope
|
|
2858
|
+
// transitions) instead of replacing with a misleading zeroed snapshot.
|
|
2859
|
+
this.outputChannel.appendLine(`[Dashboard] Could not load summary: ${err}`);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
formatModelName(model) {
|
|
2863
|
+
if (!model || model === 'unknown') {
|
|
2864
|
+
return '-';
|
|
2865
|
+
}
|
|
2866
|
+
// Shorten model names for display
|
|
2867
|
+
return model
|
|
2868
|
+
.replace('claude-', '')
|
|
2869
|
+
.replace('gpt-', '')
|
|
2870
|
+
.replace('-20', '')
|
|
2871
|
+
.slice(0, 12);
|
|
2872
|
+
}
|
|
2873
|
+
// -------------------------------------------------------------------------
|
|
2874
|
+
// Artifact Management
|
|
2875
|
+
// -------------------------------------------------------------------------
|
|
2876
|
+
/**
|
|
2877
|
+
* Fleet widget outputs `agent-fleet.html` / `agent-fleet.csv` in the active view charts dir.
|
|
2878
|
+
* Global and Workspace should not show those cards (Fleet view only), including stale files
|
|
2879
|
+
* left from older configs that ran the fleet script on every view.
|
|
2880
|
+
*/
|
|
2881
|
+
filterFleetGalleryArtifactsForActiveView(artifacts) {
|
|
2882
|
+
const id = this.getActiveViewId();
|
|
2883
|
+
if (id !== 'global' && id !== 'workspace') {
|
|
2884
|
+
return artifacts;
|
|
2885
|
+
}
|
|
2886
|
+
return artifacts.filter((a) => {
|
|
2887
|
+
const n = a.filename.trim().toLowerCase();
|
|
2888
|
+
return n !== 'agent-fleet.html' && n !== 'agent-fleet.csv';
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
async loadAndSendArtifacts() {
|
|
2892
|
+
this.syncArtifactContext();
|
|
2893
|
+
const [rawArtifacts, disabledConfig] = await Promise.all([
|
|
2894
|
+
this.artifactManager.listArtifacts(),
|
|
2895
|
+
this.loadDisabledChartsConfig(),
|
|
2896
|
+
]);
|
|
2897
|
+
const artifacts = this.filterFleetGalleryArtifactsForActiveView(rawArtifacts);
|
|
2898
|
+
this.sendArtifactsToWebview(artifacts, disabledConfig.galleryIgnore);
|
|
2899
|
+
await this.postDisabledChartsState(disabledConfig);
|
|
2900
|
+
// Also send manifest
|
|
2901
|
+
const manifest = await this.artifactManager.getManifest();
|
|
2902
|
+
if (manifest) {
|
|
2903
|
+
this.webviewView?.webview.postMessage({
|
|
2904
|
+
type: 'manifestUpdated',
|
|
2905
|
+
manifest,
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
// Refresh any open HTML preview panels so they show the latest artifact content
|
|
2909
|
+
// (e.g. Fleet in editor preview stays in sync when user clicks Manual Refresh in panel)
|
|
2910
|
+
// Run after manifest so a failure here cannot block the main panel update.
|
|
2911
|
+
try {
|
|
2912
|
+
const artifactPaths = new Set(rawArtifacts.filter(a => a?.uri).map(a => path.resolve(a.uri.fsPath)));
|
|
2913
|
+
for (const [normalizedPath, panel] of this.htmlPreviewPanels.entries()) {
|
|
2914
|
+
if (artifactPaths.has(normalizedPath)) {
|
|
2915
|
+
this.setPreviewPanelHtml(panel, normalizedPath).catch(err => {
|
|
2916
|
+
this.outputChannel.appendLine(`[Dashboard] Failed refreshing preview for ${normalizedPath}: ${err}`);
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
catch (err) {
|
|
2922
|
+
this.outputChannel.appendLine(`[Dashboard] Error refreshing open preview panels: ${err}`);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
/** Path convention for view-owned snapshot exports: exports/<viewId>.json (built-in fleet: fleet.json). */
|
|
2926
|
+
getViewExportSnapshotPath(viewId) {
|
|
2927
|
+
const id = (viewId ?? this.getActiveViewId()).trim();
|
|
2928
|
+
if (!id) {
|
|
2929
|
+
return undefined;
|
|
2930
|
+
}
|
|
2931
|
+
const exportDir = this.artifactManager.getExportDir();
|
|
2932
|
+
const view = id === this.getActiveViewId()
|
|
2933
|
+
? this.getActiveView()
|
|
2934
|
+
: this.viewsConfig.views.find(entry => entry.id === id);
|
|
2935
|
+
if (id === 'fleet' || (view && (0, viewsConfigMerge_1.viewRunsFleetWidget)(view))) {
|
|
2936
|
+
return path.join(exportDir, AnalyticsDashboardProvider.FLEET_EXPORT_FILENAME);
|
|
2937
|
+
}
|
|
2938
|
+
return path.join(exportDir, `${id}.json`);
|
|
2939
|
+
}
|
|
2940
|
+
/** Resolve fleet export JSON for read (prefers fleet.json, falls back to legacy agent-fleet.json). */
|
|
2941
|
+
resolveFleetExportReadPath() {
|
|
2942
|
+
const exportDir = this.artifactManager.getExportDir();
|
|
2943
|
+
const primary = path.join(exportDir, AnalyticsDashboardProvider.FLEET_EXPORT_FILENAME);
|
|
2944
|
+
if ((0, fs_1.existsSync)(primary)) {
|
|
2945
|
+
return primary;
|
|
2946
|
+
}
|
|
2947
|
+
const legacy = path.join(exportDir, AnalyticsDashboardProvider.LEGACY_FLEET_EXPORT_FILENAME);
|
|
2948
|
+
return (0, fs_1.existsSync)(legacy) ? legacy : primary;
|
|
2949
|
+
}
|
|
2950
|
+
/** Active view opts into iframe live updates via exports/<viewId>.json snapshots. */
|
|
2951
|
+
activeViewPushesExportSnapshot() {
|
|
2952
|
+
return (0, viewsConfigMerge_1.shouldPreserveFleetIframe)(this.getActiveView());
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Push export JSON to the panel when the active view has preserveFleetIframe.
|
|
2956
|
+
* Forwards the full parsed exports/<viewId>.json (generic passthrough); widgets normalize shape.
|
|
2957
|
+
* Large session[] arrays are chunked for iframe delivery.
|
|
2958
|
+
*/
|
|
2959
|
+
async postViewExportSnapshot(options) {
|
|
2960
|
+
if (!this.webviewView) {
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
const allowSnapshot = this.activeViewPushesExportSnapshot()
|
|
2964
|
+
|| options?.respondWhenMissing === true;
|
|
2965
|
+
if (!allowSnapshot) {
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
const exportPath = this.getViewExportSnapshotPath();
|
|
2969
|
+
if (!exportPath) {
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
const readPath = exportPath.endsWith(AnalyticsDashboardProvider.FLEET_EXPORT_FILENAME)
|
|
2973
|
+
? this.resolveFleetExportReadPath()
|
|
2974
|
+
: exportPath;
|
|
2975
|
+
if (!options?.respondWhenMissing && !(0, fs_1.existsSync)(readPath)) {
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
try {
|
|
2979
|
+
const raw = await fs_1.promises.readFile(readPath, 'utf-8');
|
|
2980
|
+
const parsed = JSON.parse(raw);
|
|
2981
|
+
const messages = (0, viewsConfigMerge_1.buildViewExportSnapshotMessages)(parsed);
|
|
2982
|
+
for (const message of messages) {
|
|
2983
|
+
this.webviewView.webview.postMessage({
|
|
2984
|
+
type: viewsConfigMerge_1.VIEW_EXPORT_SNAPSHOT_MESSAGE,
|
|
2985
|
+
payload: message.payload,
|
|
2986
|
+
meta: message.meta,
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
catch (err) {
|
|
2991
|
+
const isEnoent = err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT';
|
|
2992
|
+
if (!options?.respondWhenMissing && isEnoent) {
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
this.webviewView.webview.postMessage({
|
|
2996
|
+
type: viewsConfigMerge_1.VIEW_EXPORT_SNAPSHOT_MESSAGE,
|
|
2997
|
+
payload: {
|
|
2998
|
+
generatedAt: '',
|
|
2999
|
+
totals: {},
|
|
3000
|
+
},
|
|
3001
|
+
meta: {
|
|
3002
|
+
ok: isEnoent,
|
|
3003
|
+
sessionCount: 0,
|
|
3004
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3005
|
+
cliActive: false,
|
|
3006
|
+
daemonHint: false,
|
|
3007
|
+
},
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
sendArtifactsToWebview(artifacts, galleryIgnorePatterns) {
|
|
3012
|
+
if (!this.webviewView) {
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
const webviewArtifacts = artifacts.map(a => ({
|
|
3016
|
+
// Per-artifact cache bust using modifiedAt so only changed images reload;
|
|
3017
|
+
// unchanged charts keep their cached URL and don't flicker.
|
|
3018
|
+
webviewUri: this.webviewView.webview.asWebviewUri(a.uri).toString() + `?v=${a.modifiedAt.getTime()}`,
|
|
3019
|
+
localPath: a.uri.fsPath,
|
|
3020
|
+
filename: a.filename,
|
|
3021
|
+
size: a.size,
|
|
3022
|
+
modifiedAt: a.modifiedAt.toISOString(),
|
|
3023
|
+
type: a.type,
|
|
3024
|
+
format: a.format,
|
|
3025
|
+
generator: a.generator,
|
|
3026
|
+
}));
|
|
3027
|
+
const activeViewId = this.getActiveViewId();
|
|
3028
|
+
this.webviewView.webview.postMessage({
|
|
3029
|
+
type: 'artifactsUpdated',
|
|
3030
|
+
artifacts: webviewArtifacts,
|
|
3031
|
+
activeViewId,
|
|
3032
|
+
fleetRefreshVersion: activeViewId === 'fleet' ? this.fleetRefreshVersion : undefined,
|
|
3033
|
+
galleryIgnorePatterns,
|
|
3034
|
+
});
|
|
3035
|
+
void this.postViewExportSnapshot();
|
|
3036
|
+
}
|
|
3037
|
+
async postArtifactContent(pathValue, format) {
|
|
3038
|
+
const targetPath = pathValue?.trim();
|
|
3039
|
+
if (!targetPath || !this.webviewView) {
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
const normalizedTarget = path.resolve(targetPath);
|
|
3043
|
+
if (!this.isPathWithinAllowedAnalyticsRoots(normalizedTarget)) {
|
|
3044
|
+
this.webviewView.webview.postMessage({
|
|
3045
|
+
type: 'artifactContent',
|
|
3046
|
+
path: targetPath,
|
|
3047
|
+
format: format ?? 'html',
|
|
3048
|
+
error: 'Access denied for preview path.',
|
|
3049
|
+
});
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
try {
|
|
3053
|
+
const stat = await fs_1.promises.stat(normalizedTarget);
|
|
3054
|
+
if (!stat.isFile()) {
|
|
3055
|
+
throw new Error('Preview target is not a file.');
|
|
3056
|
+
}
|
|
3057
|
+
if (stat.size > 2 * 1024 * 1024) {
|
|
3058
|
+
throw new Error('Preview file is too large (>2MB).');
|
|
3059
|
+
}
|
|
3060
|
+
const content = await fs_1.promises.readFile(normalizedTarget, 'utf-8');
|
|
3061
|
+
this.webviewView.webview.postMessage({
|
|
3062
|
+
type: 'artifactContent',
|
|
3063
|
+
path: targetPath,
|
|
3064
|
+
format: format ?? 'html',
|
|
3065
|
+
content,
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
catch (err) {
|
|
3069
|
+
this.outputChannel.appendLine(`[Dashboard] Failed loading artifact preview for ${normalizedTarget}: ${err}`);
|
|
3070
|
+
this.webviewView.webview.postMessage({
|
|
3071
|
+
type: 'artifactContent',
|
|
3072
|
+
path: targetPath,
|
|
3073
|
+
format: format ?? 'html',
|
|
3074
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
async openHtmlArtifactPreview(pathValue) {
|
|
3079
|
+
const targetPath = pathValue?.trim();
|
|
3080
|
+
if (!targetPath) {
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
const normalizedTarget = path.resolve(targetPath);
|
|
3084
|
+
let panel = this.htmlPreviewPanels.get(normalizedTarget);
|
|
3085
|
+
if (!panel) {
|
|
3086
|
+
panel = vscode.window.createWebviewPanel('agentide.analyticsHtmlPreview', `Preview: ${path.basename(normalizedTarget)}`, vscode.ViewColumn.Active, {
|
|
3087
|
+
enableScripts: true,
|
|
3088
|
+
retainContextWhenHidden: true,
|
|
3089
|
+
localResourceRoots: [
|
|
3090
|
+
vscode.Uri.file(path.dirname(normalizedTarget)),
|
|
3091
|
+
],
|
|
3092
|
+
});
|
|
3093
|
+
this.htmlPreviewPanels.set(normalizedTarget, panel);
|
|
3094
|
+
panel.onDidDispose(() => {
|
|
3095
|
+
this.htmlPreviewPanels.delete(normalizedTarget);
|
|
3096
|
+
});
|
|
3097
|
+
// Forward widget messages (e.g. openSpotlightForSession) from preview to extension
|
|
3098
|
+
panel.webview.onDidReceiveMessage(this.handleMessage.bind(this));
|
|
3099
|
+
}
|
|
3100
|
+
else {
|
|
3101
|
+
panel.reveal(vscode.ViewColumn.Active);
|
|
3102
|
+
}
|
|
3103
|
+
await this.setPreviewPanelHtml(panel, normalizedTarget);
|
|
3104
|
+
}
|
|
3105
|
+
/** Load artifact HTML from disk and set it on the preview panel (used for open and for refresh). */
|
|
3106
|
+
async setPreviewPanelHtml(panel, normalizedTarget) {
|
|
3107
|
+
const nonce = getNonce();
|
|
3108
|
+
try {
|
|
3109
|
+
const rawHtml = await fs_1.promises.readFile(normalizedTarget, 'utf-8');
|
|
3110
|
+
const withNonce = rawHtml.replace(/<script(?![^>]*\bnonce=)([^>]*)>/gi, `<script nonce="${nonce}"$1>`);
|
|
3111
|
+
const csp = `<meta http-equiv="Content-Security-Policy" content="
|
|
3112
|
+
default-src 'none';
|
|
3113
|
+
style-src ${panel.webview.cspSource} 'unsafe-inline';
|
|
3114
|
+
script-src 'nonce-${nonce}' 'unsafe-inline';
|
|
3115
|
+
img-src ${panel.webview.cspSource} data: https: http:;
|
|
3116
|
+
font-src ${panel.webview.cspSource};
|
|
3117
|
+
">`;
|
|
3118
|
+
// Bridge: when widget runs in preview (no iframe), it posts to window.parent (=== window).
|
|
3119
|
+
// This script forwards those messages to the extension so card click → openSpotlightForSession works.
|
|
3120
|
+
const previewBridge = `<script nonce="${nonce}">(function(){try{var api=typeof acquireVsCodeApi==="function"?acquireVsCodeApi():null;if(!api)return;window.addEventListener("message",function(e){var d=e.data;if(!d||typeof d!=="object")return;if(d.type==="openSpotlightForSession"&&typeof d.sessionId==="string")api.postMessage({type:"openSpotlightForSession",sessionId:d.sessionId,channelType:d.channelType||"",channelTarget:d.channelTarget||"",model:d.model||"",engine:d.engine||"",agentId:d.agentId||""});if(d.type==="refreshView")api.postMessage({type:"refreshView"});if(d.type==="refreshWidget"){var msg={type:"refreshWidget"};if(typeof d.script==="string")msg.script=d.script;if(Array.isArray(d.scripts))msg.scripts=d.scripts;api.postMessage(msg);}if(d.type==="refreshFleetWidget")api.postMessage({type:"refreshFleetWidget"});if(d.type==="disconnectFleetChannel"&&typeof d.sessionId==="string")api.postMessage({type:"disconnectFleetChannel",sessionId:d.sessionId,channelType:d.channelType||"",channelTarget:d.channelTarget||""});if(d.type==="copySessionId"&&typeof d.sessionId==="string")api.postMessage({type:"copySessionId",sessionId:d.sessionId});if(d.type==="requestClipboardText"&&typeof d.requestId==="string")api.postMessage({type:"requestClipboardText",requestId:d.requestId});if(d.type==="writeClipboardText"&&typeof d.text==="string")api.postMessage({type:"writeClipboardText",text:d.text});});}catch(err){}})();</script>`;
|
|
3121
|
+
let html = /<head[^>]*>/i.test(withNonce)
|
|
3122
|
+
? withNonce.replace(/<head([^>]*)>/i, `<head$1>\n\t${csp}`)
|
|
3123
|
+
: `<!DOCTYPE html><html><head>${csp}<meta charset="UTF-8"></head><body>${withNonce}</body></html>`;
|
|
3124
|
+
html = html.replace(/<body(\s[^>]*)?>/i, (_m, bodyAttrs) => `<body${bodyAttrs ?? ''}>${previewBridge}`);
|
|
3125
|
+
panel.title = `Preview: ${path.basename(normalizedTarget)}`;
|
|
3126
|
+
panel.webview.html = html;
|
|
3127
|
+
}
|
|
3128
|
+
catch (err) {
|
|
3129
|
+
this.outputChannel.appendLine(`[Dashboard] Failed setting html preview for ${normalizedTarget}: ${err}`);
|
|
3130
|
+
panel.webview.html = `<!DOCTYPE html>
|
|
3131
|
+
<html>
|
|
3132
|
+
<body style="font-family: sans-serif; padding: 16px;">
|
|
3133
|
+
<h3>Unable to preview HTML artifact</h3>
|
|
3134
|
+
<p>${err instanceof Error ? err.message : String(err)}</p>
|
|
3135
|
+
</body>
|
|
3136
|
+
</html>`;
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
enqueueArtifactRefresh() {
|
|
3140
|
+
if (this.isBatchingArtifactUpdates) {
|
|
3141
|
+
this.pendingArtifactRefresh = true;
|
|
3142
|
+
return this.artifactRefreshQueue;
|
|
3143
|
+
}
|
|
3144
|
+
this.artifactRefreshQueue = this.artifactRefreshQueue
|
|
3145
|
+
.then(() => this.loadAndSendArtifacts())
|
|
3146
|
+
.catch(err => {
|
|
3147
|
+
this.outputChannel.appendLine(`[Dashboard] Incremental artifact refresh failed: ${err}`);
|
|
3148
|
+
});
|
|
3149
|
+
return this.artifactRefreshQueue;
|
|
3150
|
+
}
|
|
3151
|
+
startSmoothRefresh(options) {
|
|
3152
|
+
this.isBatchingArtifactUpdates = options?.batchArtifactUpdates ?? true;
|
|
3153
|
+
this.pendingArtifactRefresh = false;
|
|
3154
|
+
postToWebview(this.webviewView?.webview, { type: 'dataFresh' });
|
|
3155
|
+
this.postRefreshStatus('refreshStarted', options?.refresh);
|
|
3156
|
+
}
|
|
3157
|
+
async finishSmoothRefresh(options) {
|
|
3158
|
+
const notifyFinished = options?.notifyFinished ?? true;
|
|
3159
|
+
this.isBatchingArtifactUpdates = false;
|
|
3160
|
+
if (this.pendingArtifactRefresh) {
|
|
3161
|
+
this.pendingArtifactRefresh = false;
|
|
3162
|
+
await this.loadAndSendArtifacts();
|
|
3163
|
+
}
|
|
3164
|
+
if (notifyFinished) {
|
|
3165
|
+
this.postRefreshStatus('refreshFinished', options?.refresh);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
async resizeSpotlightSidebar(direction) {
|
|
3169
|
+
const sizeCommand = direction === 'expand'
|
|
3170
|
+
? 'workbench.action.increaseViewSize'
|
|
3171
|
+
: 'workbench.action.decreaseViewSize';
|
|
3172
|
+
const widthCommand = direction === 'expand'
|
|
3173
|
+
? 'workbench.action.increaseViewWidth'
|
|
3174
|
+
: 'workbench.action.decreaseViewWidth';
|
|
3175
|
+
const delayForFocus = async (ms = 50) => new Promise(resolve => setTimeout(resolve, ms));
|
|
3176
|
+
try {
|
|
3177
|
+
await vscode.commands.executeCommand('agentide.analyticsView.focus');
|
|
3178
|
+
await delayForFocus();
|
|
3179
|
+
await vscode.commands.executeCommand(sizeCommand);
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
catch (err) {
|
|
3183
|
+
this.outputChannel.appendLine(`[Dashboard] Spotlight resize primary path failed (${direction}): ${err}`);
|
|
3184
|
+
}
|
|
3185
|
+
try {
|
|
3186
|
+
await vscode.commands.executeCommand('agentide.analyticsView.focus');
|
|
3187
|
+
await delayForFocus();
|
|
3188
|
+
await vscode.commands.executeCommand(widthCommand);
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
catch (err) {
|
|
3192
|
+
this.outputChannel.appendLine(`[Dashboard] Spotlight resize width fallback failed (${direction}): ${err}`);
|
|
3193
|
+
}
|
|
3194
|
+
await vscode.commands.executeCommand('workbench.action.focusSideBar');
|
|
3195
|
+
await delayForFocus();
|
|
3196
|
+
await vscode.commands.executeCommand(sizeCommand);
|
|
3197
|
+
}
|
|
3198
|
+
// -------------------------------------------------------------------------
|
|
3199
|
+
// Message Handling
|
|
3200
|
+
// -------------------------------------------------------------------------
|
|
3201
|
+
async handleMessage(message) {
|
|
3202
|
+
switch (message.type) {
|
|
3203
|
+
case 'requestArtifacts':
|
|
3204
|
+
await this.loadAndSendArtifacts();
|
|
3205
|
+
break;
|
|
3206
|
+
case 'requestSummary':
|
|
3207
|
+
await this.sendSummaryCards();
|
|
3208
|
+
break;
|
|
3209
|
+
case 'requestFilters':
|
|
3210
|
+
await this.postFilterState();
|
|
3211
|
+
break;
|
|
3212
|
+
case 'requestViews':
|
|
3213
|
+
// Re-read views.yaml so external edits appear without requiring window reload.
|
|
3214
|
+
await this.loadAndPostViewsState();
|
|
3215
|
+
await this.postPanelState('default');
|
|
3216
|
+
break;
|
|
3217
|
+
case 'setActiveView': {
|
|
3218
|
+
const switched = await this.setActiveView(message.viewId);
|
|
3219
|
+
// Always update the webview with the correct view, even if the switch failed.
|
|
3220
|
+
// This prevents the dropdown and provider state from being out of sync.
|
|
3221
|
+
await this.postViewsState();
|
|
3222
|
+
if (!switched) {
|
|
3223
|
+
break;
|
|
3224
|
+
}
|
|
3225
|
+
this.syncArtifactContext();
|
|
3226
|
+
await this.postPanelState('default');
|
|
3227
|
+
await this.refreshAfterViewChange();
|
|
3228
|
+
await this.refreshChartsForActiveViewAfterSwitch();
|
|
3229
|
+
break;
|
|
3230
|
+
}
|
|
3231
|
+
case 'createView': {
|
|
3232
|
+
const created = await this.runViewConfigMutation(() => this.createView(message.name));
|
|
3233
|
+
if (!created) {
|
|
3234
|
+
break;
|
|
3235
|
+
}
|
|
3236
|
+
await this.postViewsState();
|
|
3237
|
+
await this.postPanelState('default');
|
|
3238
|
+
await this.refreshAfterViewChange();
|
|
3239
|
+
await this.refreshChartsForActiveViewAfterSwitch();
|
|
3240
|
+
break;
|
|
3241
|
+
}
|
|
3242
|
+
case 'duplicateView': {
|
|
3243
|
+
const duplicated = await this.runViewConfigMutation(() => this.duplicateView(message.viewId));
|
|
3244
|
+
if (!duplicated) {
|
|
3245
|
+
break;
|
|
3246
|
+
}
|
|
3247
|
+
await this.setActiveView(duplicated.id);
|
|
3248
|
+
await this.postViewsState();
|
|
3249
|
+
await this.postPanelState('default');
|
|
3250
|
+
await this.refreshAfterViewChange();
|
|
3251
|
+
await this.refreshChartsForActiveViewAfterSwitch();
|
|
3252
|
+
break;
|
|
3253
|
+
}
|
|
3254
|
+
case 'renameView': {
|
|
3255
|
+
const renamed = await this.renameView(message.viewId, message.name);
|
|
3256
|
+
if (!renamed) {
|
|
3257
|
+
break;
|
|
3258
|
+
}
|
|
3259
|
+
await this.postViewsState();
|
|
3260
|
+
break;
|
|
3261
|
+
}
|
|
3262
|
+
case 'deleteView': {
|
|
3263
|
+
const removed = await this.runViewConfigMutation(() => this.deleteView(message.viewId));
|
|
3264
|
+
if (!removed) {
|
|
3265
|
+
break;
|
|
3266
|
+
}
|
|
3267
|
+
await this.postViewsState();
|
|
3268
|
+
await this.postPanelState('default');
|
|
3269
|
+
await this.refreshAfterViewChange();
|
|
3270
|
+
await this.refreshChartsForActiveViewAfterSwitch();
|
|
3271
|
+
break;
|
|
3272
|
+
}
|
|
3273
|
+
case 'requestRenameView': {
|
|
3274
|
+
const viewId = message.viewId ?? this.getActiveViewId();
|
|
3275
|
+
const view = viewId ? this.findViewById(viewId) : undefined;
|
|
3276
|
+
if (!view || view.system) {
|
|
3277
|
+
break;
|
|
3278
|
+
}
|
|
3279
|
+
const name = await vscode.window.showInputBox({
|
|
3280
|
+
title: 'Rename view',
|
|
3281
|
+
value: view.name,
|
|
3282
|
+
prompt: 'Enter the new view name',
|
|
3283
|
+
validateInput: (value) => {
|
|
3284
|
+
const t = value?.trim();
|
|
3285
|
+
if (!t) {
|
|
3286
|
+
return 'Name cannot be empty';
|
|
3287
|
+
}
|
|
3288
|
+
return null;
|
|
3289
|
+
},
|
|
3290
|
+
});
|
|
3291
|
+
if (name !== undefined && name.trim()) {
|
|
3292
|
+
const renamed = await this.renameView(view.id, name.trim());
|
|
3293
|
+
if (renamed) {
|
|
3294
|
+
await this.postViewsState();
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
break;
|
|
3298
|
+
}
|
|
3299
|
+
case 'requestDeleteView': {
|
|
3300
|
+
const viewId = message.viewId ?? this.getActiveViewId();
|
|
3301
|
+
const view = viewId ? this.findViewById(viewId) : undefined;
|
|
3302
|
+
if (!view || view.system) {
|
|
3303
|
+
break;
|
|
3304
|
+
}
|
|
3305
|
+
const action = await vscode.window.showWarningMessage(`Delete view "${view.name}"?`, { modal: true }, 'Delete', 'Cancel');
|
|
3306
|
+
if (action === 'Delete') {
|
|
3307
|
+
const removed = await this.deleteView(view.id);
|
|
3308
|
+
if (removed) {
|
|
3309
|
+
await this.postViewsState();
|
|
3310
|
+
await this.postPanelState('default');
|
|
3311
|
+
await this.refreshAfterViewChange();
|
|
3312
|
+
await this.refreshChartsForActiveViewAfterSwitch();
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
break;
|
|
3316
|
+
}
|
|
3317
|
+
case 'editViewScripts':
|
|
3318
|
+
await this.editViewScripts(message.viewId);
|
|
3319
|
+
break;
|
|
3320
|
+
case 'refreshAllViews':
|
|
3321
|
+
await this.refreshAllViews();
|
|
3322
|
+
break;
|
|
3323
|
+
case 'refreshView':
|
|
3324
|
+
await this.refreshDashboardData();
|
|
3325
|
+
await this.refreshCharts({ viewId: this.getActiveViewId() });
|
|
3326
|
+
break;
|
|
3327
|
+
case 'refreshWidget': {
|
|
3328
|
+
const normalizedScripts = [
|
|
3329
|
+
typeof message.script === 'string' ? message.script : '',
|
|
3330
|
+
...(Array.isArray(message.scripts) ? message.scripts.filter((s) => typeof s === 'string') : []),
|
|
3331
|
+
]
|
|
3332
|
+
.map(s => s.trim())
|
|
3333
|
+
.filter(Boolean);
|
|
3334
|
+
await this.refreshWidgetScriptsOnly(normalizedScripts);
|
|
3335
|
+
break;
|
|
3336
|
+
}
|
|
3337
|
+
case 'refreshFleetWidget':
|
|
3338
|
+
await this.refreshWidgetScriptsOnly(['agent-fleet-widget.js']);
|
|
3339
|
+
break;
|
|
3340
|
+
case 'shareView': {
|
|
3341
|
+
const viewId = message.viewId?.trim() ?? '';
|
|
3342
|
+
const view = viewId ? this.findViewById(viewId) : undefined;
|
|
3343
|
+
if (!viewId || !view || view.system) {
|
|
3344
|
+
void vscode.window.showInformationMessage('Built-in Spotlight views cannot be shared. Duplicate the view or create a custom view first.');
|
|
3345
|
+
break;
|
|
3346
|
+
}
|
|
3347
|
+
const bundle = message.bundle === 'scripts-only' ? 'scripts-only' : 'full';
|
|
3348
|
+
await vscode.commands.executeCommand('agentide.spotlight.publish', viewId, { bundle });
|
|
3349
|
+
break;
|
|
3350
|
+
}
|
|
3351
|
+
case 'installView':
|
|
3352
|
+
await vscode.commands.executeCommand('agentide.store.find', { typeFilter: 'view' });
|
|
3353
|
+
break;
|
|
3354
|
+
case 'expandSidebar': {
|
|
3355
|
+
await this.resizeSpotlightSidebar('expand');
|
|
3356
|
+
break;
|
|
3357
|
+
}
|
|
3358
|
+
case 'shrinkSidebar': {
|
|
3359
|
+
await this.resizeSpotlightSidebar('shrink');
|
|
3360
|
+
break;
|
|
3361
|
+
}
|
|
3362
|
+
case 'requestPanelState':
|
|
3363
|
+
await this.postPanelState(message.panelId);
|
|
3364
|
+
break;
|
|
3365
|
+
case 'savePanelState':
|
|
3366
|
+
await this.savePanelState(message.panelId, message.state);
|
|
3367
|
+
break;
|
|
3368
|
+
case 'requestArtifactContent':
|
|
3369
|
+
await this.postArtifactContent(message.path, message.format);
|
|
3370
|
+
break;
|
|
3371
|
+
case 'requestViewExportSnapshot':
|
|
3372
|
+
await this.postViewExportSnapshot({ respondWhenMissing: true });
|
|
3373
|
+
break;
|
|
3374
|
+
case 'openChat':
|
|
3375
|
+
await this.openVsCodeChat();
|
|
3376
|
+
break;
|
|
3377
|
+
case 'openChatWithPrompt':
|
|
3378
|
+
await this.openVsCodeChat({ prompt: message.prompt });
|
|
3379
|
+
break;
|
|
3380
|
+
case 'copySessionId': {
|
|
3381
|
+
const sessionId = message.sessionId?.trim() ?? '';
|
|
3382
|
+
if (!sessionId) {
|
|
3383
|
+
break;
|
|
3384
|
+
}
|
|
3385
|
+
await vscode.env.clipboard.writeText(sessionId);
|
|
3386
|
+
this.webviewView?.webview.postMessage({
|
|
3387
|
+
type: 'sessionCopied',
|
|
3388
|
+
sessionId,
|
|
3389
|
+
});
|
|
3390
|
+
break;
|
|
3391
|
+
}
|
|
3392
|
+
case 'requestClipboardText': {
|
|
3393
|
+
const requestId = message.requestId?.trim() ?? '';
|
|
3394
|
+
if (!requestId) {
|
|
3395
|
+
break;
|
|
3396
|
+
}
|
|
3397
|
+
const text = await vscode.env.clipboard.readText();
|
|
3398
|
+
const responsePayload = {
|
|
3399
|
+
type: 'clipboardText',
|
|
3400
|
+
requestId,
|
|
3401
|
+
text,
|
|
3402
|
+
};
|
|
3403
|
+
this.webviewView?.webview.postMessage(responsePayload);
|
|
3404
|
+
for (const panel of this.htmlPreviewPanels.values()) {
|
|
3405
|
+
void panel.webview.postMessage(responsePayload);
|
|
3406
|
+
}
|
|
3407
|
+
break;
|
|
3408
|
+
}
|
|
3409
|
+
case 'writeClipboardText': {
|
|
3410
|
+
const text = typeof message.text === 'string' ? message.text : '';
|
|
3411
|
+
await vscode.env.clipboard.writeText(text);
|
|
3412
|
+
break;
|
|
3413
|
+
}
|
|
3414
|
+
case 'updateFleetCardTitle':
|
|
3415
|
+
await this.handleFleetCardTitleUpdate(message.sourceId, message.sessionId, message.cardTitle);
|
|
3416
|
+
break;
|
|
3417
|
+
case 'disconnectFleetChannel':
|
|
3418
|
+
await this.handleFleetDisconnectRequest(message.sessionId, message.channelType, message.channelTarget);
|
|
3419
|
+
break;
|
|
3420
|
+
case 'openSpotlightForSession':
|
|
3421
|
+
await this.openSpotlightForSession(message.sessionId, message.channelType, message.channelTarget, message.model, message.engine, message.agentId);
|
|
3422
|
+
break;
|
|
3423
|
+
case 'openArtifact': {
|
|
3424
|
+
const targetPath = message.path?.trim() ?? '';
|
|
3425
|
+
if (!targetPath) {
|
|
3426
|
+
break;
|
|
3427
|
+
}
|
|
3428
|
+
const normalizedTarget = path.resolve(targetPath);
|
|
3429
|
+
if (normalizedTarget.toLowerCase().endsWith('.html')) {
|
|
3430
|
+
await this.openHtmlArtifactPreview(normalizedTarget);
|
|
3431
|
+
break;
|
|
3432
|
+
}
|
|
3433
|
+
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(normalizedTarget));
|
|
3434
|
+
break;
|
|
3435
|
+
}
|
|
3436
|
+
case 'deleteArtifact':
|
|
3437
|
+
try {
|
|
3438
|
+
const artifactPath = message.path.trim();
|
|
3439
|
+
if (artifactPath &&
|
|
3440
|
+
this.isPathWithinDir(artifactPath, this.artifactManager.getChartDir())) {
|
|
3441
|
+
await this.disableChartByFilename(path.basename(artifactPath));
|
|
3442
|
+
}
|
|
3443
|
+
await vscode.workspace.fs.delete(vscode.Uri.file(message.path));
|
|
3444
|
+
this.webviewView?.webview.postMessage({
|
|
3445
|
+
type: 'artifactDeleted',
|
|
3446
|
+
path: message.path,
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
catch (err) {
|
|
3450
|
+
this.outputChannel.appendLine(`[Dashboard] Failed to delete artifact: ${err}`);
|
|
3451
|
+
}
|
|
3452
|
+
break;
|
|
3453
|
+
case 'cancelGeneration':
|
|
3454
|
+
this.generationAbortController?.abort();
|
|
3455
|
+
break;
|
|
3456
|
+
case 'retryGeneration':
|
|
3457
|
+
await this.refreshCharts();
|
|
3458
|
+
break;
|
|
3459
|
+
case 'refreshDefaults':
|
|
3460
|
+
case 'refreshCharts':
|
|
3461
|
+
await this.refreshDashboardData();
|
|
3462
|
+
await this.refreshCharts({ viewId: this.getActiveViewId() });
|
|
3463
|
+
break;
|
|
3464
|
+
case 'updateFilters':
|
|
3465
|
+
this.agentFilterId = message.agentId?.trim() || undefined;
|
|
3466
|
+
await this.refreshAfterViewChange();
|
|
3467
|
+
break;
|
|
3468
|
+
case 'resetChartsHome':
|
|
3469
|
+
await this.resetChartsAndScripts('home');
|
|
3470
|
+
break;
|
|
3471
|
+
case 'resetChartsWorkspace':
|
|
3472
|
+
await this.resetChartsAndScripts('workspace');
|
|
3473
|
+
break;
|
|
3474
|
+
case 'reenableAllCharts':
|
|
3475
|
+
await this.reenableAllDisabledCharts();
|
|
3476
|
+
break;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
// -------------------------------------------------------------------------
|
|
3480
|
+
// Reset & Re-enable Operations
|
|
3481
|
+
// -------------------------------------------------------------------------
|
|
3482
|
+
/**
|
|
3483
|
+
* Reset charts and scripts for either the home analytics folder or the
|
|
3484
|
+
* current workspace. This deletes generated charts, user scripts, manifest,
|
|
3485
|
+
* panel state, and disabled-charts config, then regenerates defaults.
|
|
3486
|
+
*/
|
|
3487
|
+
async resetChartsAndScripts(target) {
|
|
3488
|
+
const analyticsRoot = this.artifactManager.getAnalyticsRoot();
|
|
3489
|
+
this.outputChannel.appendLine(`[Dashboard] Reset requested for: ${target}`);
|
|
3490
|
+
try {
|
|
3491
|
+
// Determine which directories to clear
|
|
3492
|
+
const dirsToDelete = [];
|
|
3493
|
+
if (target === 'home') {
|
|
3494
|
+
// Home = global analytics root (under ~/.agent-hippo/analytics)
|
|
3495
|
+
// Clear: panel/, views/, panel-state/, disabled-charts.yaml.
|
|
3496
|
+
dirsToDelete.push(path.join(analyticsRoot, 'panel'), path.join(analyticsRoot, 'views'), path.join(analyticsRoot, 'panel-state'));
|
|
3497
|
+
}
|
|
3498
|
+
else {
|
|
3499
|
+
// Workspace reset = current view root only (views/<viewId>/)
|
|
3500
|
+
const viewRoot = this.getViewRoot();
|
|
3501
|
+
dirsToDelete.push(path.join(viewRoot, 'charts'), path.join(viewRoot, 'reports'), path.join(viewRoot, 'exports'));
|
|
3502
|
+
// Also delete the manifest for this view
|
|
3503
|
+
const manifestPath = path.join(viewRoot, 'manifest.json');
|
|
3504
|
+
try {
|
|
3505
|
+
await fs_1.promises.unlink(manifestPath);
|
|
3506
|
+
}
|
|
3507
|
+
catch { /* ok if missing */ }
|
|
3508
|
+
}
|
|
3509
|
+
// Delete directories recursively
|
|
3510
|
+
for (const dir of dirsToDelete) {
|
|
3511
|
+
try {
|
|
3512
|
+
await fs_1.promises.rm(dir, { recursive: true, force: true });
|
|
3513
|
+
this.outputChannel.appendLine(`[Dashboard] Deleted: ${dir}`);
|
|
3514
|
+
}
|
|
3515
|
+
catch (err) {
|
|
3516
|
+
this.outputChannel.appendLine(`[Dashboard] Could not delete ${dir}: ${err}`);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
// For home reset, also delete the disabled-charts.yaml config
|
|
3520
|
+
if (target === 'home') {
|
|
3521
|
+
const configPath = this.getDisabledChartsConfigPath();
|
|
3522
|
+
try {
|
|
3523
|
+
await fs_1.promises.unlink(configPath);
|
|
3524
|
+
}
|
|
3525
|
+
catch { /* ok if missing */ }
|
|
3526
|
+
}
|
|
3527
|
+
// Re-seed workspace scripts and regenerate defaults
|
|
3528
|
+
await this.seedWorkspaceScripts();
|
|
3529
|
+
await this.artifactManager.ensureArtifactDir();
|
|
3530
|
+
// Refresh the webview
|
|
3531
|
+
this.queryRunner.invalidateCache();
|
|
3532
|
+
const bootstrapPromise = this.fetchDashboardBootstrapData();
|
|
3533
|
+
await this.loadAndSendArtifacts();
|
|
3534
|
+
const bootstrap = await bootstrapPromise;
|
|
3535
|
+
await this.postFilterState(bootstrap?.agentIds);
|
|
3536
|
+
this.triggerSummaryCardsUpdate(bootstrap?.summary);
|
|
3537
|
+
// Run the full script set so all widgets are generated after reset
|
|
3538
|
+
await this.refreshCharts();
|
|
3539
|
+
vscode.window.showInformationMessage(target === 'home'
|
|
3540
|
+
? 'Analytics reset complete (home folder)'
|
|
3541
|
+
: 'Analytics reset complete (workspace)');
|
|
3542
|
+
}
|
|
3543
|
+
catch (err) {
|
|
3544
|
+
this.outputChannel.appendLine(`[Dashboard] Reset failed: ${err}`);
|
|
3545
|
+
vscode.window.showErrorMessage(`Reset failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
/**
|
|
3549
|
+
* Get the current view root directory path (e.g. .../analytics/views/fleet).
|
|
3550
|
+
*/
|
|
3551
|
+
getViewRoot() {
|
|
3552
|
+
return this.artifactManager.resolveViewRoot({
|
|
3553
|
+
scope: this.getActiveView()?.scope ?? 'global',
|
|
3554
|
+
projectId: this.resolveWorkspaceProjectId(),
|
|
3555
|
+
agentId: this.agentFilterId,
|
|
3556
|
+
viewId: this.getActiveViewId(),
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Re-enable all disabled charts/scripts by clearing those lists in disabled-charts.yaml.
|
|
3561
|
+
* Also clears any hidden state in the panel-state.
|
|
3562
|
+
*/
|
|
3563
|
+
async reenableAllDisabledCharts() {
|
|
3564
|
+
this.outputChannel.appendLine('[Dashboard] Re-enabling all disabled charts');
|
|
3565
|
+
try {
|
|
3566
|
+
// Clear disabled script/chart blocks while preserving galleryIgnore patterns.
|
|
3567
|
+
const disabledConfig = await this.loadDisabledChartsConfig();
|
|
3568
|
+
await this.saveDisabledChartsConfig({
|
|
3569
|
+
scriptsByView: {},
|
|
3570
|
+
charts: [],
|
|
3571
|
+
galleryIgnore: disabledConfig.galleryIgnore,
|
|
3572
|
+
});
|
|
3573
|
+
await this.postDisabledChartsState({
|
|
3574
|
+
...disabledConfig,
|
|
3575
|
+
scriptsByView: {},
|
|
3576
|
+
charts: [],
|
|
3577
|
+
});
|
|
3578
|
+
// Also clear hidden state in panel state for the default panel
|
|
3579
|
+
const statePath = this.getPanelStatePath('default');
|
|
3580
|
+
try {
|
|
3581
|
+
const raw = await fs_1.promises.readFile(statePath, 'utf-8');
|
|
3582
|
+
const parsed = JSON.parse(raw);
|
|
3583
|
+
if (parsed.charts && typeof parsed.charts === 'object') {
|
|
3584
|
+
// Set all hidden flags to false
|
|
3585
|
+
let changed = false;
|
|
3586
|
+
for (const key of Object.keys(parsed.charts)) {
|
|
3587
|
+
if (parsed.charts[key]?.hidden === true) {
|
|
3588
|
+
parsed.charts[key].hidden = false;
|
|
3589
|
+
changed = true;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
if (changed) {
|
|
3593
|
+
await fs_1.promises.writeFile(statePath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf-8');
|
|
3594
|
+
await this.postPanelState('default');
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
catch { /* ok if missing or malformed */ }
|
|
3599
|
+
// Refresh the gallery
|
|
3600
|
+
await this.loadAndSendArtifacts();
|
|
3601
|
+
vscode.window.showInformationMessage('All hidden charts have been re-enabled');
|
|
3602
|
+
}
|
|
3603
|
+
catch (err) {
|
|
3604
|
+
this.outputChannel.appendLine(`[Dashboard] Re-enable charts failed: ${err}`);
|
|
3605
|
+
vscode.window.showErrorMessage(`Failed to re-enable charts: ${err instanceof Error ? err.message : String(err)}`);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
// -------------------------------------------------------------------------
|
|
3609
|
+
// VS Code Chat Integration
|
|
3610
|
+
// -------------------------------------------------------------------------
|
|
3611
|
+
buildAnalyticsPrimer(userPrompt) {
|
|
3612
|
+
return `Use spotlight skills as needed. ${userPrompt?.trim() || ''}`;
|
|
3613
|
+
}
|
|
3614
|
+
async openVsCodeChat(options) {
|
|
3615
|
+
this.syncArtifactContext();
|
|
3616
|
+
// Ensure artifact directories exist before opening chat
|
|
3617
|
+
await this.artifactManager.ensureArtifactDir();
|
|
3618
|
+
// Set environment variables so agent knows where to save charts and scripts
|
|
3619
|
+
process.env.AGENTIDE_CHART_DIR = this.artifactManager.getChartDir();
|
|
3620
|
+
process.env.AGENTIDE_REPORT_DIR = this.artifactManager.getReportDir();
|
|
3621
|
+
process.env.AGENTIDE_EXPORT_DIR = this.artifactManager.getExportDir();
|
|
3622
|
+
process.env.AGENTIDE_IMAGE_DIR = this.artifactManager.getImageDir();
|
|
3623
|
+
process.env.AGENTIDE_ASSET_DIR = this.artifactManager.getAssetDir();
|
|
3624
|
+
process.env.AGENTIDE_VIEW_ROOT = this.artifactManager.getViewRoot();
|
|
3625
|
+
process.env.AGENTIDE_VIEW_ID = this.getActiveViewId();
|
|
3626
|
+
process.env.AGENTIDE_SCRIPT_DIR = this.artifactManager.getScriptDir();
|
|
3627
|
+
process.env.AGENTIDE_TRACES_DIR = path.join((0, index_1.getAgentideHome)(), 'analytics', 'traces');
|
|
3628
|
+
const filters = this.getActiveFilters();
|
|
3629
|
+
process.env.AGENTIDE_ANALYTICS_SCOPE = filters.scope ?? 'global';
|
|
3630
|
+
process.env.AGENTIDE_ANALYTICS_PROJECT_ID = filters.scope === 'workspace'
|
|
3631
|
+
? (filters.projectId ?? '')
|
|
3632
|
+
: '';
|
|
3633
|
+
process.env.AGENTIDE_ANALYTICS_AGENT_ID = filters.agentId ?? '';
|
|
3634
|
+
// Always open a dedicated session for analytics flows.
|
|
3635
|
+
await vscode.commands.executeCommand('workbench.action.chat.newChat', {
|
|
3636
|
+
agentMode: true,
|
|
3637
|
+
});
|
|
3638
|
+
const fullPrompt = this.buildAnalyticsPrimer(options?.prompt);
|
|
3639
|
+
const sendImmediately = options?.sendImmediately ?? false;
|
|
3640
|
+
// Open chat without participant mention and prefill by default.
|
|
3641
|
+
// NOTE: the open command does not currently expose a direct title field,
|
|
3642
|
+
// so we lead with "Agent Analytics" in the prompt and use a fresh session.
|
|
3643
|
+
await vscode.commands.executeCommand('workbench.action.chat.open', {
|
|
3644
|
+
mode: 'agent',
|
|
3645
|
+
query: fullPrompt,
|
|
3646
|
+
isPartialQuery: !sendImmediately,
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
// -------------------------------------------------------------------------
|
|
3650
|
+
// HTML Content
|
|
3651
|
+
// -------------------------------------------------------------------------
|
|
3652
|
+
getWebviewPath() {
|
|
3653
|
+
// Prefer out/ (used after full compile); fall back to src/ when only tsc watch was run so panel.js and bridge load
|
|
3654
|
+
const outPath = path.join(this.extensionContext.extensionPath, 'out', 'analytics', 'dashboard', 'webview', 'panel.js');
|
|
3655
|
+
return (0, fs_1.existsSync)(outPath) ? 'out/analytics/dashboard/webview' : 'src/analytics/dashboard/webview';
|
|
3656
|
+
}
|
|
3657
|
+
/** Share toolbar control; injected when user's panel.html lacks it (e.g. old seeded copy). */
|
|
3658
|
+
getShareViewButtonFragment() {
|
|
3659
|
+
return `
|
|
3660
|
+
<button id="share-view-btn" class="toolbar-btn icon-only" title="Publish current view to store (full bundle)" aria-label="Publish current view to store (full bundle)">
|
|
3661
|
+
<span class="btn-icon" aria-hidden="true">↑</span>
|
|
3662
|
+
</button>`;
|
|
3663
|
+
}
|
|
3664
|
+
/** Install toolbar control; injected when user's panel.html lacks it (e.g. old seeded copy). */
|
|
3665
|
+
getInstallViewButtonFragment() {
|
|
3666
|
+
return `
|
|
3667
|
+
<button id="install-view-btn" class="toolbar-btn icon-only" title="Install Spotlight view from store" aria-label="Install Spotlight view from store">
|
|
3668
|
+
<span class="btn-icon" aria-hidden="true">↓</span>
|
|
3669
|
+
</button>`;
|
|
3670
|
+
}
|
|
3671
|
+
getShareScriptsOnlyMenuFragment() {
|
|
3672
|
+
return `
|
|
3673
|
+
<button id="share-scripts-only-menu-item" class="dropdown-item" role="menuitem" data-action="share-scripts-only">
|
|
3674
|
+
<span class="dropdown-icon">↑</span>
|
|
3675
|
+
Share scripts only to store
|
|
3676
|
+
</button>`;
|
|
3677
|
+
}
|
|
3678
|
+
patchPanelHtmlForShareButton(panelHtml) {
|
|
3679
|
+
let html = panelHtml;
|
|
3680
|
+
if (!html.includes('id="share-scripts-only-menu-item')) {
|
|
3681
|
+
const editScriptsMatch = html.match(/<button class="dropdown-item"[^>]*data-action="edit-view-scripts"[^>]*>[\s\S]*?<\/button>/);
|
|
3682
|
+
if (editScriptsMatch && editScriptsMatch.index !== undefined) {
|
|
3683
|
+
const insertAt = editScriptsMatch.index + editScriptsMatch[0].length;
|
|
3684
|
+
html = `${html.slice(0, insertAt)}${this.getShareScriptsOnlyMenuFragment()}${html.slice(insertAt)}`;
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
if (!html.includes('id="share-view-btn"')) {
|
|
3688
|
+
const refreshMatch = html.match(/<button[^>]*id="refresh-charts-btn"[^>]*>[\s\S]*?<\/button>/);
|
|
3689
|
+
if (refreshMatch && refreshMatch.index !== undefined) {
|
|
3690
|
+
const insertAt = refreshMatch.index + refreshMatch[0].length;
|
|
3691
|
+
html = `${html.slice(0, insertAt)}${this.getShareViewButtonFragment()}${html.slice(insertAt)}`;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
if (!html.includes('id="install-view-btn"')) {
|
|
3695
|
+
const shareMatch = html.match(/<button[^>]*id="share-view-btn"[^>]*>[\s\S]*?<\/button>/);
|
|
3696
|
+
if (shareMatch && shareMatch.index !== undefined) {
|
|
3697
|
+
const insertAt = shareMatch.index + shareMatch[0].length;
|
|
3698
|
+
html = `${html.slice(0, insertAt)}${this.getInstallViewButtonFragment()}${html.slice(insertAt)}`;
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
// Normalize share icon and keep share to the right of refresh.
|
|
3702
|
+
html = html.replace(/(<button[^>]*id="share-view-btn"[^>]*>)\s*<span class="btn-icon"[^>]*>[\s\S]*?<\/span>/, '$1<span class="btn-icon" aria-hidden="true">↑</span>');
|
|
3703
|
+
html = html.replace(/(<button[^>]*id="install-view-btn"[^>]*>)\s*<span class="btn-icon"[^>]*>[\s\S]*?<\/span>/, '$1<span class="btn-icon" aria-hidden="true">↓</span>');
|
|
3704
|
+
const shareBeforeRefresh = /<button[^>]*id="share-view-btn"[^>]*>[\s\S]*?<\/button>\s*<button[^>]*id="refresh-charts-btn"/.test(html);
|
|
3705
|
+
if (shareBeforeRefresh) {
|
|
3706
|
+
html = html.replace(/(<button[^>]*id="share-view-btn"[^>]*>[\s\S]*?<\/button>)\s*(<button[^>]*id="refresh-charts-btn"[^>]*>[\s\S]*?<\/button>)/, '$2$1');
|
|
3707
|
+
}
|
|
3708
|
+
return html;
|
|
3709
|
+
}
|
|
3710
|
+
async patchUserPanelHtmlOnDisk() {
|
|
3711
|
+
const userPanelHtml = path.join(this.getPanelDir(), 'panel.html');
|
|
3712
|
+
if (!(0, fs_1.existsSync)(userPanelHtml)) {
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
try {
|
|
3716
|
+
const current = (0, fs_1.readFileSync)(userPanelHtml, 'utf-8');
|
|
3717
|
+
const patched = this.patchPanelHtmlForShareButton(current);
|
|
3718
|
+
if (patched !== current) {
|
|
3719
|
+
await fs_1.promises.writeFile(userPanelHtml, patched, 'utf-8');
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
catch (err) {
|
|
3723
|
+
this.outputChannel.appendLine(`[Dashboard] Failed patching panel.html for share button: ${err}`);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
/** Modal HTML for "New view name"; injected when user's panel.html lacks it (e.g. old seeded copy). */
|
|
3727
|
+
getNewViewModalFragment() {
|
|
3728
|
+
return `
|
|
3729
|
+
<!-- New view name modal (injected so Add view works without window.prompt) -->
|
|
3730
|
+
<div id="new-view-modal" class="modal-overlay" role="dialog" aria-labelledby="new-view-modal-title" aria-modal="true" aria-hidden="true" style="display: none;">
|
|
3731
|
+
<div class="modal-content">
|
|
3732
|
+
<h2 id="new-view-modal-title" class="modal-title">New view name</h2>
|
|
3733
|
+
<input type="text" id="new-view-name-input" class="modal-input" placeholder="View name" autocomplete="off" />
|
|
3734
|
+
<div class="modal-actions">
|
|
3735
|
+
<button type="button" id="new-view-modal-cancel" class="modal-btn modal-btn-secondary">Cancel</button>
|
|
3736
|
+
<button type="button" id="new-view-modal-ok" class="modal-btn modal-btn-primary">OK</button>
|
|
3737
|
+
</div>
|
|
3738
|
+
</div>
|
|
3739
|
+
</div>`;
|
|
3740
|
+
}
|
|
3741
|
+
getHtmlContent(webview) {
|
|
3742
|
+
const nonce = getNonce();
|
|
3743
|
+
const webviewBasePath = this.getWebviewPath();
|
|
3744
|
+
// Runtime bridge - always loaded from the extension bundle
|
|
3745
|
+
const bridgeUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionContext.extensionUri, webviewBasePath, 'runtime-bridge.js'));
|
|
3746
|
+
// Panel files - prefer user panel dir for HTML/CSS; always use bundled panel.js so refresh/expand/shrink and message handling are up to date
|
|
3747
|
+
const panelDir = this.getPanelDir();
|
|
3748
|
+
const userPanelCss = path.join(panelDir, 'panel.css');
|
|
3749
|
+
const userPanelHtml = path.join(panelDir, 'panel.html');
|
|
3750
|
+
const panelJsUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionContext.extensionUri, webviewBasePath, 'panel.js'));
|
|
3751
|
+
const panelCssUri = (0, fs_1.existsSync)(userPanelCss)
|
|
3752
|
+
? webview.asWebviewUri(vscode.Uri.file(userPanelCss))
|
|
3753
|
+
: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionContext.extensionUri, webviewBasePath, 'styles.css'));
|
|
3754
|
+
// Read panel HTML body: user override → bundled default → empty
|
|
3755
|
+
let panelHtml = '';
|
|
3756
|
+
if ((0, fs_1.existsSync)(userPanelHtml)) {
|
|
3757
|
+
try {
|
|
3758
|
+
panelHtml = (0, fs_1.readFileSync)(userPanelHtml, 'utf-8');
|
|
3759
|
+
}
|
|
3760
|
+
catch { /* fall through */ }
|
|
3761
|
+
}
|
|
3762
|
+
if (!panelHtml) {
|
|
3763
|
+
const bundledHtmlPath = path.join(this.extensionContext.extensionPath, webviewBasePath, 'analytics-panel.html');
|
|
3764
|
+
try {
|
|
3765
|
+
panelHtml = (0, fs_1.readFileSync)(bundledHtmlPath, 'utf-8');
|
|
3766
|
+
}
|
|
3767
|
+
catch { /* panel.js will build DOM itself */ }
|
|
3768
|
+
}
|
|
3769
|
+
panelHtml = this.patchPanelHtmlForShareButton(panelHtml);
|
|
3770
|
+
// Ensure "Add view" modal is present (user may have old seeded panel.html without it)
|
|
3771
|
+
const newViewModalFragment = this.getNewViewModalFragment();
|
|
3772
|
+
if (newViewModalFragment && !panelHtml.includes('id="new-view-modal"')) {
|
|
3773
|
+
// Append before closing </div> of #app so modal is inside #app
|
|
3774
|
+
const appClose = panelHtml.lastIndexOf('</div>');
|
|
3775
|
+
if (appClose !== -1) {
|
|
3776
|
+
panelHtml = panelHtml.slice(0, appClose) + newViewModalFragment + '\n\t' + panelHtml.slice(appClose);
|
|
3777
|
+
}
|
|
3778
|
+
else {
|
|
3779
|
+
panelHtml = panelHtml + newViewModalFragment;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
const bodyClass = this.isTeamSurface() ? ' class="spotlight-surface-team"' : '';
|
|
3783
|
+
return `<!DOCTYPE html>
|
|
3784
|
+
<html lang="en">
|
|
3785
|
+
<head>
|
|
3786
|
+
<meta charset="UTF-8">
|
|
3787
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3788
|
+
<meta http-equiv="Content-Security-Policy" content="
|
|
3789
|
+
default-src 'none';
|
|
3790
|
+
style-src ${webview.cspSource} 'unsafe-inline';
|
|
3791
|
+
script-src 'nonce-${nonce}';
|
|
3792
|
+
img-src ${webview.cspSource} data:;
|
|
3793
|
+
frame-src ${webview.cspSource};
|
|
3794
|
+
font-src ${webview.cspSource};
|
|
3795
|
+
">
|
|
3796
|
+
<link href="${panelCssUri}" rel="stylesheet">
|
|
3797
|
+
<title>Spotlight Dashboard</title>
|
|
3798
|
+
<style nonce="${nonce}">.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;z-index:1000}.modal-content{background:var(--vscode-editor-background);border:1px solid var(--vscode-panel-border);border-radius:8px;padding:16px;min-width:280px;box-shadow:0 4px 24px rgba(0,0,0,0.25)}.modal-title{font-size:1em;font-weight:600;margin:0 0 12px 0;color:var(--vscode-foreground)}.modal-input{width:100%;padding:8px 10px;margin-bottom:14px;font-family:var(--vscode-font-family);font-size:var(--vscode-font-size);color:var(--vscode-input-foreground);background:var(--vscode-input-background);border:1px solid var(--vscode-input-border);border-radius:4px;box-sizing:border-box}.modal-input:focus{outline:none;border-color:var(--vscode-focusBorder)}.modal-actions{display:flex;justify-content:flex-end;gap:8px}.modal-btn{padding:6px 14px;font-size:0.9em;border-radius:4px;cursor:pointer;border:none}.modal-btn-primary{background:var(--vscode-button-background);color:var(--vscode-button-foreground)}.modal-btn-primary:hover{background:var(--vscode-button-hoverBackground)}.modal-btn-secondary{background:var(--vscode-button-secondaryBackground);color:var(--vscode-button-secondaryForeground)}.modal-btn-secondary:hover{background:var(--vscode-button-secondaryHoverBackground)}</style>
|
|
3799
|
+
</head>
|
|
3800
|
+
<body${bodyClass}>
|
|
3801
|
+
${panelHtml}
|
|
3802
|
+
<script nonce="${nonce}" src="${bridgeUri}"></script>
|
|
3803
|
+
<script nonce="${nonce}" src="${panelJsUri}"></script>
|
|
3804
|
+
<script nonce="${nonce}">
|
|
3805
|
+
(function(){
|
|
3806
|
+
var api = typeof window.panelApi !== 'undefined' ? window.panelApi : null;
|
|
3807
|
+
if (!api) return;
|
|
3808
|
+
var overlay = document.getElementById('new-view-modal');
|
|
3809
|
+
var input = overlay && document.getElementById('new-view-name-input');
|
|
3810
|
+
var okBtn = overlay && document.getElementById('new-view-modal-ok');
|
|
3811
|
+
var cancelBtn = overlay && document.getElementById('new-view-modal-cancel');
|
|
3812
|
+
if (!overlay || !input || !okBtn || !cancelBtn) return;
|
|
3813
|
+
function showNewViewModal(onResetSelect) {
|
|
3814
|
+
if (typeof onResetSelect === 'function') onResetSelect();
|
|
3815
|
+
input.value = '';
|
|
3816
|
+
overlay.style.display = 'flex';
|
|
3817
|
+
overlay.setAttribute('aria-hidden', 'false');
|
|
3818
|
+
input.focus();
|
|
3819
|
+
function hide() {
|
|
3820
|
+
overlay.style.display = 'none';
|
|
3821
|
+
overlay.setAttribute('aria-hidden', 'true');
|
|
3822
|
+
overlay.removeEventListener('click', handleOverlayClick);
|
|
3823
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
3824
|
+
}
|
|
3825
|
+
function submit() {
|
|
3826
|
+
var trimmed = input.value.trim();
|
|
3827
|
+
hide();
|
|
3828
|
+
if (trimmed) api.postMessage({ type: 'createView', name: trimmed });
|
|
3829
|
+
}
|
|
3830
|
+
function handleOverlayClick(e) { if (e.target === overlay) hide(); }
|
|
3831
|
+
function handleKeydown(e) {
|
|
3832
|
+
if (e.key === 'Escape') { hide(); e.preventDefault(); }
|
|
3833
|
+
else if (e.key === 'Enter') { submit(); e.preventDefault(); }
|
|
3834
|
+
}
|
|
3835
|
+
overlay.addEventListener('click', handleOverlayClick);
|
|
3836
|
+
document.addEventListener('keydown', handleKeydown);
|
|
3837
|
+
okBtn.onclick = submit;
|
|
3838
|
+
cancelBtn.onclick = hide;
|
|
3839
|
+
}
|
|
3840
|
+
window.showNewViewModal = showNewViewModal;
|
|
3841
|
+
window.promptCreateView = function() { showNewViewModal(); };
|
|
3842
|
+
// Rename/Delete: use host dialogs so they work when webview prompt/confirm are unavailable (e.g. old panel.js)
|
|
3843
|
+
document.querySelectorAll('[data-action="rename-view"], [data-action="delete-view"]').forEach(function(el) {
|
|
3844
|
+
el.addEventListener('click', function(e) {
|
|
3845
|
+
var action = this.getAttribute('data-action');
|
|
3846
|
+
if (action === 'rename-view') { api.postMessage({ type: 'requestRenameView' }); e.stopImmediatePropagation(); e.preventDefault(); }
|
|
3847
|
+
if (action === 'delete-view') { api.postMessage({ type: 'requestDeleteView' }); e.stopImmediatePropagation(); e.preventDefault(); }
|
|
3848
|
+
}, true);
|
|
3849
|
+
});
|
|
3850
|
+
})();
|
|
3851
|
+
</script>
|
|
3852
|
+
</body>
|
|
3853
|
+
</html>`;
|
|
3854
|
+
}
|
|
3855
|
+
// -------------------------------------------------------------------------
|
|
3856
|
+
// Disposal
|
|
3857
|
+
// -------------------------------------------------------------------------
|
|
3858
|
+
dispose() {
|
|
3859
|
+
this.artifactWatcher?.dispose();
|
|
3860
|
+
this.viewsConfigWatcher?.dispose();
|
|
3861
|
+
if (this.viewsConfigDebounceTimeout) {
|
|
3862
|
+
clearTimeout(this.viewsConfigDebounceTimeout);
|
|
3863
|
+
}
|
|
3864
|
+
for (const panel of this.htmlPreviewPanels.values()) {
|
|
3865
|
+
panel.dispose();
|
|
3866
|
+
}
|
|
3867
|
+
this.htmlPreviewPanels.clear();
|
|
3868
|
+
if (this.pendingFilterAutoGenerateTimer) {
|
|
3869
|
+
clearTimeout(this.pendingFilterAutoGenerateTimer);
|
|
3870
|
+
}
|
|
3871
|
+
this.disposables.forEach(d => d.dispose());
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
exports.AnalyticsDashboardProvider = AnalyticsDashboardProvider;
|
|
3875
|
+
// -----------------------------------------------------------------------------
|
|
3876
|
+
// Utilities
|
|
3877
|
+
// -----------------------------------------------------------------------------
|
|
3878
|
+
function getNonce() {
|
|
3879
|
+
let text = '';
|
|
3880
|
+
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
3881
|
+
for (let i = 0; i < 32; i++) {
|
|
3882
|
+
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
|
3883
|
+
}
|
|
3884
|
+
return text;
|
|
3885
|
+
}
|