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/README.md +15 -4
- package/dist/assets/index-BFRahC0d.css +1 -0
- package/dist/assets/index-BsLpOGvG.js +40 -0
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/App.tsx +512 -14
- package/src/app-payload.test.ts +108 -1
- package/src/background-task-toolcalls-policy.test.ts +191 -0
- package/src/ingest/background-tasks.test.ts +11 -2
- package/src/ingest/tool-calls.test.ts +161 -0
- package/src/ingest/tool-calls.ts +157 -0
- package/src/server/api.test.ts +175 -53
- package/src/server/api.ts +39 -2
- package/src/server/dashboard.test.ts +41 -0
- package/src/server/dashboard.ts +81 -0
- package/src/server/dev.ts +4 -2
- package/src/server/start.ts +4 -2
- package/src/styles.css +189 -0
- package/dist/assets/index-CZM2MUUs.js +0 -40
- package/dist/assets/index-RAZRO3YN.css +0 -1
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.
|
|
935
|
-
<tr
|
|
936
|
-
<td>
|
|
937
|
-
|
|
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>
|