oh-my-opencode-dashboard 0.0.3 → 0.0.5

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,12 +13,31 @@ type BackgroundTask = {
12
13
  description: string;
13
14
  subline?: string;
14
15
  agent: string;
16
+ lastModel: string;
17
+ sessionId?: string | null;
15
18
  status: "queued" | "running" | "done" | "error" | "cancelled" | string;
16
19
  toolCalls: number;
17
20
  lastTool: string;
18
21
  timeline: string;
19
22
  };
20
23
 
24
+ type ToolCallSummary = {
25
+ sessionId?: string;
26
+ messageId: string;
27
+ callId: string;
28
+ tool: string;
29
+ status: string;
30
+ createdAtMs: number | null;
31
+ };
32
+
33
+ type ToolCallsResponse = {
34
+ ok: boolean;
35
+ sessionId: string;
36
+ toolCalls: ToolCallSummary[];
37
+ caps?: { maxMessages: number; maxToolCalls: number };
38
+ truncated?: boolean;
39
+ };
40
+
21
41
  type TimeSeriesTone = "muted" | "teal" | "red" | "green";
22
42
 
23
43
  type TimeSeriesSeriesId =
@@ -47,6 +67,7 @@ type DashboardPayload = {
47
67
  mainSession: {
48
68
  agent: string;
49
69
  currentTool: string;
70
+ currentModel: string;
50
71
  lastUpdatedLabel: string;
51
72
  session: string;
52
73
  statusPill: string;
@@ -176,6 +197,7 @@ const FALLBACK_DATA: DashboardPayload = {
176
197
  mainSession: {
177
198
  agent: "sisyphus",
178
199
  currentTool: "dashboard_start",
200
+ currentModel: "anthropic/claude-opus-4-5",
179
201
  lastUpdatedLabel: "just now",
180
202
  session: "qa-session",
181
203
  statusPill: "busy",
@@ -193,6 +215,8 @@ const FALLBACK_DATA: DashboardPayload = {
193
215
  description: "Explore: find HTTP/SSE patterns",
194
216
  subline: "task-1",
195
217
  agent: "explore",
218
+ lastModel: "opencode/gpt-5-nano",
219
+ sessionId: null,
196
220
  status: "running",
197
221
  toolCalls: 3,
198
222
  lastTool: "grep",
@@ -271,6 +295,120 @@ async function safeFetchJson(url: string): Promise<unknown> {
271
295
  }
272
296
  }
273
297
 
298
+ function toNonEmptyString(value: unknown): string | null {
299
+ if (typeof value !== "string") return null;
300
+ const trimmed = value.trim();
301
+ return trimmed.length > 0 ? trimmed : null;
302
+ }
303
+
304
+ function toToolCallSummary(value: unknown): ToolCallSummary | null {
305
+ if (!value || typeof value !== "object") return null;
306
+ const rec = value as Record<string, unknown>;
307
+
308
+ const messageId = toNonEmptyString(rec.messageId ?? rec.message_id);
309
+ const callId = toNonEmptyString(rec.callId ?? rec.call_id);
310
+ const tool = toNonEmptyString(rec.tool);
311
+ const status = toNonEmptyString(rec.status) ?? "unknown";
312
+ const createdAtRaw = toFiniteNumber(rec.createdAtMs ?? rec.created_at_ms ?? rec.createdAt ?? rec.created_at);
313
+
314
+ if (!messageId || !callId || !tool) return null;
315
+
316
+ return {
317
+ sessionId: toNonEmptyString(rec.sessionId ?? rec.session_id) ?? undefined,
318
+ messageId,
319
+ callId,
320
+ tool,
321
+ status,
322
+ createdAtMs: typeof createdAtRaw === "number" ? Math.floor(createdAtRaw) : null,
323
+ };
324
+ }
325
+
326
+ function toToolCallsResponse(value: unknown): ToolCallsResponse | null {
327
+ if (!value || typeof value !== "object") return null;
328
+ const rec = value as Record<string, unknown>;
329
+
330
+ const ok = typeof rec.ok === "boolean" ? rec.ok : false;
331
+ const sessionId = toNonEmptyString(rec.sessionId ?? rec.session_id);
332
+ const toolCallsRaw = rec.toolCalls ?? rec.tool_calls;
333
+
334
+ if (!sessionId || !Array.isArray(toolCallsRaw)) return null;
335
+
336
+ const toolCalls: ToolCallSummary[] = toolCallsRaw
337
+ .map(toToolCallSummary)
338
+ .filter((t): t is ToolCallSummary => t !== null);
339
+
340
+ const capsRaw = rec.caps;
341
+ const capsObj = capsRaw && typeof capsRaw === "object" ? (capsRaw as Record<string, unknown>) : null;
342
+ const maxMessagesRaw = capsObj ? toFiniteNumber(capsObj.maxMessages ?? capsObj.max_messages) : null;
343
+ const maxToolCallsRaw = capsObj ? toFiniteNumber(capsObj.maxToolCalls ?? capsObj.max_tool_calls) : null;
344
+ const caps =
345
+ typeof maxMessagesRaw === "number" && typeof maxToolCallsRaw === "number"
346
+ ? { maxMessages: Math.max(0, Math.floor(maxMessagesRaw)), maxToolCalls: Math.max(0, Math.floor(maxToolCallsRaw)) }
347
+ : undefined;
348
+
349
+ const truncated = typeof rec.truncated === "boolean" ? rec.truncated : undefined;
350
+
351
+ return {
352
+ ok,
353
+ sessionId,
354
+ toolCalls,
355
+ caps,
356
+ truncated,
357
+ };
358
+ }
359
+
360
+ export function formatBackgroundTaskTimelineCell(status: unknown, timeline: unknown): string {
361
+ const s = typeof status === "string" ? status.trim().toLowerCase() : "";
362
+ if (s === "unknown") return "";
363
+ if (s === "queued") return "-";
364
+
365
+ return toNonEmptyString(timeline) ?? "-";
366
+ }
367
+
368
+ export function computeToolCallsFetchPlan(params: {
369
+ sessionId: string | null;
370
+ status: string;
371
+ cachedState: "idle" | "loading" | "ok" | "error" | null;
372
+ cachedDataOk: boolean;
373
+ isExpanded: boolean;
374
+ }): { shouldFetch: boolean; force: boolean } {
375
+ const { sessionId, status, cachedState, cachedDataOk, isExpanded } = params;
376
+
377
+ if (!sessionId) {
378
+ return { shouldFetch: false, force: false };
379
+ }
380
+
381
+ if (!isExpanded) {
382
+ return { shouldFetch: false, force: false };
383
+ }
384
+
385
+ const isRunning = String(status ?? "").toLowerCase().trim() === "running";
386
+
387
+ if (isRunning) {
388
+ return { shouldFetch: true, force: true };
389
+ }
390
+
391
+ if (cachedDataOk) {
392
+ return { shouldFetch: false, force: false };
393
+ }
394
+
395
+ if (cachedState === "loading") {
396
+ return { shouldFetch: false, force: false };
397
+ }
398
+
399
+ return { shouldFetch: true, force: false };
400
+ }
401
+
402
+ export function toggleIdInSet(id: string, currentSet: Set<string>): Set<string> {
403
+ const next = new Set(currentSet);
404
+ if (next.has(id)) {
405
+ next.delete(id);
406
+ } else {
407
+ next.add(id);
408
+ }
409
+ return next;
410
+ }
411
+
274
412
  function toDashboardPayload(json: unknown): DashboardPayload {
275
413
  if (!json || typeof json !== "object") {
276
414
  return { ...FALLBACK_DATA, raw: json };
@@ -282,6 +420,22 @@ function toDashboardPayload(json: unknown): DashboardPayload {
282
420
  const plan = (anyJson.planProgress ?? anyJson.plan_progress ?? {}) as Record<string, unknown>;
283
421
  const tasks = (anyJson.backgroundTasks ?? anyJson.background_tasks ?? []) as unknown;
284
422
 
423
+ function parsePlanSteps(stepsInput: unknown): Array<{ checked: boolean; text: string }> {
424
+ if (!Array.isArray(stepsInput)) return [];
425
+
426
+ return stepsInput
427
+ .map((step): { checked: boolean; text: string } | null => {
428
+ if (!step || typeof step !== "object") return null;
429
+
430
+ const stepObj = step as Record<string, unknown>;
431
+ const checked = typeof stepObj.checked === "boolean" ? stepObj.checked : false;
432
+ const text = typeof stepObj.text === "string" ? stepObj.text : "";
433
+
434
+ return text.trim().length > 0 ? { checked, text } : null;
435
+ })
436
+ .filter((step): step is { checked: boolean; text: string } => step !== null);
437
+ }
438
+
285
439
  const backgroundTasks: BackgroundTask[] = Array.isArray(tasks)
286
440
  ? tasks.map((t, idx) => {
287
441
  const rec = (t ?? {}) as Record<string, unknown>;
@@ -295,6 +449,8 @@ function toDashboardPayload(json: unknown): DashboardPayload {
295
449
  ? rec.taskId
296
450
  : undefined,
297
451
  agent: String(rec.agent ?? rec.worker ?? "unknown"),
452
+ lastModel: toNonEmptyString(rec.lastModel ?? rec.last_model) ?? "-",
453
+ sessionId: toNonEmptyString(rec.sessionId ?? rec.session_id),
298
454
  status: String(rec.status ?? "queued"),
299
455
  toolCalls: Number(rec.toolCalls ?? rec.tool_calls ?? 0) || 0,
300
456
  lastTool: String(rec.lastTool ?? rec.last_tool ?? "-") || "-",
@@ -305,6 +461,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
305
461
 
306
462
  const completed = Number(plan.completed ?? plan.done ?? 0) || 0;
307
463
  const total = Number(plan.total ?? plan.count ?? 0) || 0;
464
+ const steps = parsePlanSteps(plan.steps);
308
465
 
309
466
  const timeSeries = normalizeTimeSeries(anyJson.timeSeries, Date.now());
310
467
 
@@ -312,6 +469,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
312
469
  mainSession: {
313
470
  agent: String(main.agent ?? FALLBACK_DATA.mainSession.agent),
314
471
  currentTool: String(main.currentTool ?? main.current_tool ?? FALLBACK_DATA.mainSession.currentTool),
472
+ currentModel: toNonEmptyString(main.currentModel ?? main.current_model) ?? "-",
315
473
  lastUpdatedLabel: String(main.lastUpdatedLabel ?? main.last_updated ?? "just now"),
316
474
  session: String(main.session ?? main.session_id ?? FALLBACK_DATA.mainSession.session),
317
475
  statusPill: String(main.statusPill ?? main.status ?? FALLBACK_DATA.mainSession.statusPill),
@@ -322,6 +480,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
322
480
  total,
323
481
  path: String(plan.path ?? FALLBACK_DATA.planProgress.path),
324
482
  statusPill: String(plan.statusPill ?? plan.status ?? FALLBACK_DATA.planProgress.statusPill),
483
+ steps,
325
484
  },
326
485
  backgroundTasks,
327
486
  timeSeries,
@@ -329,6 +488,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
329
488
  };
330
489
  }
331
490
 
491
+ export { toDashboardPayload };
332
492
  export default function App() {
333
493
  const [connected, setConnected] = React.useState(false);
334
494
  const [data, setData] = React.useState<DashboardPayload>(FALLBACK_DATA);
@@ -339,6 +499,12 @@ export default function App() {
339
499
  const [planOpen, setPlanOpen] = React.useState(false);
340
500
  const [errorHint, setErrorHint] = React.useState<string | null>(null);
341
501
 
502
+ const [expandedBgTaskIds, setExpandedBgTaskIds] = React.useState<Set<string>>(() => new Set());
503
+ const [toolCallsBySession, setToolCallsBySession] = React.useState<
504
+ Map<string, { state: "idle" | "loading" | "ok" | "error"; data: ToolCallsResponse | null; lastFetchedAtMs: number | null }>
505
+ >(() => new Map());
506
+ const toolCallsSeqRef = React.useRef<Map<string, number>>(new Map());
507
+
342
508
  const timerRef = React.useRef<number | null>(null);
343
509
  const hadSuccessRef = React.useRef(false);
344
510
  const soundEnabledRef = React.useRef(false);
@@ -394,15 +560,15 @@ export default function App() {
394
560
  }
395
561
  }
396
562
 
397
- function isWaitingForUser(payload: DashboardPayload): boolean {
563
+ const isWaitingForUser = React.useCallback((payload: DashboardPayload): boolean => {
398
564
  const status = payload.mainSession.statusPill.toLowerCase();
399
565
  const hasSession = payload.mainSession.session !== "(no session)" && payload.mainSession.session !== "";
400
566
  const idle = status.includes("idle");
401
567
  const noTool = payload.mainSession.currentTool === "-" || payload.mainSession.currentTool === "";
402
568
  return hasSession && idle && noTool;
403
- }
569
+ }, []);
404
570
 
405
- function maybePlayDings(prev: DashboardPayload | null, next: DashboardPayload) {
571
+ const maybePlayDings = React.useCallback((prev: DashboardPayload | null, next: DashboardPayload) => {
406
572
  if (!soundEnabledRef.current) return;
407
573
  if (!hadSuccessRef.current) return;
408
574
 
@@ -453,7 +619,7 @@ export default function App() {
453
619
  lastLeftWaitingAtRef.current = waitingDecision.next.lastLeftWaitingAtMs;
454
620
  prevPlanCompletedRef.current = completed;
455
621
  prevPlanTotalRef.current = total;
456
- }
622
+ }, [isWaitingForUser]);
457
623
 
458
624
  const planPercent = React.useMemo(() => {
459
625
  if (!data.planProgress.total) return 0;
@@ -469,23 +635,23 @@ export default function App() {
469
635
 
470
636
  async function tick() {
471
637
  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);
638
+ try {
639
+ const json = await safeFetchJson("/api/dashboard");
640
+ if (!alive) return;
641
+ nextConnected = true;
642
+ hadSuccessRef.current = true;
643
+ setConnected(true);
644
+ setErrorHint(null);
645
+ const next = toDashboardPayload(json);
646
+ setData((prev) => {
647
+ maybePlayDings(prev, next);
648
+ return next;
649
+ });
650
+ setLastUpdate(Date.now());
651
+ } catch (err) {
652
+ if (!alive) return;
653
+ nextConnected = false;
654
+ setConnected(false);
489
655
  const msg = err instanceof Error ? err.message : "disconnected";
490
656
  setErrorHint(msg);
491
657
  setData((prev) => {
@@ -512,7 +678,7 @@ export default function App() {
512
678
  alive = false;
513
679
  if (timerRef.current) window.clearTimeout(timerRef.current);
514
680
  };
515
- }, []);
681
+ }, [maybePlayDings]);
516
682
 
517
683
  async function onCopyRawJson() {
518
684
  setCopyState("idle");
@@ -540,10 +706,80 @@ export default function App() {
540
706
  return map;
541
707
  }, [data.timeSeries.series]);
542
708
 
709
+ async function fetchToolCalls(sessionId: string, opts: { force: boolean }) {
710
+ const existing = toolCallsBySession.get(sessionId);
711
+ if (!opts.force && existing?.data?.ok) return;
712
+
713
+ const seq = (toolCallsSeqRef.current.get(sessionId) ?? 0) + 1;
714
+ toolCallsSeqRef.current.set(sessionId, seq);
715
+
716
+ setToolCallsBySession((prev) => {
717
+ const next = new Map(prev);
718
+ const prior = next.get(sessionId);
719
+ next.set(sessionId, {
720
+ state: "loading",
721
+ data: prior?.data ?? null,
722
+ lastFetchedAtMs: prior?.lastFetchedAtMs ?? null,
723
+ });
724
+ return next;
725
+ });
726
+
727
+ try {
728
+ const raw = await safeFetchJson(`/api/tool-calls/${encodeURIComponent(sessionId)}`);
729
+ const parsed = toToolCallsResponse(raw);
730
+ if (!parsed?.ok) throw new Error("tool calls not ok");
731
+ if (toolCallsSeqRef.current.get(sessionId) !== seq) return;
732
+ setToolCallsBySession((prev) => {
733
+ const next = new Map(prev);
734
+ next.set(sessionId, { state: "ok", data: parsed, lastFetchedAtMs: Date.now() });
735
+ return next;
736
+ });
737
+ } catch {
738
+ if (toolCallsSeqRef.current.get(sessionId) !== seq) return;
739
+ setToolCallsBySession((prev) => {
740
+ const next = new Map(prev);
741
+ const prior = next.get(sessionId);
742
+ next.set(sessionId, {
743
+ state: "error",
744
+ data: prior?.data ?? null,
745
+ lastFetchedAtMs: prior?.lastFetchedAtMs ?? null,
746
+ });
747
+ return next;
748
+ });
749
+ }
750
+ }
751
+
752
+ function toggleBackgroundTaskExpanded(t: BackgroundTask) {
753
+ const nextExpanded = !expandedBgTaskIds.has(t.id);
754
+ setExpandedBgTaskIds((prev) => {
755
+ const next = new Set(prev);
756
+ if (nextExpanded) next.add(t.id);
757
+ else next.delete(t.id);
758
+ return next;
759
+ });
760
+
761
+ if (!nextExpanded) return;
762
+
763
+ const sessionId = toNonEmptyString(t.sessionId);
764
+ if (!sessionId) return;
765
+
766
+ const isRunning = String(t.status ?? "").toLowerCase().trim() === "running";
767
+ const cached = toolCallsBySession.get(sessionId);
768
+ if (isRunning) {
769
+ void fetchToolCalls(sessionId, { force: true });
770
+ return;
771
+ }
772
+
773
+ if (cached?.data?.ok) return;
774
+ if (cached?.state === "loading") return;
775
+ void fetchToolCalls(sessionId, { force: false });
776
+ }
777
+
543
778
  const buckets = Math.max(1, data.timeSeries.buckets);
544
779
  const bucketMs = Math.max(1, data.timeSeries.bucketMs);
545
780
  const viewBox = `0 0 ${buckets} 28`;
546
781
  const minuteStep = Math.max(1, Math.round(60_000 / bucketMs));
782
+ const bucketStartMs = data.timeSeries.anchorMs - (buckets - 1) * bucketMs;
547
783
 
548
784
  const overallValues = timeSeriesById.get("overall-main")?.values ?? [];
549
785
 
@@ -611,24 +847,11 @@ export default function App() {
611
847
  {(
612
848
  [
613
849
  {
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,
850
+ kind: "main-agents" as const,
851
+ label: "Main agents" as const,
630
852
  },
631
853
  {
854
+ kind: "single" as const,
632
855
  label: "background tasks (total)",
633
856
  tone: "muted" as const,
634
857
  overlayId: "background-total" as const,
@@ -644,6 +867,82 @@ export default function App() {
644
867
  const barW = 0.85;
645
868
  const barInset = (1 - barW) / 2;
646
869
 
870
+ if (row.kind === "main-agents") {
871
+ const sisyphusValues = timeSeriesById.get("agent:sisyphus")?.values ?? [];
872
+ const prometheusValues = timeSeriesById.get("agent:prometheus")?.values ?? [];
873
+ const atlasValues = timeSeriesById.get("agent:atlas")?.values ?? [];
874
+
875
+ let sumMax = 0;
876
+ for (let i = 0; i < buckets; i++) {
877
+ const rawSis = sisyphusValues[i];
878
+ const rawPro = prometheusValues[i];
879
+ const rawAtl = atlasValues[i];
880
+ const sis = typeof rawSis === "number" && Number.isFinite(rawSis) ? Math.max(0, rawSis) : 0;
881
+ const pro = typeof rawPro === "number" && Number.isFinite(rawPro) ? Math.max(0, rawPro) : 0;
882
+ const atl = typeof rawAtl === "number" && Number.isFinite(rawAtl) ? Math.max(0, rawAtl) : 0;
883
+ const s = sis + pro + atl;
884
+ if (s > sumMax) sumMax = s;
885
+ }
886
+
887
+ const scaleMax = Math.max(1, sumMax || 1);
888
+
889
+ return (
890
+ <div key="main-agents" className="timeSeriesRow">
891
+ <div className="timeSeriesRowLabel">{row.label}</div>
892
+ <div className="timeSeriesSvgWrap">
893
+ <svg className="timeSeriesSvg" viewBox={viewBox} preserveAspectRatio="none" aria-hidden="true">
894
+ {Array.from({ length: Math.floor(buckets / minuteStep) + 1 }, (_, idx) => {
895
+ const x = idx * minuteStep;
896
+ if (x < 0 || x > buckets) return null;
897
+ return (
898
+ <line
899
+ key={`g-${bucketStartMs + x * bucketMs}`}
900
+ className="timeSeriesGridline"
901
+ x1={x}
902
+ x2={x}
903
+ y1={0}
904
+ y2={H}
905
+ />
906
+ );
907
+ })}
908
+
909
+ {Array.from({ length: buckets }, (_, i) => {
910
+ const bucketMsAt = bucketStartMs + i * bucketMs;
911
+ const barX = i + barInset;
912
+ const rawSis = sisyphusValues[i];
913
+ const rawPro = prometheusValues[i];
914
+ const rawAtl = atlasValues[i];
915
+ const sis = typeof rawSis === "number" && Number.isFinite(rawSis) ? Math.max(0, rawSis) : 0;
916
+ const pro = typeof rawPro === "number" && Number.isFinite(rawPro) ? Math.max(0, rawPro) : 0;
917
+ const atl = typeof rawAtl === "number" && Number.isFinite(rawAtl) ? Math.max(0, rawAtl) : 0;
918
+ const segments = computeStackedSegments(
919
+ {
920
+ sisyphus: sis,
921
+ prometheus: pro,
922
+ atlas: atl,
923
+ },
924
+ scaleMax,
925
+ chartHeight
926
+ );
927
+
928
+ if (segments.length === 0) return null;
929
+ return segments.map((seg) => (
930
+ <rect
931
+ key={`main-agents-${bucketMsAt}-${seg.tone}`}
932
+ className={`timeSeriesBar timeSeriesBar--${seg.tone}`}
933
+ x={barX}
934
+ y={padTop + seg.y}
935
+ width={barW}
936
+ height={seg.height}
937
+ />
938
+ ));
939
+ })}
940
+ </svg>
941
+ </div>
942
+ </div>
943
+ );
944
+ }
945
+
647
946
  const overlayValues = timeSeriesById.get(row.overlayId)?.values ?? [];
648
947
  const baselineMax = row.baseline ? maxCount(overallValues) : 0;
649
948
  const overlayMax = maxCount(overlayValues);
@@ -659,7 +958,7 @@ export default function App() {
659
958
  if (x < 0 || x > buckets) return null;
660
959
  return (
661
960
  <line
662
- key={`g-${idx}`}
961
+ key={`g-${bucketStartMs + x * bucketMs}`}
663
962
  className="timeSeriesGridline"
664
963
  x1={x}
665
964
  x2={x}
@@ -674,9 +973,10 @@ export default function App() {
674
973
  const h = barHeight(v ?? 0, scaleMax, chartHeight);
675
974
  if (!h) return null;
676
975
  const barX = i + barInset;
976
+ const bucketMsAt = bucketStartMs + i * bucketMs;
677
977
  return (
678
978
  <rect
679
- key={`b-${i}`}
979
+ key={`b-${bucketMsAt}`}
680
980
  className="timeSeriesBarBaseline"
681
981
  x={barX}
682
982
  y={baselineY - h}
@@ -691,9 +991,10 @@ export default function App() {
691
991
  const h = barHeight(v ?? 0, scaleMax, chartHeight);
692
992
  if (!h) return null;
693
993
  const barX = i + barInset;
994
+ const bucketMsAt = bucketStartMs + i * bucketMs;
694
995
  return (
695
996
  <rect
696
- key={`o-${i}`}
997
+ key={`${row.overlayId}-${bucketMsAt}`}
697
998
  className="timeSeriesBar"
698
999
  x={barX}
699
1000
  y={baselineY - h}
@@ -737,6 +1038,10 @@ export default function App() {
737
1038
  <div className="kvKey">CURRENT TOOL</div>
738
1039
  <div className="kvVal mono">{data.mainSession.currentTool}</div>
739
1040
  </div>
1041
+ <div className="kvRow">
1042
+ <div className="kvKey">CURRENT MODEL</div>
1043
+ <div className="kvVal mono">{data.mainSession.currentModel}</div>
1044
+ </div>
740
1045
  <div className="kvRow">
741
1046
  <div className="kvKey">LAST UPDATED</div>
742
1047
  <div className="kvVal">{data.mainSession.lastUpdatedLabel}</div>
@@ -813,6 +1118,7 @@ export default function App() {
813
1118
  <tr>
814
1119
  <th>DESCRIPTION</th>
815
1120
  <th>AGENT</th>
1121
+ <th>LAST MODEL</th>
816
1122
  <th>STATUS</th>
817
1123
  <th>TOOL CALLS</th>
818
1124
  <th>LAST TOOL</th>
@@ -820,21 +1126,102 @@ export default function App() {
820
1126
  </tr>
821
1127
  </thead>
822
1128
  <tbody>
823
- {data.backgroundTasks.map((t) => (
824
- <tr key={t.id}>
825
- <td>
826
- <div className="taskTitle">{t.description}</div>
827
- {t.subline ? <div className="taskSub mono">{t.subline}</div> : null}
828
- </td>
829
- <td className="mono">{t.agent}</td>
830
- <td>
831
- <span className={`pill pill-${statusTone(t.status)}`}>{t.status}</span>
832
- </td>
833
- <td className="mono">{t.toolCalls}</td>
834
- <td className="mono">{t.lastTool}</td>
835
- <td className="mono muted">{t.status.toLowerCase() === "queued" ? "-" : t.timeline || "-"}</td>
836
- </tr>
837
- ))}
1129
+ {data.backgroundTasks.map((t) => {
1130
+ const expanded = expandedBgTaskIds.has(t.id);
1131
+ const sessionId = toNonEmptyString(t.sessionId);
1132
+ const detailId = `bg-toolcalls-${t.id}`;
1133
+ const entry = sessionId ? toolCallsBySession.get(sessionId) : null;
1134
+ const toolCalls = entry?.data?.ok ? entry.data.toolCalls : [];
1135
+ const showCapped = Boolean(entry?.data?.truncated);
1136
+ const caps = entry?.data?.caps;
1137
+ const showLoading = entry?.state === "loading";
1138
+ const showError = entry?.state === "error" && !entry?.data?.ok;
1139
+ const empty = sessionId ? toolCalls.length === 0 && !showLoading && !showError : true;
1140
+
1141
+ return (
1142
+ <React.Fragment key={t.id}>
1143
+ <tr>
1144
+ <td>
1145
+ <div className="bgTaskRowTitleWrap">
1146
+ <button
1147
+ type="button"
1148
+ className="bgTaskToggle"
1149
+ onClick={() => toggleBackgroundTaskExpanded(t)}
1150
+ aria-expanded={expanded}
1151
+ aria-controls={detailId}
1152
+ title={expanded ? "Collapse" : "Expand"}
1153
+ aria-label={expanded ? "Collapse tool calls" : "Expand tool calls"}
1154
+ />
1155
+ <div className="bgTaskRowTitleText">
1156
+ <div className="taskTitle">{t.description}</div>
1157
+ {t.subline ? <div className="taskSub mono">{t.subline}</div> : null}
1158
+ </div>
1159
+ </div>
1160
+ </td>
1161
+ <td className="mono">{t.agent}</td>
1162
+ <td className="mono">{t.lastModel}</td>
1163
+ <td>
1164
+ <span className={`pill pill-${statusTone(t.status)}`}>{t.status}</span>
1165
+ </td>
1166
+ <td className="mono">{t.toolCalls}</td>
1167
+ <td className="mono">{t.lastTool}</td>
1168
+ <td className="mono muted">{formatBackgroundTaskTimelineCell(t.status, t.timeline)}</td>
1169
+ </tr>
1170
+
1171
+ {expanded ? (
1172
+ <tr>
1173
+ <td colSpan={7} className="bgTaskDetailCell">
1174
+ <section id={detailId} aria-label="Tool calls" className="bgTaskDetail">
1175
+ <div className="mono muted bgTaskDetailHeader">
1176
+ Tool calls (metadata only){showLoading && toolCalls.length > 0 ? " - refreshing" : ""}
1177
+ {showCapped
1178
+ ? ` - capped${caps ? ` (max ${caps.maxMessages} messages / ${caps.maxToolCalls} tool calls)` : ""}`
1179
+ : ""}
1180
+ </div>
1181
+
1182
+ {!sessionId ? (
1183
+ <div className="muted bgTaskDetailEmpty">
1184
+ No session id available for this task.
1185
+ </div>
1186
+ ) : showError ? (
1187
+ <div className="muted bgTaskDetailEmpty">
1188
+ Tool calls unavailable.
1189
+ </div>
1190
+ ) : showLoading && toolCalls.length === 0 ? (
1191
+ <div className="muted bgTaskDetailEmpty">
1192
+ Loading tool calls...
1193
+ </div>
1194
+ ) : empty ? (
1195
+ <div className="muted bgTaskDetailEmpty">
1196
+ No tool calls recorded.
1197
+ </div>
1198
+ ) : (
1199
+ <div className="bgTaskToolCallsGrid">
1200
+ {toolCalls.map((c) => (
1201
+ <div key={c.callId} className="bgTaskToolCall">
1202
+ <div className="bgTaskToolCallRow">
1203
+ <div className="mono bgTaskToolCallTool" title={c.tool}>
1204
+ {c.tool}
1205
+ </div>
1206
+ <div className="mono muted bgTaskToolCallStatus" title={c.status}>
1207
+ {c.status}
1208
+ </div>
1209
+ </div>
1210
+ <div className="mono muted bgTaskToolCallTime">{formatTime(c.createdAtMs)}</div>
1211
+ <div className="mono muted bgTaskToolCallId" title={c.callId}>
1212
+ {c.callId}
1213
+ </div>
1214
+ </div>
1215
+ ))}
1216
+ </div>
1217
+ )}
1218
+ </section>
1219
+ </td>
1220
+ </tr>
1221
+ ) : null}
1222
+ </React.Fragment>
1223
+ );
1224
+ })}
838
1225
  </tbody>
839
1226
  </table>
840
1227
  </div>