@morscherlab/mint-sdk 1.4.0-beta.2 → 1.4.0-beta.20

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 (330) hide show
  1. package/README.md +14 -3
  2. package/dist/{ActionMenu-CScINahU.js → AlertBox-CzLg0UaL.js} +40 -209
  3. package/dist/AlertBox-CzLg0UaL.js.map +1 -0
  4. package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-6oTT-gPO.js} +1249 -239
  5. package/dist/AppSidebar-6oTT-gPO.js.map +1 -0
  6. package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-CSls2Nuv.js} +2 -2
  7. package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-CSls2Nuv.js.map} +1 -1
  8. package/dist/ExperimentPopover-9l4QZlvQ.js +337 -0
  9. package/dist/ExperimentPopover-9l4QZlvQ.js.map +1 -0
  10. package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-pR5fK3pA.js} +244 -211
  11. package/dist/ExperimentSelectorModal-pR5fK3pA.js.map +1 -0
  12. package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-CJ7EAmOq.js} +2 -2
  13. package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-CJ7EAmOq.js.map} +1 -1
  14. package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-Dyitgzfp.js} +28 -27
  15. package/dist/SettingsModal-Dyitgzfp.js.map +1 -0
  16. package/dist/__stories__/experiment-helpers.d.ts +10 -0
  17. package/dist/__tests__/components/AppTopBarOnboarding.test.d.ts +1 -0
  18. package/dist/__tests__/composables/useOnboarding.test.d.ts +1 -0
  19. package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
  20. package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
  21. package/dist/components/AppSidebar.vue.d.ts +1 -1
  22. package/dist/components/AppTopBar.vue.d.ts +20 -5
  23. package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
  24. package/dist/components/AuditTrail.vue.d.ts +1 -1
  25. package/dist/components/BaseButton.vue.d.ts +4 -2
  26. package/dist/components/BaseSelect.vue.d.ts +2 -2
  27. package/dist/components/ChartContainer.vue.d.ts +2 -0
  28. package/dist/components/DatePicker.vue.d.ts +1 -1
  29. package/dist/components/ExperimentPopover.vue.d.ts +18 -0
  30. package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -5
  31. package/dist/components/FilePicker.vue.d.ts +1 -1
  32. package/dist/components/FileUploader.vue.d.ts +2 -2
  33. package/dist/components/JobsStatusTray.vue.d.ts +15 -4
  34. package/dist/components/PluginIcon.vue.d.ts +2 -2
  35. package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
  36. package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
  37. package/dist/components/SampleSelector.vue.d.ts +6 -6
  38. package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
  39. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  40. package/dist/components/StepWizard.vue.d.ts +3 -0
  41. package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
  42. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
  43. package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +1 -1
  44. package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
  45. package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
  46. package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
  47. package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
  48. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
  49. package/dist/composables/createClient.d.ts +2 -0
  50. package/dist/composables/index.d.ts +1 -0
  51. package/dist/composables/pluginIdentity.d.ts +24 -0
  52. package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
  53. package/dist/composables/useAppExperiment.d.ts +8 -0
  54. package/dist/composables/useCalendarGrid.d.ts +1 -1
  55. package/dist/composables/useExperimentSelector.d.ts +6 -2
  56. package/dist/composables/useJobCenter.d.ts +41 -3
  57. package/dist/composables/useOnboarding.d.ts +119 -0
  58. package/dist/composables/usePlatform.d.ts +1 -0
  59. package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-DCUOx8ll.js} +2 -2
  60. package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-DCUOx8ll.js.map} +1 -1
  61. package/dist/{data-C2PwI_dn.js → data-DBnmeBZX.js} +179 -125
  62. package/dist/data-DBnmeBZX.js.map +1 -0
  63. package/dist/data.js +8 -8
  64. package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
  65. package/dist/experiment-DyO54FQB.js.map +1 -0
  66. package/dist/{experimentStatus-BZmU4czV.js → experimentStatus-BFrKpHAH.js} +3 -3
  67. package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-BFrKpHAH.js.map} +1 -1
  68. package/dist/index.js +20 -20
  69. package/dist/{install-DAID6c2-.js → install-qjELyNwc.js} +15 -14
  70. package/dist/{install-DAID6c2-.js.map → install-qjELyNwc.js.map} +1 -1
  71. package/dist/install.js +1 -1
  72. package/dist/{lab-DLwRl_Ql.js → lab-UyTbokQC.js} +136 -148
  73. package/dist/lab-UyTbokQC.js.map +1 -0
  74. package/dist/lab.js +4 -3
  75. package/dist/{records-BzTcxekG.js → records-CR_pYzIP.js} +533 -335
  76. package/dist/records-CR_pYzIP.js.map +1 -0
  77. package/dist/{runtime-D6gVat6r.js → runtime-CoHhBP10.js} +417 -54
  78. package/dist/runtime-CoHhBP10.js.map +1 -0
  79. package/dist/runtime.d.ts +1 -1
  80. package/dist/runtime.js +7 -7
  81. package/dist/{shell-5ju97nOS.js → shell-BDuZf2zb.js} +10 -5
  82. package/dist/{shell-5ju97nOS.js.map → shell-BDuZf2zb.js.map} +1 -1
  83. package/dist/shell.d.ts +1 -0
  84. package/dist/shell.js +6 -6
  85. package/dist/stores/index.d.ts +1 -1
  86. package/dist/stores/index.js +2 -2
  87. package/dist/stores/settings.d.ts +4 -12
  88. package/dist/styles.css +2732 -2083
  89. package/dist/types/componentWorkflowTypes.d.ts +2 -0
  90. package/dist/types/components.d.ts +2 -0
  91. package/dist/types/generated/platformContext.d.ts +1 -0
  92. package/dist/types/generated-ui.d.ts +4 -0
  93. package/dist/types/index.d.ts +1 -1
  94. package/dist/types/jobs.d.ts +9 -0
  95. package/dist/types/platform.d.ts +1 -1
  96. package/dist/{BaseModal-DfRIuVoj.js → useDropdownState-Bb2N4m40.js} +74 -4
  97. package/dist/useDropdownState-Bb2N4m40.js.map +1 -0
  98. package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-Dno1fFMq.js} +4 -4
  99. package/dist/{useExperimentSamples-CxcnxZ8h.js.map → useExperimentSamples-Dno1fFMq.js.map} +1 -1
  100. package/dist/{useListReorder-CcbEYxX-.js → useListReorder-CQ9OCScc.js} +183 -351
  101. package/dist/useListReorder-CQ9OCScc.js.map +1 -0
  102. package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-wL3kPrRT.js} +73 -59
  103. package/dist/useRackEditor-wL3kPrRT.js.map +1 -0
  104. package/dist/useRealtime-C3whhVvb.js +508 -0
  105. package/dist/useRealtime-C3whhVvb.js.map +1 -0
  106. package/dist/useRequest-CDkrXNU7.js +244 -0
  107. package/dist/useRequest-CDkrXNU7.js.map +1 -0
  108. package/dist/utils/time.d.ts +7 -0
  109. package/package.json +1 -1
  110. package/src/__stories__/experiment-helpers.ts +3 -3
  111. package/src/__tests__/components/AppTopBar.test.ts +257 -3
  112. package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
  113. package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
  114. package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
  115. package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
  116. package/src/__tests__/components/BaseButton.test.ts +19 -0
  117. package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
  118. package/src/__tests__/components/BaseSelect.test.ts +2 -1
  119. package/src/__tests__/components/BatchProgressList.test.ts +16 -0
  120. package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
  121. package/src/__tests__/components/ChartContainer.test.ts +11 -0
  122. package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
  123. package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
  124. package/src/__tests__/components/DatePicker.test.ts +21 -0
  125. package/src/__tests__/components/DatePicker.time.test.ts +2 -2
  126. package/src/__tests__/components/EmptyState.test.ts +1 -1
  127. package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
  128. package/src/__tests__/components/ExperimentSelectorModal.test.ts +153 -14
  129. package/src/__tests__/components/GeneratedPluginView.test.ts +23 -2
  130. package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
  131. package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
  132. package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
  133. package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
  134. package/src/__tests__/components/SampleSelector.test.ts +33 -8
  135. package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
  136. package/src/__tests__/components/SettingsModal.test.ts +9 -2
  137. package/src/__tests__/components/StepWizard.test.ts +11 -0
  138. package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
  139. package/src/__tests__/components/deprecationTags.test.ts +1 -1
  140. package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
  141. package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
  142. package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
  143. package/src/__tests__/composables/useJobCenter.test.ts +262 -3
  144. package/src/__tests__/composables/useOnboarding.test.ts +317 -0
  145. package/src/__tests__/docs/frontendDocsCatalog.test.ts +4 -3
  146. package/src/__tests__/stores/settings.test.ts +21 -21
  147. package/src/__tests__/styles/baseControlsRules.test.ts +6 -2
  148. package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
  149. package/src/__tests__/styles/cssTokens.test.ts +6 -3
  150. package/src/__tests__/styles/menusShellRefresh.test.ts +22 -12
  151. package/src/__tests__/styles/paletteTokens.test.ts +44 -0
  152. package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
  153. package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
  154. package/src/__tests__/utils/time.test.ts +18 -1
  155. package/src/components/AdductText.story.vue +7 -0
  156. package/src/components/AdductText.vue +1 -1
  157. package/src/components/AlertBox.story.vue +2 -2
  158. package/src/components/AlertBox.vue +18 -16
  159. package/src/components/AppSidebar.story.vue +0 -28
  160. package/src/components/AppTopBar.onboarding.story.vue +69 -0
  161. package/src/components/AppTopBar.story.vue +105 -15
  162. package/src/components/AppTopBar.vue +248 -122
  163. package/src/components/ArtifactSaveDialog.story.vue +23 -0
  164. package/src/components/ArtifactSaveDialog.vue +102 -12
  165. package/src/components/AuditTrail.story.vue +2 -1
  166. package/src/components/AuditTrail.vue +2 -21
  167. package/src/components/Avatar.vue +10 -14
  168. package/src/components/BaseButton.vue +16 -2
  169. package/src/components/BaseSelect.multiple.story.vue +13 -2
  170. package/src/components/BaseSelect.vue +4 -3
  171. package/src/components/BatchProgressList.vue +35 -25
  172. package/src/components/ChartContainer.story.vue +2 -1
  173. package/src/components/ChartContainer.vue +8 -6
  174. package/src/components/ConfirmDialog.story.vue +1 -1
  175. package/src/components/EmptyState.vue +1 -1
  176. package/src/components/ExperimentPopover.story.vue +165 -53
  177. package/src/components/ExperimentPopover.vue +169 -187
  178. package/src/components/ExperimentSelectorModal.story.vue +73 -31
  179. package/src/components/ExperimentSelectorModal.vue +144 -111
  180. package/src/components/FormField.story.vue +8 -48
  181. package/src/components/GeneratedPluginView.vue +11 -10
  182. package/src/components/GeneratedResultRenderer.vue +3 -4
  183. package/src/components/InstrumentStateBadge.vue +1 -1
  184. package/src/components/JobsStatusTray.story.vue +97 -43
  185. package/src/components/JobsStatusTray.vue +287 -290
  186. package/src/components/LayoutResizeHandle.story.vue +65 -4
  187. package/src/components/LoadingSpinner.vue +1 -1
  188. package/src/components/NumberInput.vue +3 -6
  189. package/src/components/PluginIcon.story.vue +2 -10
  190. package/src/components/PluginIcon.vue +2 -2
  191. package/src/components/PluginWorkspaceView.props.ts +3 -0
  192. package/src/components/PluginWorkspaceView.story.vue +1 -1
  193. package/src/components/PluginWorkspaceView.vue +17 -0
  194. package/src/components/RuntimeUpdateNotice.vue +1 -1
  195. package/src/components/SampleSelector.story.vue +22 -7
  196. package/src/components/SampleSelector.vue +34 -37
  197. package/src/components/SampleSelectorGroupNode.vue +1 -1
  198. package/src/components/SectionCard.story.vue +35 -2
  199. package/src/components/SequenceProgressBar.story.vue +9 -0
  200. package/src/components/SequenceProgressBar.vue +6 -1
  201. package/src/components/SettingsModal.schema.ts +1 -1
  202. package/src/components/SettingsModal.vue +9 -14
  203. package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
  204. package/src/components/SmartGroupManual.story.vue +1 -1
  205. package/src/components/StepWizard.story.vue +33 -0
  206. package/src/components/StepWizard.vue +8 -1
  207. package/src/components/WellPlate.story.vue +3 -1
  208. package/src/components/WellPlate.vue +7 -0
  209. package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
  210. package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
  211. package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
  212. package/src/components/internal/DatePickerDateInternal.vue +16 -40
  213. package/src/components/internal/DatePickerDateTimeInternal.vue +3 -13
  214. package/src/components/internal/DatePickerTimeInternal.vue +5 -8
  215. package/src/components/internal/OnboardingLayerInternal.vue +173 -0
  216. package/src/components/internal/ResourcePickerListInternal.vue +45 -85
  217. package/src/components/internal/SelectMultipleInternal.vue +182 -65
  218. package/src/components/internal/SelectSearchableInternal.vue +2 -3
  219. package/src/components/internal/WorkspaceShellInternal.vue +12 -0
  220. package/src/composables/createClient.ts +2 -0
  221. package/src/composables/index.ts +1 -0
  222. package/src/composables/pluginIdentity.ts +31 -0
  223. package/src/composables/pluginWorkspaceModel.ts +2 -2
  224. package/src/composables/useAppExperiment.ts +20 -0
  225. package/src/composables/useCalendarGrid.ts +4 -3
  226. package/src/composables/useExperimentSelector.ts +77 -31
  227. package/src/composables/useJobCenter.ts +188 -6
  228. package/src/composables/useOnboarding.ts +465 -0
  229. package/src/composables/usePluginWorkspace.ts +1 -1
  230. package/src/runtime.ts +2 -1
  231. package/src/shell.ts +17 -0
  232. package/src/stores/index.ts +0 -2
  233. package/src/stores/settings.ts +20 -50
  234. package/src/styles/components/adduct-text.css +13 -5
  235. package/src/styles/components/alert-box.css +39 -58
  236. package/src/styles/components/app-layout.css +7 -5
  237. package/src/styles/components/app-page-selector.css +121 -44
  238. package/src/styles/components/app-pill-nav.css +11 -12
  239. package/src/styles/components/app-sidebar.css +9 -18
  240. package/src/styles/components/app-top-bar.css +219 -55
  241. package/src/styles/components/artifact-save-dialog.css +153 -2
  242. package/src/styles/components/audit-trail.css +16 -19
  243. package/src/styles/components/avatar.css +6 -4
  244. package/src/styles/components/batch-progress-list.css +38 -36
  245. package/src/styles/components/button.css +36 -59
  246. package/src/styles/components/calendar.css +12 -12
  247. package/src/styles/components/chart-container.css +34 -23
  248. package/src/styles/components/checkbox.css +2 -12
  249. package/src/styles/components/collapsible-card.css +27 -29
  250. package/src/styles/components/confirm-dialog.css +13 -14
  251. package/src/styles/components/dataframe.css +13 -5
  252. package/src/styles/components/date-picker.css +80 -92
  253. package/src/styles/components/datetime-picker.css +31 -43
  254. package/src/styles/components/divider.css +20 -20
  255. package/src/styles/components/dose-calculator.css +3 -3
  256. package/src/styles/components/empty-state.css +20 -17
  257. package/src/styles/components/experiment-code-badge.css +2 -0
  258. package/src/styles/components/experiment-popover.css +174 -329
  259. package/src/styles/components/experiment-selector-modal.css +88 -56
  260. package/src/styles/components/file-picker.css +3 -3
  261. package/src/styles/components/file-uploader.css +38 -49
  262. package/src/styles/components/form-builder.css +3 -3
  263. package/src/styles/components/formula-input.css +1 -1
  264. package/src/styles/components/icon-button.css +18 -24
  265. package/src/styles/components/input.css +5 -4
  266. package/src/styles/components/instrument-monitor.css +13 -15
  267. package/src/styles/components/jobs-status-tray.css +140 -296
  268. package/src/styles/components/layout-resize-handle.css +59 -16
  269. package/src/styles/components/modal.css +4 -6
  270. package/src/styles/components/molecule-input.css +2 -2
  271. package/src/styles/components/multi-select.css +65 -117
  272. package/src/styles/components/number-input.css +26 -19
  273. package/src/styles/components/onboarding.css +368 -0
  274. package/src/styles/components/pill.css +3 -2
  275. package/src/styles/components/plate-editor.css +0 -1
  276. package/src/styles/components/plugin-icon.css +26 -23
  277. package/src/styles/components/popover.css +2 -2
  278. package/src/styles/components/presence-avatars.css +1 -1
  279. package/src/styles/components/progress-bar.css +3 -3
  280. package/src/styles/components/protocol-step-editor.css +1 -3
  281. package/src/styles/components/resource-card.css +2 -2
  282. package/src/styles/components/resource-picker-list.css +6 -57
  283. package/src/styles/components/sample-hierarchy-tree.css +4 -4
  284. package/src/styles/components/sample-selector.css +138 -79
  285. package/src/styles/components/schedule-calendar.css +8 -8
  286. package/src/styles/components/searchable-select.css +12 -12
  287. package/src/styles/components/section-card.css +54 -49
  288. package/src/styles/components/segmented-control.css +18 -15
  289. package/src/styles/components/select.css +3 -3
  290. package/src/styles/components/sequence-input.css +2 -2
  291. package/src/styles/components/sequence-progress-bar.css +17 -8
  292. package/src/styles/components/settings-modal.css +7 -2
  293. package/src/styles/components/smart-group.css +18 -18
  294. package/src/styles/components/step-wizard.css +52 -16
  295. package/src/styles/components/tabs.css +2 -2
  296. package/src/styles/components/textarea.css +3 -3
  297. package/src/styles/components/time-picker.css +36 -21
  298. package/src/styles/components/toast.css +12 -10
  299. package/src/styles/components/toggle.css +1 -11
  300. package/src/styles/components/tooltip.css +10 -9
  301. package/src/styles/components/unit-input.css +8 -7
  302. package/src/styles/components/well-plate.css +9 -2
  303. package/src/styles/index.css +1 -0
  304. package/src/styles/variables.css +241 -78
  305. package/src/types/componentWorkflowTypes.ts +2 -0
  306. package/src/types/components.ts +2 -0
  307. package/src/types/generated/platformContext.ts +1 -0
  308. package/src/types/generated-ui.ts +4 -0
  309. package/src/types/index.ts +1 -0
  310. package/src/types/jobs.ts +10 -0
  311. package/src/types/platform.ts +1 -1
  312. package/src/utils/time.ts +21 -0
  313. package/dist/ActionMenu-CScINahU.js.map +0 -1
  314. package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
  315. package/dist/BaseModal-DfRIuVoj.js.map +0 -1
  316. package/dist/ExperimentPopover-C4sGw-is.js +0 -359
  317. package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
  318. package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
  319. package/dist/SettingsModal-CSLE31yg.js.map +0 -1
  320. package/dist/data-C2PwI_dn.js.map +0 -1
  321. package/dist/experiment-43OJtH7R.js.map +0 -1
  322. package/dist/lab-DLwRl_Ql.js.map +0 -1
  323. package/dist/records-BzTcxekG.js.map +0 -1
  324. package/dist/runtime-D6gVat6r.js.map +0 -1
  325. package/dist/useListReorder-CcbEYxX-.js.map +0 -1
  326. package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
  327. package/dist/useRealtime-PalKue84.js +0 -696
  328. package/dist/useRealtime-PalKue84.js.map +0 -1
  329. package/dist/useRequest-D5JDfsgZ.js +0 -76
  330. package/dist/useRequest-D5JDfsgZ.js.map +0 -1
