@morscherlab/mint-sdk 1.4.0-beta.17 → 1.4.0-beta.18

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 (76) hide show
  1. package/README.md +14 -3
  2. package/dist/{AlertBox-SnsbVow-.js → AlertBox-CzLg0UaL.js} +2 -2
  3. package/dist/{AlertBox-SnsbVow-.js.map → AlertBox-CzLg0UaL.js.map} +1 -1
  4. package/dist/{AppSidebar-CB0-RQ4m.js → AppSidebar-6oTT-gPO.js} +43 -13
  5. package/dist/AppSidebar-6oTT-gPO.js.map +1 -0
  6. package/dist/{ExperimentPopover-ChKCrklH.js → ExperimentPopover-9l4QZlvQ.js} +2 -2
  7. package/dist/{ExperimentPopover-ChKCrklH.js.map → ExperimentPopover-9l4QZlvQ.js.map} +1 -1
  8. package/dist/{ExperimentSelectorModal-BqifjNaN.js → ExperimentSelectorModal-pR5fK3pA.js} +3 -3
  9. package/dist/{ExperimentSelectorModal-BqifjNaN.js.map → ExperimentSelectorModal-pR5fK3pA.js.map} +1 -1
  10. package/dist/{SegmentedControl-hOgBVdju.js → SegmentedControl-CJ7EAmOq.js} +2 -2
  11. package/dist/{SegmentedControl-hOgBVdju.js.map → SegmentedControl-CJ7EAmOq.js.map} +1 -1
  12. package/dist/{SettingsModal-CoZK3EDN.js → SettingsModal-Dyitgzfp.js} +6 -6
  13. package/dist/{SettingsModal-CoZK3EDN.js.map → SettingsModal-Dyitgzfp.js.map} +1 -1
  14. package/dist/components/AppTopBar.vue.d.ts +7 -0
  15. package/dist/components/JobsStatusTray.vue.d.ts +11 -4
  16. package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
  17. package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
  18. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
  19. package/dist/composables/useJobCenter.d.ts +11 -3
  20. package/dist/{controlSchemaModel-CIH5Hqpw.js → controlSchemaModel-DCUOx8ll.js} +2 -2
  21. package/dist/{controlSchemaModel-CIH5Hqpw.js.map → controlSchemaModel-DCUOx8ll.js.map} +1 -1
  22. package/dist/{data-OZ94DhVC.js → data-BIiV4kKw.js} +9 -9
  23. package/dist/{data-OZ94DhVC.js.map → data-BIiV4kKw.js.map} +1 -1
  24. package/dist/data.js +7 -7
  25. package/dist/index.js +16 -16
  26. package/dist/{install-BKgb5qZw.js → install-Dg-Z6ze5.js} +13 -13
  27. package/dist/{install-BKgb5qZw.js.map → install-Dg-Z6ze5.js.map} +1 -1
  28. package/dist/install.js +1 -1
  29. package/dist/{lab-ZDKlx1Oo.js → lab-BHVT1qN2.js} +9 -9
  30. package/dist/{lab-ZDKlx1Oo.js.map → lab-BHVT1qN2.js.map} +1 -1
  31. package/dist/lab.js +4 -4
  32. package/dist/{records-B4yl2oIu.js → records-CR_pYzIP.js} +62 -62
  33. package/dist/{records-B4yl2oIu.js.map → records-CR_pYzIP.js.map} +1 -1
  34. package/dist/{runtime-Cz1GJa_O.js → runtime-cHv1phcc.js} +379 -31
  35. package/dist/runtime-cHv1phcc.js.map +1 -0
  36. package/dist/runtime.d.ts +1 -1
  37. package/dist/runtime.js +6 -6
  38. package/dist/{shell-kq9p4VsX.js → shell-BDuZf2zb.js} +4 -4
  39. package/dist/{shell-kq9p4VsX.js.map → shell-BDuZf2zb.js.map} +1 -1
  40. package/dist/shell.js +4 -4
  41. package/dist/styles.css +147 -307
  42. package/dist/types/components.d.ts +2 -0
  43. package/dist/{useExperimentSamples-BLx29CkM.js → useExperimentSamples-Dno1fFMq.js} +3 -3
  44. package/dist/{useExperimentSamples-BLx29CkM.js.map → useExperimentSamples-Dno1fFMq.js.map} +1 -1
  45. package/dist/{useListReorder-C_8RYeVP.js → useListReorder-CQ9OCScc.js} +76 -76
  46. package/dist/useListReorder-CQ9OCScc.js.map +1 -0
  47. package/dist/{useRackEditor-BhYlXKFM.js → useRackEditor-wL3kPrRT.js} +6 -6
  48. package/dist/{useRackEditor-BhYlXKFM.js.map → useRackEditor-wL3kPrRT.js.map} +1 -1
  49. package/dist/useRealtime-BW02xy4Z.js +398 -0
  50. package/dist/useRealtime-BW02xy4Z.js.map +1 -0
  51. package/dist/{useRequest-C9uwM79q.js → useRequest-CDkrXNU7.js} +2 -2
  52. package/dist/{useRequest-C9uwM79q.js.map → useRequest-CDkrXNU7.js.map} +1 -1
  53. package/package.json +1 -1
  54. package/src/__tests__/components/AppTopBar.test.ts +33 -2
  55. package/src/__tests__/components/GeneratedPluginView.test.ts +1 -1
  56. package/src/__tests__/components/JobsStatusTray.test.ts +137 -71
  57. package/src/__tests__/components/PluginWorkspaceView.test.ts +56 -1
  58. package/src/__tests__/composables/useJobCenter.test.ts +223 -3
  59. package/src/__tests__/docs/frontendDocsCatalog.test.ts +3 -3
  60. package/src/components/AppTopBar.vue +14 -0
  61. package/src/components/GeneratedPluginView.vue +4 -9
  62. package/src/components/JobsStatusTray.story.vue +78 -43
  63. package/src/components/JobsStatusTray.vue +171 -279
  64. package/src/components/PluginWorkspaceView.props.ts +3 -0
  65. package/src/components/PluginWorkspaceView.vue +17 -0
  66. package/src/components/internal/WorkspaceShellInternal.vue +12 -0
  67. package/src/composables/useJobCenter.ts +127 -6
  68. package/src/runtime.ts +0 -1
  69. package/src/styles/components/app-top-bar.css +7 -0
  70. package/src/styles/components/jobs-status-tray.css +129 -296
  71. package/src/types/components.ts +2 -0
  72. package/dist/AppSidebar-CB0-RQ4m.js.map +0 -1
  73. package/dist/runtime-Cz1GJa_O.js.map +0 -1
  74. package/dist/useListReorder-C_8RYeVP.js.map +0 -1
  75. package/dist/useRealtime-CUMbW_ln.js +0 -699
  76. package/dist/useRealtime-CUMbW_ln.js.map +0 -1
