oh-my-opencode-dashboard 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/App.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { computeWaitingDing } from "./ding-policy";
3
3
  import { playDing, unlockAudio } from "./sound";
4
+ import { computeStackedSegments } from "./timeseries-stacked";
4
5
 
5
6
  const APP_VERSION =
6
7
  typeof __APP_VERSION__ === "string" && __APP_VERSION__.trim().length > 0 ? __APP_VERSION__ : "0.0.0";
@@ -12,6 +13,7 @@ type BackgroundTask = {
12
13
  description: string;
13
14
  subline?: string;
14
15
  agent: string;
16
+ lastModel: string;
15
17
  status: "queued" | "running" | "done" | "error" | "cancelled" | string;
16
18
  toolCalls: number;
17
19
  lastTool: string;
@@ -47,6 +49,7 @@ type DashboardPayload = {
47
49
  mainSession: {
48
50
  agent: string;
49
51
  currentTool: string;
52
+ currentModel: string;
50
53
  lastUpdatedLabel: string;
51
54
  session: string;
52
55
  statusPill: string;
@@ -176,6 +179,7 @@ const FALLBACK_DATA: DashboardPayload = {
176
179
  mainSession: {
177
180
  agent: "sisyphus",
178
181
  currentTool: "dashboard_start",
182
+ currentModel: "anthropic/claude-opus-4-5",
179
183
  lastUpdatedLabel: "just now",
180
184
  session: "qa-session",
181
185
  statusPill: "busy",
@@ -193,6 +197,7 @@ const FALLBACK_DATA: DashboardPayload = {
193
197
  description: "Explore: find HTTP/SSE patterns",
194
198
  subline: "task-1",
195
199
  agent: "explore",
200
+ lastModel: "opencode/gpt-5-nano",
196
201
  status: "running",
197
202
  toolCalls: 3,
198
203
  lastTool: "grep",
@@ -271,6 +276,20 @@ async function safeFetchJson(url: string): Promise<unknown> {
271
276
  }
272
277
  }
273
278
 
279
+ function toNonEmptyString(value: unknown): string | null {
280
+ if (typeof value !== "string") return null;
281
+ const trimmed = value.trim();
282
+ return trimmed.length > 0 ? trimmed : null;
283
+ }
284
+
285
+ export function formatBackgroundTaskTimelineCell(status: unknown, timeline: unknown): string {
286
+ const s = typeof status === "string" ? status.trim().toLowerCase() : "";
287
+ if (s === "unknown") return "";
288
+ if (s === "queued") return "-";
289
+
290
+ return toNonEmptyString(timeline) ?? "-";
291
+ }
292
+
274
293
  function toDashboardPayload(json: unknown): DashboardPayload {
275
294
  if (!json || typeof json !== "object") {
276
295
  return { ...FALLBACK_DATA, raw: json };
@@ -282,6 +301,22 @@ function toDashboardPayload(json: unknown): DashboardPayload {
282
301
  const plan = (anyJson.planProgress ?? anyJson.plan_progress ?? {}) as Record<string, unknown>;
283
302
  const tasks = (anyJson.backgroundTasks ?? anyJson.background_tasks ?? []) as unknown;
284
303
 
304
+ function parsePlanSteps(stepsInput: unknown): Array<{ checked: boolean; text: string }> {
305
+ if (!Array.isArray(stepsInput)) return [];
306
+
307
+ return stepsInput
308
+ .map((step): { checked: boolean; text: string } | null => {
309
+ if (!step || typeof step !== "object") return null;
310
+
311
+ const stepObj = step as Record<string, unknown>;
312
+ const checked = typeof stepObj.checked === "boolean" ? stepObj.checked : false;
313
+ const text = typeof stepObj.text === "string" ? stepObj.text : "";
314
+
315
+ return text.trim().length > 0 ? { checked, text } : null;
316
+ })
317
+ .filter((step): step is { checked: boolean; text: string } => step !== null);
318
+ }
319
+
285
320
  const backgroundTasks: BackgroundTask[] = Array.isArray(tasks)
286
321
  ? tasks.map((t, idx) => {
287
322
  const rec = (t ?? {}) as Record<string, unknown>;
@@ -295,6 +330,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
295
330
  ? rec.taskId
296
331
  : undefined,
297
332
  agent: String(rec.agent ?? rec.worker ?? "unknown"),
333
+ lastModel: toNonEmptyString(rec.lastModel ?? rec.last_model) ?? "-",
298
334
  status: String(rec.status ?? "queued"),
299
335
  toolCalls: Number(rec.toolCalls ?? rec.tool_calls ?? 0) || 0,
300
336
  lastTool: String(rec.lastTool ?? rec.last_tool ?? "-") || "-",
@@ -305,6 +341,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
305
341
 
306
342
  const completed = Number(plan.completed ?? plan.done ?? 0) || 0;
307
343
  const total = Number(plan.total ?? plan.count ?? 0) || 0;
344
+ const steps = parsePlanSteps(plan.steps);
308
345
 
309
346
  const timeSeries = normalizeTimeSeries(anyJson.timeSeries, Date.now());
310
347
 
@@ -312,6 +349,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
312
349
  mainSession: {
313
350
  agent: String(main.agent ?? FALLBACK_DATA.mainSession.agent),
314
351
  currentTool: String(main.currentTool ?? main.current_tool ?? FALLBACK_DATA.mainSession.currentTool),
352
+ currentModel: toNonEmptyString(main.currentModel ?? main.current_model) ?? "-",
315
353
  lastUpdatedLabel: String(main.lastUpdatedLabel ?? main.last_updated ?? "just now"),
316
354
  session: String(main.session ?? main.session_id ?? FALLBACK_DATA.mainSession.session),
317
355
  statusPill: String(main.statusPill ?? main.status ?? FALLBACK_DATA.mainSession.statusPill),
@@ -322,6 +360,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
322
360
  total,
323
361
  path: String(plan.path ?? FALLBACK_DATA.planProgress.path),
324
362
  statusPill: String(plan.statusPill ?? plan.status ?? FALLBACK_DATA.planProgress.statusPill),
363
+ steps,
325
364
  },
326
365
  backgroundTasks,
327
366
  timeSeries,
@@ -329,6 +368,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
329
368
  };
330
369
  }
331
370
 
371
+ export { toDashboardPayload };
332
372
  export default function App() {
333
373
  const [connected, setConnected] = React.useState(false);
334
374
  const [data, setData] = React.useState<DashboardPayload>(FALLBACK_DATA);
@@ -394,15 +434,15 @@ export default function App() {
394
434
  }
395
435
  }
396
436
 
397
- function isWaitingForUser(payload: DashboardPayload): boolean {
437
+ const isWaitingForUser = React.useCallback((payload: DashboardPayload): boolean => {
398
438
  const status = payload.mainSession.statusPill.toLowerCase();
399
439
  const hasSession = payload.mainSession.session !== "(no session)" && payload.mainSession.session !== "";
400
440
  const idle = status.includes("idle");
401
441
  const noTool = payload.mainSession.currentTool === "-" || payload.mainSession.currentTool === "";
402
442
  return hasSession && idle && noTool;
403
- }
443
+ }, []);
404
444
 
405
- function maybePlayDings(prev: DashboardPayload | null, next: DashboardPayload) {
445
+ const maybePlayDings = React.useCallback((prev: DashboardPayload | null, next: DashboardPayload) => {
406
446
  if (!soundEnabledRef.current) return;
407
447
  if (!hadSuccessRef.current) return;
408
448
 
@@ -453,7 +493,7 @@ export default function App() {
453
493
  lastLeftWaitingAtRef.current = waitingDecision.next.lastLeftWaitingAtMs;
454
494
  prevPlanCompletedRef.current = completed;
455
495
  prevPlanTotalRef.current = total;
456
- }
496
+ }, [isWaitingForUser]);
457
497
 
458
498
  const planPercent = React.useMemo(() => {
459
499
  if (!data.planProgress.total) return 0;
@@ -469,23 +509,23 @@ export default function App() {
469
509
 
470
510
  async function tick() {
471
511
  let nextConnected = false;
472
- try {
473
- const json = await safeFetchJson("/api/dashboard");
474
- if (!alive) return;
475
- nextConnected = true;
476
- hadSuccessRef.current = true;
477
- setConnected(true);
478
- setErrorHint(null);
479
- const next = toDashboardPayload(json);
480
- setData((prev) => {
481
- maybePlayDings(prev, next);
482
- return next;
483
- });
484
- setLastUpdate(Date.now());
485
- } catch (err) {
486
- if (!alive) return;
487
- nextConnected = false;
488
- setConnected(false);
512
+ try {
513
+ const json = await safeFetchJson("/api/dashboard");
514
+ if (!alive) return;
515
+ nextConnected = true;
516
+ hadSuccessRef.current = true;
517
+ setConnected(true);
518
+ setErrorHint(null);
519
+ const next = toDashboardPayload(json);
520
+ setData((prev) => {
521
+ maybePlayDings(prev, next);
522
+ return next;
523
+ });
524
+ setLastUpdate(Date.now());
525
+ } catch (err) {
526
+ if (!alive) return;
527
+ nextConnected = false;
528
+ setConnected(false);
489
529
  const msg = err instanceof Error ? err.message : "disconnected";
490
530
  setErrorHint(msg);
491
531
  setData((prev) => {
@@ -512,7 +552,7 @@ export default function App() {
512
552
  alive = false;
513
553
  if (timerRef.current) window.clearTimeout(timerRef.current);
514
554
  };
515
- }, []);
555
+ }, [maybePlayDings]);
516
556
 
517
557
  async function onCopyRawJson() {
518
558
  setCopyState("idle");
@@ -544,6 +584,7 @@ export default function App() {
544
584
  const bucketMs = Math.max(1, data.timeSeries.bucketMs);
545
585
  const viewBox = `0 0 ${buckets} 28`;
546
586
  const minuteStep = Math.max(1, Math.round(60_000 / bucketMs));
587
+ const bucketStartMs = data.timeSeries.anchorMs - (buckets - 1) * bucketMs;
547
588
 
548
589
  const overallValues = timeSeriesById.get("overall-main")?.values ?? [];
549
590
 
@@ -611,24 +652,11 @@ export default function App() {
611
652
  {(
612
653
  [
613
654
  {
614
- label: "Sisyphus",
615
- tone: "teal" as const,
616
- overlayId: "agent:sisyphus" as const,
617
- baseline: false,
618
- },
619
- {
620
- label: "Prometheus",
621
- tone: "red" as const,
622
- overlayId: "agent:prometheus" as const,
623
- baseline: false,
624
- },
625
- {
626
- label: "Atlas",
627
- tone: "green" as const,
628
- overlayId: "agent:atlas" as const,
629
- baseline: false,
655
+ kind: "main-agents" as const,
656
+ label: "Main agents" as const,
630
657
  },
631
658
  {
659
+ kind: "single" as const,
632
660
  label: "background tasks (total)",
633
661
  tone: "muted" as const,
634
662
  overlayId: "background-total" as const,
@@ -644,6 +672,82 @@ export default function App() {
644
672
  const barW = 0.85;
645
673
  const barInset = (1 - barW) / 2;
646
674
 
675
+ if (row.kind === "main-agents") {
676
+ const sisyphusValues = timeSeriesById.get("agent:sisyphus")?.values ?? [];
677
+ const prometheusValues = timeSeriesById.get("agent:prometheus")?.values ?? [];
678
+ const atlasValues = timeSeriesById.get("agent:atlas")?.values ?? [];
679
+
680
+ let sumMax = 0;
681
+ for (let i = 0; i < buckets; i++) {
682
+ const rawSis = sisyphusValues[i];
683
+ const rawPro = prometheusValues[i];
684
+ const rawAtl = atlasValues[i];
685
+ const sis = typeof rawSis === "number" && Number.isFinite(rawSis) ? Math.max(0, rawSis) : 0;
686
+ const pro = typeof rawPro === "number" && Number.isFinite(rawPro) ? Math.max(0, rawPro) : 0;
687
+ const atl = typeof rawAtl === "number" && Number.isFinite(rawAtl) ? Math.max(0, rawAtl) : 0;
688
+ const s = sis + pro + atl;
689
+ if (s > sumMax) sumMax = s;
690
+ }
691
+
692
+ const scaleMax = Math.max(1, sumMax || 1);
693
+
694
+ return (
695
+ <div key="main-agents" className="timeSeriesRow">
696
+ <div className="timeSeriesRowLabel">{row.label}</div>
697
+ <div className="timeSeriesSvgWrap">
698
+ <svg className="timeSeriesSvg" viewBox={viewBox} preserveAspectRatio="none" aria-hidden="true">
699
+ {Array.from({ length: Math.floor(buckets / minuteStep) + 1 }, (_, idx) => {
700
+ const x = idx * minuteStep;
701
+ if (x < 0 || x > buckets) return null;
702
+ return (
703
+ <line
704
+ key={`g-${bucketStartMs + x * bucketMs}`}
705
+ className="timeSeriesGridline"
706
+ x1={x}
707
+ x2={x}
708
+ y1={0}
709
+ y2={H}
710
+ />
711
+ );
712
+ })}
713
+
714
+ {Array.from({ length: buckets }, (_, i) => {
715
+ const bucketMsAt = bucketStartMs + i * bucketMs;
716
+ const barX = i + barInset;
717
+ const rawSis = sisyphusValues[i];
718
+ const rawPro = prometheusValues[i];
719
+ const rawAtl = atlasValues[i];
720
+ const sis = typeof rawSis === "number" && Number.isFinite(rawSis) ? Math.max(0, rawSis) : 0;
721
+ const pro = typeof rawPro === "number" && Number.isFinite(rawPro) ? Math.max(0, rawPro) : 0;
722
+ const atl = typeof rawAtl === "number" && Number.isFinite(rawAtl) ? Math.max(0, rawAtl) : 0;
723
+ const segments = computeStackedSegments(
724
+ {
725
+ sisyphus: sis,
726
+ prometheus: pro,
727
+ atlas: atl,
728
+ },
729
+ scaleMax,
730
+ chartHeight
731
+ );
732
+
733
+ if (segments.length === 0) return null;
734
+ return segments.map((seg) => (
735
+ <rect
736
+ key={`main-agents-${bucketMsAt}-${seg.tone}`}
737
+ className={`timeSeriesBar timeSeriesBar--${seg.tone}`}
738
+ x={barX}
739
+ y={padTop + seg.y}
740
+ width={barW}
741
+ height={seg.height}
742
+ />
743
+ ));
744
+ })}
745
+ </svg>
746
+ </div>
747
+ </div>
748
+ );
749
+ }
750
+
647
751
  const overlayValues = timeSeriesById.get(row.overlayId)?.values ?? [];
648
752
  const baselineMax = row.baseline ? maxCount(overallValues) : 0;
649
753
  const overlayMax = maxCount(overlayValues);
@@ -659,7 +763,7 @@ export default function App() {
659
763
  if (x < 0 || x > buckets) return null;
660
764
  return (
661
765
  <line
662
- key={`g-${idx}`}
766
+ key={`g-${bucketStartMs + x * bucketMs}`}
663
767
  className="timeSeriesGridline"
664
768
  x1={x}
665
769
  x2={x}
@@ -674,9 +778,10 @@ export default function App() {
674
778
  const h = barHeight(v ?? 0, scaleMax, chartHeight);
675
779
  if (!h) return null;
676
780
  const barX = i + barInset;
781
+ const bucketMsAt = bucketStartMs + i * bucketMs;
677
782
  return (
678
783
  <rect
679
- key={`b-${i}`}
784
+ key={`b-${bucketMsAt}`}
680
785
  className="timeSeriesBarBaseline"
681
786
  x={barX}
682
787
  y={baselineY - h}
@@ -691,9 +796,10 @@ export default function App() {
691
796
  const h = barHeight(v ?? 0, scaleMax, chartHeight);
692
797
  if (!h) return null;
693
798
  const barX = i + barInset;
799
+ const bucketMsAt = bucketStartMs + i * bucketMs;
694
800
  return (
695
801
  <rect
696
- key={`o-${i}`}
802
+ key={`${row.overlayId}-${bucketMsAt}`}
697
803
  className="timeSeriesBar"
698
804
  x={barX}
699
805
  y={baselineY - h}
@@ -737,6 +843,10 @@ export default function App() {
737
843
  <div className="kvKey">CURRENT TOOL</div>
738
844
  <div className="kvVal mono">{data.mainSession.currentTool}</div>
739
845
  </div>
846
+ <div className="kvRow">
847
+ <div className="kvKey">CURRENT MODEL</div>
848
+ <div className="kvVal mono">{data.mainSession.currentModel}</div>
849
+ </div>
740
850
  <div className="kvRow">
741
851
  <div className="kvKey">LAST UPDATED</div>
742
852
  <div className="kvVal">{data.mainSession.lastUpdatedLabel}</div>
@@ -813,6 +923,7 @@ export default function App() {
813
923
  <tr>
814
924
  <th>DESCRIPTION</th>
815
925
  <th>AGENT</th>
926
+ <th>LAST MODEL</th>
816
927
  <th>STATUS</th>
817
928
  <th>TOOL CALLS</th>
818
929
  <th>LAST TOOL</th>
@@ -827,12 +938,13 @@ export default function App() {
827
938
  {t.subline ? <div className="taskSub mono">{t.subline}</div> : null}
828
939
  </td>
829
940
  <td className="mono">{t.agent}</td>
941
+ <td className="mono">{t.lastModel}</td>
830
942
  <td>
831
943
  <span className={`pill pill-${statusTone(t.status)}`}>{t.status}</span>
832
944
  </td>
833
945
  <td className="mono">{t.toolCalls}</td>
834
946
  <td className="mono">{t.lastTool}</td>
835
- <td className="mono muted">{t.status.toLowerCase() === "queued" ? "-" : t.timeline || "-"}</td>
947
+ <td className="mono muted">{formatBackgroundTaskTimelineCell(t.status, t.timeline)}</td>
836
948
  </tr>
837
949
  ))}
838
950
  </tbody>
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { toDashboardPayload } from "./App"
3
+
4
+ describe('toDashboardPayload', () => {
5
+ it('should preserve planProgress.steps from server JSON', () => {
6
+ // #given: server JSON with planProgress.steps
7
+ const serverJson = {
8
+ mainSession: {
9
+ agent: "sisyphus",
10
+ currentTool: "dashboard_start",
11
+ currentModel: "anthropic/claude-opus-4-5",
12
+ lastUpdatedLabel: "just now",
13
+ session: "test-session",
14
+ statusPill: "busy",
15
+ },
16
+ planProgress: {
17
+ name: "test-plan",
18
+ completed: 2,
19
+ total: 4,
20
+ path: "/tmp/test-plan.md",
21
+ statusPill: "in progress",
22
+ steps: [
23
+ { checked: true, text: "First completed task" },
24
+ { checked: true, text: "Second completed task" },
25
+ { checked: false, text: "Third pending task" },
26
+ { checked: false, text: "Fourth pending task" },
27
+ ],
28
+ },
29
+ backgroundTasks: [],
30
+ timeSeries: {
31
+ windowMs: 300000,
32
+ buckets: 150,
33
+ bucketMs: 2000,
34
+ anchorMs: 1640995200000,
35
+ serverNowMs: 1640995500000,
36
+ series: [
37
+ {
38
+ id: "overall-main",
39
+ label: "Overall",
40
+ tone: "muted",
41
+ values: new Array(150).fill(0),
42
+ },
43
+ ],
44
+ },
45
+ }
46
+
47
+ // #when: converting to dashboard payload
48
+ const payload = toDashboardPayload(serverJson)
49
+
50
+ // #then: planProgress.steps should be preserved with correct structure
51
+ expect(payload.planProgress.steps).toBeDefined()
52
+ expect(payload.planProgress.steps).toEqual([
53
+ { checked: true, text: "First completed task" },
54
+ { checked: true, text: "Second completed task" },
55
+ { checked: false, text: "Third pending task" },
56
+ { checked: false, text: "Fourth pending task" },
57
+ ])
58
+ })
59
+
60
+ it('should handle missing or malformed planProgress.steps defensively', () => {
61
+ // #given: server JSON with malformed planProgress.steps
62
+ const serverJson = {
63
+ mainSession: {
64
+ agent: "sisyphus",
65
+ currentTool: "dashboard_start",
66
+ currentModel: "anthropic/claude-opus-4-5",
67
+ lastUpdatedLabel: "just now",
68
+ session: "test-session",
69
+ statusPill: "busy",
70
+ },
71
+ planProgress: {
72
+ name: "test-plan",
73
+ completed: 0,
74
+ total: 0,
75
+ path: "/tmp/test-plan.md",
76
+ statusPill: "not started",
77
+ steps: [
78
+ { checked: true, text: "Valid step" },
79
+ { checked: false }, // missing text
80
+ { text: "Missing checked" }, // missing checked
81
+ "invalid string", // wrong type
82
+ null, // null value
83
+ { checked: "not-boolean", text: "Invalid checked type" }, // wrong checked type
84
+ ],
85
+ },
86
+ backgroundTasks: [],
87
+ timeSeries: {
88
+ windowMs: 300000,
89
+ buckets: 150,
90
+ bucketMs: 2000,
91
+ anchorMs: 1640995200000,
92
+ serverNowMs: 1640995500000,
93
+ series: [
94
+ {
95
+ id: "overall-main",
96
+ label: "Overall",
97
+ tone: "muted",
98
+ values: new Array(150).fill(0),
99
+ },
100
+ ],
101
+ },
102
+ }
103
+
104
+ // #when: converting to dashboard payload
105
+ const payload = toDashboardPayload(serverJson)
106
+
107
+ // #then: should only include valid steps, ignore malformed ones
108
+ expect(payload.planProgress.steps).toEqual([
109
+ { checked: true, text: "Valid step" },
110
+ { checked: false, text: "Missing checked" }, // default checked to false
111
+ { checked: false, text: "Invalid checked type" }, // default checked to false for invalid boolean
112
+ ])
113
+ })
114
+
115
+ it('should handle non-array planProgress.steps', () => {
116
+ // #given: server JSON with non-array planProgress.steps
117
+ const serverJson = {
118
+ mainSession: {
119
+ agent: "sisyphus",
120
+ currentTool: "dashboard_start",
121
+ currentModel: "anthropic/claude-opus-4-5",
122
+ lastUpdatedLabel: "just now",
123
+ session: "test-session",
124
+ statusPill: "busy",
125
+ },
126
+ planProgress: {
127
+ name: "test-plan",
128
+ completed: 0,
129
+ total: 0,
130
+ path: "/tmp/test-plan.md",
131
+ statusPill: "not started",
132
+ steps: "not an array",
133
+ },
134
+ backgroundTasks: [],
135
+ timeSeries: {
136
+ windowMs: 300000,
137
+ buckets: 150,
138
+ bucketMs: 2000,
139
+ anchorMs: 1640995200000,
140
+ serverNowMs: 1640995500000,
141
+ series: [
142
+ {
143
+ id: "overall-main",
144
+ label: "Overall",
145
+ tone: "muted",
146
+ values: new Array(150).fill(0),
147
+ },
148
+ ],
149
+ },
150
+ }
151
+
152
+ // #when: converting to dashboard payload
153
+ const payload = toDashboardPayload(serverJson)
154
+
155
+ // #then: should handle non-array steps gracefully
156
+ expect(payload.planProgress.steps).toEqual([])
157
+ })
158
+ })
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { formatBackgroundTaskTimelineCell } from "./App"
3
+
4
+ describe('formatBackgroundTaskTimelineCell', () => {
5
+ it('should render "-" for queued regardless of timeline', () => {
6
+ // #given: queued status
7
+ const status = "queued"
8
+
9
+ // #when/#then
10
+ expect(formatBackgroundTaskTimelineCell(status, "")).toBe("-")
11
+ expect(formatBackgroundTaskTimelineCell(status, "2026-01-01T00:00:00Z: 2m")).toBe("-")
12
+ })
13
+
14
+ it('should render blank for unknown regardless of timeline', () => {
15
+ // #given: unknown status
16
+ const status = "unknown"
17
+
18
+ // #when/#then
19
+ expect(formatBackgroundTaskTimelineCell(status, "")).toBe("")
20
+ expect(formatBackgroundTaskTimelineCell(status, "2026-01-01T00:00:00Z: 2m")).toBe("")
21
+ })
22
+
23
+ it('should render timeline when present, otherwise "-" for other statuses', () => {
24
+ // #given: a non-queued, non-unknown status
25
+ const status = "running"
26
+
27
+ // #when/#then
28
+ expect(formatBackgroundTaskTimelineCell(status, "2026-01-01T00:00:00Z: 2m")).toBe("2026-01-01T00:00:00Z: 2m")
29
+ expect(formatBackgroundTaskTimelineCell(status, "")).toBe("-")
30
+ expect(formatBackgroundTaskTimelineCell(status, " ")).toBe("-")
31
+ })
32
+ })