oh-my-opencode-dashboard 0.0.4 → 0.1.0

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
@@ -14,12 +14,30 @@ type BackgroundTask = {
14
14
  subline?: string;
15
15
  agent: string;
16
16
  lastModel: string;
17
+ sessionId?: string | null;
17
18
  status: "queued" | "running" | "done" | "error" | "cancelled" | string;
18
19
  toolCalls: number;
19
20
  lastTool: string;
20
21
  timeline: string;
21
22
  };
22
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
+
23
41
  type TimeSeriesTone = "muted" | "teal" | "red" | "green";
24
42
 
25
43
  type TimeSeriesSeriesId =
@@ -52,6 +70,7 @@ type DashboardPayload = {
52
70
  currentModel: string;
53
71
  lastUpdatedLabel: string;
54
72
  session: string;
73
+ sessionId: string | null;
55
74
  statusPill: string;
56
75
  };
57
76
  planProgress: {
@@ -63,6 +82,7 @@ type DashboardPayload = {
63
82
  steps?: Array<{ checked: boolean; text: string }>;
64
83
  };
65
84
  backgroundTasks: BackgroundTask[];
85
+ mainSessionTasks: BackgroundTask[];
66
86
  timeSeries: TimeSeries;
67
87
  raw: unknown;
68
88
  };
@@ -182,6 +202,7 @@ const FALLBACK_DATA: DashboardPayload = {
182
202
  currentModel: "anthropic/claude-opus-4-5",
183
203
  lastUpdatedLabel: "just now",
184
204
  session: "qa-session",
205
+ sessionId: null,
185
206
  statusPill: "busy",
186
207
  },
187
208
  planProgress: {
@@ -198,12 +219,14 @@ const FALLBACK_DATA: DashboardPayload = {
198
219
  subline: "task-1",
199
220
  agent: "explore",
200
221
  lastModel: "opencode/gpt-5-nano",
222
+ sessionId: null,
201
223
  status: "running",
202
224
  toolCalls: 3,
203
225
  lastTool: "grep",
204
226
  timeline: "2026-01-01T00:00:00Z: 2m",
205
227
  },
206
228
  ],