@@ -5,20 +5,25 @@ 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
+ TopBarJob,
19
22
  } from '../types/jobs'
23
+ import { usePlatform } from './usePlatform'
20
24
 
21
- export type JobsStatusTrayTab = 'active' | 'history'
25
+ /** How long a job that just completed stays in the top-bar run slot. */
26
+ const FINISHED_HOLD_MS = 6000
22
27
 
23
28
  /**
24
29
  * The canonical owner of a collection of plugin jobs.
@@ -143,6 +148,8 @@ function duplicatePluginJobId(jobId: string): Error {
143
148
  export interface UseJobCenterOptions {
144
149
  source?: MaybeRefOrGetter<PluginJobCenterSource | undefined>
145
150
  loadOnMount?: boolean
151
+ /** Maps the chosen own active job to the top-bar chip; `null` keeps the default mapping. */
152
+ topBarJob?: (job: JobState) => TopBarJob | null
146
153
  }
147
154
 
148
155
  export interface UseJobCenterReturn {
@@ -155,14 +162,21 @@ export interface UseJobCenterReturn {
155
162
  canCancel: ComputedRef<boolean>
156
163
  canDelete: ComputedRef<boolean>
157
164
  canClearFinished: ComputedRef<boolean>
165
+ /** DOM id of the JobsStatusTray panel, for `AppTopBar`'s `jobsPanelId` prop. */
166
+ panelId: string
167
+ /** The viewer's own job for `AppTopBar`'s `job` prop. */
168
+ topBarJob: ComputedRef<TopBarJob | null>
169
+ /** Jobs in the list with status `completed`, for `AppTopBar`'s `jobsDone` prop. */
170
+ completedCount: ComputedRef<number>
158
171
  isOpen: Ref<boolean>
159
- activeTab: Ref<JobsStatusTrayTab>
160
172
  isLoading: Ref<boolean>
161
173
  error: ShallowRef<string | null>
162
174
  pendingJobIds: Ref<Set<string>>
163
175
  open: () => void
164
176
  close: () => void
165
177
  toggle: () => void
178
+ /** The top-bar chip: with the tray closed and none of the viewer's jobs active, selects the held just-completed job; else toggles. */
179
+ handleTopBarClick: (onSelect: (job: JobState) => void) => void
166
180
  refresh: () => Promise<boolean>
167
181
  cancelJob: (job: JobState) => Promise<boolean>
168
182
  deleteJob: (job: JobState) => Promise<boolean>
@@ -170,6 +184,24 @@ export interface UseJobCenterReturn {
170
184
  isPending: (job: JobState) => boolean
171
185
  }
172
186
 
187
+ /** The stored seen failures, or null when nothing is stored or storage is blocked. */
188
+ function readSeenFailed(key: string): Set<string> | null {
189
+ try {
190
+ const parsed: unknown = JSON.parse(globalThis.localStorage?.getItem(key) ?? 'null')
191
+ return Array.isArray(parsed) ? new Set(parsed.filter((id): id is string => typeof id === 'string')) : null
192
+ } catch {
193
+ return null
194
+ }
195
+ }
196
+
197
+ function writeSeenFailed(key: string, seen: Set<string>): void {
198
+ try {
199
+ globalThis.localStorage?.setItem(key, JSON.stringify([...seen]))
200
+ } catch {
201
+ // Private windows and full storage: the state lasts until reload.
202
+ }
203
+ }
204
+
173
205
  function errorMessage(error: unknown, fallback: string): string {
174
206
  return error instanceof Error ? error.message : fallback
175
207
  }
@@ -184,7 +216,6 @@ export function useJobCenter(
184
216
  options: UseJobCenterOptions = {},
185
217
  ): UseJobCenterReturn {
186
218
  const isOpen = ref(false)
187
- const activeTab = ref<JobsStatusTrayTab>('active')
188
219
  const isLoading = ref(false)
189
220
  const error = shallowRef<string | null>(null)
190
221
  const pendingJobIds = ref(new Set<string>())
@@ -200,6 +231,88 @@ export function useJobCenter(
200
231
  const historyJobs = computed(() => jobs.value.filter(job => !isActiveJobStatus(job.status)))
201
232
  const deletableJobs = computed(() => historyJobs.value.filter(job => job.can_delete))
202
233
  const featuredJob = computed(() => activeJobs.value[0] ?? historyJobs.value[0] ?? null)
234
+ const completedCount = computed(() => jobs.value.filter(job => job.status === 'completed').length)
235
+
236
+ // Top-bar run slot: the viewer's own jobs only. Standalone or with auth off
237
+ // (no signed-in user), every job is the viewer's.
238
+ const { integrated, user, plugin, isAdmin } = usePlatform()
239
+ const isOwnJob = (job: JobState) => {
240
+ if (!integrated.value) return true
241
+ // With no user, isAdmin is true only with auth off.
242
+ if (user.value == null) return isAdmin.value
243
+ return (job.owner_user_id ?? job.user_id) === String(user.value.id)
244
+ }
245
+ const ownActiveJobs = computed(() => activeJobs.value.filter(isOwnJob))
246
+ const justFinished = shallowRef<JobState | null>(null)
247
+ // The live held job; it drops out once the job leaves the list.
248
+ const heldJob = computed(() => {
249
+ const held = justFinished.value
250
+ return held ? jobs.value.find(job => job.job_id === held.job_id) ?? null : null
251
+ })
252
+ let finishedTimer: ReturnType<typeof setTimeout> | undefined
253
+ // Compare active jobs within one source only: a replaced source starts over.
254
+ watch([source, ownActiveJobs], ([currentSource, now], [previousSource, before]) => {
255
+ if (currentSource !== previousSource) {
256
+ justFinished.value = null
257
+ return
258
+ }
259
+ const nowIds = new Set(now.map(job => job.job_id))
260
+ const left = new Set(before.filter(job => !nowIds.has(job.job_id)).map(job => job.job_id))
261
+ const finished = jobs.value.find(job => left.has(job.job_id) && job.status === 'completed')
262
+ if (!finished) return
263
+ justFinished.value = finished
264
+ clearTimeout(finishedTimer)
265
+ finishedTimer = setTimeout(() => { justFinished.value = null }, FINISHED_HOLD_MS)
266
+ })
267
+ // ponytail: the seen set only grows; prune to listed jobs if the stored array ever matters.
268
+ const seenFailedKey = `mint.jobs.${plugin.value?.id ?? 'standalone'}.seen-failed`
269
+ const storedSeenFailed = readSeenFailed(seenFailedKey)
270
+ const seenFailed = shallowRef(storedSeenFailed ?? new Set<string>())
271
+ const unseenFailedJobs = computed(() => historyJobs.value.filter(job => (
272
+ job.status === 'failed' && isOwnJob(job) && !seenFailed.value.has(job.job_id)
273
+ )))
274
+ function markFailedSeen(): void {
275
+ seenFailed.value = new Set([...seenFailed.value, ...unseenFailedJobs.value.map(job => job.job_id)])
276
+ writeSeenFailed(seenFailedKey, seenFailed.value)
277
+ }
278
+ // Nothing stored (new browser, cleared or blocked storage): the failures at the
279
+ // initial load are the baseline, so only later failures alert. Without a load
280
+ // step, the first non-empty list stands in for it.
281
+ let hasBaseline = storedSeenFailed !== null
282
+ let awaitingLoad = options.loadOnMount !== false && Boolean(source.value?.refresh)
283
+ function takeBaseline(): void {
284
+ if (hasBaseline || !active) return
285
+ hasBaseline = true
286
+ markFailedSeen()
287
+ }
288
+ watch(jobs, (list) => {
289
+ if (!awaitingLoad && list.length > 0) takeBaseline()
290
+ }, { immediate: true })
291
+ // The open tray shows every failure, including one that happens while it is open.
292
+ watch([isOpen, unseenFailedJobs], ([open, unseen]) => {
293
+ if (open && unseen.length > 0) markFailedSeen()
294
+ })
295
+ const topBarJob = computed<TopBarJob | null>(() => {
296
+ const own = ownActiveJobs.value
297
+ const shown = own.find(job => job.status === 'awaiting_input') ?? own[0]
298
+ if (shown) {
299
+ return options.topBarJob?.(shown) ?? {
300
+ status: shown.status,
301
+ label: shown.progress.message || `${shown.name} ${jobStatusLabel(shown.status).toLowerCase()}`,
302
+ percent: shown.status === 'running' ? normalizeJobPercent(shown.progress.percent) : null,
303
+ }
304
+ }
305
+ if (heldJob.value) return { status: 'completed', label: `${heldJob.value.name} completed` }
306
+ const failed = unseenFailedJobs.value.length
307
+ return failed > 0 ? { status: 'failed', label: `${failed} failed` } : null
308
+ })
309
+
310
+ function handleTopBarClick(onSelect: (job: JobState) => void): void {
311
+ const finished = heldJob.value
312
+ if (!isOpen.value && ownActiveJobs.value.length === 0 && finished) onSelect(finished)
313
+ else isOpen.value = !isOpen.value
314
+ }
315
+
203
316
  const canRefresh = computed(() => Boolean(source.value?.refresh))
204
317
  const canCancel = computed(() => Boolean(source.value?.cancel))
205
318
  const canDelete = computed(() => Boolean(source.value?.delete))
@@ -290,11 +403,16 @@ export function useJobCenter(
290
403
  }
291
404
 
292
405
  onMounted(() => {
293
- if (options.loadOnMount !== false) void refresh()
406
+ if (options.loadOnMount === false) return
407
+ void refresh().then((loaded) => {
408
+ awaitingLoad = false
409
+ if (loaded || jobs.value.length > 0) takeBaseline()
410
+ })
294
411
  })
295
412
  onUnmounted(() => {
296
413
  active = false
297
414
  requestGeneration += 1
415
+ clearTimeout(finishedTimer)
298
416
  })
299
417
 
300
418
  return {
@@ -307,14 +425,17 @@ export function useJobCenter(
307
425
  canCancel,
308
426
  canDelete,
309
427
  canClearFinished,
428
+ panelId: `mint-jobs-tray-${useId()}`,
429
+ topBarJob,
430
+ completedCount,
310
431
  isOpen,
311
- activeTab,
312
432
  isLoading,
313
433
  error,
314
434
  pendingJobIds,
315
435
  open: () => { isOpen.value = true },
316
436
  close: () => { isOpen.value = false },
317
437
  toggle: () => { isOpen.value = !isOpen.value },
438
+ handleTopBarClick,
318
439
  refresh,
319
440
  cancelJob,
320
441
  deleteJob,
package/src/runtime.ts CHANGED
@@ -77,7 +77,6 @@ export {
77
77
  combinePluginJobSources,
78
78
  useJobCenter,
79
79
  type CombinedPluginJobCenterSource,
80
- type JobsStatusTrayTab,
81
80
  type PluginJobCenterSource,
82
81
  type UseJobCenterOptions,
83
82
  type UseJobCenterReturn,
@@ -338,6 +338,13 @@
338
338
  flex-shrink: 0;
339
339
  }
340
340
 
341
+ .mint-topbar__job-count {
342
+ color: var(--text-muted);
343
+ font-family: var(--font-mono);
344
+ font-size: 0.6875rem;
345
+ font-variant-numeric: tabular-nums;
346
+ }
347
+
341
348
  .mint-topbar__job-icon--spin {
342
349
  animation: mint-topbar-job-spin 1s linear infinite;
343
350
  }