oh-my-opencode-dashboard 0.0.5 → 0.1.1

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
@@ -63,6 +63,266 @@ type TimeSeries = {
63
63
  series: TimeSeriesSeries[];
64
64
  };
65
65
 
66
+ function toNonNegativeFinite(value: unknown): number {
67
+ if (typeof value !== "number") return 0;
68
+ if (!Number.isFinite(value)) return 0;
69
+ return Math.max(0, value);
70
+ }
71
+
72
+ export function computeOtherMainAgentsCount(params: {
73
+ overall: unknown;
74
+ background: unknown;
75
+ sisyphus: unknown;
76
+ prometheus: unknown;
77
+ atlas: unknown;
78
+ }): number {
79
+ const overall = toNonNegativeFinite(params.overall);
80
+ const background = toNonNegativeFinite(params.background);
81
+ const sisyphus = toNonNegativeFinite(params.sisyphus);
82
+ const prometheus = toNonNegativeFinite(params.prometheus);
83
+ const atlas = toNonNegativeFinite(params.atlas);
84
+
85
+ const mainTotal = Math.max(0, overall - background);
86
+ return Math.max(0, mainTotal - sisyphus - prometheus - atlas);
87
+ }
88
+
89
+ export function computeMainAgentsScaleMax(params: {
90
+ buckets: number;
91
+ overallValues: unknown[];
92
+ backgroundValues: unknown[];
93
+ sisyphusValues: unknown[];
94
+ prometheusValues: unknown[];
95
+ atlasValues: unknown[];
96
+ }): number {
97
+ const buckets = Math.max(0, Math.floor(params.buckets));
98
+ let sumMax = 0;
99
+
100
+ for (let i = 0; i < buckets; i++) {
101
+ const sis = toNonNegativeFinite(params.sisyphusValues[i]);
102
+ const pro = toNonNegativeFinite(params.prometheusValues[i]);
103
+ const atl = toNonNegativeFinite(params.atlasValues[i]);
104
+ const other = computeOtherMainAgentsCount({
105
+ overall: params.overallValues[i],
106
+ background: params.backgroundValues[i],
107
+ sisyphus: sis,
108
+ prometheus: pro,
109
+ atlas: atl,
110
+ });
111
+ const s = sis + pro + atl + other;
112
+ if (s > sumMax) sumMax = s;
113
+ }
114
+
115
+ return Math.max(1, sumMax || 1);
116
+ }
117
+
118
+ export function TimeSeriesActivitySection(props: { timeSeries: TimeSeries }) {
119
+ const timeSeriesById = new Map<TimeSeriesSeriesId, TimeSeriesSeries>();
120
+ for (const s of props.timeSeries.series) {
121
+ if (s && typeof s.id === "string") {
122
+ timeSeriesById.set(s.id, s);
123
+ }
124
+ }
125
+
126
+ const buckets = Math.max(1, props.timeSeries.buckets);
127
+ const bucketMs = Math.max(1, props.timeSeries.bucketMs);
128
+ const viewBox = `0 0 ${buckets} 28`;
129
+ const minuteStep = Math.max(1, Math.round(60_000 / bucketMs));
130
+ const bucketStartMs = props.timeSeries.anchorMs - (buckets - 1) * bucketMs;
131
+
132
+ const overallValues = timeSeriesById.get("overall-main")?.values ?? [];
133
+
134
+ return (
135
+ <section className="timeSeries">
136
+ <div className="timeSeriesHeader">
137
+ <h2 className="timeSeriesTitle">Time-series activity</h2>
138
+ <p className="timeSeriesSub">Last 5 minutes</p>
139
+ </div>
140
+
141
+ <div className="timeSeriesRows">
142
+ {(
143
+ [
144
+ {
145
+ kind: "main-agents" as const,
146
+ label: "Main agents" as const,
147
+ },
148
+ {
149
+ kind: "single" as const,
150
+ label: "background tasks (total)",
151
+ tone: "muted" as const,
152
+ overlayId: "background-total" as const,
153
+ baseline: false,
154
+ },
155
+ ] as const
156
+ ).map((row) => {
157
+ const H = 28;
158
+ const padTop = 2;
159
+ const padBottom = 2;
160
+ const chartHeight = H - padTop - padBottom;
161
+ const baselineY = H - padBottom;
162
+ const barW = 0.85;
163
+ const barInset = (1 - barW) / 2;
164
+
165
+ if (row.kind === "main-agents") {
166
+ const sisyphusValues = timeSeriesById.get("agent:sisyphus")?.values ?? [];
167
+ const prometheusValues = timeSeriesById.get("agent:prometheus")?.values ?? [];
168
+ const atlasValues = timeSeriesById.get("agent:atlas")?.values ?? [];
169
+ const backgroundValues = timeSeriesById.get("background-total")?.values ?? [];
170
+
171
+ const scaleMax = computeMainAgentsScaleMax({
172
+ buckets,
173
+ overallValues,
174
+ backgroundValues,
175
+ sisyphusValues,
176
+ prometheusValues,
177
+ atlasValues,
178
+ });
179
+
180
+ return (
181
+ <div key="main-agents" className="timeSeriesRow">
182
+ <div className="timeSeriesRowLabel">{row.label}</div>
183
+ <div className="timeSeriesSvgWrap">
184
+ <svg className="timeSeriesSvg" viewBox={viewBox} preserveAspectRatio="none" aria-hidden="true">
185
+ {Array.from({ length: Math.floor(buckets / minuteStep) + 1 }, (_, idx) => {
186
+ const x = idx * minuteStep;
187
+ if (x < 0 || x > buckets) return null;
188
+ return (
189
+ <line
190
+ key={`g-${bucketStartMs + x * bucketMs}`}
191
+ className="timeSeriesGridline"
192
+ x1={x}
193
+ x2={x}
194
+ y1={0}
195
+ y2={H}
196
+ />
197
+ );
198
+ })}
199
+
200
+ {Array.from({ length: buckets }, (_, i) => {
201
+ const bucketMsAt = bucketStartMs + i * bucketMs;
202
+ const barX = i + barInset;
203
+
204
+ const sis = toNonNegativeFinite(sisyphusValues[i]);
205
+ const pro = toNonNegativeFinite(prometheusValues[i]);
206
+ const atl = toNonNegativeFinite(atlasValues[i]);
207
+ const other = computeOtherMainAgentsCount({
208
+ overall: overallValues[i],
209
+ background: backgroundValues[i],
210
+ sisyphus: sis,
211
+ prometheus: pro,
212
+ atlas: atl,
213
+ });
214
+
215
+ const segments = computeStackedSegments(
216
+ {
217
+ sisyphus: sis,
218
+ prometheus: pro,
219
+ atlas: atl,
220
+ other,
221
+ },
222
+ scaleMax,
223
+ chartHeight
224
+ );
225
+
226
+ if (segments.length === 0) return null;
227
+ return segments.map((seg) => (
228
+ <rect
229
+ key={`main-agents-${bucketMsAt}-${seg.tone}`}
230
+ className={`timeSeriesBar timeSeriesBar--${seg.tone}`}
231
+ x={barX}
232
+ y={padTop + seg.y}
233
+ width={barW}
234
+ height={seg.height}
235
+ />
236
+ ));
237
+ })}
238
+ </svg>
239
+ </div>
240
+ </div>
241
+ );
242
+ }
243
+
244
+ const overlayValues = timeSeriesById.get(row.overlayId)?.values ?? [];
245
+ const baselineMax = row.baseline ? maxCount(overallValues) : 0;
246
+ const overlayMax = maxCount(overlayValues);
247
+ const scaleMax = Math.max(1, row.baseline ? Math.max(baselineMax, overlayMax) : overlayMax || 1);
248
+
249
+ return (
250
+ <div key={row.overlayId} className="timeSeriesRow" data-tone={row.tone}>
251
+ <div className="timeSeriesRowLabel">{row.label}</div>
252
+ <div className="timeSeriesSvgWrap">
253
+ <svg className="timeSeriesSvg" viewBox={viewBox} preserveAspectRatio="none" aria-hidden="true">
254
+ {Array.from({ length: Math.floor(buckets / minuteStep) + 1 }, (_, idx) => {
255
+ const x = idx * minuteStep;
256
+ if (x < 0 || x > buckets) return null;
257
+ return (
258
+ <line
259
+ key={`g-${bucketStartMs + x * bucketMs}`}
260
+ className="timeSeriesGridline"
261
+ x1={x}
262
+ x2={x}
263
+ y1={0}
264
+ y2={H}
265
+ />
266
+ );
267
+ })}
268
+
269
+ {row.baseline
270
+ ? overallValues.slice(0, buckets).map((v, i) => {
271
+ const h = barHeight(v ?? 0, scaleMax, chartHeight);
272
+ if (!h) return null;
273
+ const barX = i + barInset;
274
+ const bucketMsAt = bucketStartMs + i * bucketMs;
275
+ return (
276
+ <rect
277
+ key={`b-${bucketMsAt}`}
278
+ className="timeSeriesBarBaseline"
279
+ x={barX}
280
+ y={baselineY - h}
281
+ width={barW}
282
+ height={h}
283
+ />
284
+ );
285
+ })
286
+ : null}
287
+
288
+ {overlayValues.slice(0, buckets).map((v, i) => {
289
+ const h = barHeight(v ?? 0, scaleMax, chartHeight);
290
+ if (!h) return null;
291
+ const barX = i + barInset;
292
+ const bucketMsAt = bucketStartMs + i * bucketMs;
293
+ return (
294
+ <rect
295
+ key={`${row.overlayId}-${bucketMsAt}`}
296
+ className="timeSeriesBar"
297
+ x={barX}
298
+ y={baselineY - h}
299
+ width={barW}
300
+ height={h}
301
+ />
302
+ );
303
+ })}
304
+ </svg>
305
+ </div>
306
+ </div>
307
+ );
308
+ })}
309
+ </div>
310
+
311
+ <div className="timeSeriesAxisBottom" aria-hidden="true">
312
+ <div />
313
+ <div className="timeSeriesAxisBottomLabels">
314
+ <span className="timeSeriesAxisBottomLabel">-5m</span>
315
+ <span className="timeSeriesAxisBottomLabel">-4m</span>
316
+ <span className="timeSeriesAxisBottomLabel">-3m</span>
317
+ <span className="timeSeriesAxisBottomLabel">-2m</span>
318
+ <span className="timeSeriesAxisBottomLabel">-1m</span>
319
+ <span className="timeSeriesAxisBottomLabel">Now</span>
320
+ </div>
321
+ </div>
322
+ </section>
323
+ );
324
+ }
325
+
66
326
  type DashboardPayload = {
67
327
  mainSession: {
68
328
  agent: string;
@@ -70,6 +330,7 @@ type DashboardPayload = {
70
330
  currentModel: string;
71
331
  lastUpdatedLabel: string;
72
332
  session: string;
333
+ sessionId: string | null;
73
334
  statusPill: string;
74
335
  };
75
336
  planProgress: {
@@ -81,6 +342,7 @@ type DashboardPayload = {
81
342
  steps?: Array<{ checked: boolean; text: string }>;
82
343
  };
83
344
  backgroundTasks: BackgroundTask[];
345
+ mainSessionTasks: BackgroundTask[];
84
346
  timeSeries: TimeSeries;
85
347
  raw: unknown;
86
348
  };
@@ -200,6 +462,7 @@ const FALLBACK_DATA: DashboardPayload = {
200
462
  currentModel: "anthropic/claude-opus-4-5",
201
463
  lastUpdatedLabel: "just now",
202
464
  session: "qa-session",
465
+ sessionId: null,
203
466
  statusPill: "busy",
204
467
  },
205
468
  planProgress: {
@@ -223,6 +486,7 @@ const FALLBACK_DATA: DashboardPayload = {
223
486
  timeline: "2026-01-01T00:00:00Z: 2m",
224
487
  },
225
488
  ],
489
+ mainSessionTasks: [],
226
490
  timeSeries: makeZeroTimeSeries({
227
491
  windowMs: TIME_SERIES_DEFAULT_WINDOW_MS,
228
492
  bucketMs: TIME_SERIES_DEFAULT_BUCKET_MS,
@@ -419,6 +683,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
419
683
  const main = (anyJson.mainSession ?? anyJson.main_session ?? {}) as Record<string, unknown>;
420
684
  const plan = (anyJson.planProgress ?? anyJson.plan_progress ?? {}) as Record<string, unknown>;
421
685
  const tasks = (anyJson.backgroundTasks ?? anyJson.background_tasks ?? []) as unknown;
686
+ const mainTasks = (anyJson.mainSessionTasks ?? anyJson.main_session_tasks ?? []) as unknown;
422
687
 
423
688
  function parsePlanSteps(stepsInput: unknown): Array<{ checked: boolean; text: string }> {
424
689
  if (!Array.isArray(stepsInput)) return [];
@@ -459,6 +724,29 @@ function toDashboardPayload(json: unknown): DashboardPayload {
459
724
  })
460
725
  : FALLBACK_DATA.backgroundTasks;
461
726
 
727
+ const mainSessionTasks: BackgroundTask[] = Array.isArray(mainTasks)
728
+ ? mainTasks.map((t, idx) => {
729
+ const rec = (t ?? {}) as Record<string, unknown>;
730
+ return {
731
+ id: String(rec.id ?? rec.taskId ?? rec.task_id ?? `main-task-${idx + 1}`),
732
+ description: String(rec.description ?? rec.name ?? "(no description)"),
733
+ subline:
734
+ typeof rec.subline === "string"
735
+ ? rec.subline
736
+ : typeof rec.taskId === "string"
737
+ ? rec.taskId
738
+ : undefined,
739
+ agent: String(rec.agent ?? rec.worker ?? "unknown"),
740
+ lastModel: toNonEmptyString(rec.lastModel ?? rec.last_model) ?? "-",
741
+ sessionId: toNonEmptyString(rec.sessionId ?? rec.session_id),
742
+ status: String(rec.status ?? "queued"),
743
+ toolCalls: Number(rec.toolCalls ?? rec.tool_calls ?? 0) || 0,
744
+ lastTool: String(rec.lastTool ?? rec.last_tool ?? "-") || "-",
745
+ timeline: String(rec.timeline ?? "") || "",
746
+ };
747
+ })
748
+ : [];
749
+
462
750
  const completed = Number(plan.completed ?? plan.done ?? 0) || 0;
463
751
  const total = Number(plan.total ?? plan.count ?? 0) || 0;
464
752
  const steps = parsePlanSteps(plan.steps);
@@ -472,6 +760,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
472
760
  currentModel: toNonEmptyString(main.currentModel ?? main.current_model) ?? "-",
473
761
  lastUpdatedLabel: String(main.lastUpdatedLabel ?? main.last_updated ?? "just now"),
474
762
  session: String(main.session ?? main.session_id ?? FALLBACK_DATA.mainSession.session),
763
+ sessionId: toNonEmptyString(main.sessionId ?? main.session_id),
475
764
  statusPill: String(main.statusPill ?? main.status ?? FALLBACK_DATA.mainSession.statusPill),
476
765
  },
477
766
  planProgress: {
@@ -483,6 +772,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
483
772
  steps,
484
773
  },
485
774
  backgroundTasks,
775
+ mainSessionTasks,
486
776
  timeSeries,
487
777
  raw: json,
488
778
  };
@@ -500,9 +790,11 @@ export default function App() {
500
790
  const [errorHint, setErrorHint] = React.useState<string | null>(null);
501
791
 
502
792
  const [expandedBgTaskIds, setExpandedBgTaskIds] = React.useState<Set<string>>(() => new Set());
793
+ const [expandedMainTaskIds, setExpandedMainTaskIds] = React.useState<Set<string>>(() => new Set());
503
794
  const [toolCallsBySession, setToolCallsBySession] = React.useState<
504
795
  Map<string, { state: "idle" | "loading" | "ok" | "error"; data: ToolCallsResponse | null; lastFetchedAtMs: number | null }>
505
796
  >(() => new Map());
797
+ const toolCallsBySessionRef = React.useRef(toolCallsBySession);
506
798
  const toolCallsSeqRef = React.useRef<Map<string, number>>(new Map());
507
799
 
508
800
  const timerRef = React.useRef<number | null>(null);
@@ -527,6 +819,10 @@ export default function App() {
527
819
  soundEnabledRef.current = soundEnabled;
528
820
  }, [soundEnabled]);
529
821
 
822
+ React.useEffect(() => {
823
+ toolCallsBySessionRef.current = toolCallsBySession;
824
+ }, [toolCallsBySession]);
825
+
530
826
  React.useEffect(() => {
531
827
  try {
532
828
  const raw = window.localStorage.getItem("omoDashboardSoundEnabled");
@@ -696,18 +992,9 @@ export default function App() {
696
992
  const liveLabel = connected ? "Live" : "Disconnected";
697
993
  const liveTone = connected ? "teal" : "sand";
698
994
 
699
- const timeSeriesById = React.useMemo(() => {
700
- const map = new Map<TimeSeriesSeriesId, TimeSeriesSeries>();
701
- for (const s of data.timeSeries.series) {
702
- if (s && typeof s.id === "string") {
703
- map.set(s.id, s);
704
- }
705
- }
706
- return map;
707
- }, [data.timeSeries.series]);
708
995
 
709
- async function fetchToolCalls(sessionId: string, opts: { force: boolean }) {
710
- const existing = toolCallsBySession.get(sessionId);
996
+ const fetchToolCalls = React.useCallback(async (sessionId: string, opts: { force: boolean }) => {
997
+ const existing = toolCallsBySessionRef.current.get(sessionId);
711
998
  if (!opts.force && existing?.data?.ok) return;
712
999
 
713
1000
  const seq = (toolCallsSeqRef.current.get(sessionId) ?? 0) + 1;
@@ -747,7 +1034,7 @@ export default function App() {
747
1034
  return next;
748
1035
  });
749
1036
  }
750
- }
1037
+ }, []);
751
1038
 
752
1039
  function toggleBackgroundTaskExpanded(t: BackgroundTask) {
753
1040
  const nextExpanded = !expandedBgTaskIds.has(t.id);
@@ -764,7 +1051,7 @@ export default function App() {
764
1051
  if (!sessionId) return;
765
1052
 
766
1053
  const isRunning = String(t.status ?? "").toLowerCase().trim() === "running";
767
- const cached = toolCallsBySession.get(sessionId);
1054
+ const cached = toolCallsBySessionRef.current.get(sessionId);
768
1055
  if (isRunning) {
769
1056
  void fetchToolCalls(sessionId, { force: true });
770
1057
  return;
@@ -775,13 +1062,65 @@ export default function App() {
775
1062
  void fetchToolCalls(sessionId, { force: false });
776
1063
  }
777
1064
 
778
- const buckets = Math.max(1, data.timeSeries.buckets);
779
- const bucketMs = Math.max(1, data.timeSeries.bucketMs);
780
- const viewBox = `0 0 ${buckets} 28`;
781
- const minuteStep = Math.max(1, Math.round(60_000 / bucketMs));
782
- const bucketStartMs = data.timeSeries.anchorMs - (buckets - 1) * bucketMs;
1065
+ function toggleMainTaskExpanded(t: BackgroundTask) {
1066
+ const nextExpanded = !expandedMainTaskIds.has(t.id);
1067
+ setExpandedMainTaskIds((prev) => {
1068
+ const next = new Set(prev);
1069
+ if (nextExpanded) next.add(t.id);
1070
+ else next.delete(t.id);
1071
+ return next;
1072
+ });
783
1073
 
784
- const overallValues = timeSeriesById.get("overall-main")?.values ?? [];
1074
+ if (!nextExpanded) return;
1075
+
1076
+ const sessionId = toNonEmptyString(t.sessionId);
1077
+ if (!sessionId) return;
1078
+
1079
+ const isRunning = String(t.status ?? "").toLowerCase().trim() === "running";
1080
+ const cached = toolCallsBySessionRef.current.get(sessionId);
1081
+ if (isRunning) {
1082
+ void fetchToolCalls(sessionId, { force: true });
1083
+ return;
1084
+ }
1085
+
1086
+ if (cached?.data?.ok) return;
1087
+ if (cached?.state === "loading") return;
1088
+ void fetchToolCalls(sessionId, { force: false });
1089
+ }
1090
+
1091
+ React.useEffect(() => {
1092
+ if (!connected) return;
1093
+
1094
+ for (const t of data.backgroundTasks) {
1095
+ const sessionId = toNonEmptyString(t.sessionId);
1096
+ const cached = sessionId ? toolCallsBySessionRef.current.get(sessionId) : null;
1097
+ const plan = computeToolCallsFetchPlan({
1098
+ sessionId,
1099
+ status: t.status,
1100
+ cachedState: cached?.state ?? null,
1101
+ cachedDataOk: Boolean(cached?.data?.ok),
1102
+ isExpanded: expandedBgTaskIds.has(t.id),
1103
+ });
1104
+ if (plan.shouldFetch && sessionId) {
1105
+ void fetchToolCalls(sessionId, { force: plan.force });
1106
+ }
1107
+ }
1108
+
1109
+ for (const t of data.mainSessionTasks) {
1110
+ const sessionId = toNonEmptyString(t.sessionId);
1111
+ const cached = sessionId ? toolCallsBySessionRef.current.get(sessionId) : null;
1112
+ const plan = computeToolCallsFetchPlan({
1113
+ sessionId,
1114
+ status: t.status,
1115
+ cachedState: cached?.state ?? null,
1116
+ cachedDataOk: Boolean(cached?.data?.ok),
1117
+ isExpanded: expandedMainTaskIds.has(t.id),
1118
+ });
1119
+ if (plan.shouldFetch && sessionId) {
1120
+ void fetchToolCalls(sessionId, { force: plan.force });
1121
+ }
1122
+ }
1123
+ }, [connected, data.backgroundTasks, data.mainSessionTasks, expandedBgTaskIds, expandedMainTaskIds, fetchToolCalls]);
785
1124
 
786
1125
  return (
787
1126
  <div className="page">
@@ -827,201 +1166,7 @@ export default function App() {
827
1166
  </header>
828
1167
 
829
1168
  <main className="stack">
830
- <section className="timeSeries">
831
- <div className="timeSeriesHeader">
832
- <h2 className="timeSeriesTitle">Time-series activity</h2>
833
- <p className="timeSeriesSub">Last 5 minutes</p>
834
- </div>
835
-
836
- <div className="timeSeriesAxisTop" aria-hidden="true">
837
- <div />
838
- <div className="timeSeriesAxisTopLabels">
839
- <span className="timeSeriesAxisTopLabel">-5m</span>
840
- <span className="timeSeriesAxisTopLabel">-4m</span>
841
- <span className="timeSeriesAxisTopLabel">-3m</span>
842
- <span className="timeSeriesAxisTopLabel">-1m</span>
843
- </div>
844
- </div>
845
-
846
- <div className="timeSeriesRows">
847
- {(
848
- [
849
- {
850
- kind: "main-agents" as const,
851
- label: "Main agents" as const,
852
- },
853
- {
854
- kind: "single" as const,
855
- label: "background tasks (total)",
856
- tone: "muted" as const,
857
- overlayId: "background-total" as const,
858
- baseline: false,
859
- },
860
- ] as const
861
- ).map((row) => {
862
- const H = 28;
863
- const padTop = 2;
864
- const padBottom = 2;
865
- const chartHeight = H - padTop - padBottom;
866
- const baselineY = H - padBottom;
867
- const barW = 0.85;
868
- const barInset = (1 - barW) / 2;
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
-
946
- const overlayValues = timeSeriesById.get(row.overlayId)?.values ?? [];
947
- const baselineMax = row.baseline ? maxCount(overallValues) : 0;
948
- const overlayMax = maxCount(overlayValues);
949
- const scaleMax = Math.max(1, row.baseline ? Math.max(baselineMax, overlayMax) : overlayMax || 1);
950
-
951
- return (
952
- <div key={row.overlayId} className="timeSeriesRow" data-tone={row.tone}>
953
- <div className="timeSeriesRowLabel">{row.label}</div>
954
- <div className="timeSeriesSvgWrap">
955
- <svg className="timeSeriesSvg" viewBox={viewBox} preserveAspectRatio="none" aria-hidden="true">
956
- {Array.from({ length: Math.floor(buckets / minuteStep) + 1 }, (_, idx) => {
957
- const x = idx * minuteStep;
958
- if (x < 0 || x > buckets) return null;
959
- return (
960
- <line
961
- key={`g-${bucketStartMs + x * bucketMs}`}
962
- className="timeSeriesGridline"
963
- x1={x}
964
- x2={x}
965
- y1={0}
966
- y2={H}
967
- />
968
- );
969
- })}
970
-
971
- {row.baseline
972
- ? overallValues.slice(0, buckets).map((v, i) => {
973
- const h = barHeight(v ?? 0, scaleMax, chartHeight);
974
- if (!h) return null;
975
- const barX = i + barInset;
976
- const bucketMsAt = bucketStartMs + i * bucketMs;
977
- return (
978
- <rect
979
- key={`b-${bucketMsAt}`}
980
- className="timeSeriesBarBaseline"
981
- x={barX}
982
- y={baselineY - h}
983
- width={barW}
984
- height={h}
985
- />
986
- );
987
- })
988
- : null}
989
-
990
- {overlayValues.slice(0, buckets).map((v, i) => {
991
- const h = barHeight(v ?? 0, scaleMax, chartHeight);
992
- if (!h) return null;
993
- const barX = i + barInset;
994
- const bucketMsAt = bucketStartMs + i * bucketMs;
995
- return (
996
- <rect
997
- key={`${row.overlayId}-${bucketMsAt}`}
998
- className="timeSeriesBar"
999
- x={barX}
1000
- y={baselineY - h}
1001
- width={barW}
1002
- height={h}
1003
- />
1004
- );
1005
- })}
1006
- </svg>
1007
- </div>
1008
- </div>
1009
- );
1010
- })}
1011
- </div>
1012
-
1013
- <div className="timeSeriesAxisBottom" aria-hidden="true">
1014
- <div />
1015
- <div className="timeSeriesAxisBottomLabels">
1016
- <span className="timeSeriesAxisBottomLabel">-5m</span>
1017
- <span className="timeSeriesAxisBottomLabel">-4m</span>
1018
- <span className="timeSeriesAxisBottomLabel">-3m</span>
1019
- <span className="timeSeriesAxisBottomLabel">-2m</span>
1020
- <span className="timeSeriesAxisBottomLabel">-1m</span>
1021
- <span className="timeSeriesAxisBottomLabel">Now</span>
1022
- </div>
1023
- </div>
1024
- </section>
1169
+ <TimeSeriesActivitySection timeSeries={data.timeSeries} />
1025
1170
 
1026
1171
  <section className="grid2">
1027
1172
  <article className="card">
@@ -1105,6 +1250,126 @@ export default function App() {
1105
1250
  </article>
1106
1251
  </section>
1107
1252
 
1253
+ <section className="card">
1254
+ <div className="cardHeader">
1255
+ <h2>Main session tasks</h2>
1256
+ <span className="badge">{data.mainSessionTasks.length}</span>
1257
+ </div>
1258
+
1259
+ <div className="tableWrap">
1260
+ <table className="table">
1261
+ <thead>
1262
+ <tr>
1263
+ <th>DESCRIPTION</th>
1264
+ <th>AGENT</th>
1265
+ <th>LAST MODEL</th>
1266
+ <th>STATUS</th>
1267
+ <th>TOOL CALLS</th>
1268
+ <th>LAST TOOL</th>
1269
+ <th>TIMELINE</th>
1270
+ </tr>
1271
+ </thead>
1272
+ <tbody>
1273
+ {data.mainSessionTasks.length === 0 ? (
1274
+ <tr>
1275
+ <td colSpan={7} className="muted" style={{ padding: 16 }}>
1276
+ No main session tasks detected yet.
1277
+ </td>
1278
+ </tr>
1279
+ ) : null}
1280
+ {data.mainSessionTasks.map((t) => {
1281
+ const expanded = expandedMainTaskIds.has(t.id);
1282
+ const sessionId = toNonEmptyString(t.sessionId);
1283
+ const detailId = `main-toolcalls-${t.id}`;
1284
+ const entry = sessionId ? toolCallsBySession.get(sessionId) : null;
1285
+ const toolCalls = entry?.data?.ok ? entry.data.toolCalls : [];
1286
+ const showCapped = Boolean(entry?.data?.truncated);
1287
+ const caps = entry?.data?.caps;
1288
+ const showLoading = entry?.state === "loading";
1289
+ const showError = entry?.state === "error" && !entry?.data?.ok;
1290
+ const empty = sessionId ? toolCalls.length === 0 && !showLoading && !showError : true;
1291
+
1292
+ return (
1293
+ <React.Fragment key={t.id}>
1294
+ <tr>
1295
+ <td>
1296
+ <div className="bgTaskRowTitleWrap">
1297
+ <button
1298
+ type="button"
1299
+ className="bgTaskToggle"
1300
+ onClick={() => toggleMainTaskExpanded(t)}
1301
+ aria-expanded={expanded}
1302
+ aria-controls={detailId}
1303
+ title={expanded ? "Collapse" : "Expand"}
1304
+ aria-label={expanded ? "Collapse tool calls" : "Expand tool calls"}
1305
+ />
1306
+ <div className="bgTaskRowTitleText">
1307
+ <div className="taskTitle">{t.description}</div>
1308
+ {t.subline ? <div className="taskSub mono">{t.subline}</div> : null}
1309
+ </div>
1310
+ </div>
1311
+ </td>
1312
+ <td className="mono">{t.agent}</td>
1313
+ <td className="mono">{t.lastModel}</td>
1314
+ <td>
1315
+ <span className={`pill pill-${statusTone(t.status)}`}>{t.status}</span>
1316
+ </td>
1317
+ <td className="mono">{t.toolCalls}</td>
1318
+ <td className="mono">{t.lastTool}</td>
1319
+ <td className="mono muted">{formatBackgroundTaskTimelineCell(t.status, t.timeline)}</td>
1320
+ </tr>
1321
+
1322
+ {expanded ? (
1323
+ <tr>
1324
+ <td colSpan={7} className="bgTaskDetailCell">
1325
+ <section id={detailId} aria-label="Tool calls" className="bgTaskDetail">
1326
+ <div className="mono muted bgTaskDetailHeader">
1327
+ Tool calls (metadata only){showLoading && toolCalls.length > 0 ? " - refreshing" : ""}
1328
+ {showCapped
1329
+ ? ` - capped${caps ? ` (max ${caps.maxMessages} messages / ${caps.maxToolCalls} tool calls)` : ""}`
1330
+ : ""}
1331
+ </div>
1332
+
1333
+ {!sessionId ? (
1334
+ <div className="muted bgTaskDetailEmpty">No session id available for this task.</div>
1335
+ ) : showError ? (
1336
+ <div className="muted bgTaskDetailEmpty">Tool calls unavailable.</div>
1337
+ ) : showLoading && toolCalls.length === 0 ? (
1338
+ <div className="muted bgTaskDetailEmpty">Loading tool calls...</div>
1339
+ ) : empty ? (
1340
+ <div className="muted bgTaskDetailEmpty">No tool calls recorded.</div>
1341
+ ) : (
1342
+ <div className="bgTaskToolCallsGrid">
1343
+ {toolCalls.map((c) => (
1344
+ <div key={c.callId} className="bgTaskToolCall">
1345
+ <div className="bgTaskToolCallRow">
1346
+ <div className="mono bgTaskToolCallTool" title={c.tool}>
1347
+ {c.tool}
1348
+ </div>
1349
+ <div className="mono muted bgTaskToolCallStatus" title={c.status}>
1350
+ {c.status}
1351
+ </div>
1352
+ </div>
1353
+ <div className="mono muted bgTaskToolCallTime">{formatTime(c.createdAtMs)}</div>
1354
+ <div className="mono muted bgTaskToolCallId" title={c.callId}>
1355
+ {c.callId}
1356
+ </div>
1357
+ </div>
1358
+ ))}
1359
+ </div>
1360
+ )}
1361
+ </section>
1362
+ </td>
1363
+ </tr>
1364
+ ) : null}
1365
+ </React.Fragment>
1366
+ );
1367
+ })}
1368
+ </tbody>
1369
+ </table>
1370
+ </div>
1371
+ </section>
1372
+
1108
1373
  <section className="card">
1109
1374
  <div className="cardHeader">
1110
1375
  <h2>Background tasks</h2>
@@ -1126,6 +1391,13 @@ export default function App() {
1126
1391
  </tr>
1127
1392
  </thead>
1128
1393
  <tbody>
1394
+ {data.backgroundTasks.length === 0 ? (
1395
+ <tr>
1396
+ <td colSpan={7} className="muted" style={{ padding: 16 }}>
1397
+ No background tasks detected yet. When you run background agents, they will appear here.
1398
+ </td>
1399
+ </tr>
1400
+ ) : null}
1129
1401
  {data.backgroundTasks.map((t) => {
1130
1402
  const expanded = expandedBgTaskIds.has(t.id);
1131
1403
  const sessionId = toNonEmptyString(t.sessionId);