@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.
Files changed (451) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/LICENSE +21 -0
  3. package/README.md +760 -5
  4. package/bin/legacy-mcp-config.js +363 -0
  5. package/bin/orbit.js +810 -0
  6. package/bin/pi-extension-path.d.ts +4 -0
  7. package/bin/pi-extension-path.js +44 -0
  8. package/bin/provider-auth.d.ts +22 -0
  9. package/bin/provider-auth.js +77 -0
  10. package/bin/thinking-pref.js +38 -0
  11. package/bin/update-check.d.ts +15 -0
  12. package/bin/update-check.js +137 -0
  13. package/extensions/cli-update/index.ts +32 -0
  14. package/extensions/orbit/activity-hooks.ts +141 -0
  15. package/extensions/orbit/activity.ts +116 -0
  16. package/extensions/orbit/agent-dir.ts +11 -0
  17. package/extensions/orbit/auto-resume.ts +352 -0
  18. package/extensions/orbit/config.ts +3 -0
  19. package/extensions/orbit/confusables.ts +1 -0
  20. package/extensions/orbit/context.ts +957 -0
  21. package/extensions/orbit/dashboard-commands.ts +191 -0
  22. package/extensions/orbit/dashboard-store.ts +473 -0
  23. package/extensions/orbit/dashboard-tools.ts +1185 -0
  24. package/extensions/orbit/empty-completion.ts +99 -0
  25. package/extensions/orbit/evidence-gate.ts +1276 -0
  26. package/extensions/orbit/evidence-override-command.ts +294 -0
  27. package/extensions/orbit/exec-guard/bash-risk.ts +1408 -0
  28. package/extensions/orbit/exec-guard/gate.ts +181 -0
  29. package/extensions/orbit/exec-guard/guardian-config.ts +54 -0
  30. package/extensions/orbit/exec-guard/index.ts +1 -0
  31. package/extensions/orbit/exec-guard/model-tier.ts +36 -0
  32. package/extensions/orbit/exec-guard/path-jail.ts +54 -0
  33. package/extensions/orbit/exec-guard/policy.ts +342 -0
  34. package/extensions/orbit/exec-guard/runtime-state.ts +15 -0
  35. package/extensions/orbit/exec-guard/sensitive-read.ts +525 -0
  36. package/extensions/orbit/exec-guard/types.ts +42 -0
  37. package/extensions/orbit/execution-commands.ts +134 -0
  38. package/extensions/orbit/feedback-command.ts +81 -0
  39. package/extensions/orbit/feedback.ts +96 -0
  40. package/extensions/orbit/galaxy-api.test.ts +122 -0
  41. package/extensions/orbit/galaxy-api.ts +606 -0
  42. package/extensions/orbit/galaxy-artifact-link-guidance.ts +5 -0
  43. package/extensions/orbit/galaxy-destructive-gate.ts +113 -0
  44. package/extensions/orbit/galaxy-enrich.ts +1108 -0
  45. package/extensions/orbit/galaxy-failure.ts +101 -0
  46. package/extensions/orbit/galaxy-fixture.ts +126 -0
  47. package/extensions/orbit/galaxy-history-guard.ts +233 -0
  48. package/extensions/orbit/galaxy-job-block.ts +347 -0
  49. package/extensions/orbit/galaxy-job-ops.ts +288 -0
  50. package/extensions/orbit/galaxy-live-source.ts +929 -0
  51. package/extensions/orbit/galaxy-markdown-adapter.ts +248 -0
  52. package/extensions/orbit/galaxy-op-policy.ts +332 -0
  53. package/extensions/orbit/galaxy-ops-tools.ts +326 -0
  54. package/extensions/orbit/galaxy-output-store.ts +80 -0
  55. package/extensions/orbit/galaxy-output.ts +346 -0
  56. package/extensions/orbit/galaxy-page-binding.ts +222 -0
  57. package/extensions/orbit/galaxy-page-markdown-guidance.ts +15 -0
  58. package/extensions/orbit/galaxy-page-ops.ts +159 -0
  59. package/extensions/orbit/galaxy-page-sync.test.ts +417 -0
  60. package/extensions/orbit/galaxy-page-sync.ts +318 -0
  61. package/extensions/orbit/galaxy-pages-api.ts +203 -0
  62. package/extensions/orbit/galaxy-pages-sync.ts +409 -0
  63. package/extensions/orbit/galaxy-poll-guard.ts +189 -0
  64. package/extensions/orbit/galaxy-poller.ts +706 -0
  65. package/extensions/orbit/galaxy-provenance.ts +623 -0
  66. package/extensions/orbit/galaxy-reconcile.ts +956 -0
  67. package/extensions/orbit/galaxy-recovery.ts +176 -0
  68. package/extensions/orbit/galaxy-repeat-guard.ts +55 -0
  69. package/extensions/orbit/galaxy-submission-capture.ts +707 -0
  70. package/extensions/orbit/galaxy-submission.ts +502 -0
  71. package/extensions/orbit/galaxy-tool-source.ts +78 -0
  72. package/extensions/orbit/galaxy-tool-wrap.ts +234 -0
  73. package/extensions/orbit/galaxy-udt-block.ts +127 -0
  74. package/extensions/orbit/galaxy-upload-tus.ts +334 -0
  75. package/extensions/orbit/galaxy-upload.ts +300 -0
  76. package/extensions/orbit/galaxy-version-capture.ts +65 -0
  77. package/extensions/orbit/git.ts +173 -0
  78. package/extensions/orbit/harness-block-fields.ts +597 -0
  79. package/extensions/orbit/index.ts +593 -0
  80. package/extensions/orbit/init-gate.ts +284 -0
  81. package/extensions/orbit/instructions-command.ts +136 -0
  82. package/extensions/orbit/invocation-failure-hint.ts +57 -0
  83. package/extensions/orbit/iwc-plan-check.ts +94 -0
  84. package/extensions/orbit/lesson-command.ts +303 -0
  85. package/extensions/orbit/lesson-hint.ts +245 -0
  86. package/extensions/orbit/lesson-nudge.ts +106 -0
  87. package/extensions/orbit/lessons/compose.ts +177 -0
  88. package/extensions/orbit/lessons/counters.ts +120 -0
  89. package/extensions/orbit/lessons/enabled.ts +116 -0
  90. package/extensions/orbit/lessons/matcher.ts +346 -0
  91. package/extensions/orbit/lessons/paths.ts +286 -0
  92. package/extensions/orbit/lessons/pi-event-contract.ts +105 -0
  93. package/extensions/orbit/lessons/propose-replay.ts +103 -0
  94. package/extensions/orbit/lessons/propose-tool.ts +222 -0
  95. package/extensions/orbit/lessons/propose.ts +502 -0
  96. package/extensions/orbit/lessons/replay.ts +89 -0
  97. package/extensions/orbit/lessons/reproduction-index.ts +83 -0
  98. package/extensions/orbit/lessons/search-tool.ts +99 -0
  99. package/extensions/orbit/lessons/search.ts +136 -0
  100. package/extensions/orbit/lessons/store.ts +415 -0
  101. package/extensions/orbit/lessons/text.ts +36 -0
  102. package/extensions/orbit/lessons/types.ts +69 -0
  103. package/extensions/orbit/lessons/user-dir.ts +24 -0
  104. package/extensions/orbit/lessons/user-lesson.ts +88 -0
  105. package/extensions/orbit/lessons/wrapper.ts +61 -0
  106. package/extensions/orbit/lessons-command.ts +147 -0
  107. package/extensions/orbit/local-exec.ts +39 -0
  108. package/extensions/orbit/mcp-output.ts +179 -0
  109. package/extensions/orbit/mcp-servers.test.ts +24 -0
  110. package/extensions/orbit/mcp-servers.ts +25 -0
  111. package/extensions/orbit/model-script.ts +351 -0
  112. package/extensions/orbit/notebook-anchors.ts +342 -0
  113. package/extensions/orbit/notebook-writer.ts +863 -0
  114. package/extensions/orbit/observation-allowlists.ts +1109 -0
  115. package/extensions/orbit/observation-replay.ts +148 -0
  116. package/extensions/orbit/observation-triggers.ts +563 -0
  117. package/extensions/orbit/observation-ui.ts +175 -0
  118. package/extensions/orbit/observations-command.ts +421 -0
  119. package/extensions/orbit/observations-config.ts +200 -0
  120. package/extensions/orbit/observations.ts +1381 -0
  121. package/extensions/orbit/profiles.ts +270 -0
  122. package/extensions/orbit/progress-updates.ts +95 -0
  123. package/extensions/orbit/proposal-commands.ts +882 -0
  124. package/extensions/orbit/provenance-fs-file.ts +59 -0
  125. package/extensions/orbit/provenance-fs.ts +32 -0
  126. package/extensions/orbit/provenance-memory-fs.ts +59 -0
  127. package/extensions/orbit/record-store-file.ts +177 -0
  128. package/extensions/orbit/record-store-page.ts +77 -0
  129. package/extensions/orbit/record-store-seam.ts +47 -0
  130. package/extensions/orbit/record-store.ts +149 -0
  131. package/extensions/orbit/registry-carrier.ts +107 -0
  132. package/extensions/orbit/registry-eligibility.ts +95 -0
  133. package/extensions/orbit/registry-evaluation.ts +403 -0
  134. package/extensions/orbit/registry-evaluator.ts +519 -0
  135. package/extensions/orbit/registry-import.ts +91 -0
  136. package/extensions/orbit/registry-lock.ts +221 -0
  137. package/extensions/orbit/registry-memory-fs.ts +42 -0
  138. package/extensions/orbit/registry-page-carrier.ts +103 -0
  139. package/extensions/orbit/registry-proposal.ts +1348 -0
  140. package/extensions/orbit/registry-runtime.ts +234 -0
  141. package/extensions/orbit/registry-schema.ts +735 -0
  142. package/extensions/orbit/registry-submitter.ts +59 -0
  143. package/extensions/orbit/registry-templates.ts +346 -0
  144. package/extensions/orbit/registry.ts +476 -0
  145. package/extensions/orbit/sandbox/index.ts +120 -0
  146. package/extensions/orbit/sandbox/sandbox-bash.ts +93 -0
  147. package/extensions/orbit/sandbox/sandbox-config.ts +87 -0
  148. package/extensions/orbit/sandbox/sandbox-init-message.ts +28 -0
  149. package/extensions/orbit/secret-redaction.ts +247 -0
  150. package/extensions/orbit/session-index/cwd.ts +18 -0
  151. package/extensions/orbit/session-index/db.ts +56 -0
  152. package/extensions/orbit/session-index/indexer.ts +154 -0
  153. package/extensions/orbit/session-index/is-enabled.ts +17 -0
  154. package/extensions/orbit/session-index/parse.ts +254 -0
  155. package/extensions/orbit/session-index/query.ts +209 -0
  156. package/extensions/orbit/session-index/schema.ts +72 -0
  157. package/extensions/orbit/session-index/tools.ts +152 -0
  158. package/extensions/orbit/session-lifecycle.ts +321 -0
  159. package/extensions/orbit/skill-triggers.ts +159 -0
  160. package/extensions/orbit/skills-command.ts +48 -0
  161. package/extensions/orbit/skills-discovery.ts +435 -0
  162. package/extensions/orbit/skills.ts +84 -0
  163. package/extensions/orbit/sra-import-gate.ts +77 -0
  164. package/extensions/orbit/state.ts +265 -0
  165. package/extensions/orbit/submission-replay.ts +147 -0
  166. package/extensions/orbit/sync-command.ts +157 -0
  167. package/extensions/orbit/teams/critic-parser.ts +93 -0
  168. package/extensions/orbit/teams/dispatcher.ts +210 -0
  169. package/extensions/orbit/teams/is-enabled.ts +22 -0
  170. package/extensions/orbit/teams/tool.ts +187 -0
  171. package/extensions/orbit/teams/types.ts +57 -0
  172. package/extensions/orbit/teams/validate.ts +42 -0
  173. package/extensions/orbit/tester-id-command.ts +133 -0
  174. package/extensions/orbit/tools-sync.ts +279 -0
  175. package/extensions/orbit/tools.ts +1300 -0
  176. package/extensions/orbit/types.ts +41 -0
  177. package/extensions/orbit/ui-bridge.ts +55 -0
  178. package/extensions/orbit/ulid.ts +60 -0
  179. package/extensions/orbit/user-instructions.ts +388 -0
  180. package/extensions/orbit/vendor/skills/_catalog.json +68 -0
  181. package/extensions/orbit/vendor/skills/_manifest.json +1106 -0
  182. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/SKILL.md +112 -0
  183. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_provenance.json +218 -0
  184. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_required_tools.json +33 -0
  185. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_verify.json +4 -0
  186. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/add.json +50 -0
  187. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-extract.json +51 -0
  188. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-next-step.json +42 -0
  189. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-validate.json +140 -0
  190. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/list.json +32 -0
  191. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/galaxy-tool-job-failure-reference.md +137 -0
  192. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/open-requirements-ledger.md +173 -0
  193. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-tool-summary.schema.json +175 -0
  194. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
  195. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/SKILL.md +82 -0
  196. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_provenance.json +133 -0
  197. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_required_tools.json +16 -0
  198. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_verify.json +4 -0
  199. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/cli/draft-validate.json +140 -0
  200. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/galaxy-workflow-draft-format.md +135 -0
  201. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
  202. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
  203. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/SKILL.md +84 -0
  204. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_provenance.json +166 -0
  205. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_verify.json +29 -0
  206. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/galaxy-workflow-testability-design.md +136 -0
  207. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  208. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-test-data-conventions.md +308 -0
  209. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-asserts-idioms.md +246 -0
  210. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-workflow-test-architecture.md +151 -0
  211. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
  212. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  213. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/tests-format.schema.json +7328 -0
  214. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/SKILL.md +127 -0
  215. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_provenance.json +198 -0
  216. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_required_tools.json +16 -0
  217. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_verify.json +4 -0
  218. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/cli/convert.json +123 -0
  219. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/galaxy-data-flow-draft-contract.md +129 -0
  220. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  221. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-test-data-conventions.md +308 -0
  222. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/open-requirements-ledger.md +173 -0
  223. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/SKILL.md +73 -0
  224. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_provenance.json +160 -0
  225. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_verify.json +4 -0
  226. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.md +43 -0
  227. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.yml +1250 -0
  228. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-tool-job-failure-reference.md +137 -0
  229. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  230. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  231. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/nextflow-operators-to-galaxy-collection-recipes.md +98 -0
  232. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-asserts-idioms.md +246 -0
  233. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-workflow-test-architecture.md +151 -0
  234. package/extensions/orbit/vendor/skills/discover-shed-tool/SKILL.md +174 -0
  235. package/extensions/orbit/vendor/skills/discover-shed-tool/_provenance.json +130 -0
  236. package/extensions/orbit/vendor/skills/discover-shed-tool/_required_tools.json +18 -0
  237. package/extensions/orbit/vendor/skills/discover-shed-tool/_verify.json +17 -0
  238. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-revisions.json +48 -0
  239. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-search.json +95 -0
  240. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-versions.json +39 -0
  241. package/extensions/orbit/vendor/skills/discover-shed-tool/references/notes/component-tool-shed-search.md +230 -0
  242. package/extensions/orbit/vendor/skills/discover-shed-tool/references/schemas/galaxy-tool-discovery.schema.json +231 -0
  243. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/SKILL.md +93 -0
  244. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_provenance.json +201 -0
  245. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_required_tools.json +28 -0
  246. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_verify.json +17 -0
  247. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/planemo.md +44 -0
  248. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/validate-tests.json +40 -0
  249. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/galaxy-workflow-testability-design.md +136 -0
  250. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  251. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-test-data-conventions.md +308 -0
  252. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
  253. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
  254. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
  255. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/tests-format.schema.json +7328 -0
  256. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/SKILL.md +81 -0
  257. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_provenance.json +93 -0
  258. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_verify.json +17 -0
  259. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
  260. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  261. package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/SKILL.md +81 -0
  262. package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/_assembly.json +23 -0
  263. package/extensions/orbit/vendor/skills/run-workflow-test/SKILL.md +79 -0
  264. package/extensions/orbit/vendor/skills/run-workflow-test/_provenance.json +125 -0
  265. package/extensions/orbit/vendor/skills/run-workflow-test/_required_tools.json +28 -0
  266. package/extensions/orbit/vendor/skills/run-workflow-test/_verify.json +4 -0
  267. package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/planemo.md +44 -0
  268. package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/validate-tests.json +40 -0
  269. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  270. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
  271. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
  272. package/extensions/orbit/vendor/skills/run-workflow-test/references/schemas/tests-format.schema.json +7328 -0
  273. package/extensions/orbit/vendor/skills/skills/LICENSE +21 -0
  274. package/extensions/orbit/vendor/skills/skills/collection-manipulation/README.md +39 -0
  275. package/extensions/orbit/vendor/skills/skills/collection-manipulation/SKILL.md +892 -0
  276. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/api-patterns.md +927 -0
  277. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/apply-rules.md +1482 -0
  278. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/test-patterns.md +996 -0
  279. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/tools.md +609 -0
  280. package/extensions/orbit/vendor/skills/skills/galaxy-data-libraries/SKILL.md +170 -0
  281. package/extensions/orbit/vendor/skills/skills/galaxy-integration/README.md +162 -0
  282. package/extensions/orbit/vendor/skills/skills/galaxy-integration/SKILL.md +99 -0
  283. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/README.md +119 -0
  284. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/tool-checking.md +419 -0
  285. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/workflow-testing.md +617 -0
  286. package/extensions/orbit/vendor/skills/skills/galaxy-integration/galaxy-integration.md +614 -0
  287. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/SKILL.md +144 -0
  288. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/average_col3.ipynb +68 -0
  289. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/extract_sample_metadata.ipynb +190 -0
  290. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/variant_annotation.ipynb +816 -0
  291. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/vcp_variant_map.ipynb +443 -0
  292. package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/README.md +165 -0
  293. package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/galaxy_tool_checker.py +521 -0
  294. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/SKILL.md +128 -0
  295. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/gotchas.md +135 -0
  296. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/history-access.md +132 -0
  297. package/extensions/orbit/vendor/skills/skills/reproduciblify/SKILL.md +146 -0
  298. package/extensions/orbit/vendor/skills/skills/reproduciblify/references/directives.yml +496 -0
  299. package/extensions/orbit/vendor/skills/skills/udt-authoring/SKILL.md +245 -0
  300. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/01-minimal-echo.yml +27 -0
  301. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/02-single-data-in-out.yml +27 -0
  302. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/03-multiple-data-js.yml +28 -0
  303. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/04-select-integer-boolean.yml +41 -0
  304. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/05-resource-requirements.yml +25 -0
  305. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/06-biocontainer-tool.yml +38 -0
  306. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/07-inline-python-script.yml +56 -0
  307. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/08-configfile-script.yml +74 -0
  308. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/common-mistakes.md +76 -0
  309. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/schema-reference.md +155 -0
  310. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/templating.md +196 -0
  311. package/extensions/orbit/vendor/skills/skills/udt-authoring/scripts/validate.py +109 -0
  312. package/extensions/orbit/vendor/skills/skills/workflow-reports/README.md +71 -0
  313. package/extensions/orbit/vendor/skills/skills/workflow-reports/SKILL.md +190 -0
  314. package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/histology-staining.md +145 -0
  315. package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/tissue-microarray-analysis.md +176 -0
  316. package/extensions/orbit/vendor/skills/skills/workflow-reports/references/directives.md +415 -0
  317. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/SKILL.md +98 -0
  318. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_provenance.json +93 -0
  319. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_required_tools.json +28 -0
  320. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_verify.json +17 -0
  321. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/convert.json +123 -0
  322. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/foundry.md +25 -0
  323. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/validate.json +142 -0
  324. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  325. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/SKILL.md +74 -0
  326. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_provenance.json +71 -0
  327. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_required_tools.json +16 -0
  328. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_verify.json +4 -0
  329. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/cli/validate.json +142 -0
  330. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  331. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-testability-design.md +136 -0
  332. package/extensions/orbit/vendor-skills.ts +283 -0
  333. package/extensions/orbit/workspace-state-dir.ts +46 -0
  334. package/extensions/orbit-handoff/index.ts +73 -0
  335. package/extensions/orbit-handoff/orbit-launcher.ts +142 -0
  336. package/extensions/whats-new/index.ts +98 -0
  337. package/lessons/LICENSE +17 -0
  338. package/lessons/README.md +117 -0
  339. package/lessons/SCHEMA.md +187 -0
  340. package/lessons/build-snapshot.mjs +311 -0
  341. package/lessons/data/downloaded-file-is-not-what-its-extension-says.md +61 -0
  342. package/lessons/galaxy-api/403-history-is-a-hard-stop.md +57 -0
  343. package/lessons/galaxy-api/collection-into-single-dataset-input.md +61 -0
  344. package/lessons/galaxy-api/connectedvalue-in-command-line.md +62 -0
  345. package/lessons/galaxy-api/hid-is-not-an-id.md +59 -0
  346. package/lessons/galaxy-api/invoke-workflow-inputs-not-params.md +60 -0
  347. package/lessons/galaxy-api/repeat-param-pipe-keys.md +63 -0
  348. package/lessons/galaxy-api/run-tool-returns-on-submit.md +61 -0
  349. package/lessons/galaxy-tools/reference-index-not-on-server.md +62 -0
  350. package/lessons/log.md +42 -0
  351. package/lessons/reproduction/input-population-mismatch.md +59 -0
  352. package/lessons/reproduction/methods-text-vs-executed-parameters.md +62 -0
  353. package/lessons/snapshot.json +1004 -0
  354. package/lessons/stats/de-contrast-direction-and-sample-labels.md +63 -0
  355. package/lessons/stats/na-coerced-to-zero-in-filters.md +59 -0
  356. package/lessons/validate.mjs +1219 -0
  357. package/package.json +103 -5
  358. package/shared/agent-prompt.d.ts +8 -0
  359. package/shared/agent-prompt.js +15 -0
  360. package/shared/approval-prompt.d.ts +4 -0
  361. package/shared/approval-prompt.js +61 -0
  362. package/shared/brain-env.d.ts +14 -0
  363. package/shared/brain-env.js +108 -0
  364. package/shared/brain-exit.d.ts +20 -0
  365. package/shared/brain-exit.js +149 -0
  366. package/shared/capability-profile.d.ts +16 -0
  367. package/shared/capability-profile.js +75 -0
  368. package/shared/config-source.d.ts +13 -0
  369. package/shared/config-source.js +17 -0
  370. package/shared/confusables.d.ts +3 -0
  371. package/shared/confusables.js +93 -0
  372. package/shared/custom-provider.d.ts +45 -0
  373. package/shared/custom-provider.js +169 -0
  374. package/shared/dashboard-contract.d.ts +123 -0
  375. package/shared/dashboard-contract.js +560 -0
  376. package/shared/dashboard-layout-store.d.ts +44 -0
  377. package/shared/dashboard-layout-store.js +281 -0
  378. package/shared/feedback-contract.d.ts +61 -0
  379. package/shared/feedback-contract.js +146 -0
  380. package/shared/galaxy-artifact-links.d.ts +30 -0
  381. package/shared/galaxy-artifact-links.js +166 -0
  382. package/shared/galaxy-credentials.d.ts +26 -0
  383. package/shared/galaxy-credentials.js +71 -0
  384. package/shared/galaxy-destructive.d.ts +21 -0
  385. package/shared/galaxy-destructive.js +269 -0
  386. package/shared/galaxy-guidance.d.ts +2 -0
  387. package/shared/galaxy-guidance.js +53 -0
  388. package/shared/galaxy-live-contract.d.ts +92 -0
  389. package/shared/galaxy-live-contract.js +180 -0
  390. package/shared/galaxy-page-cells.d.ts +2 -0
  391. package/shared/galaxy-page-cells.js +22 -0
  392. package/shared/galaxy-tool-family.d.ts +5 -0
  393. package/shared/galaxy-tool-family.js +27 -0
  394. package/shared/galaxy-url.d.ts +1 -0
  395. package/shared/galaxy-url.js +48 -0
  396. package/shared/gtn.d.ts +31 -0
  397. package/shared/gtn.js +253 -0
  398. package/shared/handled-prompt.d.ts +3 -0
  399. package/shared/handled-prompt.js +26 -0
  400. package/shared/known-provider.d.ts +11 -0
  401. package/shared/known-provider.js +89 -0
  402. package/shared/lesson-hint-marker.d.ts +7 -0
  403. package/shared/lesson-hint-marker.js +32 -0
  404. package/shared/lesson-rules.d.ts +150 -0
  405. package/shared/lesson-rules.js +203 -0
  406. package/shared/malformed-object-ids.d.ts +1 -0
  407. package/shared/malformed-object-ids.js +37 -0
  408. package/shared/markdown-sections.d.ts +10 -0
  409. package/shared/markdown-sections.js +118 -0
  410. package/shared/notebook-fences.d.ts +12 -0
  411. package/shared/notebook-fences.js +53 -0
  412. package/shared/observation-contract.d.ts +91 -0
  413. package/shared/observation-contract.js +544 -0
  414. package/shared/orbit-config.d.ts +226 -0
  415. package/shared/orbit-config.js +189 -0
  416. package/shared/orbit-env.d.ts +13 -0
  417. package/shared/orbit-env.js +124 -0
  418. package/shared/orbit-shell-contract.d.ts +74 -0
  419. package/shared/orbit-shell-contract.js +30 -0
  420. package/shared/page-content-hash.d.ts +1 -0
  421. package/shared/page-content-hash.js +13 -0
  422. package/shared/pi-default-model.d.ts +7 -0
  423. package/shared/pi-default-model.js +41 -0
  424. package/shared/prompt-blocks.d.ts +87 -0
  425. package/shared/prompt-blocks.js +898 -0
  426. package/shared/provider-auth-caps.d.ts +22 -0
  427. package/shared/provider-auth-caps.js +57 -0
  428. package/shared/random.d.ts +2 -0
  429. package/shared/random.js +15 -0
  430. package/shared/record-excerpt.d.ts +4 -0
  431. package/shared/record-excerpt.js +16 -0
  432. package/shared/redirect-guard.d.ts +36 -0
  433. package/shared/redirect-guard.js +235 -0
  434. package/shared/skills-pin.d.ts +8 -0
  435. package/shared/skills-pin.js +9 -0
  436. package/shared/skills-router.d.ts +17 -0
  437. package/shared/skills-router.js +63 -0
  438. package/shared/sra-import.d.ts +34 -0
  439. package/shared/sra-import.js +206 -0
  440. package/shared/state-dir.d.ts +21 -0
  441. package/shared/state-dir.js +240 -0
  442. package/shared/team-dispatch-contract.d.ts +28 -0
  443. package/shared/team-dispatch-contract.js +1 -0
  444. package/shared/truncate-middle.d.ts +11 -0
  445. package/shared/truncate-middle.js +68 -0
  446. package/shared/version-compare.d.ts +10 -0
  447. package/shared/version-compare.js +74 -0
  448. package/shared/whats-new.d.ts +31 -0
  449. package/shared/whats-new.js +83 -0
  450. package/shared/wsl.d.ts +10 -0
  451. package/shared/wsl.js +24 -0
