@galaxyproject/orbit 0.0.0 → 0.9.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/CHANGELOG.md +123 -0
- package/LICENSE +21 -0
- package/README.md +760 -5
- package/bin/legacy-mcp-config.js +363 -0
- package/bin/orbit.js +810 -0
- package/bin/pi-extension-path.d.ts +4 -0
- package/bin/pi-extension-path.js +44 -0
- package/bin/provider-auth.d.ts +22 -0
- package/bin/provider-auth.js +77 -0
- package/bin/thinking-pref.js +38 -0
- package/bin/update-check.d.ts +15 -0
- package/bin/update-check.js +137 -0
- package/extensions/cli-update/index.ts +32 -0
- package/extensions/orbit/activity-hooks.ts +141 -0
- package/extensions/orbit/activity.ts +116 -0
- package/extensions/orbit/agent-dir.ts +11 -0
- package/extensions/orbit/auto-resume.ts +352 -0
- package/extensions/orbit/config.ts +3 -0
- package/extensions/orbit/confusables.ts +1 -0
- package/extensions/orbit/context.ts +957 -0
- package/extensions/orbit/dashboard-commands.ts +191 -0
- package/extensions/orbit/dashboard-store.ts +473 -0
- package/extensions/orbit/dashboard-tools.ts +1185 -0
- package/extensions/orbit/empty-completion.ts +99 -0
- package/extensions/orbit/evidence-gate.ts +1276 -0
- package/extensions/orbit/evidence-override-command.ts +294 -0
- package/extensions/orbit/exec-guard/bash-risk.ts +1408 -0
- package/extensions/orbit/exec-guard/gate.ts +181 -0
- package/extensions/orbit/exec-guard/guardian-config.ts +54 -0
- package/extensions/orbit/exec-guard/index.ts +1 -0
- package/extensions/orbit/exec-guard/model-tier.ts +36 -0
- package/extensions/orbit/exec-guard/path-jail.ts +54 -0
- package/extensions/orbit/exec-guard/policy.ts +342 -0
- package/extensions/orbit/exec-guard/runtime-state.ts +15 -0
- package/extensions/orbit/exec-guard/sensitive-read.ts +525 -0
- package/extensions/orbit/exec-guard/types.ts +42 -0
- package/extensions/orbit/execution-commands.ts +134 -0
- package/extensions/orbit/feedback-command.ts +81 -0
- package/extensions/orbit/feedback.ts +96 -0
- package/extensions/orbit/galaxy-api.test.ts +122 -0
- package/extensions/orbit/galaxy-api.ts +606 -0
- package/extensions/orbit/galaxy-artifact-link-guidance.ts +5 -0
- package/extensions/orbit/galaxy-destructive-gate.ts +113 -0
- package/extensions/orbit/galaxy-enrich.ts +1108 -0
- package/extensions/orbit/galaxy-failure.ts +101 -0
- package/extensions/orbit/galaxy-fixture.ts +126 -0
- package/extensions/orbit/galaxy-history-guard.ts +233 -0
- package/extensions/orbit/galaxy-job-block.ts +347 -0
- package/extensions/orbit/galaxy-job-ops.ts +288 -0
- package/extensions/orbit/galaxy-live-source.ts +929 -0
- package/extensions/orbit/galaxy-markdown-adapter.ts +248 -0
- package/extensions/orbit/galaxy-op-policy.ts +332 -0
- package/extensions/orbit/galaxy-ops-tools.ts +326 -0
- package/extensions/orbit/galaxy-output-store.ts +80 -0
- package/extensions/orbit/galaxy-output.ts +346 -0
- package/extensions/orbit/galaxy-page-binding.ts +222 -0
- package/extensions/orbit/galaxy-page-markdown-guidance.ts +15 -0
- package/extensions/orbit/galaxy-page-ops.ts +159 -0
- package/extensions/orbit/galaxy-page-sync.test.ts +417 -0
- package/extensions/orbit/galaxy-page-sync.ts +318 -0
- package/extensions/orbit/galaxy-pages-api.ts +203 -0
- package/extensions/orbit/galaxy-pages-sync.ts +409 -0
- package/extensions/orbit/galaxy-poll-guard.ts +189 -0
- package/extensions/orbit/galaxy-poller.ts +706 -0
- package/extensions/orbit/galaxy-provenance.ts +623 -0
- package/extensions/orbit/galaxy-reconcile.ts +956 -0
- package/extensions/orbit/galaxy-recovery.ts +176 -0
- package/extensions/orbit/galaxy-repeat-guard.ts +55 -0
- package/extensions/orbit/galaxy-submission-capture.ts +707 -0
- package/extensions/orbit/galaxy-submission.ts +502 -0
- package/extensions/orbit/galaxy-tool-source.ts +78 -0
- package/extensions/orbit/galaxy-tool-wrap.ts +234 -0
- package/extensions/orbit/galaxy-udt-block.ts +127 -0
- package/extensions/orbit/galaxy-upload-tus.ts +334 -0
- package/extensions/orbit/galaxy-upload.ts +300 -0
- package/extensions/orbit/galaxy-version-capture.ts +65 -0
- package/extensions/orbit/git.ts +173 -0
- package/extensions/orbit/harness-block-fields.ts +597 -0
- package/extensions/orbit/index.ts +593 -0
- package/extensions/orbit/init-gate.ts +284 -0
- package/extensions/orbit/instructions-command.ts +136 -0
- package/extensions/orbit/invocation-failure-hint.ts +57 -0
- package/extensions/orbit/iwc-plan-check.ts +94 -0
- package/extensions/orbit/lesson-command.ts +303 -0
- package/extensions/orbit/lesson-hint.ts +245 -0
- package/extensions/orbit/lesson-nudge.ts +106 -0
- package/extensions/orbit/lessons/compose.ts +177 -0
- package/extensions/orbit/lessons/counters.ts +120 -0
- package/extensions/orbit/lessons/enabled.ts +116 -0
- package/extensions/orbit/lessons/matcher.ts +346 -0
- package/extensions/orbit/lessons/paths.ts +286 -0
- package/extensions/orbit/lessons/pi-event-contract.ts +105 -0
- package/extensions/orbit/lessons/propose-replay.ts +103 -0
- package/extensions/orbit/lessons/propose-tool.ts +222 -0
- package/extensions/orbit/lessons/propose.ts +502 -0
- package/extensions/orbit/lessons/replay.ts +89 -0
- package/extensions/orbit/lessons/reproduction-index.ts +83 -0
- package/extensions/orbit/lessons/search-tool.ts +99 -0
- package/extensions/orbit/lessons/search.ts +136 -0
- package/extensions/orbit/lessons/store.ts +415 -0
- package/extensions/orbit/lessons/text.ts +36 -0
- package/extensions/orbit/lessons/types.ts +69 -0
- package/extensions/orbit/lessons/user-dir.ts +24 -0
- package/extensions/orbit/lessons/user-lesson.ts +88 -0
- package/extensions/orbit/lessons/wrapper.ts +61 -0
- package/extensions/orbit/lessons-command.ts +147 -0
- package/extensions/orbit/local-exec.ts +39 -0
- package/extensions/orbit/mcp-output.ts +179 -0
- package/extensions/orbit/mcp-servers.test.ts +24 -0
- package/extensions/orbit/mcp-servers.ts +25 -0
- package/extensions/orbit/model-script.ts +351 -0
- package/extensions/orbit/notebook-anchors.ts +342 -0
- package/extensions/orbit/notebook-writer.ts +863 -0
- package/extensions/orbit/observation-allowlists.ts +1109 -0
- package/extensions/orbit/observation-replay.ts +148 -0
- package/extensions/orbit/observation-triggers.ts +563 -0
- package/extensions/orbit/observation-ui.ts +175 -0
- package/extensions/orbit/observations-command.ts +421 -0
- package/extensions/orbit/observations-config.ts +200 -0
- package/extensions/orbit/observations.ts +1381 -0
- package/extensions/orbit/profiles.ts +270 -0
- package/extensions/orbit/progress-updates.ts +95 -0
- package/extensions/orbit/proposal-commands.ts +882 -0
- package/extensions/orbit/provenance-fs-file.ts +59 -0
- package/extensions/orbit/provenance-fs.ts +32 -0
- package/extensions/orbit/provenance-memory-fs.ts +59 -0
- package/extensions/orbit/record-store-file.ts +177 -0
- package/extensions/orbit/record-store-page.ts +77 -0
- package/extensions/orbit/record-store-seam.ts +47 -0
- package/extensions/orbit/record-store.ts +149 -0
- package/extensions/orbit/registry-carrier.ts +107 -0
- package/extensions/orbit/registry-eligibility.ts +95 -0
- package/extensions/orbit/registry-evaluation.ts +403 -0
- package/extensions/orbit/registry-evaluator.ts +519 -0
- package/extensions/orbit/registry-import.ts +91 -0
- package/extensions/orbit/registry-lock.ts +221 -0
- package/extensions/orbit/registry-memory-fs.ts +42 -0
- package/extensions/orbit/registry-page-carrier.ts +103 -0
- package/extensions/orbit/registry-proposal.ts +1348 -0
- package/extensions/orbit/registry-runtime.ts +234 -0
- package/extensions/orbit/registry-schema.ts +735 -0
- package/extensions/orbit/registry-submitter.ts +59 -0
- package/extensions/orbit/registry-templates.ts +346 -0
- package/extensions/orbit/registry.ts +476 -0
- package/extensions/orbit/sandbox/index.ts +120 -0
- package/extensions/orbit/sandbox/sandbox-bash.ts +93 -0
- package/extensions/orbit/sandbox/sandbox-config.ts +87 -0
- package/extensions/orbit/sandbox/sandbox-init-message.ts +28 -0
- package/extensions/orbit/secret-redaction.ts +247 -0
- package/extensions/orbit/session-index/cwd.ts +18 -0
- package/extensions/orbit/session-index/db.ts +56 -0
- package/extensions/orbit/session-index/indexer.ts +154 -0
- package/extensions/orbit/session-index/is-enabled.ts +17 -0
- package/extensions/orbit/session-index/parse.ts +254 -0
- package/extensions/orbit/session-index/query.ts +209 -0
- package/extensions/orbit/session-index/schema.ts +72 -0
- package/extensions/orbit/session-index/tools.ts +152 -0
- package/extensions/orbit/session-lifecycle.ts +321 -0
- package/extensions/orbit/skill-triggers.ts +159 -0
- package/extensions/orbit/skills-command.ts +48 -0
- package/extensions/orbit/skills-discovery.ts +435 -0
- package/extensions/orbit/skills.ts +84 -0
- package/extensions/orbit/sra-import-gate.ts +77 -0
- package/extensions/orbit/state.ts +265 -0
- package/extensions/orbit/submission-replay.ts +147 -0
- package/extensions/orbit/sync-command.ts +157 -0
- package/extensions/orbit/teams/critic-parser.ts +93 -0
- package/extensions/orbit/teams/dispatcher.ts +210 -0
- package/extensions/orbit/teams/is-enabled.ts +22 -0
- package/extensions/orbit/teams/tool.ts +187 -0
- package/extensions/orbit/teams/types.ts +57 -0
- package/extensions/orbit/teams/validate.ts +42 -0
- package/extensions/orbit/tester-id-command.ts +133 -0
- package/extensions/orbit/tools-sync.ts +279 -0
- package/extensions/orbit/tools.ts +1300 -0
- package/extensions/orbit/types.ts +41 -0
- package/extensions/orbit/ui-bridge.ts +55 -0
- package/extensions/orbit/ulid.ts +60 -0
- package/extensions/orbit/user-instructions.ts +388 -0
- package/extensions/orbit/vendor/skills/_catalog.json +68 -0
- package/extensions/orbit/vendor/skills/_manifest.json +1106 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/SKILL.md +112 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_provenance.json +218 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_required_tools.json +33 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/add.json +50 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-extract.json +51 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-next-step.json +42 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-validate.json +140 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/list.json +32 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/galaxy-tool-job-failure-reference.md +137 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-tool-summary.schema.json +175 -0
- package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/SKILL.md +82 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_provenance.json +133 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/cli/draft-validate.json +140 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/galaxy-workflow-draft-format.md +135 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/SKILL.md +84 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_provenance.json +166 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_verify.json +29 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/SKILL.md +127 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_provenance.json +198 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/cli/convert.json +123 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/galaxy-data-flow-draft-contract.md +129 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/SKILL.md +73 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_provenance.json +160 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.md +43 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.yml +1250 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-tool-job-failure-reference.md +137 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/nextflow-operators-to-galaxy-collection-recipes.md +98 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/SKILL.md +174 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_provenance.json +130 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_required_tools.json +18 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-revisions.json +48 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-search.json +95 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-versions.json +39 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/notes/component-tool-shed-search.md +230 -0
- package/extensions/orbit/vendor/skills/discover-shed-tool/references/schemas/galaxy-tool-discovery.schema.json +231 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/SKILL.md +93 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_provenance.json +201 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/planemo.md +44 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/validate-tests.json +40 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-test-data-conventions.md +308 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
- package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/SKILL.md +81 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_provenance.json +93 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
- package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/SKILL.md +81 -0
- package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/_assembly.json +23 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/SKILL.md +79 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_provenance.json +125 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/planemo.md +44 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/validate-tests.json +40 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
- package/extensions/orbit/vendor/skills/run-workflow-test/references/schemas/tests-format.schema.json +7328 -0
- package/extensions/orbit/vendor/skills/skills/LICENSE +21 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/README.md +39 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/SKILL.md +892 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/api-patterns.md +927 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/apply-rules.md +1482 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/test-patterns.md +996 -0
- package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/tools.md +609 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-data-libraries/SKILL.md +170 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/README.md +162 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/SKILL.md +99 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/README.md +119 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/tool-checking.md +419 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/workflow-testing.md +617 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/galaxy-integration.md +614 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/SKILL.md +144 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/average_col3.ipynb +68 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/extract_sample_metadata.ipynb +190 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/variant_annotation.ipynb +816 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/vcp_variant_map.ipynb +443 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/README.md +165 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/galaxy_tool_checker.py +521 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/SKILL.md +128 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/gotchas.md +135 -0
- package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/history-access.md +132 -0
- package/extensions/orbit/vendor/skills/skills/reproduciblify/SKILL.md +146 -0
- package/extensions/orbit/vendor/skills/skills/reproduciblify/references/directives.yml +496 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/SKILL.md +245 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/01-minimal-echo.yml +27 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/02-single-data-in-out.yml +27 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/03-multiple-data-js.yml +28 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/04-select-integer-boolean.yml +41 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/05-resource-requirements.yml +25 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/06-biocontainer-tool.yml +38 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/07-inline-python-script.yml +56 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/08-configfile-script.yml +74 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/common-mistakes.md +76 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/schema-reference.md +155 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/references/templating.md +196 -0
- package/extensions/orbit/vendor/skills/skills/udt-authoring/scripts/validate.py +109 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/README.md +71 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/SKILL.md +190 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/histology-staining.md +145 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/tissue-microarray-analysis.md +176 -0
- package/extensions/orbit/vendor/skills/skills/workflow-reports/references/directives.md +415 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/SKILL.md +98 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_provenance.json +93 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_required_tools.json +28 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_verify.json +17 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/convert.json +123 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/foundry.md +25 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/validate.json +142 -0
- package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/schemas/summary-galaxy-workflow.schema.json +687 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/SKILL.md +74 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_provenance.json +71 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_required_tools.json +16 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_verify.json +4 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/cli/validate.json +142 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
- package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-testability-design.md +136 -0
- package/extensions/orbit/vendor-skills.ts +283 -0
- package/extensions/orbit/workspace-state-dir.ts +46 -0
- package/extensions/orbit-handoff/index.ts +73 -0
- package/extensions/orbit-handoff/orbit-launcher.ts +142 -0
- package/extensions/whats-new/index.ts +98 -0
- package/lessons/LICENSE +17 -0
- package/lessons/README.md +117 -0
- package/lessons/SCHEMA.md +187 -0
- package/lessons/build-snapshot.mjs +311 -0
- package/lessons/data/downloaded-file-is-not-what-its-extension-says.md +61 -0
- package/lessons/galaxy-api/403-history-is-a-hard-stop.md +57 -0
- package/lessons/galaxy-api/collection-into-single-dataset-input.md +61 -0
- package/lessons/galaxy-api/connectedvalue-in-command-line.md +62 -0
- package/lessons/galaxy-api/hid-is-not-an-id.md +59 -0
- package/lessons/galaxy-api/invoke-workflow-inputs-not-params.md +60 -0
- package/lessons/galaxy-api/repeat-param-pipe-keys.md +63 -0
- package/lessons/galaxy-api/run-tool-returns-on-submit.md +61 -0
- package/lessons/galaxy-tools/reference-index-not-on-server.md +62 -0
- package/lessons/log.md +42 -0
- package/lessons/reproduction/input-population-mismatch.md +59 -0
- package/lessons/reproduction/methods-text-vs-executed-parameters.md +62 -0
- package/lessons/snapshot.json +1004 -0
- package/lessons/stats/de-contrast-direction-and-sample-labels.md +63 -0
- package/lessons/stats/na-coerced-to-zero-in-filters.md +59 -0
- package/lessons/validate.mjs +1219 -0
- package/package.json +103 -5
- package/shared/agent-prompt.d.ts +8 -0
- package/shared/agent-prompt.js +15 -0
- package/shared/approval-prompt.d.ts +4 -0
- package/shared/approval-prompt.js +61 -0
- package/shared/brain-env.d.ts +14 -0
- package/shared/brain-env.js +108 -0
- package/shared/brain-exit.d.ts +20 -0
- package/shared/brain-exit.js +149 -0
- package/shared/capability-profile.d.ts +16 -0
- package/shared/capability-profile.js +75 -0
- package/shared/config-source.d.ts +13 -0
- package/shared/config-source.js +17 -0
- package/shared/confusables.d.ts +3 -0
- package/shared/confusables.js +93 -0
- package/shared/custom-provider.d.ts +45 -0
- package/shared/custom-provider.js +169 -0
- package/shared/dashboard-contract.d.ts +123 -0
- package/shared/dashboard-contract.js +560 -0
- package/shared/dashboard-layout-store.d.ts +44 -0
- package/shared/dashboard-layout-store.js +281 -0
- package/shared/feedback-contract.d.ts +61 -0
- package/shared/feedback-contract.js +146 -0
- package/shared/galaxy-artifact-links.d.ts +30 -0
- package/shared/galaxy-artifact-links.js +166 -0
- package/shared/galaxy-credentials.d.ts +26 -0
- package/shared/galaxy-credentials.js +71 -0
- package/shared/galaxy-destructive.d.ts +21 -0
- package/shared/galaxy-destructive.js +269 -0
- package/shared/galaxy-guidance.d.ts +2 -0
- package/shared/galaxy-guidance.js +53 -0
- package/shared/galaxy-live-contract.d.ts +92 -0
- package/shared/galaxy-live-contract.js +180 -0
- package/shared/galaxy-page-cells.d.ts +2 -0
- package/shared/galaxy-page-cells.js +22 -0
- package/shared/galaxy-tool-family.d.ts +5 -0
- package/shared/galaxy-tool-family.js +27 -0
- package/shared/galaxy-url.d.ts +1 -0
- package/shared/galaxy-url.js +48 -0
- package/shared/gtn.d.ts +31 -0
- package/shared/gtn.js +253 -0
- package/shared/handled-prompt.d.ts +3 -0
- package/shared/handled-prompt.js +26 -0
- package/shared/known-provider.d.ts +11 -0
- package/shared/known-provider.js +89 -0
- package/shared/lesson-hint-marker.d.ts +7 -0
- package/shared/lesson-hint-marker.js +32 -0
- package/shared/lesson-rules.d.ts +150 -0
- package/shared/lesson-rules.js +203 -0
- package/shared/malformed-object-ids.d.ts +1 -0
- package/shared/malformed-object-ids.js +37 -0
- package/shared/markdown-sections.d.ts +10 -0
- package/shared/markdown-sections.js +118 -0
- package/shared/notebook-fences.d.ts +12 -0
- package/shared/notebook-fences.js +53 -0
- package/shared/observation-contract.d.ts +91 -0
- package/shared/observation-contract.js +544 -0
- package/shared/orbit-config.d.ts +226 -0
- package/shared/orbit-config.js +189 -0
- package/shared/orbit-env.d.ts +13 -0
- package/shared/orbit-env.js +124 -0
- package/shared/orbit-shell-contract.d.ts +74 -0
- package/shared/orbit-shell-contract.js +30 -0
- package/shared/page-content-hash.d.ts +1 -0
- package/shared/page-content-hash.js +13 -0
- package/shared/pi-default-model.d.ts +7 -0
- package/shared/pi-default-model.js +41 -0
- package/shared/prompt-blocks.d.ts +87 -0
- package/shared/prompt-blocks.js +898 -0
- package/shared/provider-auth-caps.d.ts +22 -0
- package/shared/provider-auth-caps.js +57 -0
- package/shared/random.d.ts +2 -0
- package/shared/random.js +15 -0
- package/shared/record-excerpt.d.ts +4 -0
- package/shared/record-excerpt.js +16 -0
- package/shared/redirect-guard.d.ts +36 -0
- package/shared/redirect-guard.js +235 -0
- package/shared/skills-pin.d.ts +8 -0
- package/shared/skills-pin.js +9 -0
- package/shared/skills-router.d.ts +17 -0
- package/shared/skills-router.js +63 -0
- package/shared/sra-import.d.ts +34 -0
- package/shared/sra-import.js +206 -0
- package/shared/state-dir.d.ts +21 -0
- package/shared/state-dir.js +240 -0
- package/shared/team-dispatch-contract.d.ts +28 -0
- package/shared/team-dispatch-contract.js +1 -0
- package/shared/truncate-middle.d.ts +11 -0
- package/shared/truncate-middle.js +68 -0
- package/shared/version-compare.d.ts +10 -0
- package/shared/version-compare.js +74 -0
- package/shared/whats-new.d.ts +31 -0
- package/shared/whats-new.js +83 -0
- package/shared/wsl.d.ts +10 -0
- package/shared/wsl.js +24 -0
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard tools -- how "show me my running jobs next to the plan" becomes a
|
|
3
|
+
* layout.
|
|
4
|
+
*
|
|
5
|
+
* Two tools: one reads the layout document, one changes it. Both are
|
|
6
|
+
* shell-neutral. The document is the same JSON the shells persist, validated by
|
|
7
|
+
* the same `shared/dashboard-contract` validator, and writing it IS the
|
|
8
|
+
* transport (see `dashboard-store.ts`) -- there is no separate widget message
|
|
9
|
+
* for layout, and in the CLI the write simply happens with nothing attached to
|
|
10
|
+
* read it.
|
|
11
|
+
*
|
|
12
|
+
* Two rules shape everything here, both from the curation design:
|
|
13
|
+
*
|
|
14
|
+
* 1. The agent changes the dashboard when the user asks, and never on its own
|
|
15
|
+
* initiative. That is stated in the tool description, where the model
|
|
16
|
+
* reads it.
|
|
17
|
+
* 2. A panel the user placed or pinned is not the agent's to remove, move or
|
|
18
|
+
* rewrite. `provenanceViolations` enforces that on the finished document,
|
|
19
|
+
* so it holds for a wholesale replace exactly as it does for a patch.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
24
|
+
import { Type } from "@sinclair/typebox";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
import {
|
|
27
|
+
DASHBOARD_PRESETS,
|
|
28
|
+
KNOWN_WIDGET_TYPES,
|
|
29
|
+
MAX_DASHBOARDS,
|
|
30
|
+
MAX_PANELS,
|
|
31
|
+
dashboardFromPreset,
|
|
32
|
+
parseDashboardDocument,
|
|
33
|
+
validateDashboardDocument,
|
|
34
|
+
} from "../../shared/dashboard-contract.js";
|
|
35
|
+
import type {
|
|
36
|
+
Dashboard,
|
|
37
|
+
DashboardDocument,
|
|
38
|
+
DashboardPanel,
|
|
39
|
+
DashboardProblem,
|
|
40
|
+
} from "../../shared/dashboard-contract.js";
|
|
41
|
+
import { appendActivityEvent } from "./activity";
|
|
42
|
+
import { readDashboardDocument, updateDashboardDocument } from "./dashboard-store";
|
|
43
|
+
import { capabilityProfile } from "../../shared/capability-profile.js";
|
|
44
|
+
|
|
45
|
+
/** More than this in one call is a rewrite, and a rewrite should say so. */
|
|
46
|
+
const MAX_ACTIONS = 20;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How much of the document `dashboard_read` will put in front of the model.
|
|
50
|
+
*
|
|
51
|
+
* The file may be up to 256 KB and a panel config can hold whatever a previous
|
|
52
|
+
* write put there, so returning it whole would let one layout eat the context
|
|
53
|
+
* window. Past this the summary carries the panel ids, which is what a write
|
|
54
|
+
* actually needs.
|
|
55
|
+
*/
|
|
56
|
+
export const MAX_READ_CHARS = 20_000;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* And how much of everything else.
|
|
60
|
+
*
|
|
61
|
+
* Swapping the document out for the summary bounds nothing on its own: the
|
|
62
|
+
* summary is built from ids and titles the agent itself writes into the file,
|
|
63
|
+
* and nothing caps their length, so a 261 KB layout came back as a 170,866-
|
|
64
|
+
* character tool result of which 170,014 was summary. The validation problems
|
|
65
|
+
* are worse -- one of them quotes the rejected `activeId` back, so a 240,000-
|
|
66
|
+
* character id becomes a 240,000-character diagnostic about a call that failed.
|
|
67
|
+
*
|
|
68
|
+
* Each of these is one budget spent across its whole field rather than a
|
|
69
|
+
* per-entry cap: twenty problems at a thousand characters each is twenty
|
|
70
|
+
* thousand characters, which is the cap over again.
|
|
71
|
+
*/
|
|
72
|
+
const MAX_SUMMARY_CHARS = 4_000;
|
|
73
|
+
const MAX_PROBLEM_CHARS = 2_000;
|
|
74
|
+
const MAX_PROBLEMS = 20;
|
|
75
|
+
const MAX_ERROR_CHARS = 2_000;
|
|
76
|
+
/** No dashboard's line is squeezed below this, however many there are. */
|
|
77
|
+
const MIN_SUMMARY_LINE_CHARS = 200;
|
|
78
|
+
|
|
79
|
+
/** Keep the head of one string, and say how much was dropped. */
|
|
80
|
+
function capText(text: string, max: number): string {
|
|
81
|
+
if (text.length <= max) return text;
|
|
82
|
+
return `${text.slice(0, max)}... (${text.length - max} more characters)`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Keep whole lines until the budget runs out, then say how many were dropped. */
|
|
86
|
+
function capLines(lines: string[], budget: number, noun: string): string[] {
|
|
87
|
+
const kept: string[] = [];
|
|
88
|
+
let used = 0;
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (used >= budget) {
|
|
91
|
+
kept.push(`... ${lines.length - kept.length} more ${noun} not shown`);
|
|
92
|
+
return kept;
|
|
93
|
+
}
|
|
94
|
+
const capped = capText(line, budget - used);
|
|
95
|
+
kept.push(capped);
|
|
96
|
+
used += capped.length;
|
|
97
|
+
}
|
|
98
|
+
return kept;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The summary as the model should see it: every dashboard, and a bound.
|
|
103
|
+
*
|
|
104
|
+
* Every dashboard, deliberately. Spending one budget head-first lets a single
|
|
105
|
+
* crowded dashboard eat all of it and collapses the rest into "9 more not
|
|
106
|
+
* shown" -- and when the document has been omitted too, the response tells the
|
|
107
|
+
* model to work from the summary while `dashboard_read` takes no arguments, so
|
|
108
|
+
* there is no second call that would fetch the rest. Each line gets its share
|
|
109
|
+
* and loses its own tail instead.
|
|
110
|
+
*/
|
|
111
|
+
function cappedSummary(document: DashboardDocument): string[] {
|
|
112
|
+
const lines = summarizeDocument(document);
|
|
113
|
+
const share = Math.max(
|
|
114
|
+
MIN_SUMMARY_LINE_CHARS,
|
|
115
|
+
Math.floor(MAX_SUMMARY_CHARS / Math.max(1, lines.length)),
|
|
116
|
+
);
|
|
117
|
+
return capLines(
|
|
118
|
+
lines.map((line) => capText(line, share)),
|
|
119
|
+
MAX_SUMMARY_CHARS,
|
|
120
|
+
"dashboard(s)",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validation diagnostics as the model should see them, inside one shared
|
|
126
|
+
* budget. Both fields are capped: a problem's `path` is structural, but its
|
|
127
|
+
* `message` quotes the input.
|
|
128
|
+
*/
|
|
129
|
+
function cappedProblems(problems: DashboardProblem[]): DashboardProblem[] {
|
|
130
|
+
const kept: DashboardProblem[] = [];
|
|
131
|
+
let used = 0;
|
|
132
|
+
for (const problem of problems.slice(0, MAX_PROBLEMS)) {
|
|
133
|
+
if (used >= MAX_PROBLEM_CHARS) break;
|
|
134
|
+
const left = MAX_PROBLEM_CHARS - used;
|
|
135
|
+
const path = capText(problem.path, left);
|
|
136
|
+
const message = capText(problem.message, Math.max(0, left - path.length));
|
|
137
|
+
kept.push({ path, message });
|
|
138
|
+
used += path.length + message.length;
|
|
139
|
+
}
|
|
140
|
+
const dropped = problems.length - kept.length;
|
|
141
|
+
if (dropped > 0) kept.push({ path: "", message: `... ${dropped} more problem(s)` });
|
|
142
|
+
return kept;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Widget types the agent is allowed to create. */
|
|
146
|
+
function creatableWidgetTypes(): string[] {
|
|
147
|
+
return KNOWN_WIDGET_TYPES.slice();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The widget vocabulary the tool description advertises, derived from the
|
|
152
|
+
* shared contract rather than typed out a second time: the types come from
|
|
153
|
+
* `KNOWN_WIDGET_TYPES` and each one's example config from the first shipped
|
|
154
|
+
* preset that uses it. Anything richer -- a human label, a description of what
|
|
155
|
+
* a widget draws -- lives only in the renderer's registry, which the brain
|
|
156
|
+
* cannot see.
|
|
157
|
+
*/
|
|
158
|
+
export function widgetCatalogLines(): string[] {
|
|
159
|
+
const example = new Map<string, string>();
|
|
160
|
+
for (const preset of DASHBOARD_PRESETS) {
|
|
161
|
+
for (const panel of preset.dashboard.panels) {
|
|
162
|
+
if (!example.has(panel.widget)) example.set(panel.widget, JSON.stringify(panel.config));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return creatableWidgetTypes().map((type) => `${type} (config ${example.get(type) ?? "{}"})`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The shipped presets, for the tool description and for `create_dashboard`. */
|
|
169
|
+
export function presetLines(): string[] {
|
|
170
|
+
return DASHBOARD_PRESETS.map((preset) => `${preset.id} -- ${preset.description}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
174
|
+
// Provenance
|
|
175
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A panel the agent must leave alone.
|
|
179
|
+
*
|
|
180
|
+
* `pinned` is the user saying so outright. Absent provenance counts as the
|
|
181
|
+
* user's too: the only panels that arrive without it are hand-written ones, and
|
|
182
|
+
* treating an unlabelled panel as the agent's own would make a hand-edited
|
|
183
|
+
* layout the one case where curation quietly deletes work.
|
|
184
|
+
*/
|
|
185
|
+
export function isProtectedPanel(panel: DashboardPanel): boolean {
|
|
186
|
+
if (panel.pinned === true) return true;
|
|
187
|
+
return panel.addedBy !== "agent" && panel.addedBy !== "preset";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Key-order-independent comparison, so a merged config is not a false change. */
|
|
191
|
+
function stableJson(value: unknown): string {
|
|
192
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
193
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
194
|
+
const entries = Object.keys(value as Record<string, unknown>)
|
|
195
|
+
.sort()
|
|
196
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson((value as Record<string, unknown>)[key])}`);
|
|
197
|
+
return `{${entries.join(",")}}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function samePanel(a: DashboardPanel, b: DashboardPanel): boolean {
|
|
201
|
+
return (
|
|
202
|
+
a.widget === b.widget &&
|
|
203
|
+
(a.title ?? "") === (b.title ?? "") &&
|
|
204
|
+
a.layout.span === b.layout.span &&
|
|
205
|
+
a.layout.rows === b.layout.rows &&
|
|
206
|
+
a.pinned === b.pinned &&
|
|
207
|
+
a.addedBy === b.addedBy &&
|
|
208
|
+
// `reason` is the user-visible "why is this here". Leaving it out of the
|
|
209
|
+
// comparison let an update_panel rewrite it on a panel the user pinned.
|
|
210
|
+
(a.reason ?? "") === (b.reason ?? "") &&
|
|
211
|
+
stableJson(a.config) === stableJson(b.config)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function reasonProtected(panel: DashboardPanel): string {
|
|
216
|
+
if (panel.pinned === true) return "is pinned";
|
|
217
|
+
return panel.addedBy === "user" ? "was placed by the user" : "was not added by the agent";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* What an agent-proposed document would do to panels that are not its to touch.
|
|
222
|
+
*
|
|
223
|
+
* Checked on the finished document rather than per action, so the wholesale
|
|
224
|
+
* replace and the patch path get the same guarantee and a future action cannot
|
|
225
|
+
* quietly escape it. Adding panels is always fine; curation appends.
|
|
226
|
+
*/
|
|
227
|
+
export function provenanceViolations(
|
|
228
|
+
before: DashboardDocument,
|
|
229
|
+
after: DashboardDocument,
|
|
230
|
+
): string[] {
|
|
231
|
+
const violations: string[] = [];
|
|
232
|
+
for (const dashboard of before.dashboards) {
|
|
233
|
+
const protectedPanels = dashboard.panels.filter(isProtectedPanel);
|
|
234
|
+
if (protectedPanels.length === 0) continue;
|
|
235
|
+
|
|
236
|
+
const target = after.dashboards.find((d) => d.id === dashboard.id);
|
|
237
|
+
if (!target) {
|
|
238
|
+
violations.push(
|
|
239
|
+
`dashboard "${dashboard.id}" holds ${protectedPanels.length} panel(s) the user placed or pinned, so it cannot be removed`,
|
|
240
|
+
);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const panel of protectedPanels) {
|
|
245
|
+
const kept = target.panels.find((p) => p.id === panel.id);
|
|
246
|
+
if (!kept) {
|
|
247
|
+
violations.push(`panel "${panel.id}" ${reasonProtected(panel)}, so it cannot be removed`);
|
|
248
|
+
} else if (!samePanel(panel, kept)) {
|
|
249
|
+
violations.push(`panel "${panel.id}" ${reasonProtected(panel)}, so it cannot be changed`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Relative order of the protected panels, compared as a subsequence: new
|
|
254
|
+
// panels may land between them, but they may not be shuffled past each
|
|
255
|
+
// other. Displacing a panel the user merely placed is allowed -- placing is
|
|
256
|
+
// not pinning.
|
|
257
|
+
const expected = protectedPanels
|
|
258
|
+
.map((p) => p.id)
|
|
259
|
+
.filter((id) => target.panels.some((p) => p.id === id));
|
|
260
|
+
const actual = target.panels.map((p) => p.id).filter((id) => expected.includes(id));
|
|
261
|
+
if (JSON.stringify(expected) !== JSON.stringify(actual)) {
|
|
262
|
+
violations.push(
|
|
263
|
+
`reordering the panels the user placed in "${dashboard.id}" is not the agent's to do`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// A pin, though, holds a panel's place as well as its content.
|
|
268
|
+
//
|
|
269
|
+
// The subsequence check above compares protected panels only against each
|
|
270
|
+
// other, so with one pinned panel and three agent panels it saw the same
|
|
271
|
+
// one-element sequence however far the pinned panel moved: `move_panel`
|
|
272
|
+
// pushed a pinned panel from the top to the bottom and reported success.
|
|
273
|
+
// A pin that does not survive one tool call is not much of a pin, and
|
|
274
|
+
// "keep this where I can see it" is most of why someone pins a panel.
|
|
275
|
+
//
|
|
276
|
+
// So for a pinned panel, the already-existing panels ahead of it must stay
|
|
277
|
+
// the same. New panels may still be inserted anywhere, and unprotected
|
|
278
|
+
// panels may still be removed.
|
|
279
|
+
const surviving = new Set(
|
|
280
|
+
dashboard.panels.map((p) => p.id).filter((id) => target.panels.some((q) => q.id === id)),
|
|
281
|
+
);
|
|
282
|
+
const aheadIn = (panels: { id: string }[], id: string): Set<string> => {
|
|
283
|
+
const ahead = new Set<string>();
|
|
284
|
+
for (const p of panels) {
|
|
285
|
+
if (p.id === id) break;
|
|
286
|
+
if (surviving.has(p.id)) ahead.add(p.id);
|
|
287
|
+
}
|
|
288
|
+
return ahead;
|
|
289
|
+
};
|
|
290
|
+
const sameSet = (a: Set<string>, b: Set<string>): boolean =>
|
|
291
|
+
a.size === b.size && [...a].every((id) => b.has(id));
|
|
292
|
+
|
|
293
|
+
for (const panel of protectedPanels) {
|
|
294
|
+
if (panel.pinned !== true || !surviving.has(panel.id)) continue;
|
|
295
|
+
if (!sameSet(aheadIn(dashboard.panels, panel.id), aheadIn(target.panels, panel.id))) {
|
|
296
|
+
violations.push(`panel "${panel.id}" is pinned, so the agent cannot move it`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return violations;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Take provenance out of the model's hands.
|
|
305
|
+
*
|
|
306
|
+
* `actions` cannot set `addedBy` or `pinned` -- the schema has no field for
|
|
307
|
+
* them -- but a whole-document replace hands the model the raw JSON, and a
|
|
308
|
+
* panel it stamps `addedBy: "user"` or `pinned: true` is a panel it has
|
|
309
|
+
* laundered into one nothing here will ever touch again, including the user's
|
|
310
|
+
* own reading of who put it there. So: a panel that was already in this
|
|
311
|
+
* dashboard keeps exactly the provenance it had, and a panel this write
|
|
312
|
+
* introduces is the agent's, with the agent's reason.
|
|
313
|
+
*/
|
|
314
|
+
export function reassertProvenance(
|
|
315
|
+
before: DashboardDocument,
|
|
316
|
+
candidate: DashboardDocument,
|
|
317
|
+
reason: string,
|
|
318
|
+
): DashboardDocument {
|
|
319
|
+
for (const dashboard of candidate.dashboards) {
|
|
320
|
+
const previous = before.dashboards.find((d) => d.id === dashboard.id);
|
|
321
|
+
for (const panel of dashboard.panels) {
|
|
322
|
+
const existing = previous?.panels.find((p) => p.id === panel.id);
|
|
323
|
+
if (existing && existing.widget === panel.widget) {
|
|
324
|
+
if (existing.addedBy === undefined) delete panel.addedBy;
|
|
325
|
+
else panel.addedBy = existing.addedBy;
|
|
326
|
+
if (existing.pinned === undefined) delete panel.pinned;
|
|
327
|
+
else panel.pinned = existing.pinned;
|
|
328
|
+
// The reason is the one provenance field the agent may legitimately
|
|
329
|
+
// refresh -- it is the sentence the user reads to decide whether the
|
|
330
|
+
// panel belongs there -- but only on a panel that is the agent's to
|
|
331
|
+
// change at all. On a protected one it is the user's words.
|
|
332
|
+
if (isProtectedPanel(existing)) {
|
|
333
|
+
if (existing.reason === undefined) delete panel.reason;
|
|
334
|
+
else panel.reason = existing.reason;
|
|
335
|
+
} else if (panel.reason === undefined && existing.reason !== undefined) {
|
|
336
|
+
panel.reason = existing.reason;
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
panel.addedBy = "agent";
|
|
340
|
+
panel.reason = reason;
|
|
341
|
+
delete panel.pinned;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return candidate;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Widget types this change brings into the document: a panel id the dashboard
|
|
350
|
+
* did not have, or one whose widget type changed. Panels already on disk are
|
|
351
|
+
* left alone even when their type is unknown to this build, because that is how
|
|
352
|
+
* a layout written by a newer build survives being opened by an older one.
|
|
353
|
+
*/
|
|
354
|
+
export function introducedWidgetTypes(
|
|
355
|
+
before: DashboardDocument,
|
|
356
|
+
after: DashboardDocument,
|
|
357
|
+
): string[] {
|
|
358
|
+
const introduced = new Set<string>();
|
|
359
|
+
for (const dashboard of after.dashboards) {
|
|
360
|
+
const previous = before.dashboards.find((d) => d.id === dashboard.id);
|
|
361
|
+
for (const panel of dashboard.panels) {
|
|
362
|
+
const existing = previous?.panels.find((p) => p.id === panel.id);
|
|
363
|
+
if (!existing || existing.widget !== panel.widget) introduced.add(panel.widget);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return [...introduced];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* What the validator had to throw away to fit its own caps.
|
|
371
|
+
*
|
|
372
|
+
* The build step produces a well-formed document either way -- the action path
|
|
373
|
+
* from typed inputs, the document path from an already-validated one -- so a
|
|
374
|
+
* panel or dashboard that comes out the far side missing was truncated at the
|
|
375
|
+
* 20-dashboard or 40-panel ceiling, not repaired. Counts rather than ids,
|
|
376
|
+
* because the validator also renames duplicates and a by-id comparison would
|
|
377
|
+
* call that a loss.
|
|
378
|
+
*
|
|
379
|
+
* It matters because truncation keeps the FIRST N: an insert at position 0 into
|
|
380
|
+
* a full dashboard pushes the last panel off the end, and the model would
|
|
381
|
+
* otherwise be told its add succeeded.
|
|
382
|
+
*/
|
|
383
|
+
export function truncatedByValidation(
|
|
384
|
+
proposed: DashboardDocument,
|
|
385
|
+
validated: DashboardDocument,
|
|
386
|
+
): string | null {
|
|
387
|
+
if (validated.dashboards.length < proposed.dashboards.length) {
|
|
388
|
+
return `this layout already holds the most dashboards a layout can have (${validated.dashboards.length}), so the new one would not fit`;
|
|
389
|
+
}
|
|
390
|
+
const count = (document: DashboardDocument): number =>
|
|
391
|
+
document.dashboards.reduce((total, d) => total + d.panels.length, 0);
|
|
392
|
+
const kept = count(validated);
|
|
393
|
+
if (kept < count(proposed)) {
|
|
394
|
+
return `that dashboard is full (${kept} panels), so adding to it would push another panel off the end`;
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Did the model's document exceed a cap, so that saving it would drop panels?
|
|
401
|
+
*
|
|
402
|
+
* The actions path catches this by comparing the proposal with the validated
|
|
403
|
+
* result, but a whole-document replace is validated on the way in, so there is
|
|
404
|
+
* nothing un-truncated left to compare against -- `commitDashboardChange` ends
|
|
405
|
+
* up comparing the truncated document with itself and finding nothing missing.
|
|
406
|
+
* That is why prepending one panel to a full dashboard reported success and
|
|
407
|
+
* deleted the last one.
|
|
408
|
+
*
|
|
409
|
+
* Tested against the caps rather than by counting survivors, because the
|
|
410
|
+
* validator also drops genuinely malformed panels, and that is a repair to
|
|
411
|
+
* report rather than a refusal to raise.
|
|
412
|
+
*/
|
|
413
|
+
export function documentOverflow(documentText: string): string | null {
|
|
414
|
+
let raw: unknown;
|
|
415
|
+
try {
|
|
416
|
+
raw = JSON.parse(documentText);
|
|
417
|
+
} catch {
|
|
418
|
+
return null; // Unparseable text never got this far.
|
|
419
|
+
}
|
|
420
|
+
const dashboards = (raw as { dashboards?: unknown } | null)?.dashboards;
|
|
421
|
+
if (!Array.isArray(dashboards)) return null;
|
|
422
|
+
|
|
423
|
+
if (dashboards.length > MAX_DASHBOARDS) {
|
|
424
|
+
return `Nothing was changed: a layout holds at most ${MAX_DASHBOARDS} dashboards and that one has ${dashboards.length}, so ${dashboards.length - MAX_DASHBOARDS} would have been dropped.`;
|
|
425
|
+
}
|
|
426
|
+
for (const dashboard of dashboards) {
|
|
427
|
+
const panels = (dashboard as { panels?: unknown } | null)?.panels;
|
|
428
|
+
if (!Array.isArray(panels) || panels.length <= MAX_PANELS) continue;
|
|
429
|
+
const id = (dashboard as { id?: unknown })?.id;
|
|
430
|
+
const name = typeof id === "string" ? `"${id}"` : "a dashboard";
|
|
431
|
+
return `Nothing was changed: ${name} holds at most ${MAX_PANELS} panels and that one has ${panels.length}, so ${panels.length - MAX_PANELS} would have been dropped. Remove some first.`;
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Refuse a widget type the agent is not allowed to create. */
|
|
437
|
+
export function unsupportedWidgetMessage(type: string): string | null {
|
|
438
|
+
if (creatableWidgetTypes().includes(type)) return null;
|
|
439
|
+
return `"${type}" is not a widget this build can draw. Available: ${creatableWidgetTypes().join(", ")}.`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
443
|
+
// Actions
|
|
444
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* One change, as the model sent it.
|
|
448
|
+
*
|
|
449
|
+
* Every field is `unknown` on purpose. The schema asks for strings and
|
|
450
|
+
* integers, but nothing between the model and this function enforces that, and
|
|
451
|
+
* a `null` where a string was promised must come back as a refusal the model
|
|
452
|
+
* can read rather than as a TypeError out of the tool.
|
|
453
|
+
*/
|
|
454
|
+
export type DashboardAction = {
|
|
455
|
+
action?: unknown;
|
|
456
|
+
dashboardId?: unknown;
|
|
457
|
+
panelId?: unknown;
|
|
458
|
+
widget?: unknown;
|
|
459
|
+
title?: unknown;
|
|
460
|
+
config?: unknown;
|
|
461
|
+
span?: unknown;
|
|
462
|
+
rows?: unknown;
|
|
463
|
+
position?: unknown;
|
|
464
|
+
preset?: unknown;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
export type ApplyResult =
|
|
468
|
+
| { ok: true; document: DashboardDocument; notes: string[]; problems?: DashboardProblem[] }
|
|
469
|
+
| { ok: false; error: string };
|
|
470
|
+
|
|
471
|
+
function fail(error: string): ApplyResult {
|
|
472
|
+
return { ok: false, error };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function cloneDocument(document: DashboardDocument): DashboardDocument {
|
|
476
|
+
return JSON.parse(JSON.stringify(document)) as DashboardDocument;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function findDashboard(document: DashboardDocument, id: unknown): Dashboard | null {
|
|
480
|
+
const wanted = asText(id) || document.activeId;
|
|
481
|
+
return document.dashboards.find((d) => d.id === wanted) ?? null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function dashboardIds(document: DashboardDocument): string {
|
|
485
|
+
return document.dashboards.map((d) => d.id).join(", ");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function slugify(value: string): string {
|
|
489
|
+
return value
|
|
490
|
+
.toLowerCase()
|
|
491
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
492
|
+
.replace(/^-+|-+$/g, "")
|
|
493
|
+
.slice(0, 40);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function uniquePanelId(dashboard: Dashboard, base: string): string {
|
|
497
|
+
const slug = slugify(base) || "panel";
|
|
498
|
+
if (!dashboard.panels.some((p) => p.id === `p-${slug}`)) return `p-${slug}`;
|
|
499
|
+
let n = 2;
|
|
500
|
+
while (dashboard.panels.some((p) => p.id === `p-${slug}-${n}`)) n++;
|
|
501
|
+
return `p-${slug}-${n}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function uniqueDashboardId(document: DashboardDocument, base: string): string {
|
|
505
|
+
const slug = slugify(base) || "dashboard";
|
|
506
|
+
if (!document.dashboards.some((d) => d.id === slug)) return slug;
|
|
507
|
+
let n = 2;
|
|
508
|
+
while (document.dashboards.some((d) => d.id === `${slug}-${n}`)) n++;
|
|
509
|
+
return `${slug}-${n}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* A model-supplied string field, or "" for anything else.
|
|
514
|
+
*
|
|
515
|
+
* The schema says these are strings, but nothing between the model and here
|
|
516
|
+
* enforces that: a `null` title or a numeric reason arrives as-is and used to
|
|
517
|
+
* take `.trim()` with it, throwing out of `execute` instead of coming back as
|
|
518
|
+
* something the model could correct.
|
|
519
|
+
*/
|
|
520
|
+
function asText(value: unknown): string {
|
|
521
|
+
return typeof value === "string" ? value.trim() : "";
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Whether the model supplied this field at all, ignoring how badly. */
|
|
525
|
+
function given(value: unknown): boolean {
|
|
526
|
+
return value !== undefined && value !== null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function asInteger(value: unknown): number | undefined {
|
|
530
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.round(value);
|
|
531
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
|
|
532
|
+
return Math.round(Number(value));
|
|
533
|
+
}
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
type ParsedConfig = { ok: true; config: Record<string, unknown> } | { ok: false; error: string };
|
|
538
|
+
|
|
539
|
+
function isConfigObject(value: unknown): value is Record<string, unknown> {
|
|
540
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function parseConfig(raw: unknown): ParsedConfig {
|
|
544
|
+
if (raw === undefined || raw === null) return { ok: true, config: {} };
|
|
545
|
+
// The schema asks for JSON text because a free-form object parameter is not
|
|
546
|
+
// portable across every provider's function-calling schema, but a model that
|
|
547
|
+
// sends the object itself is being reasonable and should not be punished for
|
|
548
|
+
// it.
|
|
549
|
+
if (isConfigObject(raw)) return { ok: true, config: raw };
|
|
550
|
+
if (typeof raw !== "string") {
|
|
551
|
+
return { ok: false, error: 'config must be a JSON object, e.g. {"follow":false}' };
|
|
552
|
+
}
|
|
553
|
+
if (raw.trim() === "") return { ok: true, config: {} };
|
|
554
|
+
let parsed: unknown;
|
|
555
|
+
try {
|
|
556
|
+
parsed = JSON.parse(raw);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
return {
|
|
559
|
+
ok: false,
|
|
560
|
+
error: `config is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (!isConfigObject(parsed)) {
|
|
564
|
+
return { ok: false, error: 'config must be a JSON object, e.g. {"follow":false}' };
|
|
565
|
+
}
|
|
566
|
+
return { ok: true, config: parsed };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function clampRows(rows: unknown): number {
|
|
570
|
+
const value = asInteger(rows);
|
|
571
|
+
return value === undefined ? 2 : Math.min(6, Math.max(1, value));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Where an insert lands, clamped to the grid and to the panels the user pinned.
|
|
576
|
+
*
|
|
577
|
+
* `pinned` is the user saying "leave this one alone", and a panel that keeps
|
|
578
|
+
* its contents but loses the top-left slot has not been left alone. So an agent
|
|
579
|
+
* insert never goes above a pinned panel, however the model numbers it. Panels
|
|
580
|
+
* merely placed by the user are not protected this way -- they did not ask for
|
|
581
|
+
* a fixed spot, and a dashboard that can only ever grow at the bottom is worse.
|
|
582
|
+
*/
|
|
583
|
+
function clampPosition(position: unknown, panels: DashboardPanel[]): number {
|
|
584
|
+
const floor = panels.reduce((last, panel, i) => (panel.pinned === true ? i + 1 : last), 0);
|
|
585
|
+
const value = asInteger(position);
|
|
586
|
+
if (value === undefined) return panels.length;
|
|
587
|
+
return Math.min(panels.length, Math.max(floor, value));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
type Located = { dashboard: Dashboard; panel: DashboardPanel; index: number } | { error: string };
|
|
591
|
+
|
|
592
|
+
function locatePanel(document: DashboardDocument, action: DashboardAction): Located {
|
|
593
|
+
const panelId = asText(action.panelId);
|
|
594
|
+
if (!panelId) return { error: `${action.action} needs a panelId.` };
|
|
595
|
+
const wanted = asText(action.dashboardId) || undefined;
|
|
596
|
+
if (wanted && !document.dashboards.some((d) => d.id === wanted)) {
|
|
597
|
+
return { error: `no dashboard "${wanted}". Have: ${dashboardIds(document)}.` };
|
|
598
|
+
}
|
|
599
|
+
// Panel ids are unique per dashboard, not per document, and the shipped
|
|
600
|
+
// presets reuse "p-jobs" and "p-notebook" deliberately. Without the active
|
|
601
|
+
// dashboard first, an unqualified action edits whichever dashboard happens to
|
|
602
|
+
// be first in the file -- one the user is not even looking at.
|
|
603
|
+
const scoped = wanted
|
|
604
|
+
? document.dashboards.filter((d) => d.id === wanted)
|
|
605
|
+
: [...document.dashboards].sort((a, b) => {
|
|
606
|
+
if (a.id === document.activeId) return -1;
|
|
607
|
+
if (b.id === document.activeId) return 1;
|
|
608
|
+
return 0;
|
|
609
|
+
});
|
|
610
|
+
for (const dashboard of scoped) {
|
|
611
|
+
const index = dashboard.panels.findIndex((p) => p.id === panelId);
|
|
612
|
+
if (index >= 0) return { dashboard, panel: dashboard.panels[index], index };
|
|
613
|
+
}
|
|
614
|
+
return { error: `no panel "${panelId}". Call dashboard_read to see the panel ids.` };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Apply the agent's actions to a copy of the document.
|
|
619
|
+
*
|
|
620
|
+
* Nothing here enforces provenance or the widget allowlist; both are checked
|
|
621
|
+
* against the finished document by the caller, so the patch path and the
|
|
622
|
+
* wholesale-replace path cannot diverge.
|
|
623
|
+
*/
|
|
624
|
+
export function applyDashboardActions(
|
|
625
|
+
document: DashboardDocument,
|
|
626
|
+
actions: DashboardAction[],
|
|
627
|
+
reason: string,
|
|
628
|
+
): ApplyResult {
|
|
629
|
+
if (!Array.isArray(actions)) return fail("actions must be a list of changes.");
|
|
630
|
+
if (actions.length === 0) return fail("No actions given.");
|
|
631
|
+
if (actions.length > MAX_ACTIONS) {
|
|
632
|
+
return fail(`${actions.length} actions in one call; at most ${MAX_ACTIONS}.`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const next = cloneDocument(document);
|
|
636
|
+
const notes: string[] = [];
|
|
637
|
+
|
|
638
|
+
for (const [index, action] of actions.entries()) {
|
|
639
|
+
const at = `actions[${index}]`;
|
|
640
|
+
if (action === null || typeof action !== "object") {
|
|
641
|
+
return fail(`${at}: expected an object describing one change.`);
|
|
642
|
+
}
|
|
643
|
+
switch (asText(action.action)) {
|
|
644
|
+
case "add_panel": {
|
|
645
|
+
const dashboard = findDashboard(next, action.dashboardId);
|
|
646
|
+
if (!dashboard) {
|
|
647
|
+
return fail(
|
|
648
|
+
`${at}: no dashboard "${asText(action.dashboardId)}". Have: ${dashboardIds(next)}.`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const widget = asText(action.widget);
|
|
652
|
+
if (!widget) return fail(`${at}: add_panel needs a widget type.`);
|
|
653
|
+
const config = parseConfig(action.config);
|
|
654
|
+
if (!config.ok) return fail(`${at}: ${config.error}`);
|
|
655
|
+
const panel: DashboardPanel = {
|
|
656
|
+
id: uniquePanelId(dashboard, widget),
|
|
657
|
+
widget,
|
|
658
|
+
config: config.config,
|
|
659
|
+
layout: { span: asInteger(action.span) === 2 ? 2 : 1, rows: clampRows(action.rows) },
|
|
660
|
+
addedBy: "agent",
|
|
661
|
+
reason,
|
|
662
|
+
};
|
|
663
|
+
const title = asText(action.title);
|
|
664
|
+
if (title) panel.title = title;
|
|
665
|
+
const insertAt = clampPosition(action.position, dashboard.panels);
|
|
666
|
+
dashboard.panels.splice(insertAt, 0, panel);
|
|
667
|
+
notes.push(`added ${widget} as "${panel.id}" in "${dashboard.id}"`);
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
case "remove_panel": {
|
|
672
|
+
const found = locatePanel(next, action);
|
|
673
|
+
if ("error" in found) return fail(`${at}: ${found.error}`);
|
|
674
|
+
found.dashboard.panels.splice(found.index, 1);
|
|
675
|
+
notes.push(`removed "${found.panel.id}" from "${found.dashboard.id}"`);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
case "update_panel": {
|
|
680
|
+
const found = locatePanel(next, action);
|
|
681
|
+
if ("error" in found) return fail(`${at}: ${found.error}`);
|
|
682
|
+
if (
|
|
683
|
+
!given(action.widget) &&
|
|
684
|
+
!given(action.title) &&
|
|
685
|
+
!given(action.config) &&
|
|
686
|
+
!given(action.span) &&
|
|
687
|
+
!given(action.rows)
|
|
688
|
+
) {
|
|
689
|
+
// Reporting "updated" for a call that changed nothing would have the
|
|
690
|
+
// model tell the user their dashboard moved when it did not.
|
|
691
|
+
return fail(`${at}: update_panel needs something to change.`);
|
|
692
|
+
}
|
|
693
|
+
const panel = found.panel;
|
|
694
|
+
const newWidget = asText(action.widget);
|
|
695
|
+
if (newWidget) panel.widget = newWidget;
|
|
696
|
+
if (given(action.title)) {
|
|
697
|
+
const title = asText(action.title);
|
|
698
|
+
if (title) panel.title = title;
|
|
699
|
+
else delete panel.title;
|
|
700
|
+
}
|
|
701
|
+
if (given(action.config)) {
|
|
702
|
+
const config = parseConfig(action.config);
|
|
703
|
+
if (!config.ok) return fail(`${at}: ${config.error}`);
|
|
704
|
+
// Merge: a model changing one setting should not silently drop the
|
|
705
|
+
// rest of a panel's config.
|
|
706
|
+
panel.config = { ...panel.config, ...config.config };
|
|
707
|
+
}
|
|
708
|
+
if (given(action.span)) panel.layout.span = asInteger(action.span) === 2 ? 2 : 1;
|
|
709
|
+
if (given(action.rows)) panel.layout.rows = clampRows(action.rows);
|
|
710
|
+
panel.reason = reason;
|
|
711
|
+
notes.push(`updated "${panel.id}" in "${found.dashboard.id}"`);
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
case "move_panel": {
|
|
716
|
+
const found = locatePanel(next, action);
|
|
717
|
+
if ("error" in found) return fail(`${at}: ${found.error}`);
|
|
718
|
+
if (asInteger(action.position) === undefined) {
|
|
719
|
+
return fail(`${at}: move_panel needs a position.`);
|
|
720
|
+
}
|
|
721
|
+
const [panel] = found.dashboard.panels.splice(found.index, 1);
|
|
722
|
+
const to = clampPosition(action.position, found.dashboard.panels);
|
|
723
|
+
found.dashboard.panels.splice(to, 0, panel);
|
|
724
|
+
notes.push(`moved "${panel.id}" to position ${to} in "${found.dashboard.id}"`);
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
case "create_dashboard": {
|
|
729
|
+
const fromPreset = asText(action.preset) || undefined;
|
|
730
|
+
let dashboard: Dashboard;
|
|
731
|
+
if (fromPreset) {
|
|
732
|
+
const preset = dashboardFromPreset(fromPreset);
|
|
733
|
+
if (!preset) {
|
|
734
|
+
return fail(
|
|
735
|
+
`${at}: no preset "${fromPreset}". Have: ${DASHBOARD_PRESETS.map((p) => p.id).join(", ")}.`,
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
dashboard = preset;
|
|
739
|
+
} else {
|
|
740
|
+
dashboard = { id: "", title: "", panels: [] };
|
|
741
|
+
}
|
|
742
|
+
const title = asText(action.title) || asText(dashboard.title);
|
|
743
|
+
if (!title) return fail(`${at}: create_dashboard needs a title or a preset.`);
|
|
744
|
+
dashboard.title = title;
|
|
745
|
+
dashboard.id = uniqueDashboardId(next, asText(action.dashboardId) || fromPreset || title);
|
|
746
|
+
next.dashboards.push(dashboard);
|
|
747
|
+
notes.push(`created dashboard "${dashboard.id}"`);
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
case "switch_dashboard": {
|
|
752
|
+
const dashboard = findDashboard(next, action.dashboardId);
|
|
753
|
+
if (!dashboard) {
|
|
754
|
+
return fail(
|
|
755
|
+
`${at}: no dashboard "${asText(action.dashboardId)}". Have: ${dashboardIds(next)}.`,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
next.activeId = dashboard.id;
|
|
759
|
+
notes.push(`showing "${dashboard.id}"`);
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
default:
|
|
764
|
+
return fail(
|
|
765
|
+
`${at}: unknown action "${asText(action.action)}". Use add_panel, remove_panel, update_panel, move_panel, create_dashboard or switch_dashboard.`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return { ok: true, document: next, notes };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
774
|
+
// The write, end to end
|
|
775
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
776
|
+
|
|
777
|
+
export type CommitOutcome =
|
|
778
|
+
| {
|
|
779
|
+
ok: true;
|
|
780
|
+
document: DashboardDocument;
|
|
781
|
+
problems: DashboardProblem[];
|
|
782
|
+
notes: string[];
|
|
783
|
+
path: string;
|
|
784
|
+
}
|
|
785
|
+
| { ok: false; error: string; problems?: DashboardProblem[] };
|
|
786
|
+
|
|
787
|
+
type Refusal = { error: string; problems?: DashboardProblem[] };
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Validate the candidate, check it against what it is replacing, and persist it.
|
|
791
|
+
*
|
|
792
|
+
* Shared by the tool's patch path, the tool's replace path and the parts of
|
|
793
|
+
* `/dashboard` that write, so every one of them gets the same refusals. The
|
|
794
|
+
* checks run inside the store's compare-and-swap callback, so they are made
|
|
795
|
+
* against the document as it is on disk at the moment of the write, not against
|
|
796
|
+
* a copy read earlier.
|
|
797
|
+
*/
|
|
798
|
+
export async function commitDashboardChange(
|
|
799
|
+
build: (current: DashboardDocument, exists: boolean) => ApplyResult,
|
|
800
|
+
options: { asUser?: boolean; reason?: string } = {},
|
|
801
|
+
): Promise<CommitOutcome> {
|
|
802
|
+
// `asUser` means a person typed this, so the panels they placed are theirs to
|
|
803
|
+
// discard. It does NOT relax the widget allowlist: whether this build can
|
|
804
|
+
// draw a widget is a fact about the build, not about who asked, and a preset
|
|
805
|
+
// that one day carries a flag-gated widget must not install it through the
|
|
806
|
+
// slash command either.
|
|
807
|
+
const enforce = options.asUser !== true;
|
|
808
|
+
const buildReason = options.reason ?? "";
|
|
809
|
+
// A holder rather than plain locals: the callback below runs inside the
|
|
810
|
+
// store's retry loop, and what it learns has to survive back out here.
|
|
811
|
+
const seen: { problems: DashboardProblem[]; notes: string[]; refusal: Refusal | null } = {
|
|
812
|
+
problems: [],
|
|
813
|
+
notes: [],
|
|
814
|
+
refusal: null,
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const written = await updateDashboardDocument((current, exists) => {
|
|
818
|
+
seen.problems = [];
|
|
819
|
+
seen.notes = [];
|
|
820
|
+
seen.refusal = null;
|
|
821
|
+
|
|
822
|
+
const built = build(current, exists);
|
|
823
|
+
if (!built.ok) {
|
|
824
|
+
seen.refusal = { error: built.error };
|
|
825
|
+
return { ok: false, error: built.error };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const proposed = enforce
|
|
829
|
+
? reassertProvenance(current, built.document, buildReason)
|
|
830
|
+
: built.document;
|
|
831
|
+
const validated = validateDashboardDocument(proposed);
|
|
832
|
+
if (!validated.ok) {
|
|
833
|
+
seen.refusal = {
|
|
834
|
+
error: "That layout is not a valid dashboard document.",
|
|
835
|
+
problems: validated.problems,
|
|
836
|
+
};
|
|
837
|
+
return { ok: false, error: seen.refusal.error };
|
|
838
|
+
}
|
|
839
|
+
// Repairs the build step already made -- a panel the model's document was
|
|
840
|
+
// missing a widget for, say -- are the model's to hear about, and they are
|
|
841
|
+
// gone by the time the result is re-validated.
|
|
842
|
+
seen.problems = [...(built.problems ?? []), ...validated.problems];
|
|
843
|
+
seen.notes = built.notes;
|
|
844
|
+
|
|
845
|
+
const truncated = truncatedByValidation(proposed, validated.document);
|
|
846
|
+
if (truncated) {
|
|
847
|
+
seen.refusal = {
|
|
848
|
+
error: `Nothing was changed: ${truncated}. Remove something first.`,
|
|
849
|
+
};
|
|
850
|
+
return { ok: false, error: seen.refusal.error };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
for (const type of introducedWidgetTypes(current, validated.document)) {
|
|
854
|
+
const message = unsupportedWidgetMessage(type);
|
|
855
|
+
if (message) {
|
|
856
|
+
seen.refusal = { error: `${message} Nothing was changed.` };
|
|
857
|
+
return { ok: false, error: seen.refusal.error };
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (enforce) {
|
|
862
|
+
const violations = provenanceViolations(current, validated.document);
|
|
863
|
+
if (violations.length > 0) {
|
|
864
|
+
seen.refusal = {
|
|
865
|
+
error: `Nothing was changed: ${violations.join("; ")}. Ask the user to make that change in the dashboard's own controls.`,
|
|
866
|
+
};
|
|
867
|
+
return { ok: false, error: seen.refusal.error };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return { ok: true, document: validated.document };
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
if (!written.ok) {
|
|
875
|
+
return seen.refusal
|
|
876
|
+
? { ok: false, error: seen.refusal.error, problems: seen.refusal.problems }
|
|
877
|
+
: { ok: false, error: written.error };
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
ok: true,
|
|
881
|
+
document: written.document,
|
|
882
|
+
problems: seen.problems,
|
|
883
|
+
notes: seen.notes,
|
|
884
|
+
path: written.path,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** One line per dashboard, for a tool result or a slash command. */
|
|
889
|
+
export function summarizeDocument(document: DashboardDocument): string[] {
|
|
890
|
+
return document.dashboards.map((dashboard) => {
|
|
891
|
+
const mark = dashboard.id === document.activeId ? "* " : " ";
|
|
892
|
+
const panels = dashboard.panels.length
|
|
893
|
+
? dashboard.panels.map(describePanel).join(", ")
|
|
894
|
+
: "no panels";
|
|
895
|
+
return `${mark}${dashboard.id} (${dashboard.title}): ${panels}`;
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function describePanel(panel: DashboardPanel): string {
|
|
900
|
+
const marks: string[] = [];
|
|
901
|
+
if (panel.pinned) marks.push("pinned");
|
|
902
|
+
if (panel.addedBy && panel.addedBy !== "preset") marks.push(`added by ${panel.addedBy}`);
|
|
903
|
+
return `${panel.id} [${panel.widget}]${marks.length ? ` (${marks.join(", ")})` : ""}`;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** True when a shell with a dashboard pane is attached. */
|
|
907
|
+
function hasPane(): boolean {
|
|
908
|
+
return capabilityProfile().ui === "desktop";
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** Where the change landed, said in one line, honestly, in either shell. */
|
|
912
|
+
export function landedLine(): string {
|
|
913
|
+
return hasPane()
|
|
914
|
+
? "The Dashboard tab picks this up within a few seconds."
|
|
915
|
+
: "There is no dashboard pane in the terminal; the layout file is updated and the next Orbit session will show it.";
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Note the change in the activity log. The only durable trace that the agent,
|
|
920
|
+
* rather than the user, rearranged what they are looking at.
|
|
921
|
+
*/
|
|
922
|
+
export function logDashboardChange(filePath: string, notes: string[], source: string): void {
|
|
923
|
+
try {
|
|
924
|
+
appendActivityEvent(path.dirname(filePath), {
|
|
925
|
+
timestamp: new Date().toISOString(),
|
|
926
|
+
kind: "dashboard.changed",
|
|
927
|
+
source,
|
|
928
|
+
payload: { changes: notes },
|
|
929
|
+
});
|
|
930
|
+
} catch {
|
|
931
|
+
// The layout is written; failing to note it is not worth failing the call.
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export function countPanels(document: DashboardDocument): number {
|
|
936
|
+
return document.dashboards.reduce((total, d) => total + d.panels.length, 0);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
940
|
+
// Registration
|
|
941
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
942
|
+
|
|
943
|
+
const UPDATE_DESCRIPTION = [
|
|
944
|
+
"Change the analysis dashboard: the panels the user sees beside the chat.",
|
|
945
|
+
"",
|
|
946
|
+
"Only when the user asks. Do not add, remove or rearrange panels on your own",
|
|
947
|
+
"initiative -- not to be helpful, not because a run started, not because you",
|
|
948
|
+
"think another view would suit them better. A panel the user placed or pinned",
|
|
949
|
+
"is refused outright; ask them to change that one in the dashboard's own controls.",
|
|
950
|
+
"",
|
|
951
|
+
"Call dashboard_read first so you use the real panel ids. Pass EITHER `actions`",
|
|
952
|
+
"(add_panel, remove_panel, update_panel, move_panel, create_dashboard,",
|
|
953
|
+
"switch_dashboard) or `document` (the whole layout as JSON text), never both.",
|
|
954
|
+
"`reason` is required and is recorded on every panel you add or change, so the",
|
|
955
|
+
'user can see why it is there -- name the fact, e.g. "you asked to watch the',
|
|
956
|
+
'alignment run".',
|
|
957
|
+
"",
|
|
958
|
+
`Widget types: ${widgetCatalogLines().join("; ")}.`,
|
|
959
|
+
"Presets for create_dashboard:",
|
|
960
|
+
...presetLines().map((line) => ` ${line}`),
|
|
961
|
+
"Panels sit in a two-column grid: span is 1 or 2 columns, rows is 1-6 height",
|
|
962
|
+
"units, position is the index within the dashboard (omit it to append).",
|
|
963
|
+
].join("\n");
|
|
964
|
+
|
|
965
|
+
const ActionSchema = Type.Object({
|
|
966
|
+
action: Type.Union(
|
|
967
|
+
[
|
|
968
|
+
Type.Literal("add_panel"),
|
|
969
|
+
Type.Literal("remove_panel"),
|
|
970
|
+
Type.Literal("update_panel"),
|
|
971
|
+
Type.Literal("move_panel"),
|
|
972
|
+
Type.Literal("create_dashboard"),
|
|
973
|
+
Type.Literal("switch_dashboard"),
|
|
974
|
+
],
|
|
975
|
+
{ description: "What to do." },
|
|
976
|
+
),
|
|
977
|
+
dashboardId: Type.Optional(
|
|
978
|
+
Type.String({ description: "Which dashboard. Defaults to the one on screen." }),
|
|
979
|
+
),
|
|
980
|
+
panelId: Type.Optional(
|
|
981
|
+
Type.String({ description: "Panel to act on, for remove_panel/update_panel/move_panel." }),
|
|
982
|
+
),
|
|
983
|
+
widget: Type.Optional(Type.String({ description: "Widget type, for add_panel." })),
|
|
984
|
+
title: Type.Optional(
|
|
985
|
+
Type.String({ description: "Panel or dashboard title. Omit to use the widget's own label." }),
|
|
986
|
+
),
|
|
987
|
+
config: Type.Optional(
|
|
988
|
+
Type.String({
|
|
989
|
+
description:
|
|
990
|
+
'Widget config as a JSON object in a string, e.g. {"follow":false}. On update_panel it is merged into the panel\'s existing config.',
|
|
991
|
+
}),
|
|
992
|
+
),
|
|
993
|
+
span: Type.Optional(Type.Integer({ minimum: 1, maximum: 2, description: "Columns: 1 or 2." })),
|
|
994
|
+
rows: Type.Optional(Type.Integer({ minimum: 1, maximum: 6, description: "Height units: 1-6." })),
|
|
995
|
+
position: Type.Optional(
|
|
996
|
+
Type.Integer({ minimum: 0, description: "Index within the dashboard. Omit to append." }),
|
|
997
|
+
),
|
|
998
|
+
preset: Type.Optional(
|
|
999
|
+
Type.String({ description: "Preset id to build a new dashboard from, for create_dashboard." }),
|
|
1000
|
+
),
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
function toolFailure(error: string, problems?: DashboardProblem[]) {
|
|
1004
|
+
return {
|
|
1005
|
+
content: [
|
|
1006
|
+
{
|
|
1007
|
+
type: "text" as const,
|
|
1008
|
+
text: JSON.stringify(
|
|
1009
|
+
{
|
|
1010
|
+
success: false,
|
|
1011
|
+
error: capText(error, MAX_ERROR_CHARS),
|
|
1012
|
+
...(problems ? { problems: cappedProblems(problems) } : {}),
|
|
1013
|
+
},
|
|
1014
|
+
null,
|
|
1015
|
+
2,
|
|
1016
|
+
),
|
|
1017
|
+
},
|
|
1018
|
+
],
|
|
1019
|
+
details: { error: true } as Record<string, unknown>,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
export function registerDashboardTools(pi: ExtensionAPI): void {
|
|
1024
|
+
pi.registerTool({
|
|
1025
|
+
name: "dashboard_read",
|
|
1026
|
+
label: "Read Dashboard Layout",
|
|
1027
|
+
description:
|
|
1028
|
+
"Read the analysis dashboard: the named dashboards, their panels, which one is on screen " +
|
|
1029
|
+
"and who put each panel there. Call this before dashboard_update so you change panels by " +
|
|
1030
|
+
"their real ids.",
|
|
1031
|
+
parameters: Type.Object({}),
|
|
1032
|
+
async execute() {
|
|
1033
|
+
const read = await readDashboardDocument();
|
|
1034
|
+
if (!read.ok) return toolFailure(read.error);
|
|
1035
|
+
// Measured the way it is emitted. The response is written with two-space
|
|
1036
|
+
// indentation a few lines below, which roughly doubles it, so comparing
|
|
1037
|
+
// the compact form against the cap let a document that just squeaked
|
|
1038
|
+
// under land in front of the model at twice the size.
|
|
1039
|
+
const serialized = JSON.stringify(read.document, null, 2);
|
|
1040
|
+
const tooBig = serialized.length > MAX_READ_CHARS;
|
|
1041
|
+
return {
|
|
1042
|
+
content: [
|
|
1043
|
+
{
|
|
1044
|
+
type: "text",
|
|
1045
|
+
text: JSON.stringify(
|
|
1046
|
+
{
|
|
1047
|
+
success: true,
|
|
1048
|
+
exists: read.exists,
|
|
1049
|
+
...(tooBig
|
|
1050
|
+
? {
|
|
1051
|
+
documentOmitted: `The layout is ${serialized.length} characters, too large to show in full. Work from the summary below -- it carries the panel ids -- and change panels with actions rather than replacing the whole document.`,
|
|
1052
|
+
}
|
|
1053
|
+
: { document: read.document }),
|
|
1054
|
+
summary: cappedSummary(read.document),
|
|
1055
|
+
widgetTypes: widgetCatalogLines(),
|
|
1056
|
+
presets: presetLines(),
|
|
1057
|
+
...(read.problems.length ? { problems: cappedProblems(read.problems) } : {}),
|
|
1058
|
+
...(read.exists
|
|
1059
|
+
? {}
|
|
1060
|
+
: { note: "No layout file yet; this is the default the user sees." }),
|
|
1061
|
+
},
|
|
1062
|
+
null,
|
|
1063
|
+
2,
|
|
1064
|
+
),
|
|
1065
|
+
},
|
|
1066
|
+
],
|
|
1067
|
+
details: { panels: countPanels(read.document) },
|
|
1068
|
+
};
|
|
1069
|
+
},
|
|
1070
|
+
renderResult: (result) => {
|
|
1071
|
+
const d = result.details as { panels?: number; error?: boolean } | undefined;
|
|
1072
|
+
if (d?.error) return new Text("Could not read the dashboard");
|
|
1073
|
+
return new Text(`Dashboard: ${d?.panels ?? 0} panel(s)`);
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
pi.registerTool({
|
|
1078
|
+
name: "dashboard_update",
|
|
1079
|
+
label: "Update Dashboard Layout",
|
|
1080
|
+
description: UPDATE_DESCRIPTION,
|
|
1081
|
+
parameters: Type.Object({
|
|
1082
|
+
reason: Type.String({
|
|
1083
|
+
minLength: 1,
|
|
1084
|
+
description:
|
|
1085
|
+
"Why this change, in the user's terms. Recorded on every panel you add or change.",
|
|
1086
|
+
}),
|
|
1087
|
+
actions: Type.Optional(
|
|
1088
|
+
Type.Array(ActionSchema, {
|
|
1089
|
+
minItems: 1,
|
|
1090
|
+
maxItems: MAX_ACTIONS,
|
|
1091
|
+
description: "The changes to make, applied in order.",
|
|
1092
|
+
}),
|
|
1093
|
+
),
|
|
1094
|
+
document: Type.Optional(
|
|
1095
|
+
Type.String({
|
|
1096
|
+
description:
|
|
1097
|
+
"The whole layout as JSON text, replacing what is there. Use actions instead unless the user asked for a wholesale rebuild.",
|
|
1098
|
+
}),
|
|
1099
|
+
),
|
|
1100
|
+
}),
|
|
1101
|
+
async execute(_toolCallId, params) {
|
|
1102
|
+
const raw = params as Record<string, unknown>;
|
|
1103
|
+
const reason = asText(raw.reason);
|
|
1104
|
+
if (!reason) return toolFailure("reason is required: say why the user wants this change.");
|
|
1105
|
+
|
|
1106
|
+
if (given(raw.actions) && !Array.isArray(raw.actions)) {
|
|
1107
|
+
return toolFailure("actions must be a list of changes.");
|
|
1108
|
+
}
|
|
1109
|
+
if (given(raw.document) && typeof raw.document !== "string") {
|
|
1110
|
+
return toolFailure("document must be the whole layout as JSON text.");
|
|
1111
|
+
}
|
|
1112
|
+
const actions = (Array.isArray(raw.actions) ? raw.actions : []) as DashboardAction[];
|
|
1113
|
+
const documentText = typeof raw.document === "string" ? raw.document : undefined;
|
|
1114
|
+
|
|
1115
|
+
if (actions.length > 0 && documentText !== undefined) {
|
|
1116
|
+
return toolFailure("Pass either actions or document, not both.");
|
|
1117
|
+
}
|
|
1118
|
+
if (actions.length === 0 && documentText === undefined) {
|
|
1119
|
+
return toolFailure("Nothing to do: pass actions or document.");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const outcome = await commitDashboardChange(
|
|
1123
|
+
(current) => {
|
|
1124
|
+
if (documentText !== undefined) {
|
|
1125
|
+
const parsed = parseDashboardDocument(documentText);
|
|
1126
|
+
if (!parsed.ok) {
|
|
1127
|
+
return {
|
|
1128
|
+
ok: false,
|
|
1129
|
+
error: `document could not be read: ${parsed.problems
|
|
1130
|
+
.map((p) => `${p.path || "document"}: ${p.message}`)
|
|
1131
|
+
.join("; ")}`,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
// The caps have already been applied by the time we get here: the
|
|
1135
|
+
// validator enforces them by keeping the first N and calling it a
|
|
1136
|
+
// repair, so `commitDashboardChange` would compare this truncated
|
|
1137
|
+
// document against itself and find nothing missing. That is why
|
|
1138
|
+
// prepending one panel to a full dashboard reported success and
|
|
1139
|
+
// deleted the last one -- the guard that exists for exactly this
|
|
1140
|
+
// is unreachable on this path. Count what the model actually sent.
|
|
1141
|
+
const overflow = documentOverflow(documentText);
|
|
1142
|
+
if (overflow) return { ok: false, error: overflow };
|
|
1143
|
+
return {
|
|
1144
|
+
ok: true,
|
|
1145
|
+
document: parsed.document,
|
|
1146
|
+
notes: ["replaced the whole layout"],
|
|
1147
|
+
problems: parsed.problems,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
return applyDashboardActions(current, actions, reason);
|
|
1151
|
+
},
|
|
1152
|
+
{ reason },
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
if (!outcome.ok) return toolFailure(outcome.error, outcome.problems);
|
|
1156
|
+
|
|
1157
|
+
logDashboardChange(outcome.path, outcome.notes, "dashboard_update");
|
|
1158
|
+
return {
|
|
1159
|
+
content: [
|
|
1160
|
+
{
|
|
1161
|
+
type: "text",
|
|
1162
|
+
text: JSON.stringify(
|
|
1163
|
+
{
|
|
1164
|
+
success: true,
|
|
1165
|
+
changes: capLines(outcome.notes, MAX_SUMMARY_CHARS, "change(s)"),
|
|
1166
|
+
summary: cappedSummary(outcome.document),
|
|
1167
|
+
where: landedLine(),
|
|
1168
|
+
undo: "The user can put this back with /dashboard undo.",
|
|
1169
|
+
...(outcome.problems.length ? { repairs: cappedProblems(outcome.problems) } : {}),
|
|
1170
|
+
},
|
|
1171
|
+
null,
|
|
1172
|
+
2,
|
|
1173
|
+
),
|
|
1174
|
+
},
|
|
1175
|
+
],
|
|
1176
|
+
details: { changes: outcome.notes.length },
|
|
1177
|
+
};
|
|
1178
|
+
},
|
|
1179
|
+
renderResult: (result) => {
|
|
1180
|
+
const d = result.details as { changes?: number; error?: boolean } | undefined;
|
|
1181
|
+
if (d?.error) return new Text("Dashboard unchanged");
|
|
1182
|
+
return new Text(`Dashboard updated (${d?.changes ?? 0} change(s))`);
|
|
1183
|
+
},
|
|
1184
|
+
});
|
|
1185
|
+
}
|