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

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.
@@ -1,398 +0,0 @@
1
- import { d as isActiveJobStatus, m as normalizeJobPercent, p as jobStatusLabel } from "./records-CR_pYzIP.js";
2
- import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, getCurrentScope, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onScopeDispose, openBlock, readonly, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, toValue, unref, watch, withCtx } from "vue";
3
- import { CircleAlert, CircleCheck, CircleSlash, CircleX, Clock, LoaderCircle, RefreshCw, Square, Trash2, X } from "lucide-vue-next";
4
- //#region src/components/JobsStatusTray.vue?vue&type=script&setup=true&lang.ts
5
- var _hoisted_1 = { class: "mint-jobs-tray" };
6
- var _hoisted_2 = ["id"];
7
- var _hoisted_3 = { class: "mint-jobs-tray__head" };
8
- var _hoisted_4 = { class: "mint-jobs-tray__head-count" };
9
- var _hoisted_5 = ["disabled"];
10
- var _hoisted_6 = {
11
- key: 0,
12
- class: "mint-jobs-tray__error",
13
- role: "alert"
14
- };
15
- var _hoisted_7 = { class: "mint-jobs-tray__body" };
16
- var _hoisted_8 = {
17
- key: 0,
18
- class: "mint-jobs-tray__empty"
19
- };
20
- var _hoisted_9 = ["data-section"];
21
- var _hoisted_10 = { class: "mint-jobs-tray__section-count" };
22
- var _hoisted_11 = ["data-status"];
23
- var _hoisted_12 = ["onClick"];
24
- var _hoisted_13 = { class: "mint-jobs-tray__tools" };
25
- var _hoisted_14 = { class: "mint-jobs-tray__value" };
26
- var _hoisted_15 = [
27
- "aria-label",
28
- "disabled",
29
- "onClick"
30
- ];
31
- var _hoisted_16 = [
32
- "aria-label",
33
- "disabled",
34
- "onClick"
35
- ];
36
- var _hoisted_17 = ["aria-label", "aria-valuenow"];
37
- //#endregion
38
- //#region src/components/JobsStatusTray.vue
39
- var JobsStatusTray_default = /* @__PURE__ */ defineComponent({
40
- __name: "JobsStatusTray",
41
- props: {
42
- center: {},
43
- title: { default: "Jobs" },
44
- teleportTo: {
45
- type: [String, Boolean],
46
- default: "body"
47
- }
48
- },
49
- emits: ["select", "error"],
50
- setup(__props, { emit: __emit }) {
51
- /**
52
- * Compact, non-modal job panel under the top bar, driven by a job center the parent owns.
53
- *
54
- * The parent passes the center's `topBarJob`, `completedCount` and `handleTopBarClick`
55
- * to `AppTopBar`; its run slot opens this panel. `PluginWorkspaceView` does this for you
56
- * when its spec sets `jobs`.
57
- *
58
- * @example Recommended
59
- * ```vue
60
- * <AppTopBar :job="center.topBarJob.value" :jobs-done="center.completedCount.value"
61
- * @job-click="center.handleTopBarClick(openResult)" />
62
- * <JobsStatusTray :center="center" @select="openResult" />
63
- * ```
64
- */
65
- const props = __props;
66
- const emit = __emit;
67
- const STATUS_ICONS = {
68
- running: LoaderCircle,
69
- awaiting_input: CircleAlert,
70
- queued: Clock,
71
- completed: CircleCheck,
72
- failed: CircleX,
73
- cancelled: CircleSlash
74
- };
75
- const panelRef = ref(null);
76
- const teleportTarget = computed(() => props.teleportTo || "body");
77
- const tray = props.center;
78
- const titleId = `${tray.panelId}-title`;
79
- const activeRows = computed(() => [...tray.activeJobs.value.filter((job) => job.status === "awaiting_input"), ...tray.activeJobs.value.filter((job) => job.status !== "awaiting_input")]);
80
- const sections = computed(() => [{
81
- key: "active",
82
- title: "Active",
83
- jobs: activeRows.value
84
- }, {
85
- key: "finished",
86
- title: "Finished",
87
- jobs: tray.historyJobs.value
88
- }].filter((section) => section.jobs.length > 0));
89
- const headCount = computed(() => {
90
- const parts = [
91
- [tray.activeJobs.value.length, "active"],
92
- [tray.completedCount.value, "done"],
93
- [tray.historyJobs.value.filter((job) => job.status === "failed").length, "failed"]
94
- ].filter(([n]) => n).map(([n, word]) => `${n} ${word}`);
95
- return parts.length ? parts.join(" · ") : "No jobs";
96
- });
97
- const canClear = computed(() => tray.deletableJobs.value.length > 0 && tray.canClearFinished.value);
98
- let opener = null;
99
- function onDocumentKeydown(event) {
100
- if (event.key === "Escape") closeTray();
101
- }
102
- watch(tray.isOpen, async (open) => {
103
- if (!open) {
104
- document.removeEventListener("keydown", onDocumentKeydown);
105
- return;
106
- }
107
- opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
108
- document.addEventListener("keydown", onDocumentKeydown);
109
- await nextTick();
110
- panelRef.value?.focus();
111
- }, { flush: "sync" });
112
- onBeforeUnmount(() => document.removeEventListener("keydown", onDocumentKeydown));
113
- function closeTray() {
114
- tray.close();
115
- (opener?.isConnected && opener !== document.body ? opener : document.querySelector(".mint-topbar__job"))?.focus();
116
- }
117
- function progressText(job) {
118
- if (job.error) return job.error;
119
- if (job.status === "awaiting_input") return job.progress.message || "Waiting for external input";
120
- if (job.status === "queued" && job.queue_position > 0) return `Queue position ${job.queue_position}`;
121
- return job.progress.message || job.progress.stage || jobStatusLabel(job.status);
122
- }
123
- function formatTime(value) {
124
- if (!value) return "";
125
- const date = new Date(value);
126
- return Number.isNaN(date.getTime()) ? "" : date.toLocaleTimeString([], {
127
- hour: "2-digit",
128
- minute: "2-digit"
129
- });
130
- }
131
- function jobPercent(job) {
132
- return Math.round(normalizeJobPercent(job.progress.percent));
133
- }
134
- /** Right-hand value: the percent while a job runs, else when it ended or started. */
135
- function jobValue(job) {
136
- if (job.status === "running") return `${jobPercent(job)}%`;
137
- return formatTime(job.completed_at || job.started_at || job.created_at);
138
- }
139
- async function cancelJob(job) {
140
- if (!await tray.cancelJob(job) && tray.error.value) emit("error", tray.error.value);
141
- }
142
- async function deleteJob(job) {
143
- if (!await tray.deleteJob(job) && tray.error.value) emit("error", tray.error.value);
144
- }
145
- async function clearFinished() {
146
- if (!await tray.clearFinished() && tray.error.value) emit("error", tray.error.value);
147
- }
148
- return (_ctx, _cache) => {
149
- return openBlock(), createBlock(Teleport, {
150
- to: teleportTarget.value,
151
- disabled: __props.teleportTo === false
152
- }, [createElementVNode("div", _hoisted_1, [unref(tray).isOpen.value ? (openBlock(), createElementBlock("div", {
153
- key: 0,
154
- class: "mint-jobs-tray__scrim",
155
- "aria-hidden": "true",
156
- onClick: _cache[0] || (_cache[0] = ($event) => unref(tray).close())
157
- })) : createCommentVNode("", true), createVNode(Transition, { name: "mint-jobs-tray-fade" }, {
158
- default: withCtx(() => [unref(tray).isOpen.value ? (openBlock(), createElementBlock("section", {
159
- key: 0,
160
- id: unref(tray).panelId,
161
- ref_key: "panelRef",
162
- ref: panelRef,
163
- class: "mint-jobs-tray__panel",
164
- "aria-labelledby": titleId,
165
- tabindex: "-1"
166
- }, [
167
- createElementVNode("header", _hoisted_3, [
168
- createElementVNode("h2", {
169
- id: titleId,
170
- class: "mint-jobs-tray__title"
171
- }, toDisplayString(__props.title), 1),
172
- createElementVNode("span", _hoisted_4, toDisplayString(headCount.value), 1),
173
- unref(tray).canRefresh.value ? (openBlock(), createElementBlock("button", {
174
- key: 0,
175
- type: "button",
176
- class: "mint-jobs-tray__icon-button",
177
- "aria-label": "Refresh jobs",
178
- title: "Refresh jobs",
179
- disabled: unref(tray).isLoading.value,
180
- onClick: _cache[1] || (_cache[1] = (...args) => unref(tray).refresh && unref(tray).refresh(...args))
181
- }, [createVNode(unref(RefreshCw), {
182
- size: 14,
183
- "aria-hidden": "true"
184
- })], 8, _hoisted_5)) : createCommentVNode("", true),
185
- createElementVNode("button", {
186
- type: "button",
187
- class: "mint-jobs-tray__icon-button",
188
- "aria-label": "Close jobs",
189
- title: "Close",
190
- onClick: closeTray
191
- }, [createVNode(unref(X), {
192
- size: 14,
193
- "aria-hidden": "true"
194
- })])
195
- ]),
196
- unref(tray).error.value ? (openBlock(), createElementBlock("p", _hoisted_6, toDisplayString(unref(tray).error.value), 1)) : createCommentVNode("", true),
197
- createElementVNode("div", _hoisted_7, [sections.value.length === 0 ? (openBlock(), createElementBlock("p", _hoisted_8, "No jobs yet")) : createCommentVNode("", true), (openBlock(true), createElementBlock(Fragment, null, renderList(sections.value, (section) => {
198
- return openBlock(), createElementBlock(Fragment, { key: section.key }, [createElementVNode("div", {
199
- class: "mint-jobs-tray__section",
200
- "data-section": section.key
201
- }, [
202
- createTextVNode(toDisplayString(section.title) + " ", 1),
203
- createElementVNode("span", _hoisted_10, "· " + toDisplayString(section.jobs.length), 1),
204
- section.key === "finished" && canClear.value ? (openBlock(), createElementBlock("button", {
205
- key: 0,
206
- type: "button",
207
- class: "mint-jobs-tray__section-action",
208
- onClick: clearFinished
209
- }, " Clear ")) : createCommentVNode("", true)
210
- ], 8, _hoisted_9), (openBlock(true), createElementBlock(Fragment, null, renderList(section.jobs, (job) => {
211
- return openBlock(), createElementBlock("div", {
212
- key: job.job_id,
213
- class: normalizeClass(["mint-jobs-tray__row", { "mint-jobs-tray__row--awaiting-input": job.status === "awaiting_input" }]),
214
- "data-status": job.status
215
- }, [
216
- (openBlock(), createBlock(resolveDynamicComponent(STATUS_ICONS[job.status]), {
217
- size: 15,
218
- class: normalizeClass(["mint-jobs-tray__status", `mint-jobs-tray__status--${job.status.replace("_", "-")}`]),
219
- role: "img",
220
- "aria-label": unref(jobStatusLabel)(job.status)
221
- }, null, 8, ["class", "aria-label"])),
222
- createElementVNode("button", {
223
- type: "button",
224
- class: "mint-jobs-tray__row-title",
225
- onClick: ($event) => emit("select", job)
226
- }, [renderSlot(_ctx.$slots, "job-title", { job }, () => [createTextVNode(toDisplayString(job.name), 1)])], 8, _hoisted_12),
227
- createElementVNode("span", _hoisted_13, [
228
- createElementVNode("span", _hoisted_14, toDisplayString(jobValue(job)), 1),
229
- renderSlot(_ctx.$slots, "job-actions", { job }),
230
- unref(isActiveJobStatus)(job.status) && job.can_cancel && unref(tray).canCancel.value ? (openBlock(), createElementBlock("button", {
231
- key: 0,
232
- type: "button",
233
- class: "mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger",
234
- "aria-label": `Cancel ${job.name}`,
235
- title: "Cancel",
236
- disabled: unref(tray).isPending(job),
237
- onClick: ($event) => cancelJob(job)
238
- }, [createVNode(unref(Square), {
239
- size: 12,
240
- "aria-hidden": "true"
241
- })], 8, _hoisted_15)) : createCommentVNode("", true),
242
- job.can_delete && unref(tray).canDelete.value ? (openBlock(), createElementBlock("button", {
243
- key: 1,
244
- type: "button",
245
- class: "mint-jobs-tray__icon-button",
246
- "aria-label": `Delete ${job.name}`,
247
- title: "Delete",
248
- disabled: unref(tray).isPending(job),
249
- onClick: ($event) => deleteJob(job)
250
- }, [createVNode(unref(Trash2), {
251
- size: 14,
252
- "aria-hidden": "true"
253
- })], 8, _hoisted_16)) : createCommentVNode("", true)
254
- ]),
255
- job.status === "running" ? (openBlock(), createElementBlock("div", {
256
- key: 0,
257
- class: "mint-jobs-tray__progress",
258
- role: "progressbar",
259
- "aria-label": `${job.name} progress`,
260
- "aria-valuemin": "0",
261
- "aria-valuemax": "100",
262
- "aria-valuenow": jobPercent(job)
263
- }, [createElementVNode("span", { style: normalizeStyle({ width: `${jobPercent(job)}%` }) }, null, 4)], 8, _hoisted_17)) : createCommentVNode("", true),
264
- createElementVNode("span", { class: normalizeClass(["mint-jobs-tray__detail", { "mint-jobs-tray__detail--error": job.status === "failed" }]) }, [renderSlot(_ctx.$slots, "job-detail", { job }, () => [createTextVNode(toDisplayString(progressText(job)), 1)])], 2)
265
- ], 10, _hoisted_11);
266
- }), 128))], 64);
267
- }), 128))])
268
- ], 8, _hoisted_2)) : createCommentVNode("", true)]),
269
- _: 3
270
- })])], 8, ["to", "disabled"]);
271
- };
272
- }
273
- });
274
- //#endregion
275
- //#region src/composables/useRealtime.ts
276
- var EVENTS_URL = "/api/events";
277
- var RECONNECT_BASE_MS = 1e3;
278
- var RECONNECT_MAX_MS = 3e4;
279
- /** Merges the subscriptions of components that mount together into one connection. */
280
- var TOPIC_DEBOUNCE_MS = 50;
281
- var RESYNC = { type: "resync" };
282
- var listeners = /* @__PURE__ */ new Set();
283
- /** The last `presence.changed` of each topic of the open stream; the server sends one per topic at open. */
284
- var presence = /* @__PURE__ */ new Map();
285
- var connected = ref(false);
286
- var source = null;
287
- var sourceTopics = "";
288
- var failures = 0;
289
- var syncTimer = null;
290
- var retryTimer = null;
291
- function dispatch(event) {
292
- const isPresence = event.type === "presence.changed";
293
- if (isPresence && event.topic) presence.set(event.topic, event);
294
- for (const listener of [...listeners]) {
295
- if (isPresence && !listener.presence) continue;
296
- if (event.type === "resync" || event.topic && listener.topics().includes(event.topic)) listener.handler(event);
297
- }
298
- }
299
- function close() {
300
- source?.close();
301
- source = null;
302
- sourceTopics = "";
303
- presence.clear();
304
- connected.value = false;
305
- }
306
- function open(topics) {
307
- const stream = new EventSource(`${EVENTS_URL}?topics=${encodeURIComponent(topics)}`);
308
- source = stream;
309
- sourceTopics = topics;
310
- stream.onopen = () => {
311
- connected.value = true;
312
- dispatch(RESYNC);
313
- failures = 0;
314
- };
315
- stream.onmessage = (message) => {
316
- dispatch(JSON.parse(message.data));
317
- };
318
- stream.onerror = () => {
319
- if (source !== stream) return;
320
- close();
321
- if (syncTimer !== null) clearTimeout(syncTimer);
322
- syncTimer = null;
323
- retryTimer = setTimeout(() => {
324
- retryTimer = null;
325
- sync();
326
- }, Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** failures));
327
- failures += 1;
328
- };
329
- }
330
- function sync() {
331
- syncTimer = null;
332
- const topics = [...new Set([...listeners].flatMap((listener) => listener.topics()))].sort().join(",");
333
- if (source && topics === sourceTopics) return;
334
- close();
335
- if (topics) open(topics);
336
- }
337
- function schedule() {
338
- if (retryTimer !== null) return;
339
- if (syncTimer !== null) clearTimeout(syncTimer);
340
- syncTimer = setTimeout(sync, TOPIC_DEBOUNCE_MS);
341
- }
342
- /**
343
- * Subscribe to topics of the platform's realtime stream.
344
- *
345
- * Every caller in a tab shares one `EventSource`, opened on the union of
346
- * their topics and reopened when that union changes. `handler` receives the
347
- * write events of the caller's own topics (not `presence.changed`, which goes
348
- * to `usePresence`) plus every `resync`: one comes from the
349
- * server when this tab fell behind, and one follows each open of the stream:
350
- * the first open, each reconnect (backoff from 1 s to 30 s) and each reopen for
351
- * a changed topic union. On `resync`, refetch. The union of all callers' topics
352
- * in a tab may hold at most 50 topics; the platform refuses a larger stream.
353
- *
354
- * The stream authenticates with the session cookie, so it works in the
355
- * platform and in plugin frontends served by it. It exists only when the
356
- * platform's `realtime` feature flag is on; check that flag first.
357
- *
358
- * A plugin topic reaches a caller whose role may use the plugin; an
359
- * experiment plugin topic also needs a caller who may view the experiment.
360
- *
361
- * @example
362
- * useRealtime(() => [`experiment:${id.value}`], () => refetch())
363
- * @example
364
- * useRealtime(['plugin:drp'], (event) => {
365
- * if (event.type === 'drp.run.progress') progress.value = event.payload as RunProgress
366
- * })
367
- */
368
- function useRealtime(topics, handler) {
369
- return subscribeRealtime(topics, handler, false);
370
- }
371
- /** The last `presence.changed` of `topic` while the open stream views it (for `usePresence`). */
372
- function currentPresence(topic) {
373
- return presence.get(topic);
374
- }
375
- /** `useRealtime`, and with `withPresence` also the `presence.changed` events (for `usePresence`). */
376
- function subscribeRealtime(topics, handler, withPresence) {
377
- const listener = {
378
- topics: () => toValue(topics),
379
- handler,
380
- presence: withPresence
381
- };
382
- listeners.add(listener);
383
- const stopWatch = watch(() => listener.topics().join(","), () => schedule());
384
- schedule();
385
- function stop() {
386
- stopWatch();
387
- if (listeners.delete(listener)) schedule();
388
- }
389
- if (getCurrentScope()) onScopeDispose(stop);
390
- return {
391
- connected: readonly(connected),
392
- stop
393
- };
394
- }
395
- //#endregion
396
- export { JobsStatusTray_default as i, subscribeRealtime as n, useRealtime as r, currentPresence as t };
397
-
398
- //# sourceMappingURL=useRealtime-BW02xy4Z.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useRealtime-BW02xy4Z.js","names":[],"sources":["../src/components/JobsStatusTray.vue","../src/components/JobsStatusTray.vue","../src/composables/useRealtime.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Compact, non-modal job panel under the top bar, driven by a job center the parent owns.\n *\n * The parent passes the center's `topBarJob`, `completedCount` and `handleTopBarClick`\n * to `AppTopBar`; its run slot opens this panel. `PluginWorkspaceView` does this for you\n * when its spec sets `jobs`.\n *\n * @example Recommended\n * ```vue\n * <AppTopBar :job=\"center.topBarJob.value\" :jobs-done=\"center.completedCount.value\"\n * @job-click=\"center.handleTopBarClick(openResult)\" />\n * <JobsStatusTray :center=\"center\" @select=\"openResult\" />\n * ```\n */\nimport { computed, nextTick, onBeforeUnmount, ref, watch, type Component } from 'vue'\nimport {\n CircleAlert,\n CircleCheck,\n CircleSlash,\n CircleX,\n Clock,\n LoaderCircle,\n RefreshCw,\n Square,\n Trash2,\n X,\n} from 'lucide-vue-next'\n\nimport { isActiveJobStatus, jobStatusLabel, normalizeJobPercent } from '../jobs'\nimport type { UseJobCenterReturn } from '../composables/useJobCenter'\nimport type { JobState, JobStatus } from '../types/jobs'\n\ninterface Props {\n /** Job center from `useJobCenter()`; the parent owns it and opens the panel through it. */\n center: UseJobCenterReturn\n title?: string\n teleportTo?: string | HTMLElement | false\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n title: 'Jobs',\n teleportTo: 'body',\n})\n\nconst emit = defineEmits<{\n select: [job: JobState]\n error: [message: string]\n}>()\n\ndefineSlots<{\n /** Extra per-row actions, rendered before the built-in Cancel / Delete. */\n 'job-actions'?: (props: { job: JobState }) => unknown\n /** Row title. Default: the job name. */\n 'job-title'?: (props: { job: JobState }) => unknown\n /** Detail line under the title. Default: the error, progress message or status. */\n 'job-detail'?: (props: { job: JobState }) => unknown\n}>()\n\nconst STATUS_ICONS: Record<JobStatus, Component> = {\n running: LoaderCircle,\n awaiting_input: CircleAlert,\n queued: Clock,\n completed: CircleCheck,\n failed: CircleX,\n cancelled: CircleSlash,\n}\n\nconst panelRef = ref<HTMLElement | null>(null)\nconst teleportTarget = computed(() => props.teleportTo || 'body')\nconst tray = props.center\nconst titleId = `${tray.panelId}-title`\n// A job waiting for input is the only one that needs a person: pin it first.\nconst activeRows = computed(() => [\n ...tray.activeJobs.value.filter(job => job.status === 'awaiting_input'),\n ...tray.activeJobs.value.filter(job => job.status !== 'awaiting_input'),\n])\nconst sections = computed(() => [\n { key: 'active', title: 'Active', jobs: activeRows.value },\n { key: 'finished', title: 'Finished', jobs: tray.historyJobs.value },\n].filter(section => section.jobs.length > 0))\nconst headCount = computed(() => {\n const parts = [\n [tray.activeJobs.value.length, 'active'],\n [tray.completedCount.value, 'done'],\n [tray.historyJobs.value.filter(job => job.status === 'failed').length, 'failed'],\n ].filter(([n]) => n).map(([n, word]) => `${n} ${word}`)\n return parts.length ? parts.join(' · ') : 'No jobs'\n})\nconst canClear = computed(() => (\n tray.deletableJobs.value.length > 0\n && tray.canClearFinished.value\n))\n\n// Escape returns focus to the element that opened the panel (the top-bar chip).\n// The panel is non-modal, so Escape is heard on the document while it is open.\nlet opener: HTMLElement | null = null\nfunction onDocumentKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') closeTray()\n}\nwatch(tray.isOpen, async (open) => {\n if (!open) {\n document.removeEventListener('keydown', onDocumentKeydown)\n return\n }\n opener = document.activeElement instanceof HTMLElement ? document.activeElement : null\n document.addEventListener('keydown', onDocumentKeydown)\n await nextTick()\n panelRef.value?.focus()\n}, { flush: 'sync' })\nonBeforeUnmount(() => document.removeEventListener('keydown', onDocumentKeydown))\n\nfunction closeTray(): void {\n tray.close()\n // Safari leaves a clicked button unfocused (opener = body), and the chip node is\n // replaced when it switches between job chip and idle button: fall back to the\n // top-bar jobs control.\n const target = opener?.isConnected && opener !== document.body\n ? opener\n : document.querySelector<HTMLElement>('.mint-topbar__job')\n target?.focus()\n}\n\nfunction progressText(job: JobState): string {\n if (job.error) return job.error\n if (job.status === 'awaiting_input') {\n return job.progress.message || 'Waiting for external input'\n }\n if (job.status === 'queued' && job.queue_position > 0) {\n return `Queue position ${job.queue_position}`\n }\n return job.progress.message || job.progress.stage || jobStatusLabel(job.status)\n}\n\nfunction formatTime(value: string | null): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime())\n ? ''\n : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })\n}\n\nfunction jobPercent(job: JobState): number {\n return Math.round(normalizeJobPercent(job.progress.percent))\n}\n\n/** Right-hand value: the percent while a job runs, else when it ended or started. */\nfunction jobValue(job: JobState): string {\n if (job.status === 'running') return `${jobPercent(job)}%`\n return formatTime(job.completed_at || job.started_at || job.created_at)\n}\n\nasync function cancelJob(job: JobState): Promise<void> {\n const succeeded = await tray.cancelJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function deleteJob(job: JobState): Promise<void> {\n const succeeded = await tray.deleteJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function clearFinished(): Promise<void> {\n const succeeded = await tray.clearFinished()\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n</script>\n\n<template>\n <Teleport :to=\"teleportTarget\" :disabled=\"teleportTo === false\">\n <div class=\"mint-jobs-tray\">\n <!-- Clicks outside close the panel; the page is not dimmed. -->\n <div\n v-if=\"tray.isOpen.value\"\n class=\"mint-jobs-tray__scrim\"\n aria-hidden=\"true\"\n @click=\"tray.close()\"\n />\n\n <Transition name=\"mint-jobs-tray-fade\">\n <section\n v-if=\"tray.isOpen.value\"\n :id=\"tray.panelId\"\n ref=\"panelRef\"\n class=\"mint-jobs-tray__panel\"\n :aria-labelledby=\"titleId\"\n tabindex=\"-1\"\n >\n <header class=\"mint-jobs-tray__head\">\n <h2 :id=\"titleId\" class=\"mint-jobs-tray__title\">{{ title }}</h2>\n <span class=\"mint-jobs-tray__head-count\">{{ headCount }}</span>\n <button\n v-if=\"tray.canRefresh.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Refresh jobs\"\n title=\"Refresh jobs\"\n :disabled=\"tray.isLoading.value\"\n @click=\"tray.refresh\"\n >\n <RefreshCw :size=\"14\" aria-hidden=\"true\" />\n </button>\n <button\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Close jobs\"\n title=\"Close\"\n @click=\"closeTray\"\n >\n <X :size=\"14\" aria-hidden=\"true\" />\n </button>\n </header>\n\n <p v-if=\"tray.error.value\" class=\"mint-jobs-tray__error\" role=\"alert\">\n {{ tray.error.value }}\n </p>\n\n <div class=\"mint-jobs-tray__body\">\n <p v-if=\"sections.length === 0\" class=\"mint-jobs-tray__empty\">No jobs yet</p>\n\n <template v-for=\"section in sections\" :key=\"section.key\">\n <div class=\"mint-jobs-tray__section\" :data-section=\"section.key\">\n {{ section.title }}\n <span class=\"mint-jobs-tray__section-count\">· {{ section.jobs.length }}</span>\n <button\n v-if=\"section.key === 'finished' && canClear\"\n type=\"button\"\n class=\"mint-jobs-tray__section-action\"\n @click=\"clearFinished\"\n >\n Clear\n </button>\n </div>\n\n <div\n v-for=\"job in section.jobs\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__row\"\n :class=\"{ 'mint-jobs-tray__row--awaiting-input': job.status === 'awaiting_input' }\"\n :data-status=\"job.status\"\n >\n <component\n :is=\"STATUS_ICONS[job.status]\"\n :size=\"15\"\n :class=\"['mint-jobs-tray__status', `mint-jobs-tray__status--${job.status.replace('_', '-')}`]\"\n role=\"img\"\n :aria-label=\"jobStatusLabel(job.status)\"\n />\n <button type=\"button\" class=\"mint-jobs-tray__row-title\" @click=\"emit('select', job)\">\n <slot name=\"job-title\" :job=\"job\">{{ job.name }}</slot>\n </button>\n <span class=\"mint-jobs-tray__tools\">\n <span class=\"mint-jobs-tray__value\">{{ jobValue(job) }}</span>\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"isActiveJobStatus(job.status) && job.can_cancel && tray.canCancel.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n :aria-label=\"`Cancel ${job.name}`\"\n title=\"Cancel\"\n :disabled=\"tray.isPending(job)\"\n @click=\"cancelJob(job)\"\n >\n <Square :size=\"12\" aria-hidden=\"true\" />\n </button>\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n :aria-label=\"`Delete ${job.name}`\"\n title=\"Delete\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <Trash2 :size=\"14\" aria-hidden=\"true\" />\n </button>\n </span>\n <div\n v-if=\"job.status === 'running'\"\n class=\"mint-jobs-tray__progress\"\n role=\"progressbar\"\n :aria-label=\"`${job.name} progress`\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n :aria-valuenow=\"jobPercent(job)\"\n >\n <span :style=\"{ width: `${jobPercent(job)}%` }\" />\n </div>\n <span\n class=\"mint-jobs-tray__detail\"\n :class=\"{ 'mint-jobs-tray__detail--error': job.status === 'failed' }\"\n >\n <slot name=\"job-detail\" :job=\"job\">{{ progressText(job) }}</slot>\n </span>\n </div>\n </template>\n </div>\n </section>\n </Transition>\n </div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Compact, non-modal job panel under the top bar, driven by a job center the parent owns.\n *\n * The parent passes the center's `topBarJob`, `completedCount` and `handleTopBarClick`\n * to `AppTopBar`; its run slot opens this panel. `PluginWorkspaceView` does this for you\n * when its spec sets `jobs`.\n *\n * @example Recommended\n * ```vue\n * <AppTopBar :job=\"center.topBarJob.value\" :jobs-done=\"center.completedCount.value\"\n * @job-click=\"center.handleTopBarClick(openResult)\" />\n * <JobsStatusTray :center=\"center\" @select=\"openResult\" />\n * ```\n */\nimport { computed, nextTick, onBeforeUnmount, ref, watch, type Component } from 'vue'\nimport {\n CircleAlert,\n CircleCheck,\n CircleSlash,\n CircleX,\n Clock,\n LoaderCircle,\n RefreshCw,\n Square,\n Trash2,\n X,\n} from 'lucide-vue-next'\n\nimport { isActiveJobStatus, jobStatusLabel, normalizeJobPercent } from '../jobs'\nimport type { UseJobCenterReturn } from '../composables/useJobCenter'\nimport type { JobState, JobStatus } from '../types/jobs'\n\ninterface Props {\n /** Job center from `useJobCenter()`; the parent owns it and opens the panel through it. */\n center: UseJobCenterReturn\n title?: string\n teleportTo?: string | HTMLElement | false\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n title: 'Jobs',\n teleportTo: 'body',\n})\n\nconst emit = defineEmits<{\n select: [job: JobState]\n error: [message: string]\n}>()\n\ndefineSlots<{\n /** Extra per-row actions, rendered before the built-in Cancel / Delete. */\n 'job-actions'?: (props: { job: JobState }) => unknown\n /** Row title. Default: the job name. */\n 'job-title'?: (props: { job: JobState }) => unknown\n /** Detail line under the title. Default: the error, progress message or status. */\n 'job-detail'?: (props: { job: JobState }) => unknown\n}>()\n\nconst STATUS_ICONS: Record<JobStatus, Component> = {\n running: LoaderCircle,\n awaiting_input: CircleAlert,\n queued: Clock,\n completed: CircleCheck,\n failed: CircleX,\n cancelled: CircleSlash,\n}\n\nconst panelRef = ref<HTMLElement | null>(null)\nconst teleportTarget = computed(() => props.teleportTo || 'body')\nconst tray = props.center\nconst titleId = `${tray.panelId}-title`\n// A job waiting for input is the only one that needs a person: pin it first.\nconst activeRows = computed(() => [\n ...tray.activeJobs.value.filter(job => job.status === 'awaiting_input'),\n ...tray.activeJobs.value.filter(job => job.status !== 'awaiting_input'),\n])\nconst sections = computed(() => [\n { key: 'active', title: 'Active', jobs: activeRows.value },\n { key: 'finished', title: 'Finished', jobs: tray.historyJobs.value },\n].filter(section => section.jobs.length > 0))\nconst headCount = computed(() => {\n const parts = [\n [tray.activeJobs.value.length, 'active'],\n [tray.completedCount.value, 'done'],\n [tray.historyJobs.value.filter(job => job.status === 'failed').length, 'failed'],\n ].filter(([n]) => n).map(([n, word]) => `${n} ${word}`)\n return parts.length ? parts.join(' · ') : 'No jobs'\n})\nconst canClear = computed(() => (\n tray.deletableJobs.value.length > 0\n && tray.canClearFinished.value\n))\n\n// Escape returns focus to the element that opened the panel (the top-bar chip).\n// The panel is non-modal, so Escape is heard on the document while it is open.\nlet opener: HTMLElement | null = null\nfunction onDocumentKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') closeTray()\n}\nwatch(tray.isOpen, async (open) => {\n if (!open) {\n document.removeEventListener('keydown', onDocumentKeydown)\n return\n }\n opener = document.activeElement instanceof HTMLElement ? document.activeElement : null\n document.addEventListener('keydown', onDocumentKeydown)\n await nextTick()\n panelRef.value?.focus()\n}, { flush: 'sync' })\nonBeforeUnmount(() => document.removeEventListener('keydown', onDocumentKeydown))\n\nfunction closeTray(): void {\n tray.close()\n // Safari leaves a clicked button unfocused (opener = body), and the chip node is\n // replaced when it switches between job chip and idle button: fall back to the\n // top-bar jobs control.\n const target = opener?.isConnected && opener !== document.body\n ? opener\n : document.querySelector<HTMLElement>('.mint-topbar__job')\n target?.focus()\n}\n\nfunction progressText(job: JobState): string {\n if (job.error) return job.error\n if (job.status === 'awaiting_input') {\n return job.progress.message || 'Waiting for external input'\n }\n if (job.status === 'queued' && job.queue_position > 0) {\n return `Queue position ${job.queue_position}`\n }\n return job.progress.message || job.progress.stage || jobStatusLabel(job.status)\n}\n\nfunction formatTime(value: string | null): string {\n if (!value) return ''\n const date = new Date(value)\n return Number.isNaN(date.getTime())\n ? ''\n : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })\n}\n\nfunction jobPercent(job: JobState): number {\n return Math.round(normalizeJobPercent(job.progress.percent))\n}\n\n/** Right-hand value: the percent while a job runs, else when it ended or started. */\nfunction jobValue(job: JobState): string {\n if (job.status === 'running') return `${jobPercent(job)}%`\n return formatTime(job.completed_at || job.started_at || job.created_at)\n}\n\nasync function cancelJob(job: JobState): Promise<void> {\n const succeeded = await tray.cancelJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function deleteJob(job: JobState): Promise<void> {\n const succeeded = await tray.deleteJob(job)\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n\nasync function clearFinished(): Promise<void> {\n const succeeded = await tray.clearFinished()\n if (!succeeded && tray.error.value) emit('error', tray.error.value)\n}\n</script>\n\n<template>\n <Teleport :to=\"teleportTarget\" :disabled=\"teleportTo === false\">\n <div class=\"mint-jobs-tray\">\n <!-- Clicks outside close the panel; the page is not dimmed. -->\n <div\n v-if=\"tray.isOpen.value\"\n class=\"mint-jobs-tray__scrim\"\n aria-hidden=\"true\"\n @click=\"tray.close()\"\n />\n\n <Transition name=\"mint-jobs-tray-fade\">\n <section\n v-if=\"tray.isOpen.value\"\n :id=\"tray.panelId\"\n ref=\"panelRef\"\n class=\"mint-jobs-tray__panel\"\n :aria-labelledby=\"titleId\"\n tabindex=\"-1\"\n >\n <header class=\"mint-jobs-tray__head\">\n <h2 :id=\"titleId\" class=\"mint-jobs-tray__title\">{{ title }}</h2>\n <span class=\"mint-jobs-tray__head-count\">{{ headCount }}</span>\n <button\n v-if=\"tray.canRefresh.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Refresh jobs\"\n title=\"Refresh jobs\"\n :disabled=\"tray.isLoading.value\"\n @click=\"tray.refresh\"\n >\n <RefreshCw :size=\"14\" aria-hidden=\"true\" />\n </button>\n <button\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n aria-label=\"Close jobs\"\n title=\"Close\"\n @click=\"closeTray\"\n >\n <X :size=\"14\" aria-hidden=\"true\" />\n </button>\n </header>\n\n <p v-if=\"tray.error.value\" class=\"mint-jobs-tray__error\" role=\"alert\">\n {{ tray.error.value }}\n </p>\n\n <div class=\"mint-jobs-tray__body\">\n <p v-if=\"sections.length === 0\" class=\"mint-jobs-tray__empty\">No jobs yet</p>\n\n <template v-for=\"section in sections\" :key=\"section.key\">\n <div class=\"mint-jobs-tray__section\" :data-section=\"section.key\">\n {{ section.title }}\n <span class=\"mint-jobs-tray__section-count\">· {{ section.jobs.length }}</span>\n <button\n v-if=\"section.key === 'finished' && canClear\"\n type=\"button\"\n class=\"mint-jobs-tray__section-action\"\n @click=\"clearFinished\"\n >\n Clear\n </button>\n </div>\n\n <div\n v-for=\"job in section.jobs\"\n :key=\"job.job_id\"\n class=\"mint-jobs-tray__row\"\n :class=\"{ 'mint-jobs-tray__row--awaiting-input': job.status === 'awaiting_input' }\"\n :data-status=\"job.status\"\n >\n <component\n :is=\"STATUS_ICONS[job.status]\"\n :size=\"15\"\n :class=\"['mint-jobs-tray__status', `mint-jobs-tray__status--${job.status.replace('_', '-')}`]\"\n role=\"img\"\n :aria-label=\"jobStatusLabel(job.status)\"\n />\n <button type=\"button\" class=\"mint-jobs-tray__row-title\" @click=\"emit('select', job)\">\n <slot name=\"job-title\" :job=\"job\">{{ job.name }}</slot>\n </button>\n <span class=\"mint-jobs-tray__tools\">\n <span class=\"mint-jobs-tray__value\">{{ jobValue(job) }}</span>\n <slot name=\"job-actions\" :job=\"job\" />\n <button\n v-if=\"isActiveJobStatus(job.status) && job.can_cancel && tray.canCancel.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger\"\n :aria-label=\"`Cancel ${job.name}`\"\n title=\"Cancel\"\n :disabled=\"tray.isPending(job)\"\n @click=\"cancelJob(job)\"\n >\n <Square :size=\"12\" aria-hidden=\"true\" />\n </button>\n <button\n v-if=\"job.can_delete && tray.canDelete.value\"\n type=\"button\"\n class=\"mint-jobs-tray__icon-button\"\n :aria-label=\"`Delete ${job.name}`\"\n title=\"Delete\"\n :disabled=\"tray.isPending(job)\"\n @click=\"deleteJob(job)\"\n >\n <Trash2 :size=\"14\" aria-hidden=\"true\" />\n </button>\n </span>\n <div\n v-if=\"job.status === 'running'\"\n class=\"mint-jobs-tray__progress\"\n role=\"progressbar\"\n :aria-label=\"`${job.name} progress`\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n :aria-valuenow=\"jobPercent(job)\"\n >\n <span :style=\"{ width: `${jobPercent(job)}%` }\" />\n </div>\n <span\n class=\"mint-jobs-tray__detail\"\n :class=\"{ 'mint-jobs-tray__detail--error': job.status === 'failed' }\"\n >\n <slot name=\"job-detail\" :job=\"job\">{{ progressText(job) }}</slot>\n </span>\n </div>\n </template>\n </div>\n </section>\n </Transition>\n </div>\n </Teleport>\n</template>\n","import {\n getCurrentScope,\n onScopeDispose,\n readonly,\n ref,\n toValue,\n watch,\n type MaybeRefOrGetter,\n type Ref,\n} from 'vue'\n\n/**\n * One event of the platform's realtime stream (`GET /api/events`).\n *\n * Platform events carry no entity data: refetch the entity through REST.\n * `type` is `resync` (refetch everything you show), the audit action of a\n * committed write, such as `experiment.update` or `analysis.artifact_create`,\n * or the `\"<plugin>.<event>\"` name of a plugin event, which carries `payload`.\n * Only `usePresence` receives `presence.changed`.\n */\nexport interface RealtimeEvent {\n type: string\n id?: string\n /**\n * `experiment:{id}`, `project:{id}`, `experiments`, `user:{id}`, or a plugin's\n * `plugin:{name}` / `plugin:{name}:experiment:{id}`; absent on `resync`.\n */\n topic?: string\n /** A plugin event: the JSON payload the plugin published (at most 16 KiB). */\n payload?: unknown\n /** Changed fields of an update, when the write names them. */\n fields?: string[] | null\n actor_id?: string | null\n request_id?: string | null\n at?: string\n /** `presence.changed`: the topic's viewers (at most 20, `editing` first). */\n viewers?: { user_id: string, activity: string }[]\n /** `presence.changed`: the number of viewers, also those beyond `viewers`. */\n count?: number\n}\n\nexport type RealtimeHandler = (event: RealtimeEvent) => void\n\nexport interface UseRealtimeReturn {\n /** True while the shared stream is open. */\n connected: Readonly<Ref<boolean>>\n /** Unsubscribe; also runs when the calling component or effect scope is disposed. */\n stop: () => void\n}\n\nconst EVENTS_URL = '/api/events'\nconst RECONNECT_BASE_MS = 1_000\nconst RECONNECT_MAX_MS = 30_000\n/** Merges the subscriptions of components that mount together into one connection. */\nconst TOPIC_DEBOUNCE_MS = 50\nconst RESYNC: RealtimeEvent = { type: 'resync' }\n\ninterface Listener {\n topics: () => string[]\n handler: RealtimeHandler\n /** Receives `presence.changed`; a write listener would refetch on each viewer change. */\n presence: boolean\n}\n\nconst listeners = new Set<Listener>()\n/** The last `presence.changed` of each topic of the open stream; the server sends one per topic at open. */\nconst presence = new Map<string, RealtimeEvent>()\nconst connected = ref(false)\nlet source: EventSource | null = null\nlet sourceTopics = ''\nlet failures = 0\nlet syncTimer: ReturnType<typeof setTimeout> | null = null\nlet retryTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction dispatch(event: RealtimeEvent): void {\n const isPresence = event.type === 'presence.changed'\n if (isPresence && event.topic) presence.set(event.topic, event)\n for (const listener of [...listeners]) {\n if (isPresence && !listener.presence) continue\n if (event.type === 'resync' || (event.topic && listener.topics().includes(event.topic))) {\n listener.handler(event)\n }\n }\n}\n\nfunction close(): void {\n source?.close()\n source = null\n sourceTopics = ''\n presence.clear()\n connected.value = false\n}\n\nfunction open(topics: string): void {\n const stream = new EventSource(`${EVENTS_URL}?topics=${encodeURIComponent(topics)}`)\n source = stream\n sourceTopics = topics\n stream.onopen = () => {\n connected.value = true\n // The server subscribes only now: a write between a caller's REST load (or\n // the close of the previous stream) and this point sent no event, so every caller refetches.\n dispatch(RESYNC)\n failures = 0\n }\n stream.onmessage = (message) => {\n dispatch(JSON.parse(message.data) as RealtimeEvent)\n }\n stream.onerror = () => {\n // EventSource retries a dropped connection itself but gives up after an\n // HTTP error, so one backoff path handles both.\n if (source !== stream) return\n close()\n // A topic change during the wait does not shorten it: the retry reads the topics when it runs.\n if (syncTimer !== null) clearTimeout(syncTimer)\n syncTimer = null\n retryTimer = setTimeout(() => {\n retryTimer = null\n sync()\n }, Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** failures))\n failures += 1\n }\n}\n\nfunction sync(): void {\n syncTimer = null\n const topics = [...new Set([...listeners].flatMap(listener => listener.topics()))].sort().join(',')\n if (source && topics === sourceTopics) return\n close()\n if (topics) open(topics)\n}\n\nfunction schedule(): void {\n if (retryTimer !== null) return\n if (syncTimer !== null) clearTimeout(syncTimer)\n syncTimer = setTimeout(sync, TOPIC_DEBOUNCE_MS)\n}\n\n/**\n * Subscribe to topics of the platform's realtime stream.\n *\n * Every caller in a tab shares one `EventSource`, opened on the union of\n * their topics and reopened when that union changes. `handler` receives the\n * write events of the caller's own topics (not `presence.changed`, which goes\n * to `usePresence`) plus every `resync`: one comes from the\n * server when this tab fell behind, and one follows each open of the stream:\n * the first open, each reconnect (backoff from 1 s to 30 s) and each reopen for\n * a changed topic union. On `resync`, refetch. The union of all callers' topics\n * in a tab may hold at most 50 topics; the platform refuses a larger stream.\n *\n * The stream authenticates with the session cookie, so it works in the\n * platform and in plugin frontends served by it. It exists only when the\n * platform's `realtime` feature flag is on; check that flag first.\n *\n * A plugin topic reaches a caller whose role may use the plugin; an\n * experiment plugin topic also needs a caller who may view the experiment.\n *\n * @example\n * useRealtime(() => [`experiment:${id.value}`], () => refetch())\n * @example\n * useRealtime(['plugin:drp'], (event) => {\n * if (event.type === 'drp.run.progress') progress.value = event.payload as RunProgress\n * })\n */\nexport function useRealtime(\n topics: MaybeRefOrGetter<string[]>,\n handler: RealtimeHandler,\n): UseRealtimeReturn {\n return subscribeRealtime(topics, handler, false)\n}\n\n/** The last `presence.changed` of `topic` while the open stream views it (for `usePresence`). */\nexport function currentPresence(topic: string): RealtimeEvent | undefined {\n return presence.get(topic)\n}\n\n/** `useRealtime`, and with `withPresence` also the `presence.changed` events (for `usePresence`). */\nexport function subscribeRealtime(\n topics: MaybeRefOrGetter<string[]>,\n handler: RealtimeHandler,\n withPresence: boolean,\n): UseRealtimeReturn {\n const listener: Listener = { topics: () => toValue(topics), handler, presence: withPresence }\n listeners.add(listener)\n const stopWatch = watch(() => listener.topics().join(','), () => schedule())\n schedule()\n\n function stop(): void {\n stopWatch()\n if (listeners.delete(listener)) schedule()\n }\n\n if (getCurrentScope()) onScopeDispose(stop)\n return { connected: readonly(connected), stop }\n}\n"],"mappings":"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"}