@@ -0,0 +1,929 @@
1
+ /**
2
+ * Live Galaxy history snapshot for the dashboard's Galaxy panel.
3
+ *
4
+ * Why the brain and not a shell: the brain is the only process that can obtain
5
+ * GALAXY_URL/GALAXY_API_KEY from all three places they come from -- an env var
6
+ * Orbit injects after decrypting safeStorage, an env var a developer exported,
7
+ * and a plaintext profile on disk. A shell-side proxy would have to be written
8
+ * once per shell and would still be dead in whichever one the user happened to
9
+ * be in. Reading here and pushing the projection over the existing
10
+ * `ctx.ui.setWidget` channel costs no new credential surface and no new IPC.
11
+ *
12
+ * Cost control, measured against usegalaxy.org and usegalaxy.eu on 2026-09-18:
13
+ * 1. The summary probe is 179 bytes and already carries the headline -- name,
14
+ * update_time, overall state and the active/hidden/deleted counts. A tick
15
+ * where nothing moved never touches the contents endpoint.
16
+ * 2. The contents index IGNORES `keys=` and `view=`: it returns the full
17
+ * serialization of every item including deleted and hidden ones (547 KB
18
+ * for one real history). The `q`/`qv` filters are what actually work
19
+ * (deleted=False, visible=True -> 17.5 KB), and `order=hid-dsc` puts the
20
+ * newest first so `limit` caps at the server rather than after half a
21
+ * megabyte is already on the wire.
22
+ * 3. Contents are refetched only when Galaxy's own update_time moves.
23
+ *
24
+ * Galaxy 26.1 is the floor. The `v=dev` behaviour above was not checked against
25
+ * anything older; on a server that ignores `v=dev` the endpoint fails the other
26
+ * way (the deprecated filters apply and `limit` does not), so the result is
27
+ * over-fetching rather than showing deleted rows as live.
28
+ */
29
+
30
+ import * as fsp from "node:fs/promises";
31
+
32
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
33
+ import {
34
+ GALAXY_LIVE_MAX_ITEMS,
35
+ GALAXY_LIVE_MAX_TOKEN,
36
+ GALAXY_LIVE_SCHEMA_VERSION,
37
+ clampText,
38
+ normalizeState,
39
+ } from "../../shared/galaxy-live-contract.js";
40
+ import type {
41
+ GalaxyLiveHistory,
42
+ GalaxyLiveItem,
43
+ GalaxyLivePayload,
44
+ GalaxyLiveState,
45
+ } from "../../shared/galaxy-live-contract.js";
46
+ import { OrbitWidgetKey, encodeJsonWidget } from "../../shared/orbit-shell-contract.js";
47
+ import {
48
+ GalaxyApiError,
49
+ galaxyGet,
50
+ galaxyGetMostRecentHistory,
51
+ getGalaxyConfig,
52
+ sameGalaxyServer,
53
+ type GalaxyHistorySummary,
54
+ } from "./galaxy-api.js";
55
+ import { findGalaxyPageBlocks } from "./galaxy-page-binding.js";
56
+ import { findJobBlocks } from "./galaxy-job-block.js";
57
+ import { findInvocationBlocks } from "./notebook-writer.js";
58
+ import { setPollTickHook } from "./galaxy-poller.js";
59
+ import { getDashboardPath, readDashboardDocument } from "./dashboard-store.js";
60
+ import { capabilityProfile } from "../../shared/capability-profile.js";
61
+
62
+ /** The only Galaxy reads this source is allowed to make. Anything not built by
63
+ * these two functions never leaves the module, so there is no path from a
64
+ * renderer-supplied string to an arbitrary Galaxy path. */
65
+ const SUMMARY_KEYS = "name,update_time,state,contents_active";
66
+
67
+ function historySummaryPath(historyId: string): string {
68
+ return `/histories/${encodeURIComponent(historyId)}?keys=${SUMMARY_KEYS}`;
69
+ }
70
+
71
+ /**
72
+ * Ask for one row more than we intend to keep. Without the extra row a history
73
+ * with exactly `limit` items and a history with ten thousand are
74
+ * indistinguishable, and `truncated` silently reports 0 on the second -- which
75
+ * is the panel telling the user there is nothing more to see.
76
+ */
77
+ function historyContentsPath(historyId: string, limit: number): string {
78
+ // Every value here is a literal. Nothing a renderer or a model supplies ever
79
+ // reaches the query string -- the only caller-supplied value is historyId,
80
+ // which is checked against ENCODED_ID_RE first.
81
+ const query = [
82
+ "v=dev",
83
+ "q=deleted&qv=False",
84
+ "q=visible&qv=True",
85
+ "order=hid-dsc",
86
+ `limit=${limit}`,
87
+ ].join("&");
88
+ return `/histories/${encodeURIComponent(historyId)}/contents?${query}`;
89
+ }
90
+
91
+ /**
92
+ * Galaxy encoded ids are hex, and are a multiple of 16 characters.
93
+ *
94
+ * The guard is not decoration. A value like `.` survives encodeURIComponent,
95
+ * and URL normalization turns the SUMMARY path `/api/histories/.` into
96
+ * `/api/histories/` -- the *collection* endpoint, which answers 200 with `[]`
97
+ * (verified against usegalaxy.org). Without this check the probe would take
98
+ * that 200 as a real history and the panel would draw an empty "Untitled
99
+ * history" for an id that does not exist. The contents path behaves
100
+ * differently -- `/api/histories/./contents` is a 400 -- which is exactly why
101
+ * checking only the second call would miss it.
102
+ *
103
+ * The length bound keeps a megabyte of hex from being pasted into a URL;
104
+ * Galaxy rejects a non-multiple-of-16 id itself, so this only avoids asking.
105
+ */
106
+ const ENCODED_ID_RE = /^[0-9a-fA-F]{16,64}$/;
107
+
108
+ interface HistorySummary {
109
+ name?: unknown;
110
+ update_time?: unknown;
111
+ state?: unknown;
112
+ /** {active, hidden, deleted} over the history's items. Used ONLY to decide
113
+ * whether rows were left behind, never to count states -- see projectHistory. */
114
+ contents_active?: unknown;
115
+ }
116
+
117
+ interface ContentsRow {
118
+ id?: unknown;
119
+ hid?: unknown;
120
+ name?: unknown;
121
+ state?: unknown;
122
+ extension?: unknown;
123
+ deleted?: unknown;
124
+ visible?: unknown;
125
+ history_content_type?: unknown;
126
+ element_count?: unknown;
127
+ populated_state?: unknown;
128
+ job_state_summary?: unknown;
129
+ }
130
+
131
+ /** Host only -- the panel shows "usegalaxy.org", never a URL that could carry a
132
+ * query string, and never the key. */
133
+ export function serverHostOf(url: string | undefined): string | null {
134
+ if (!url) return null;
135
+ try {
136
+ return new URL(url).host;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Collapse a collection's per-element job states into one state for the row.
144
+ * Galaxy reports `job_state_summary` counts; the panel wants the worst thing
145
+ * happening inside. Error beats paused beats running beats queued beats ok,
146
+ * because the point of the row is "is this going well".
147
+ */
148
+ function collectionState(row: ContentsRow): GalaxyLiveState {
149
+ const summary = row.job_state_summary;
150
+ if (summary && typeof summary === "object") {
151
+ const counts = summary as Record<string, unknown>;
152
+ const n = (k: string): number => (typeof counts[k] === "number" ? (counts[k] as number) : 0);
153
+ if (n("error") > 0 || n("failed") > 0) return "error";
154
+ if (n("paused") > 0) return "paused";
155
+ if (n("running") > 0) return "running";
156
+ if (n("new") > 0 || n("queued") > 0) return "queued";
157
+ }
158
+ // populated_state is the collection's own build state; "new" means Galaxy is
159
+ // still filling it in.
160
+ if (row.populated_state === "failed") return "error";
161
+ if (row.populated_state === "new") return "new";
162
+ return "ok";
163
+ }
164
+
165
+ /**
166
+ * Project one Galaxy contents row into a panel row. Returns null for rows the
167
+ * panel never shows (deleted, hidden) so the caller's cap applies to visible
168
+ * rows rather than being eaten by invisible ones.
169
+ */
170
+ export function projectRow(raw: unknown): GalaxyLiveItem | null {
171
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
172
+ const row = raw as ContentsRow;
173
+ if (row.deleted === true) return null;
174
+ if (row.visible === false) return null;
175
+ if (typeof row.id !== "string" || !row.id) return null;
176
+ const isCollection = row.history_content_type === "dataset_collection";
177
+ const item: GalaxyLiveItem = {
178
+ // Clamped like every other Galaxy-supplied string. Capping the row count is
179
+ // not capping the payload: a server answering with a hundred-thousand-
180
+ // character id per row turns 200 rows into twenty megabytes on one line.
181
+ id: clampText(row.id, GALAXY_LIVE_MAX_TOKEN),
182
+ hid: typeof row.hid === "number" ? row.hid : 0,
183
+ name: clampText(row.name),
184
+ state: isCollection ? collectionState(row) : normalizeState(row.state),
185
+ extension: clampText(typeof row.extension === "string" ? row.extension : "", 32),
186
+ kind: isCollection ? "collection" : "dataset",
187
+ };
188
+ if (isCollection && typeof row.element_count === "number") {
189
+ item.elementCount = row.element_count;
190
+ }
191
+ return item;
192
+ }
193
+
194
+ /** A JSON object, as opposed to null, an array, or a string a proxy sent. */
195
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
196
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
197
+ }
198
+
199
+ function activeCount(raw: unknown): number | null {
200
+ if (!isPlainObject(raw)) return null;
201
+ const active = raw.active;
202
+ return typeof active === "number" && active >= 0 ? active : null;
203
+ }
204
+
205
+ /** A positive, finite row cap. `slice(0, NaN)` keeps nothing, which would turn a
206
+ * full history into an empty panel. */
207
+ function keepCount(keep: number): number {
208
+ return Number.isFinite(keep) && keep >= 1 ? Math.floor(keep) : GALAXY_LIVE_MAX_ITEMS;
209
+ }
210
+
211
+ /**
212
+ * Build the history projection from the two raw responses. Pure, so the shape
213
+ * the renderer sees is testable without a network.
214
+ *
215
+ * **Counts are computed from the rows, deliberately.** Galaxy hands you a
216
+ * ready-made histogram in `contents_states`, and using it was the first thing
217
+ * this did; it is wrong here. `contents_states` scores a dataset collection by
218
+ * the collection's own state, while the row below it is scored by the worst job
219
+ * inside it -- so on a real usegalaxy.org history the header read "10 failed"
220
+ * above a list showing 19 red rows, 9 of them collections. A summary that
221
+ * disagrees with the list under it is worse than one that counts a smaller set
222
+ * correctly. So the header counts exactly the rows that were fetched,
223
+ * `countsComplete` says whether that was all of them, and `contents_active` is
224
+ * used for nothing except working out how many were left behind.
225
+ */
226
+ export function projectHistory(
227
+ historyId: string,
228
+ summary: unknown,
229
+ contents: unknown,
230
+ keep: number = GALAXY_LIVE_MAX_ITEMS,
231
+ ): GalaxyLiveHistory {
232
+ const s = (
233
+ summary && typeof summary === "object" && !Array.isArray(summary) ? summary : {}
234
+ ) as HistorySummary;
235
+ const rows = Array.isArray(contents) ? contents : [];
236
+ const items: GalaxyLiveItem[] = [];
237
+ for (const raw of rows) {
238
+ const item = projectRow(raw);
239
+ if (item) items.push(item);
240
+ }
241
+ // Galaxy is asked for hid-descending, but an older server that ignores
242
+ // `order` would hand back ascending; sort so the newest row is first either
243
+ // way. The thing a user is waiting on is the thing they just launched.
244
+ items.sort((a, b) => b.hid - a.hid);
245
+ const capped = items.slice(0, keepCount(keep));
246
+
247
+ const counts: Partial<Record<GalaxyLiveState, number>> = {};
248
+ for (const item of capped) counts[item.state] = (counts[item.state] ?? 0) + 1;
249
+
250
+ // Two independent ways to know rows were left behind, and the answer is the
251
+ // larger of them rather than whichever one happens to be present.
252
+ // `contents_active.active` is exact but absent on a server that silently
253
+ // drops the `keys=` we asked for (it answers 200 either way). The overflow
254
+ // row -- we request keep+1 and keep `keep` -- is always available but only
255
+ // proves "at least one more". Preferring the server's count outright was
256
+ // wrong: the summary and the contents are two requests a second apart, so a
257
+ // history that grew in between returns more rows than the count admits to,
258
+ // and the panel then reports nothing hidden while rows are missing.
259
+ const active = activeCount(s.contents_active);
260
+ const overflow = Math.max(0, items.length - capped.length);
261
+ const fromServer = active !== null ? Math.max(0, active - capped.length) : 0;
262
+ const truncated = Math.max(fromServer, overflow);
263
+ const truncatedExact = active !== null ? fromServer >= overflow : overflow === 0;
264
+
265
+ return {
266
+ id: historyId,
267
+ name: clampText(s.name) || "Untitled history",
268
+ updateTime: clampText(s.update_time, GALAXY_LIVE_MAX_TOKEN),
269
+ counts,
270
+ countsComplete: truncated === 0,
271
+ items: capped,
272
+ truncated,
273
+ truncatedExact,
274
+ };
275
+ }
276
+
277
+ /** Per-tick wall-clock bound. Comfortably above the ~1 s a cold contents fetch
278
+ * took against usegalaxy.org, well under the 15 s poller interval. */
279
+ const DEFAULT_TIMEOUT_MS = 10_000;
280
+
281
+ /**
282
+ * How many contents rows we will project, whatever Galaxy sends.
283
+ *
284
+ * We request `GALAXY_LIVE_MAX_ITEMS + 1`; a server that honours `limit` never
285
+ * reaches this. One that ignores it hands back the whole history, and then
286
+ * `projectRow` and the sort run over the lot on the brain's 15 s timer, every
287
+ * 15 s, for as long as the session lasts.
288
+ *
289
+ * Extra rows are dropped rather than refused. Refusing pins the panel on
290
+ * "Galaxy did not answer" for the rest of the session -- the next tick gets the
291
+ * same oversized answer and the one after that too -- while Galaxy is in fact
292
+ * answering, and the user can see their history perfectly well in Galaxy's own
293
+ * UI. Ten of what we asked for is enough slack that this only fires on a server
294
+ * that is genuinely ignoring us.
295
+ *
296
+ * This bounds the work, NOT the transfer -- by the time it is consulted the
297
+ * body has already been buffered and parsed, because `galaxyGet` ends in
298
+ * `resp.json()`. A real byte bound has to go there, which is outside this file.
299
+ */
300
+ const MAX_CONTENTS_ROWS = GALAXY_LIVE_MAX_ITEMS * 10;
301
+
302
+ /**
303
+ * The rows worth projecting, from the end of the list that holds them.
304
+ *
305
+ * We ask for `order=hid-dsc`, so the newest are at the head -- but an older
306
+ * server that ignored `order` hands back ascending, and `projectHistory` only
307
+ * sorts what it is given. Cutting the head there would hand it the oldest rows
308
+ * in the history and the panel would confidently show a user work they finished
309
+ * last year. Read the direction off the rows instead of trusting the query.
310
+ */
311
+ function boundContentsRows(rows: unknown[]): unknown[] {
312
+ if (rows.length <= MAX_CONTENTS_ROWS) return rows;
313
+ const hid = (row: unknown): number | null => {
314
+ const value = (row as { hid?: unknown } | null)?.hid;
315
+ return typeof value === "number" ? value : null;
316
+ };
317
+ const first = hid(rows[0]);
318
+ const last = hid(rows[rows.length - 1]);
319
+ const ascending = first !== null && last !== null && last > first;
320
+ return ascending ? rows.slice(-MAX_CONTENTS_ROWS) : rows.slice(0, MAX_CONTENTS_ROWS);
321
+ }
322
+
323
+ /**
324
+ * Which failure the user is looking at. The split that matters is 401 from
325
+ * 403: Galaxy answers 401 for a key it rejects, and 403 for a good key on a
326
+ * history that is not yours. Collapsing them sends someone to rotate a
327
+ * credential that is fine.
328
+ */
329
+ function classifyError(err: unknown): NonNullable<GalaxyLivePayload["unavailable"]> {
330
+ if (!(err instanceof GalaxyApiError)) return "unreachable";
331
+ if (err.status === 401) return "unauthorized";
332
+ if (err.status === 403) return "forbidden";
333
+ if (err.status === 400 || err.status === 404) return "no-history";
334
+ return "unreachable";
335
+ }
336
+
337
+ export interface GalaxyLiveDeps {
338
+ /** Injected so the whole source is testable without a network or env. */
339
+ get: <T>(path: string, signal?: AbortSignal) => Promise<T>;
340
+ config: () => { url: string; apiKey: string } | null;
341
+ now: () => Date;
342
+ }
343
+
344
+ const defaultDeps: GalaxyLiveDeps = {
345
+ get: galaxyGet,
346
+ config: getGalaxyConfig,
347
+ now: () => new Date(),
348
+ };
349
+
350
+ export interface SnapshotOptions {
351
+ /** The update_time the caller already has. When it still matches, contents
352
+ * are not refetched and the result carries no payload. */
353
+ knownUpdateTime?: string;
354
+ signal?: AbortSignal;
355
+ /**
356
+ * Wall-clock bound per tick. `galaxyGet` is a bare fetch with no timeout of
357
+ * its own, and this runs on a repeating timer -- without a bound, one hung
358
+ * Galaxy stacks in-flight requests until something falls over.
359
+ */
360
+ timeoutMs?: number;
361
+ }
362
+
363
+ export interface SnapshotResult {
364
+ /**
365
+ * null means "do not push anything" -- either Galaxy said nothing moved, or
366
+ * the tick was aborted. Making it null rather than a history-less payload is
367
+ * the point: the alternative shape renders as "No history yet.", so a caller
368
+ * that forwarded an unchanged result would blank a populated panel and tell
369
+ * the user their analysis has no history.
370
+ */
371
+ payload: GalaxyLivePayload | null;
372
+ /** Galaxy answered and nothing had changed since `knownUpdateTime`. */
373
+ unchanged: boolean;
374
+ /** The caller's signal fired. Not a Galaxy failure and must not be shown as one. */
375
+ aborted: boolean;
376
+ }
377
+
378
+ /**
379
+ * One read: probe, and refetch contents only if Galaxy says the history moved.
380
+ *
381
+ * Never throws. A Galaxy failure becomes an `unavailable` payload, because the
382
+ * panel's job is to say "I cannot see Galaxy right now" rather than to vanish.
383
+ */
384
+ export async function fetchGalaxyLiveSnapshot(
385
+ historyId: string,
386
+ opts: SnapshotOptions = {},
387
+ deps: GalaxyLiveDeps = defaultDeps,
388
+ ): Promise<SnapshotResult> {
389
+ const cfg = deps.config();
390
+ const host = serverHostOf(cfg?.url);
391
+
392
+ // Stamped when the payload is built, not when the tick started, so
393
+ // `updatedAt` means what the contract says it means.
394
+ const bare = (unavailable: GalaxyLivePayload["unavailable"]): SnapshotResult => ({
395
+ payload: {
396
+ version: GALAXY_LIVE_SCHEMA_VERSION,
397
+ serverHost: host,
398
+ history: null,
399
+ unavailable,
400
+ updatedAt: deps.now().toISOString(),
401
+ },
402
+ unchanged: false,
403
+ aborted: false,
404
+ });
405
+ const quiet = (aborted: boolean): SnapshotResult => ({
406
+ payload: null,
407
+ unchanged: !aborted,
408
+ aborted,
409
+ });
410
+
411
+ if (!cfg) return bare("not-configured");
412
+ if (!historyId || !ENCODED_ID_RE.test(historyId)) return bare("no-history");
413
+
414
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
415
+ const timer = AbortSignal.timeout(timeoutMs);
416
+ const signal = opts.signal ? AbortSignal.any([opts.signal, timer]) : timer;
417
+ /** A caller-cancelled tick is not a Galaxy failure; a timeout is. */
418
+ const cancelled = (): boolean => Boolean(opts.signal?.aborted);
419
+
420
+ let raw: unknown;
421
+ try {
422
+ raw = await deps.get<unknown>(historySummaryPath(historyId), signal);
423
+ } catch (err) {
424
+ if (cancelled()) return quiet(true);
425
+ return bare(classifyError(err));
426
+ }
427
+ // A 200 is not an answer. Galaxy returning JSON `null` here -- which a proxy
428
+ // in front of it can do too -- used to throw straight out of a function whose
429
+ // whole contract is that it does not, so the tick died in the poller's catch
430
+ // with no payload and no backoff. Anything that is not an object is the same
431
+ // problem: unreadable, retry, say so.
432
+ if (!isPlainObject(raw)) return bare("unreachable");
433
+ const summary = raw as HistorySummary;
434
+
435
+ const updateTime = typeof summary.update_time === "string" ? summary.update_time : "";
436
+ if (opts.knownUpdateTime && updateTime && opts.knownUpdateTime === updateTime) {
437
+ return quiet(false);
438
+ }
439
+
440
+ let contents: unknown;
441
+ try {
442
+ contents = await deps.get<unknown>(
443
+ historyContentsPath(historyId, GALAXY_LIVE_MAX_ITEMS + 1),
444
+ signal,
445
+ );
446
+ } catch (err) {
447
+ if (cancelled()) return quiet(true);
448
+ return bare(classifyError(err));
449
+ }
450
+ // `projectHistory` turns a non-array into no rows, which is indistinguishable
451
+ // from an empty history -- so a server answering 200 with an error object, or
452
+ // an HTML login page a proxy substituted, drew a confident "No datasets yet"
453
+ // over an analysis that has them. Refuse the shape instead.
454
+ if (!Array.isArray(contents)) return bare("unreachable");
455
+
456
+ return {
457
+ payload: {
458
+ version: GALAXY_LIVE_SCHEMA_VERSION,
459
+ serverHost: host,
460
+ history: projectHistory(historyId, summary, boundContentsRows(contents)),
461
+ updatedAt: deps.now().toISOString(),
462
+ },
463
+ unchanged: false,
464
+ aborted: false,
465
+ };
466
+ }
467
+
468
+ // ── Which history ────────────────────────────────────────────────────────────
469
+
470
+ /**
471
+ * The history this analysis is attached to, as Orbit already tracks it. No
472
+ * user-typed ids: the widget cannot ask for one anyway (the widget channel is
473
+ * push-only) and every id that reaches a request has to be one Orbit wrote down.
474
+ *
475
+ * The notebook's `loom-galaxy-page` binding wins, and only when it names the
476
+ * server we are actually talking to -- a notebook carried over from another
477
+ * Galaxy would otherwise have us probe a stranger's id and render "this history
478
+ * belongs to another account" forever.
479
+ *
480
+ * `getCurrentHistoryId()` from state.ts is deliberately not used: it follows
481
+ * the last create_history, not the history the notebook is bound to.
482
+ */
483
+ export function historyIdFromNotebook(content: string | null, serverUrl: string): string | null {
484
+ if (!content) return null;
485
+ const bindings = findGalaxyPageBlocks(content).filter((b) =>
486
+ sameGalaxyServer(b.galaxyServerUrl, serverUrl),
487
+ );
488
+ const last = bindings[bindings.length - 1];
489
+ return last?.historyId || null;
490
+ }
491
+
492
+ /** Is anything Orbit launched still going? Drives the poll cadence, nothing else. */
493
+ export function notebookHasLiveWork(content: string | null): boolean {
494
+ if (!content) return false;
495
+ if (findInvocationBlocks(content).some((b) => b.status === "in_progress")) return true;
496
+ return findJobBlocks(content).some((b) => b.status === "in_progress");
497
+ }
498
+
499
+ // ── Cadence ──────────────────────────────────────────────────────────────────
500
+
501
+ /**
502
+ * Cadences, in milliseconds. The ticker does not own a timer: it is driven by
503
+ * the existing Galaxy poller's 15 s tick and these are minimum spacings
504
+ * measured against that tick, so there is one interval in the process, one
505
+ * place that knows whether anything is in flight, and one thing to stop.
506
+ */
507
+ const ACTIVE_INTERVAL_MS = 15_000;
508
+ /** Nothing Orbit launched is running. The history can still move -- a user can
509
+ * upload in Galaxy's own UI -- so we keep looking, just not four times a minute. */
510
+ const IDLE_INTERVAL_MS = 60_000;
511
+ /** Consecutive failures widen the gap from ACTIVE up to this. */
512
+ const MAX_BACKOFF_MS = 300_000;
513
+ /** Re-push an unchanged payload this often so the panel's "checked N ago" line
514
+ * cannot claim we have stopped asking when we have not. */
515
+ const STALE_REFRESH_MS = 60_000;
516
+ /** How long a resolved most-recently-used history is reused before re-asking.
517
+ * Only consulted when the notebook carries no binding. */
518
+ const HISTORY_RESOLVE_TTL_MS = 300_000;
519
+
520
+ export interface GalaxyLiveTickerDeps {
521
+ snapshot: typeof fetchGalaxyLiveSnapshot;
522
+ config: () => { url: string; apiKey: string } | null;
523
+ mostRecentHistory: (signal?: AbortSignal) => Promise<GalaxyHistorySummary | null>;
524
+ /** Hand a payload to the shell. Must not throw. */
525
+ push: (payload: GalaxyLivePayload) => void;
526
+ now: () => number;
527
+ /**
528
+ * Is there a panel to draw this? Checked at the top of every tick, before
529
+ * the cadence, so a session whose dashboards never include the live history
530
+ * asks Galaxy nothing at all. Absent means always.
531
+ */
532
+ wanted?: () => Promise<boolean>;
533
+ }
534
+
535
+ /** Everything except `updatedAt`, which moves on every read and would defeat
536
+ * the comparison it is supposed to take part in. */
537
+ function payloadFingerprint(payload: GalaxyLivePayload): string {
538
+ const { updatedAt: _ignored, ...rest } = payload;
539
+ return JSON.stringify(rest);
540
+ }
541
+
542
+ /**
543
+ * Decides when to ask Galaxy and what to push, given the notebook the poller
544
+ * already read. Separate from the fetch so the cadence rules can be tested
545
+ * with a clock and no network.
546
+ */
547
+ export class GalaxyLiveTicker {
548
+ private running = false;
549
+ private lastAttemptAt = 0;
550
+ private lastPushAt = 0;
551
+ private lastFingerprint: string | null = null;
552
+ /**
553
+ * The last payload that actually carried a history. Deliberately NOT "the
554
+ * last payload": an `unchanged` tick re-stamps this to refresh the staleness
555
+ * clock, and re-stamping an error payload wedged the panel on "Galaxy did not
556
+ * answer" for the rest of the session. One dropped packet, and a settled
557
+ * history whose `update_time` never moves again could never clear it.
558
+ */
559
+ private lastHistoryPayload: GalaxyLivePayload | null = null;
560
+ private knownUpdateTime: string | null = null;
561
+ private historyId: string | null = null;
562
+ private failures = 0;
563
+ private resolved: { id: string; at: number; server: string } | null = null;
564
+ /** Whether the previous tick found a panel. Starts false so the first tick
565
+ * that finds one is treated as the panel appearing. */
566
+ private panelWanted = false;
567
+ /**
568
+ * Everything this ticker asks Galaxy hangs off this, so `stop()` abandons
569
+ * whatever is in flight. Without it a `session_shutdown` landing on a read
570
+ * held the loop open for as long as the read took -- and `galaxyGet` is a
571
+ * bare fetch with no timeout of its own, which against a stalling server
572
+ * measured 301 seconds before it threw.
573
+ */
574
+ private readonly abort = new AbortController();
575
+ private stopped = false;
576
+
577
+ constructor(private deps: GalaxyLiveTickerDeps) {}
578
+
579
+ /** Stop ticking and abandon anything already in flight. Not reversible. */
580
+ stop(): void {
581
+ this.stopped = true;
582
+ this.abort.abort();
583
+ }
584
+
585
+ /**
586
+ * A signal for one tick, aborted when the session is.
587
+ *
588
+ * Deliberately NOT `AbortSignal.any([this.abort.signal, ...])`: `any`
589
+ * registers the composite on each source for as long as that source lives and
590
+ * Node never takes it off again, so hanging one off a signal that lives for
591
+ * the whole session grows a list nothing empties -- measured at one retained
592
+ * entry per call. This holds a single listener instead, and `done()` removes
593
+ * it. Anything downstream may compose onto the returned signal freely: it
594
+ * dies with the tick.
595
+ */
596
+ private tickSignal(): { signal: AbortSignal; done: () => void } {
597
+ const controller = new AbortController();
598
+ const onStop = (): void => controller.abort();
599
+ this.abort.signal.addEventListener("abort", onStop, { once: true });
600
+ return {
601
+ signal: controller.signal,
602
+ done: () => this.abort.signal.removeEventListener("abort", onStop),
603
+ };
604
+ }
605
+
606
+ /** Minimum spacing for the next attempt, given what the notebook says. */
607
+ private interval(live: boolean): number {
608
+ const base = live ? ACTIVE_INTERVAL_MS : IDLE_INTERVAL_MS;
609
+ if (this.failures === 0) return base;
610
+ // Doubling from the active cadence, not from the idle one: a server that is
611
+ // down should be asked about at the same widening rate either way.
612
+ const backoff = Math.min(MAX_BACKOFF_MS, ACTIVE_INTERVAL_MS * 2 ** this.failures);
613
+ return Math.max(base, backoff);
614
+ }
615
+
616
+ private emit(payload: GalaxyLivePayload, now: number): void {
617
+ const fingerprint = payloadFingerprint(payload);
618
+ // Only a payload the panel draws a "checked N ago" line under is worth
619
+ // re-sending unchanged. An `unavailable` sentence has no clock on it, so
620
+ // re-pushing it every minute would be a wire message that changes nothing.
621
+ const stale = Boolean(payload.history) && now - this.lastPushAt >= STALE_REFRESH_MS;
622
+ if (fingerprint === this.lastFingerprint && !stale) return;
623
+ this.lastFingerprint = fingerprint;
624
+ if (payload.history) this.lastHistoryPayload = payload;
625
+ this.lastPushAt = now;
626
+ this.deps.push(payload);
627
+ }
628
+
629
+ private async resolveHistoryId(
630
+ content: string | null,
631
+ serverUrl: string,
632
+ signal: AbortSignal,
633
+ ): Promise<string | null> {
634
+ const bound = historyIdFromNotebook(content, serverUrl);
635
+ if (bound) return bound;
636
+ const cached = this.resolved;
637
+ if (
638
+ cached &&
639
+ cached.server === serverUrl &&
640
+ this.deps.now() - cached.at < HISTORY_RESOLVE_TTL_MS
641
+ ) {
642
+ return cached.id;
643
+ }
644
+ // Deliberately not caught here. Returning null for a failed lookup would
645
+ // report "this analysis is not attached to a Galaxy history yet" when the
646
+ // truth is that Galaxy did not answer or rejected the key -- two sentences
647
+ // that send the user somewhere completely different. Galaxy answering
648
+ // "you have no histories" is the only real no-history, and that is the
649
+ // null below.
650
+ //
651
+ // The resolve goes straight to `galaxyGet`, which is a bare fetch with no
652
+ // timeout of its own, so the wall clock has to come from here. Composing
653
+ // onto the tick's signal rather than the session's is what keeps it from
654
+ // accumulating; both sources die with the tick.
655
+ const summary = await this.deps.mostRecentHistory(
656
+ AbortSignal.any([signal, AbortSignal.timeout(DEFAULT_TIMEOUT_MS)]),
657
+ );
658
+ if (!summary?.id) return null;
659
+ this.resolved = { id: summary.id, at: this.deps.now(), server: serverUrl };
660
+ return summary.id;
661
+ }
662
+
663
+ /**
664
+ * One poller tick. `content` is the notebook as the poller just read it, or
665
+ * null when there is no notebook to read. Never throws.
666
+ */
667
+ async tick(content: string | null): Promise<void> {
668
+ // The poller fires this without awaiting it, so nothing outside stops two
669
+ // ticks overlapping while Galaxy is slow. This does.
670
+ if (this.running || this.stopped) return;
671
+ this.running = true;
672
+ try {
673
+ await this.runTick(content);
674
+ } catch (err) {
675
+ console.error("[galaxy-live] tick failed:", err);
676
+ } finally {
677
+ this.running = false;
678
+ }
679
+ }
680
+
681
+ /** The payload for "Galaxy answered, and there is nothing to draw". */
682
+ private unavailable(
683
+ reason: NonNullable<GalaxyLivePayload["unavailable"]>,
684
+ serverUrl: string | undefined,
685
+ now: number,
686
+ ): GalaxyLivePayload {
687
+ return {
688
+ version: GALAXY_LIVE_SCHEMA_VERSION,
689
+ serverHost: serverHostOf(serverUrl),
690
+ history: null,
691
+ unavailable: reason,
692
+ updatedAt: new Date(now).toISOString(),
693
+ };
694
+ }
695
+
696
+ private async runTick(content: string | null): Promise<void> {
697
+ const wanted = this.deps.wanted ? await this.deps.wanted() : true;
698
+ if (!wanted) {
699
+ this.panelWanted = false;
700
+ return;
701
+ }
702
+ if (!this.panelWanted) {
703
+ // The panel just appeared, or this is the first tick. Ask now rather than
704
+ // up to a minute from now, and push whatever comes back even if it is
705
+ // what was pushed before the panel went away: the widget that was just
706
+ // added has never seen it.
707
+ this.panelWanted = true;
708
+ this.lastAttemptAt = 0;
709
+ this.lastFingerprint = null;
710
+ }
711
+ const now = this.deps.now();
712
+ const cfg = this.deps.config();
713
+ if (!cfg) {
714
+ // No credentials is a steady state, not a failure: say it once and stop
715
+ // asking. The fingerprint check means a disconnected session costs one
716
+ // push for the whole session.
717
+ this.emit(this.unavailable("not-configured", undefined, now), now);
718
+ return;
719
+ }
720
+
721
+ const live = notebookHasLiveWork(content);
722
+ if (now - this.lastAttemptAt < this.interval(live)) return;
723
+ this.lastAttemptAt = now;
724
+
725
+ // One signal for everything this tick asks, released when it is done.
726
+ const tick = this.tickSignal();
727
+ try {
728
+ await this.askGalaxy(content, cfg, now, tick.signal);
729
+ } finally {
730
+ tick.done();
731
+ }
732
+ }
733
+
734
+ /** The half of a tick that talks to Galaxy, once the cadence has allowed it. */
735
+ private async askGalaxy(
736
+ content: string | null,
737
+ cfg: { url: string; apiKey: string },
738
+ now: number,
739
+ signal: AbortSignal,
740
+ ): Promise<void> {
741
+ let historyId: string | null;
742
+ try {
743
+ historyId = await this.resolveHistoryId(content, cfg.url, signal);
744
+ } catch (err) {
745
+ // A stop that aborted the request mid-flight is not something to report
746
+ // or to back off from; the session is over.
747
+ if (this.stopped) return;
748
+ // Asking Galaxy which history is current failed. That is the same class
749
+ // of problem as the read below failing, and it reads the same way.
750
+ this.failures++;
751
+ this.emit(this.unavailable(classifyError(err), cfg.url, this.deps.now()), this.deps.now());
752
+ return;
753
+ }
754
+ // The resolve may have come back after the session ended, either because it
755
+ // was aborted or because it finished first. Either way there is nothing to
756
+ // draw on and nothing worth asking Galaxy for.
757
+ if (this.stopped) return;
758
+ if (historyId !== this.historyId) {
759
+ // A different history: the update_time we were comparing against belongs
760
+ // to the old one, and reusing it would suppress the first real read.
761
+ this.knownUpdateTime = null;
762
+ this.historyId = historyId;
763
+ }
764
+ if (!historyId) {
765
+ // Galaxy answered and said there is no history here. Not a failure, so
766
+ // no backoff: the user creating one should show up on the next tick.
767
+ this.failures = 0;
768
+ this.emit(this.unavailable("no-history", cfg.url, now), now);
769
+ return;
770
+ }
771
+
772
+ const result = await this.deps.snapshot(historyId, {
773
+ knownUpdateTime: this.knownUpdateTime ?? undefined,
774
+ // The snapshot applies its own timeout and reads this one as "the caller
775
+ // gave up", which is the right reading only for a stop.
776
+ signal,
777
+ });
778
+ if (result.aborted || this.stopped) return;
779
+
780
+ if (result.unchanged) {
781
+ this.failures = 0;
782
+ // Galaxy answered and nothing moved. Re-stamp the history we already have
783
+ // so the panel's staleness line reflects when we last asked, not when the
784
+ // history last changed -- otherwise a quiet run looks abandoned.
785
+ if (this.lastHistoryPayload) {
786
+ this.emit(
787
+ { ...this.lastHistoryPayload, updatedAt: new Date(this.deps.now()).toISOString() },
788
+ this.deps.now(),
789
+ );
790
+ }
791
+ return;
792
+ }
793
+
794
+ const payload = result.payload;
795
+ if (!payload) return;
796
+ if (payload.unavailable) {
797
+ this.failures++;
798
+ // Forget what we were comparing against. Otherwise the next healthy tick
799
+ // sees an unmoved `update_time`, takes the cheap "nothing changed" path,
800
+ // and never refetches the contents that would put the history back on
801
+ // screen in place of the error.
802
+ this.knownUpdateTime = null;
803
+ } else {
804
+ this.failures = 0;
805
+ this.knownUpdateTime = payload.history?.updateTime || null;
806
+ }
807
+ this.emit(payload, this.deps.now());
808
+ }
809
+ }
810
+
811
+ // ── Arming ───────────────────────────────────────────────────────────────────
812
+
813
+ const GALAXY_HISTORY_WIDGET = "galaxy-history";
814
+
815
+ /** The last answer, keyed on the layout file's identity, so a tick that finds
816
+ * the file unchanged pays one lstat and no parse. */
817
+ let wantedCache: { key: string; wanted: boolean } | null = null;
818
+
819
+ /**
820
+ * Does any dashboard in this analysis hold the live-history panel?
821
+ *
822
+ * Neither shipped preset does, so for most sessions the answer is no and the
823
+ * ticker never talks to Galaxy. A layout the brain cannot read -- a symlink,
824
+ * over the cap -- counts as no: nothing it would draw for can be trusted to
825
+ * exist. Read through the brain's own store, so the file is read the way every
826
+ * other reader reads it.
827
+ */
828
+ export async function layoutWantsGalaxyLive(): Promise<boolean> {
829
+ const filePath = getDashboardPath();
830
+ if (!filePath) return false;
831
+ let key: string;
832
+ try {
833
+ const st = await fsp.lstat(filePath);
834
+ key = `${filePath}|${st.ino}|${st.size}|${st.mtimeMs}`;
835
+ } catch (err) {
836
+ if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") return false;
837
+ key = `${filePath}|absent`;
838
+ }
839
+ if (wantedCache?.key === key) return wantedCache.wanted;
840
+ const read = await readDashboardDocument();
841
+ const wanted =
842
+ read.ok &&
843
+ read.document.dashboards.some((d) =>
844
+ d.panels.some((panel) => panel.widget === GALAXY_HISTORY_WIDGET),
845
+ );
846
+ wantedCache = { key, wanted };
847
+ return wanted;
848
+ }
849
+
850
+ /** For tests: forget the cached answer. */
851
+ export function resetGalaxyLiveWantedCache(): void {
852
+ wantedCache = null;
853
+ }
854
+
855
+ let ticker: GalaxyLiveTicker | null = null;
856
+ /** Bumped by every arm and every disarm, so a push from a tick that was already
857
+ * in flight when the session ended lands nowhere. */
858
+ let armGeneration = 0;
859
+
860
+ /**
861
+ * Shells that draw a dashboard. In the terminal a widget push collapses to
862
+ * nothing, so arming there would spend real Galaxy requests on a panel that
863
+ * does not exist -- the CLI must cost exactly zero, which it does by never
864
+ * registering the hook at all.
865
+ */
866
+ function shellDrawsDashboard(): boolean {
867
+ return capabilityProfile().ui === "desktop";
868
+ }
869
+
870
+ /**
871
+ * Start pushing the live Galaxy history to the shell. Called from
872
+ * `session-lifecycle.ts`, which owns startup and shutdown; idempotent, so a
873
+ * brain restart that fires `session_start` twice does not leave two tickers.
874
+ */
875
+ export function armGalaxyLivePanel(ctx: ExtensionContext): void {
876
+ if (!shellDrawsDashboard()) {
877
+ disarmGalaxyLivePanel();
878
+ return;
879
+ }
880
+ // A fresh ticker per arming is deliberate: `session_start` means a new
881
+ // session, and a new session wants the panel filled now rather than up to a
882
+ // minute later. The generation is what makes shutdown mean shutdown -- the
883
+ // hook is fired and not awaited, so a tick already in flight can still be
884
+ // holding this closure after the session ends.
885
+ const generation = ++armGeneration;
886
+ const push = (payload: GalaxyLivePayload): void => {
887
+ if (generation !== armGeneration) return;
888
+ try {
889
+ ctx.ui.setWidget(OrbitWidgetKey.GalaxyLive, encodeJsonWidget(payload));
890
+ } catch (err) {
891
+ // Same guard as the notebook widget: a tick that lands after session
892
+ // teardown fires against a ctx pi has invalidated, and touching ctx.ui
893
+ // throws "ctx is stale after session replacement or reload". That one is
894
+ // terminal for this arming, so stop. Anything else is surfaced and the
895
+ // panel keeps trying -- one transient throw must not quietly cost the
896
+ // user their panel for the rest of the session.
897
+ if (err instanceof Error && /ctx is stale/i.test(err.message)) {
898
+ disarmGalaxyLivePanel();
899
+ return;
900
+ }
901
+ console.error("[galaxy-live] widget push failed:", err);
902
+ }
903
+ };
904
+ // A ticker being replaced must let go of whatever it is waiting on; the new
905
+ // one is about to ask the same questions.
906
+ ticker?.stop();
907
+ ticker = new GalaxyLiveTicker({
908
+ snapshot: fetchGalaxyLiveSnapshot,
909
+ config: getGalaxyConfig,
910
+ mostRecentHistory: galaxyGetMostRecentHistory,
911
+ push,
912
+ now: () => Date.now(),
913
+ wanted: layoutWantsGalaxyLive,
914
+ });
915
+ const own = ticker;
916
+ setPollTickHook((content) => (own === ticker ? own.tick(content) : Promise.resolve()));
917
+ }
918
+
919
+ /** Stop pushing. Called from `session_shutdown`, beside `stopGalaxyPoller()`. */
920
+ export function disarmGalaxyLivePanel(): void {
921
+ armGeneration++;
922
+ // The generation guard already drops a late push. This is the other half:
923
+ // stop waiting for the answer at all. Shutdown never blocked on it -- the
924
+ // poller fires the hook without awaiting it -- but an in-flight bare fetch
925
+ // holds the event loop, which is what the controller on the ticker is for.
926
+ ticker?.stop();
927
+ ticker = null;
928
+ setPollTickHook(null);
929
+ }