@@ -28,6 +28,8 @@ export interface UseExperimentSelectorOptions {
28
28
  apiBaseUrl?: string
29
29
  limit?: number
30
30
  immediate?: boolean
31
+ /** Keep `statusCounts` in step with the non-status filters (one stats request per change). */
32
+ withStatusCounts?: boolean
31
33
  }
32
34
 
33
35
  export interface UseExperimentSelectorReturn {
@@ -57,9 +59,8 @@ export interface UseExperimentSelectorReturn {
57
59
  experimentTypes: Ref<ExperimentTypeOption[]>
58
60
  /** Available project options for filter UI. */
59
61
  projects: Ref<SelectOption<string>[]>
60
- // Grouped view
61
- /** Fetched experiments grouped by project name. */
62
- groupedByProject: ComputedRef<[string, ExperimentSummary[]][]>
62
+ /** Experiment count per status under every filter except status itself. */
63
+ statusCounts: Ref<Record<string, number>>
63
64
  // Methods
64
65
  /** Fetch the current page with the current filters. */
65
66
  fetch: () => Promise<void>
@@ -73,6 +74,8 @@ export interface UseExperimentSelectorReturn {
73
74
  clear: () => void
74
75
  /** Fetch experiment type and project filter options. */
75
76
  fetchFilterOptions: () => Promise<void>
77
+ /** Fetch `statusCounts` from `GET /experiments/stats`. */
78
+ fetchStatusCounts: () => Promise<void>
76
79
  }
77
80
 
78
81
  /** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */
@@ -85,6 +88,7 @@ export function useExperimentSelector(
85
88
  experimentType,
86
89
  allowedExperimentTypes: optionAllowedExperimentTypes,
87
90
  apiBaseUrl,
91
+ withStatusCounts = false,
88
92
  } = options
89
93
  const platformBase = apiBaseUrl ?? getPlatformApiUrl()
90
94
  const api = useApi()
@@ -107,6 +111,10 @@ export function useExperimentSelector(
107
111
  const projects = ref<SelectOption<string>[]>([])
108
112
  let filterOptionsFetched = false
109
113
 
114
+ const statusCounts = ref<Record<string, number>>({})
115
+ let statsGeneration = 0
116
+ let countedScope: string | null = null
117
+
110
118
  const hasMore = computed(() => experiments.value.length < total.value)
111
119
 
112
120
  const filters: ExperimentFilters = reactive({
@@ -115,6 +123,7 @@ export function useExperimentSelector(
115
123
  project: undefined,
116
124
  experimentType: undefined,
117
125
  datePreset: undefined,
126
+ mine: undefined,
118
127
  })
119
128
 
120
129
  function normalizeExperimentTypes(values: string[] | null | undefined): string[] | null {
@@ -163,21 +172,27 @@ export function useExperimentSelector(
163
172
  return { kind: 'all' }
164
173
  }
165
174
 
166
- function buildExperimentListUrl(typeFilter: string | undefined, skip: number, pageLimit: number): string {
175
+ /** Query params shared by the list and the stats route; status, sort and paging are list-only. */
176
+ function scopeParams(typeFilter: string | undefined): URLSearchParams {
167
177
  const params = new URLSearchParams()
168
178
  if (typeFilter) params.set('experiment_type', typeFilter)
169
- if (filters.status) params.set('status', filters.status)
170
179
  if (filters.search) params.set('search', filters.search)
171
180
  if (filters.project) params.set('project', filters.project)
181
+ if (filters.mine) params.set('mine', 'true')
182
+ if (filters.datePreset) {
183
+ params.set('created_after', datePresetToISO(filters.datePreset))
184
+ }
185
+ return params
186
+ }
187
+
188
+ function buildExperimentListUrl(typeFilter: string | undefined, skip: number, pageLimit: number): string {
189
+ const params = scopeParams(typeFilter)
190
+ if (filters.status) params.set('status', filters.status)
172
191
 
173
192
  const { sortBy, sortOrder } = parseSortKey()
174
193
  params.set('sort_by', sortBy)
175
194
  params.set('sort_order', sortOrder)
176
195
 
177
- if (filters.datePreset) {
178
- params.set('created_after', datePresetToISO(filters.datePreset))
179
- }
180
-
181
196
  params.set('limit', String(pageLimit))
182
197
  params.set('skip', String(skip))
183
198
 
@@ -298,6 +313,51 @@ export function useExperimentSelector(
298
313
  }
299
314
  }
300
315
 
316
+ /** The filters the stats route reads; status, sort and paging do not change the counts. */
317
+ function countScope(): string {
318
+ return JSON.stringify([filters.search, filters.project, filters.experimentType, filters.datePreset, filters.mine])
319
+ }
320
+
321
+ function refreshStatusCountsOnScopeChange(): void {
322
+ if (withStatusCounts && countScope() !== countedScope) fetchStatusCounts()
323
+ }
324
+
325
+ async function fetchStatusCounts(): Promise<void> {
326
+ const generation = ++statsGeneration
327
+ countedScope = countScope()
328
+ const scope = resolveExperimentTypeScope()
329
+ if (scope.kind === 'blocked') {
330
+ statusCounts.value = {}
331
+ return
332
+ }
333
+ const types = scope.kind === 'multi'
334
+ ? scope.experimentTypes
335
+ : [scope.kind === 'single' ? scope.experimentType : undefined]
336
+ const base = platformBase ?? ''
337
+ try {
338
+ const responses = await Promise.all(
339
+ types.map(type => api.get<{ total: number; by_status: Record<string, number> }>(
340
+ `${base}/experiments/stats?${scopeParams(type)}`,
341
+ )),
342
+ )
343
+ if (generation !== statsGeneration) return
344
+ const counts: Record<string, number> = {}
345
+ for (const response of responses) {
346
+ for (const [status, count] of Object.entries(response.by_status)) {
347
+ counts[status] = (counts[status] ?? 0) + count
348
+ }
349
+ }
350
+ statusCounts.value = counts
351
+ } catch {
352
+ // Counts only decorate the status menu; without them it shows bare labels.
353
+ // Forget the scope so the next status or sort change retries the request.
354
+ if (generation === statsGeneration) {
355
+ statusCounts.value = {}
356
+ countedScope = null
357
+ }
358
+ }
359
+ }
360
+
301
361
  async function loadMore(): Promise<void> {
302
362
  if (!hasMore.value || isLoading.value) return
303
363
  page.value++
@@ -323,31 +383,12 @@ export function useExperimentSelector(
323
383
  filters.project = undefined
324
384
  filters.experimentType = undefined
325
385
  filters.datePreset = undefined
386
+ filters.mine = undefined
326
387
  sortKey.value = 'created_at:desc'
327
388
  page.value = 0
328
389
  request.clearError()
329
390
  }
330
391
 
331
- // Group experiments by project (client-side)
332
- const groupedByProject = computed<[string, ExperimentSummary[]][]>(() => {
333
- const groups = new Map<string, ExperimentSummary[]>()
334
- for (const exp of experiments.value) {
335
- const key = exp.project_name ?? exp.project ?? 'No project'
336
- const list = groups.get(key)
337
- if (list) {
338
- list.push(exp)
339
- } else {
340
- groups.set(key, [exp])
341
- }
342
- }
343
- // Sort alphabetically, "No project" last
344
- return [...groups.entries()].sort(([a], [b]) => {
345
- if (a === 'No project') return 1
346
- if (b === 'No project') return -1
347
- return a.localeCompare(b)
348
- })
349
- })
350
-
351
392
  watch(
352
393
  () => [
353
394
  filters.search,
@@ -355,6 +396,7 @@ export function useExperimentSelector(
355
396
  filters.project,
356
397
  filters.experimentType,
357
398
  filters.datePreset,
399
+ filters.mine,
358
400
  sortKey.value,
359
401
  ],
360
402
  () => invalidateFetch(),
@@ -366,17 +408,20 @@ export function useExperimentSelector(
366
408
  () => {
367
409
  page.value = 0
368
410
  fetchExperiments()
411
+ refreshStatusCountsOnScopeChange()
369
412
  },
370
413
  { delay: 300 },
371
414
  )
372
415
 
373
416
  // Immediate watch on discrete filters and sort (cancel any pending search debounce)
374
417
  watch(
375
- () => [filters.status, filters.project, filters.experimentType, filters.datePreset, sortKey.value],
418
+ () => [filters.status, filters.project, filters.experimentType, filters.datePreset, filters.mine, sortKey.value],
376
419
  () => {
377
420
  searchWatch.cancel()
378
421
  page.value = 0
379
422
  fetchExperiments()
423
+ // Also checked on a status or sort change: the cancelled debounce may hold a new search.
424
+ refreshStatusCountsOnScopeChange()
380
425
  },
381
426
  )
382
427
 
@@ -397,12 +442,13 @@ export function useExperimentSelector(
397
442
  sortKey,
398
443
  experimentTypes,
399
444
  projects,
400
- groupedByProject,
445
+ statusCounts,
401
446
  fetch: fetchExperiments,
402
447
  loadMore,
403
448
  reset,
404
449
  select,
405
450
  clear,
406
451
  fetchFilterOptions,
452
+ fetchStatusCounts,
407
453
  }
408
454
  }
@@ -5,20 +5,26 @@ import {
5
5
  ref,
6
6
  shallowRef,
7
7
  toValue,
8
+ useId,
9
+ watch,
8
10
  type ComputedRef,
9
11
  type MaybeRefOrGetter,
10
12
  type Ref,
11
13
  type ShallowRef,
12
14
  } from 'vue'
13
15
 
14
- import { isActiveJobStatus, normalizeJobState } from '../jobs'
16
+ import { isActiveJobStatus, jobStatusLabel, normalizeJobPercent, normalizeJobState } from '../jobs'
15
17
  import type {
16
18
  JobListPayload,
17
19
  JobState,
18
20
  JobStateInput,
21
+ JobStatus,
22
+ TopBarJob,
19
23
  } from '../types/jobs'
24
+ import { usePlatform } from './usePlatform'
20
25
 
21
- export type JobsStatusTrayTab = 'active' | 'history'
26
+ /** How long a job that just completed stays in the top-bar run slot. */
27
+ const FINISHED_HOLD_MS = 6000
22
28
 
23
29
  /**
24
30
  * The canonical owner of a collection of plugin jobs.
@@ -143,9 +149,41 @@ function duplicatePluginJobId(jobId: string): Error {
143
149
  export interface UseJobCenterOptions {
144
150
  source?: MaybeRefOrGetter<PluginJobCenterSource | undefined>
145
151
  loadOnMount?: boolean
152
+ /**
153
+ * Maps the job that the top-bar chip shows. It is called for the chosen own
154
+ * active job AND for the held just-completed job, so check `job.status`.
155
+ * `null` keeps the default mapping.
156
+ */
157
+ topBarJob?: (job: JobState) => TopBarJob | null
158
+ /**
159
+ * Overrides how a tray row looks: `status` (icon, awaiting-input tint and
160
+ * pinning), `percent` (progress bar and value) and `message` (detail line).
161
+ * A missing or `undefined` field, or a `null` status or message, keeps the
162
+ * job's own; `percent: null` hides the bar and shows the time, and a status
163
+ * alone keeps the job's own percent. The job's section, actions and counts
164
+ * do not change.
165
+ */
166
+ rowJob?: (job: JobState) => JobRowOverride | null
167
+ }
168
+
169
+ /** A `rowJob` result; see `UseJobCenterOptions.rowJob`. */
170
+ export interface JobRowOverride {
171
+ status?: JobStatus | null
172
+ percent?: number | null
173
+ message?: string | null
174
+ }
175
+
176
+ /** How a tray row shows a job; see `UseJobCenterReturn.rowView`. */
177
+ export interface JobRowView {
178
+ status: JobStatus
179
+ /** 0-100, rounded; `null` hides the progress bar and shows the time instead. */
180
+ percent: number | null
181
+ message: string
146
182
  }
147
183
 
148
184
  export interface UseJobCenterReturn {
185
+ /** The current source; a new identity is a new job list. */
186
+ source: ComputedRef<PluginJobCenterSource | undefined>
149
187
  jobs: ComputedRef<JobState[]>
150
188
  activeJobs: ComputedRef<JobState[]>
151
189
  historyJobs: ComputedRef<JobState[]>
@@ -155,14 +193,23 @@ export interface UseJobCenterReturn {
155
193
  canCancel: ComputedRef<boolean>
156
194
  canDelete: ComputedRef<boolean>
157
195
  canClearFinished: ComputedRef<boolean>
196
+ /** DOM id of the JobsStatusTray panel, for `AppTopBar`'s `jobsPanelId` prop. */
197
+ panelId: string
198
+ /** The row view of a job, with the `rowJob` option applied. */
199
+ rowView: (job: JobState) => JobRowView
200
+ /** The viewer's own job for `AppTopBar`'s `job` prop. */
201
+ topBarJob: ComputedRef<TopBarJob | null>
202
+ /** Jobs in the list with status `completed`, for `AppTopBar`'s `jobsDone` prop. */
203
+ completedCount: ComputedRef<number>
158
204
  isOpen: Ref<boolean>
159
- activeTab: Ref<JobsStatusTrayTab>
160
205
  isLoading: Ref<boolean>
161
206
  error: ShallowRef<string | null>
162
207
  pendingJobIds: Ref<Set<string>>
163
208
  open: () => void
164
209
  close: () => void
165
210
  toggle: () => void
211
+ /** The top-bar chip: with the tray closed and none of the viewer's jobs active, selects the held just-completed job; else toggles. */
212
+ handleTopBarClick: (onSelect: (job: JobState) => void) => void
166
213
  refresh: () => Promise<boolean>
167
214
  cancelJob: (job: JobState) => Promise<boolean>
168
215
  deleteJob: (job: JobState) => Promise<boolean>
@@ -170,6 +217,35 @@ export interface UseJobCenterReturn {
170
217
  isPending: (job: JobState) => boolean
171
218
  }
172
219
 
220
+ /** The stored seen failures, or null when nothing is stored or storage is blocked. */
221
+ function readSeenFailed(key: string): Set<string> | null {
222
+ try {
223
+ const parsed: unknown = JSON.parse(globalThis.localStorage?.getItem(key) ?? 'null')
224
+ return Array.isArray(parsed) ? new Set(parsed.filter((id): id is string => typeof id === 'string')) : null
225
+ } catch {
226
+ return null
227
+ }
228
+ }
229
+
230
+ function writeSeenFailed(key: string, seen: Set<string>): void {
231
+ try {
232
+ globalThis.localStorage?.setItem(key, JSON.stringify([...seen]))
233
+ } catch {
234
+ // Private windows and full storage: the state lasts until reload.
235
+ }
236
+ }
237
+
238
+ function jobMessage(job: JobState): string {
239
+ if (job.error) return job.error
240
+ if (job.status === 'awaiting_input') {
241
+ return job.progress.message || 'Waiting for external input'
242
+ }
243
+ if (job.status === 'queued' && job.queue_position > 0) {
244
+ return `Queue position ${job.queue_position}`
245
+ }
246
+ return job.progress.message || job.progress.stage || jobStatusLabel(job.status)
247
+ }
248
+
173
249
  function errorMessage(error: unknown, fallback: string): string {
174
250
  return error instanceof Error ? error.message : fallback
175
251
  }
@@ -184,7 +260,6 @@ export function useJobCenter(
184
260
  options: UseJobCenterOptions = {},
185
261
  ): UseJobCenterReturn {
186
262
  const isOpen = ref(false)
187
- const activeTab = ref<JobsStatusTrayTab>('active')
188
263
  const isLoading = ref(false)
189
264
  const error = shallowRef<string | null>(null)
190
265
  const pendingJobIds = ref(new Set<string>())
@@ -200,6 +275,103 @@ export function useJobCenter(
200
275
  const historyJobs = computed(() => jobs.value.filter(job => !isActiveJobStatus(job.status)))
201
276
  const deletableJobs = computed(() => historyJobs.value.filter(job => job.can_delete))
202
277
  const featuredJob = computed(() => activeJobs.value[0] ?? historyJobs.value[0] ?? null)
278
+ const completedCount = computed(() => jobs.value.filter(job => job.status === 'completed').length)
279
+
280
+ // Top-bar run slot: the viewer's own jobs only. Standalone or with auth off
281
+ // (no signed-in user), every job is the viewer's.
282
+ const { integrated, user, plugin, isAdmin } = usePlatform()
283
+ const isOwnJob = (job: JobState) => {
284
+ if (!integrated.value) return true
285
+ // With no user, isAdmin is true only with auth off.
286
+ if (user.value == null) return isAdmin.value
287
+ return (job.owner_user_id ?? job.user_id) === String(user.value.id)
288
+ }
289
+ const ownActiveJobs = computed(() => activeJobs.value.filter(isOwnJob))
290
+ const justFinished = shallowRef<JobState | null>(null)
291
+ // The live held job; it drops out once the job leaves the list.
292
+ const heldJob = computed(() => {
293
+ const held = justFinished.value
294
+ return held ? jobs.value.find(job => job.job_id === held.job_id) ?? null : null
295
+ })
296
+ let finishedTimer: ReturnType<typeof setTimeout> | undefined
297
+ // Compare active jobs within one source only: a replaced source starts over.
298
+ watch([source, ownActiveJobs], ([currentSource, now], [previousSource, before]) => {
299
+ if (currentSource !== previousSource) {
300
+ justFinished.value = null
301
+ return
302
+ }
303
+ const nowIds = new Set(now.map(job => job.job_id))
304
+ const left = new Set(before.filter(job => !nowIds.has(job.job_id)).map(job => job.job_id))
305
+ const finished = jobs.value.find(job => left.has(job.job_id) && job.status === 'completed')
306
+ if (!finished) return
307
+ justFinished.value = finished
308
+ clearTimeout(finishedTimer)
309
+ finishedTimer = setTimeout(() => { justFinished.value = null }, FINISHED_HOLD_MS)
310
+ })
311
+ // ponytail: the seen set only grows; prune to listed jobs if the stored array ever matters.
312
+ const seenFailedKey = `mint.jobs.${plugin.value?.id ?? 'standalone'}.seen-failed`
313
+ const storedSeenFailed = readSeenFailed(seenFailedKey)
314
+ const seenFailed = shallowRef(storedSeenFailed ?? new Set<string>())
315
+ const unseenFailedJobs = computed(() => historyJobs.value.filter(job => (
316
+ job.status === 'failed' && isOwnJob(job) && !seenFailed.value.has(job.job_id)
317
+ )))
318
+ function markFailedSeen(): void {
319
+ seenFailed.value = new Set([...seenFailed.value, ...unseenFailedJobs.value.map(job => job.job_id)])
320
+ writeSeenFailed(seenFailedKey, seenFailed.value)
321
+ }
322
+ // Nothing stored (new browser, cleared or blocked storage): the failures at the
323
+ // initial load are the baseline, so only later failures alert. Without a load
324
+ // step, the first non-empty list stands in for it.
325
+ let hasBaseline = storedSeenFailed !== null
326
+ let awaitingLoad = options.loadOnMount !== false && Boolean(source.value?.refresh)
327
+ function takeBaseline(): void {
328
+ if (hasBaseline || !active) return
329
+ hasBaseline = true
330
+ markFailedSeen()
331
+ }
332
+ watch(jobs, (list) => {
333
+ if (!awaitingLoad && list.length > 0) takeBaseline()
334
+ }, { immediate: true })
335
+ // The open tray shows every failure, including one that happens while it is open.
336
+ watch([isOpen, unseenFailedJobs], ([open, unseen]) => {
337
+ if (open && unseen.length > 0) markFailedSeen()
338
+ })
339
+ const topBarJob = computed<TopBarJob | null>(() => {
340
+ const own = ownActiveJobs.value
341
+ const shown = own.find(job => job.status === 'awaiting_input') ?? own[0]
342
+ if (shown) {
343
+ return options.topBarJob?.(shown) ?? {
344
+ status: shown.status,
345
+ label: shown.progress.message || `${shown.name} ${jobStatusLabel(shown.status).toLowerCase()}`,
346
+ percent: shown.status === 'running' ? normalizeJobPercent(shown.progress.percent) : null,
347
+ }
348
+ }
349
+ const held = heldJob.value
350
+ if (held) return options.topBarJob?.(held) ?? { status: 'completed', label: `${held.name} completed` }
351
+ const failed = unseenFailedJobs.value.length
352
+ return failed > 0 ? { status: 'failed', label: `${failed} failed` } : null
353
+ })
354
+
355
+ function rowView(job: JobState): JobRowView {
356
+ const override = options.rowJob?.(job) ?? {}
357
+ const status = override.status ?? job.status
358
+ // Only an explicit percent changes the bar; a status override keeps the job's own.
359
+ const percent = override.percent !== undefined
360
+ ? override.percent
361
+ : job.status === 'running' ? job.progress.percent : null
362
+ return {
363
+ status,
364
+ percent: percent == null ? null : Math.round(normalizeJobPercent(percent)),
365
+ message: override.message ?? jobMessage(job),
366
+ }
367
+ }
368
+
369
+ function handleTopBarClick(onSelect: (job: JobState) => void): void {
370
+ const finished = heldJob.value
371
+ if (!isOpen.value && ownActiveJobs.value.length === 0 && finished) onSelect(finished)
372
+ else isOpen.value = !isOpen.value
373
+ }
374
+
203
375
  const canRefresh = computed(() => Boolean(source.value?.refresh))
204
376
  const canCancel = computed(() => Boolean(source.value?.cancel))
205
377
  const canDelete = computed(() => Boolean(source.value?.delete))
@@ -290,14 +462,20 @@ export function useJobCenter(
290
462
  }
291
463
 
292
464
  onMounted(() => {
293
- if (options.loadOnMount !== false) void refresh()
465
+ if (options.loadOnMount === false) return
466
+ void refresh().then((loaded) => {
467
+ awaitingLoad = false
468
+ if (loaded || jobs.value.length > 0) takeBaseline()
469
+ })
294
470
  })
295
471
  onUnmounted(() => {
296
472
  active = false
297
473
  requestGeneration += 1
474
+ clearTimeout(finishedTimer)
298
475
  })
299
476
 
300
477
  return {
478
+ source,
301
479
  jobs,
302
480
  activeJobs,
303
481
  historyJobs,
@@ -307,14 +485,18 @@ export function useJobCenter(
307
485
  canCancel,
308
486
  canDelete,
309
487
  canClearFinished,
488
+ panelId: `mint-jobs-tray-${useId()}`,
489
+ rowView,
490
+ topBarJob,
491
+ completedCount,
310
492
  isOpen,
311
- activeTab,
312
493
  isLoading,
313
494
  error,
314
495
  pendingJobIds,
315
496
  open: () => { isOpen.value = true },
316
497
  close: () => { isOpen.value = false },
317
498
  toggle: () => { isOpen.value = !isOpen.value },
499
+ handleTopBarClick,
318
500
  refresh,
319
501
  cancelJob,
320
502
  deleteJob,