229
+ mainSessionTasks: [],
207
230
  timeSeries: makeZeroTimeSeries({
208
231
  windowMs: TIME_SERIES_DEFAULT_WINDOW_MS,
209
232
  bucketMs: TIME_SERIES_DEFAULT_BUCKET_MS,
@@ -282,6 +305,62 @@ function toNonEmptyString(value: unknown): string | null {
282
305
  return trimmed.length > 0 ? trimmed : null;
283
306
  }
284
307
 
308
+ function toToolCallSummary(value: unknown): ToolCallSummary | null {
309
+ if (!value || typeof value !== "object") return null;
310
+ const rec = value as Record<string, unknown>;
311
+
312
+ const messageId = toNonEmptyString(rec.messageId ?? rec.message_id);
313
+ const callId = toNonEmptyString(rec.callId ?? rec.call_id);
314
+ const tool = toNonEmptyString(rec.tool);
315
+ const status = toNonEmptyString(rec.status) ?? "unknown";
316
+ const createdAtRaw = toFiniteNumber(rec.createdAtMs ?? rec.created_at_ms ?? rec.createdAt ?? rec.created_at);
317
+
318
+ if (!messageId || !callId || !tool) return null;
319
+
320
+ return {
321
+ sessionId: toNonEmptyString(rec.sessionId ?? rec.session_id) ?? undefined,
322
+ messageId,
323
+ callId,
324
+ tool,
325
+ status,
326
+ createdAtMs: typeof createdAtRaw === "number" ? Math.floor(createdAtRaw) : null,
327
+ };
328
+ }
329
+
330
+ function toToolCallsResponse(value: unknown): ToolCallsResponse | null {
331
+ if (!value || typeof value !== "object") return null;
332
+ const rec = value as Record<string, unknown>;
333
+
334
+ const ok = typeof rec.ok === "boolean" ? rec.ok : false;
335
+ const sessionId = toNonEmptyString(rec.sessionId ?? rec.session_id);
336
+ const toolCallsRaw = rec.toolCalls ?? rec.tool_calls;
337
+
338
+ if (!sessionId || !Array.isArray(toolCallsRaw)) return null;
339
+
340
+ const toolCalls: ToolCallSummary[] = toolCallsRaw
341
+ .map(toToolCallSummary)
342
+ .filter((t): t is ToolCallSummary => t !== null);
343
+
344
+ const capsRaw = rec.caps;
345
+ const capsObj = capsRaw && typeof capsRaw === "object" ? (capsRaw as Record<string, unknown>) : null;
346
+ const maxMessagesRaw = capsObj ? toFiniteNumber(capsObj.maxMessages ?? capsObj.max_messages) : null;
347
+ const maxToolCallsRaw = capsObj ? toFiniteNumber(capsObj.maxToolCalls ?? capsObj.max_tool_calls) : null;
348
+ const caps =
349
+ typeof maxMessagesRaw === "number" && typeof maxToolCallsRaw === "number"
350
+ ? { maxMessages: Math.max(0, Math.floor(maxMessagesRaw)), maxToolCalls: Math.max(0, Math.floor(maxToolCallsRaw)) }
351
+ : undefined;
352
+
353
+ const truncated = typeof rec.truncated === "boolean" ? rec.truncated : undefined;
354
+
355
+ return {
356
+ ok,
357
+ sessionId,
358
+ toolCalls,
359
+ caps,
360
+ truncated,
361
+ };
362
+ }
363
+
285
364
  export function formatBackgroundTaskTimelineCell(status: unknown, timeline: unknown): string {
286
365
  const s = typeof status === "string" ? status.trim().toLowerCase() : "";
287
366
  if (s === "unknown") return "";
@@ -290,6 +369,50 @@ export function formatBackgroundTaskTimelineCell(status: unknown, timeline: unkn
290
369
  return toNonEmptyString(timeline) ?? "-";
291
370
  }
292
371
 
372
+ export function computeToolCallsFetchPlan(params: {
373
+ sessionId: string | null;
374
+ status: string;
375
+ cachedState: "idle" | "loading" | "ok" | "error" | null;
376
+ cachedDataOk: boolean;
377
+ isExpanded: boolean;
378
+ }): { shouldFetch: boolean; force: boolean } {
379
+ const { sessionId, status, cachedState, cachedDataOk, isExpanded } = params;
380
+
381
+ if (!sessionId) {
382
+ return { shouldFetch: false, force: false };
383
+ }
384
+
385
+ if (!isExpanded) {
386
+ return { shouldFetch: false, force: false };
387
+ }
388
+
389
+ const isRunning = String(status ?? "").toLowerCase().trim() === "running";
390
+
391
+ if (isRunning) {
392
+ return { shouldFetch: true, force: true };
393
+ }
394
+
395
+ if (cachedDataOk) {
396
+ return { shouldFetch: false, force: false };
397
+ }
398
+
399
+ if (cachedState === "loading") {
400
+ return { shouldFetch: false, force: false };
401
+ }
402
+
403
+ return { shouldFetch: true, force: false };
404
+ }
405
+
406
+ export function toggleIdInSet(id: string, currentSet: Set<string>): Set<string> {
407
+ const next = new Set(currentSet);
408
+ if (next.has(id)) {
409
+ next.delete(id);
410
+ } else {
411
+ next.add(id);
412
+ }
413
+ return next;
414
+ }
415
+
293
416
  function toDashboardPayload(json: unknown): DashboardPayload {
294
417
  if (!json || typeof json !== "object") {
295
418
  return { ...FALLBACK_DATA, raw: json };
@@ -300,6 +423,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
300
423
  const main = (anyJson.mainSession ?? anyJson.main_session ?? {}) as Record<string, unknown>;
301
424
  const plan = (anyJson.planProgress ?? anyJson.plan_progress ?? {}) as Record<string, unknown>;
302
425
  const tasks = (anyJson.backgroundTasks ?? anyJson.background_tasks ?? []) as unknown;
426
+ const mainTasks = (anyJson.mainSessionTasks ?? anyJson.main_session_tasks ?? []) as unknown;
303
427
 
304
428
  function parsePlanSteps(stepsInput: unknown): Array<{ checked: boolean; text: string }> {
305
429
  if (!Array.isArray(stepsInput)) return [];
@@ -331,6 +455,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
331
455
  : undefined,
332
456
  agent: String(rec.agent ?? rec.worker ?? "unknown"),
333
457
  lastModel: toNonEmptyString(rec.lastModel ?? rec.last_model) ?? "-",
458
+ sessionId: toNonEmptyString(rec.sessionId ?? rec.session_id),
334
459
  status: String(rec.status ?? "queued"),
335
460
  toolCalls: Number(rec.toolCalls ?? rec.tool_calls ?? 0) || 0,
336
461
  lastTool: String(rec.lastTool ?? rec.last_tool ?? "-") || "-",
@@ -339,6 +464,29 @@ function toDashboardPayload(json: unknown): DashboardPayload {
339
464
  })
340
465
  : FALLBACK_DATA.backgroundTasks;
341
466
 
467
+ const mainSessionTasks: BackgroundTask[] = Array.isArray(mainTasks)
468
+ ? mainTasks.map((t, idx) => {
469
+ const rec = (t ?? {}) as Record<string, unknown>;
470
+ return {
471
+ id: String(rec.id ?? rec.taskId ?? rec.task_id ?? `main-task-${idx + 1}`),
472
+ description: String(rec.description ?? rec.name ?? "(no description)"),
473
+ subline:
474
+ typeof rec.subline === "string"
475
+ ? rec.subline
476
+ : typeof rec.taskId === "string"
477
+ ? rec.taskId
478
+ : undefined,
479
+ agent: String(rec.agent ?? rec.worker ?? "unknown"),
480
+ lastModel: toNonEmptyString(rec.lastModel ?? rec.last_model) ?? "-",
481
+ sessionId: toNonEmptyString(rec.sessionId ?? rec.session_id),
482
+ status: String(rec.status ?? "queued"),
483
+ toolCalls: Number(rec.toolCalls ?? rec.tool_calls ?? 0) || 0,
484
+ lastTool: String(rec.lastTool ?? rec.last_tool ?? "-") || "-",
485
+ timeline: String(rec.timeline ?? "") || "",
486
+ };
487
+ })
488
+ : [];
489
+
342
490
  const completed = Number(plan.completed ?? plan.done ?? 0) || 0;
343
491
  const total = Number(plan.total ?? plan.count ?? 0) || 0;
344
492
  const steps = parsePlanSteps(plan.steps);
@@ -352,6 +500,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
352
500
  currentModel: toNonEmptyString(main.currentModel ?? main.current_model) ?? "-",
353
501
  lastUpdatedLabel: String(main.lastUpdatedLabel ?? main.last_updated ?? "just now"),
354
502
  session: String(main.session ?? main.session_id ?? FALLBACK_DATA.mainSession.session),
503
+ sessionId: toNonEmptyString(main.sessionId ?? main.session_id),
355
504
  statusPill: String(main.statusPill ?? main.status ?? FALLBACK_DATA.mainSession.statusPill),
356
505
  },
357
506
  planProgress: {
@@ -363,6 +512,7 @@ function toDashboardPayload(json: unknown): DashboardPayload {
363
512
  steps,
364
513
  },
365
514
  backgroundTasks,
515
+ mainSessionTasks,
366
516
  timeSeries,
367
517
  raw: json,
368
518
  };
@@ -379,6 +529,14 @@ export default function App() {
379
529
  const [planOpen, setPlanOpen] = React.useState(false);
380
530
  const [errorHint, setErrorHint] = React.useState<string | null>(null);
381
531
 
532
+ const [expandedBgTaskIds, setExpandedBgTaskIds] = React.useState<Set<string>>(() => new Set());
533
+ const [expandedMainTaskIds, setExpandedMainTaskIds] = React.useState<Set<string>>(() => new Set());
534
+ const [toolCallsBySession, setToolCallsBySession] = React.useState<
535
+ Map<string, { state: "idle" | "loading" | "ok" | "error"; data: ToolCallsResponse | null; lastFetchedAtMs: number | null }>
536
+ >(() => new Map());
537
+ const toolCallsBySessionRef = React.useRef(toolCallsBySession);
538
+ const toolCallsSeqRef = React.useRef<Map<string, number>>(new Map());
539
+
382
540
  const timerRef = React.useRef<number | null>(null);
383
541
  const hadSuccessRef = React.useRef(false);
384
542
  const soundEnabledRef = React.useRef(false);
@@ -401,6 +559,10 @@ export default function App() {
401
559
  soundEnabledRef.current = soundEnabled;
402
560
  }, [soundEnabled]);
403
561
 
562
+ React.useEffect(() => {
563
+ toolCallsBySessionRef.current = toolCallsBySession;
564
+ }, [toolCallsBySession]);
565
+
404
566
  React.useEffect(() => {
405
567
  try {
406
568
  const raw = window.localStorage.getItem("omoDashboardSoundEnabled");
@@ -580,6 +742,135 @@ export default function App() {
580
742
  return map;
581
743
  }, [data.timeSeries.series]);
582
744
 
745
+ const fetchToolCalls = React.useCallback(async (sessionId: string, opts: { force: boolean }) => {
746
+ const existing = toolCallsBySessionRef.current.get(sessionId);
747
+ if (!opts.force && existing?.data?.ok) return;
748
+
749
+ const seq = (toolCallsSeqRef.current.get(sessionId) ?? 0) + 1;
750
+ toolCallsSeqRef.current.set(sessionId, seq);
751
+
752
+ setToolCallsBySession((prev) => {
753
+ const next = new Map(prev);
754
+ const prior = next.get(sessionId);
755
+ next.set(sessionId, {
756
+ state: "loading",
757
+ data: prior?.data ?? null,
758
+ lastFetchedAtMs: prior?.lastFetchedAtMs ?? null,
759
+ });
760
+ return next;
761
+ });
762
+
763
+ try {
764
+ const raw = await safeFetchJson(`/api/tool-calls/${encodeURIComponent(sessionId)}`);
765
+ const parsed = toToolCallsResponse(raw);
766
+ if (!parsed?.ok) throw new Error("tool calls not ok");
767
+ if (toolCallsSeqRef.current.get(sessionId) !== seq) return;
768
+ setToolCallsBySession((prev) => {
769
+ const next = new Map(prev);
770
+ next.set(sessionId, { state: "ok", data: parsed, lastFetchedAtMs: Date.now() });
771
+ return next;
772
+ });
773
+ } catch {
774
+ if (toolCallsSeqRef.current.get(sessionId) !== seq) return;
775
+ setToolCallsBySession((prev) => {
776
+ const next = new Map(prev);
777
+ const prior = next.get(sessionId);
778
+ next.set(sessionId, {
779
+ state: "error",
780
+ data: prior?.data ?? null,
781
+ lastFetchedAtMs: prior?.lastFetchedAtMs ?? null,
782
+ });
783
+ return next;
784
+ });
785
+ }
786
+ }, []);
787
+
788
+ function toggleBackgroundTaskExpanded(t: BackgroundTask) {
789
+ const nextExpanded = !expandedBgTaskIds.has(t.id);
790
+ setExpandedBgTaskIds((prev) => {
791
+ const next = new Set(prev);
792
+ if (nextExpanded) next.add(t.id);
793
+ else next.delete(t.id);
794
+ return next;
795
+ });
796
+
797
+ if (!nextExpanded) return;
798
+
799
+ const sessionId = toNonEmptyString(t.sessionId);
800
+ if (!sessionId) return;
801
+
802
+ const isRunning = String(t.status ?? "").toLowerCase().trim() === "running";
803
+ const cached = toolCallsBySessionRef.current.get(sessionId);
804
+ if (isRunning) {
805
+ void fetchToolCalls(sessionId, { force: true });
806
+ return;
807
+ }
808
+
809
+ if (cached?.data?.ok) return;
810
+ if (cached?.state === "loading") return;
811
+ void fetchToolCalls(sessionId, { force: false });
812
+ }
813
+
814
+ function toggleMainTaskExpanded(t: BackgroundTask) {
815
+ const nextExpanded = !expandedMainTaskIds.has(t.id);
816
+ setExpandedMainTaskIds((prev) => {
817
+ const next = new Set(prev);
818
+ if (nextExpanded) next.add(t.id);
819
+ else next.delete(t.id);
820
+ return next;
821
+ });
822
+
823
+ if (!nextExpanded) return;
824
+
825
+ const sessionId = toNonEmptyString(t.sessionId);
826
+ if (!sessionId) return;
827
+
828
+ const isRunning = String(t.status ?? "").toLowerCase().trim() === "running";
829
+ const cached = toolCallsBySessionRef.current.get(sessionId);
830
+ if (isRunning) {
831
+ void fetchToolCalls(sessionId, { force: true });
832
+ return;
833
+ }
834
+
835
+ if (cached?.data?.ok) return;
836
+ if (cached?.state === "loading") return;
837
+ void fetchToolCalls(sessionId, { force: false });
838
+ }
839
+
840
+ React.useEffect(() => {
841
+ if (!connected) return;
842
+
843
+ for (const t of data.backgroundTasks) {
844
+ const sessionId = toNonEmptyString(t.sessionId);
845
+ const cached = sessionId ? toolCallsBySessionRef.current.get(sessionId) : null;
846
+ const plan = computeToolCallsFetchPlan({
847
+ sessionId,
848
+ status: t.status,
849
+ cachedState: cached?.state ?? null,
850
+ cachedDataOk: Boolean(cached?.data?.ok),
851
+ isExpanded: expandedBgTaskIds.has(t.id),
852
+ });
853
+ if (plan.shouldFetch && sessionId) {
854
+ void fetchToolCalls(sessionId, { force: plan.force });
855
+ }
856
+ }
857
+
858
+ for (const t of data.mainSessionTasks) {
859
+ const sessionId = toNonEmptyString(t.sessionId);
860
+ const cached = sessionId ? toolCallsBySessionRef.current.get(sessionId) : null;
861
+ const plan = computeToolCallsFetchPlan({
862
+ sessionId,
863
+ status: t.status,
864
+ cachedState: cached?.state ?? null,
865
+ cachedDataOk: Boolean(cached?.data?.ok),
866
+ isExpanded: expandedMainTaskIds.has(t.id),
867
+ });
868
+ if (plan.shouldFetch && sessionId) {
869
+ void fetchToolCalls(sessionId, { force: plan.force });
870
+ }
871
+ }
872
+ }, [connected, data.backgroundTasks, data.mainSessionTasks, expandedBgTaskIds, expandedMainTaskIds, fetchToolCalls]);
873
+
583
874
  const buckets = Math.max(1, data.timeSeries.buckets);
584
875
  const bucketMs = Math.max(1, data.timeSeries.bucketMs);
585
876
  const viewBox = `0 0 ${buckets} 28`;
@@ -910,6 +1201,126 @@ export default function App() {
910
1201
  </article>
911
1202
  </section>
912
1203
 
1204
+ <section className="card">
1205
+ <div className="cardHeader">
1206
+ <h2>Main session tasks</h2>
1207
+ <span className="badge">{data.mainSessionTasks.length}</span>
1208
+ </div>
1209
+
1210
+ <div className="tableWrap">
1211
+ <table className="table">
1212
+ <thead>
1213
+ <tr>
1214
+ <th>DESCRIPTION</th>
1215
+ <th>AGENT</th>
1216
+ <th>LAST MODEL</th>
1217
+ <th>STATUS</th>
1218
+ <th>TOOL CALLS</th>
1219
+ <th>LAST TOOL</th>
1220
+ <th>TIMELINE</th>
1221
+ </tr>
1222
+ </thead>
1223
+ <tbody>
1224
+ {data.mainSessionTasks.length === 0 ? (
1225
+ <tr>
1226
+ <td colSpan={7} className="muted" style={{ padding: 16 }}>
1227
+ No main session tasks detected yet.
1228
+ </td>
1229
+ </tr>
1230
+ ) : null}
1231
+ {data.mainSessionTasks.map((t) => {
1232
+ const expanded = expandedMainTaskIds.has(t.id);
1233
+ const sessionId = toNonEmptyString(t.sessionId);
1234
+ const detailId = `main-toolcalls-${t.id}`;
1235
+ const entry = sessionId ? toolCallsBySession.get(sessionId) : null;
1236
+ const toolCalls = entry?.data?.ok ? entry.data.toolCalls : [];
1237
+ const showCapped = Boolean(entry?.data?.truncated);
1238
+ const caps = entry?.data?.caps;
1239
+ const showLoading = entry?.state === "loading";
1240
+ const showError = entry?.state === "error" && !entry?.data?.ok;
1241
+ const empty = sessionId ? toolCalls.length === 0 && !showLoading && !showError : true;
1242
+
1243
+ return (
1244
+ <React.Fragment key={t.id}>
1245
+ <tr>
1246
+ <td>
1247
+ <div className="bgTaskRowTitleWrap">
1248
+ <button
1249
+ type="button"
1250
+ className="bgTaskToggle"
1251
+ onClick={() => toggleMainTaskExpanded(t)}
1252
+ aria-expanded={expanded}
1253
+ aria-controls={detailId}
1254
+ title={expanded ? "Collapse" : "Expand"}
1255
+ aria-label={expanded ? "Collapse tool calls" : "Expand tool calls"}
1256
+ />
1257
+ <div className="bgTaskRowTitleText">
1258
+ <div className="taskTitle">{t.description}</div>
1259
+ {t.subline ? <div className="taskSub mono">{t.subline}</div> : null}
1260
+ </div>
1261
+ </div>
1262
+ </td>
1263
+ <td className="mono">{t.agent}</td>
1264
+ <td className="mono">{t.lastModel}</td>
1265
+ <td>
1266
+ <span className={`pill pill-${statusTone(t.status)}`}>{t.status}</span>
1267
+ </td>
1268
+ <td className="mono">{t.toolCalls}</td>
1269
+ <td className="mono">{t.lastTool}</td>
1270
+ <td className="mono muted">{formatBackgroundTaskTimelineCell(t.status, t.timeline)}</td>
1271
+ </tr>
1272
+
1273
+ {expanded ? (
1274
+ <tr>
1275
+ <td colSpan={7} className="bgTaskDetailCell">
1276
+ <section id={detailId} aria-label="Tool calls" className="bgTaskDetail">
1277
+ <div className="mono muted bgTaskDetailHeader">
1278
+ Tool calls (metadata only){showLoading && toolCalls.length > 0 ? " - refreshing" : ""}
1279
+ {showCapped
1280
+ ? ` - capped${caps ? ` (max ${caps.maxMessages} messages / ${caps.maxToolCalls} tool calls)` : ""}`
1281
+ : ""}
1282
+ </div>
1283
+
1284
+ {!sessionId ? (
1285
+ <div className="muted bgTaskDetailEmpty">No session id available for this task.</div>
1286
+ ) : showError ? (
1287
+ <div className="muted bgTaskDetailEmpty">Tool calls unavailable.</div>
1288
+ ) : showLoading && toolCalls.length === 0 ? (
1289
+ <div className="muted bgTaskDetailEmpty">Loading tool calls...</div>
1290
+ ) : empty ? (
1291
+ <div className="muted bgTaskDetailEmpty">No tool calls recorded.</div>
1292
+ ) : (
1293
+ <div className="bgTaskToolCallsGrid">
1294
+ {toolCalls.map((c) => (
1295
+ <div key={c.callId} className="bgTaskToolCall">
1296
+ <div className="bgTaskToolCallRow">
1297
+ <div className="mono bgTaskToolCallTool" title={c.tool}>
1298
+ {c.tool}
1299
+ </div>
1300
+ <div className="mono muted bgTaskToolCallStatus" title={c.status}>
1301
+ {c.status}
1302
+ </div>
1303
+ </div>
1304
+ <div className="mono muted bgTaskToolCallTime">{formatTime(c.createdAtMs)}</div>
1305
+ <div className="mono muted bgTaskToolCallId" title={c.callId}>
1306
+ {c.callId}
1307
+ </div>
1308
+ </div>
1309
+ ))}
1310
+ </div>
1311
+ )}
1312
+ </section>
1313
+ </td>
1314
+ </tr>
1315
+ ) : null}
1316
+ </React.Fragment>
1317
+ );
1318
+ })}
1319
+ </tbody>
1320
+ </table>
1321
+ </div>
1322
+ </section>
1323
+
913
1324
  <section className="card">
914
1325
  <div className="cardHeader">
915
1326
  <h2>Background tasks</h2>
@@ -931,22 +1342,109 @@ export default function App() {
931
1342
  </tr>
932
1343
  </thead>
933
1344
  <tbody>
934
- {data.backgroundTasks.map((t) => (
935
- <tr key={t.id}>
936
- <td>
937
- <div className="taskTitle">{t.description}</div>
938
- {t.subline ? <div className="taskSub mono">{t.subline}</div> : null}
939
- </td>
940
- <td className="mono">{t.agent}</td>
941
- <td className="mono">{t.lastModel}</td>
942
- <td>
943
- <span className={`pill pill-${statusTone(t.status)}`}>{t.status}</span>
1345
+ {data.backgroundTasks.length === 0 ? (
1346
+ <tr>
1347
+ <td colSpan={7} className="muted" style={{ padding: 16 }}>
1348
+ No background tasks detected yet. When you run background agents, they will appear here.
944
1349
  </td>
945
- <td className="mono">{t.toolCalls}</td>
946
- <td className="mono">{t.lastTool}</td>
947
- <td className="mono muted">{formatBackgroundTaskTimelineCell(t.status, t.timeline)}</td>
948
1350
  </tr>
949
- ))}
1351
+ ) : null}
1352
+ {data.backgroundTasks.map((t) => {
1353
+ const expanded = expandedBgTaskIds.has(t.id);
1354
+ const sessionId = toNonEmptyString(t.sessionId);
1355
+ const detailId = `bg-toolcalls-${t.id}`;
1356
+ const entry = sessionId ? toolCallsBySession.get(sessionId) : null;
1357
+ const toolCalls = entry?.data?.ok ? entry.data.toolCalls : [];
1358
+ const showCapped = Boolean(entry?.data?.truncated);
1359
+ const caps = entry?.data?.caps;
1360
+ const showLoading = entry?.state === "loading";
1361
+ const showError = entry?.state === "error" && !entry?.data?.ok;
1362
+ const empty = sessionId ? toolCalls.length === 0 && !showLoading && !showError : true;
1363
+
1364
+ return (
1365
+ <React.Fragment key={t.id}>
1366
+ <tr>
1367
+ <td>
1368
+ <div className="bgTaskRowTitleWrap">
1369
+ <button
1370
+ type="button"
1371
+ className="bgTaskToggle"
1372
+ onClick={() => toggleBackgroundTaskExpanded(t)}
1373
+ aria-expanded={expanded}
1374
+ aria-controls={detailId}
1375
+ title={expanded ? "Collapse" : "Expand"}
1376
+ aria-label={expanded ? "Collapse tool calls" : "Expand tool calls"}
1377
+ />
1378
+ <div className="bgTaskRowTitleText">
1379
+ <div className="taskTitle">{t.description}</div>
1380
+ {t.subline ? <div className="taskSub mono">{t.subline}</div> : null}
1381
+ </div>
1382
+ </div>
1383
+ </td>
1384
+ <td className="mono">{t.agent}</td>
1385
+ <td className="mono">{t.lastModel}</td>
1386
+ <td>
1387
+ <span className={`pill pill-${statusTone(t.status)}`}>{t.status}</span>
1388
+ </td>
1389
+ <td className="mono">{t.toolCalls}</td>
1390
+ <td className="mono">{t.lastTool}</td>
1391
+ <td className="mono muted">{formatBackgroundTaskTimelineCell(t.status, t.timeline)}</td>
1392
+ </tr>
1393
+
1394
+ {expanded ? (
1395
+ <tr>
1396
+ <td colSpan={7} className="bgTaskDetailCell">
1397
+ <section id={detailId} aria-label="Tool calls" className="bgTaskDetail">
1398
+ <div className="mono muted bgTaskDetailHeader">
1399
+ Tool calls (metadata only){showLoading && toolCalls.length > 0 ? " - refreshing" : ""}
1400
+ {showCapped
1401
+ ? ` - capped${caps ? ` (max ${caps.maxMessages} messages / ${caps.maxToolCalls} tool calls)` : ""}`
1402
+ : ""}
1403
+ </div>
1404
+
1405
+ {!sessionId ? (
1406
+ <div className="muted bgTaskDetailEmpty">
1407
+ No session id available for this task.
1408
+ </div>
1409
+ ) : showError ? (
1410
+ <div className="muted bgTaskDetailEmpty">
1411
+ Tool calls unavailable.
1412
+ </div>
1413
+ ) : showLoading && toolCalls.length === 0 ? (
1414
+ <div className="muted bgTaskDetailEmpty">
1415
+ Loading tool calls...
1416
+ </div>
1417
+ ) : empty ? (
1418
+ <div className="muted bgTaskDetailEmpty">
1419
+ No tool calls recorded.
1420
+ </div>
1421
+ ) : (
1422
+ <div className="bgTaskToolCallsGrid">
1423
+ {toolCalls.map((c) => (
1424
+ <div key={c.callId} className="bgTaskToolCall">
1425
+ <div className="bgTaskToolCallRow">
1426
+ <div className="mono bgTaskToolCallTool" title={c.tool}>
1427
+ {c.tool}
1428
+ </div>
1429
+ <div className="mono muted bgTaskToolCallStatus" title={c.status}>
1430
+ {c.status}
1431
+ </div>
1432
+ </div>
1433
+ <div className="mono muted bgTaskToolCallTime">{formatTime(c.createdAtMs)}</div>
1434
+ <div className="mono muted bgTaskToolCallId" title={c.callId}>
1435
+ {c.callId}
1436
+ </div>
1437
+ </div>
1438
+ ))}
1439
+ </div>
1440
+ )}
1441
+ </section>
1442
+ </td>
1443
+ </tr>
1444
+ ) : null}
1445
+ </React.Fragment>
1446
+ );
1447
+ })}
950
1448
  </tbody>
951
1449
  </table>
952
1450
  </div>