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/dist/assets/index--GqzhA4-.css +1 -0
- package/dist/assets/index-CiC6k4Yg.js +40 -0
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/App.tsx +443 -56
- package/src/app-payload.test.ts +158 -0
- package/src/background-task-timeline.test.ts +32 -0
- package/src/background-task-toolcalls-policy.test.ts +191 -0
- package/src/ingest/background-tasks.test.ts +304 -2
- package/src/ingest/background-tasks.ts +67 -28
- package/src/ingest/model.ts +79 -0
- package/src/ingest/session.test.ts +119 -0
- package/src/ingest/session.ts +4 -0
- package/src/ingest/tool-calls.test.ts +161 -0
- package/src/ingest/tool-calls.ts +157 -0
- package/src/server/api.test.ts +162 -53
- package/src/server/api.ts +39 -2
- package/src/server/dashboard.test.ts +139 -0
- package/src/server/dashboard.ts +40 -3
- package/src/server/dev.ts +4 -2
- package/src/server/start.ts +4 -2
- package/src/styles.css +131 -0
- package/src/timeseries-stacked.test.ts +261 -0
- package/src/timeseries-stacked.ts +145 -0
- package/dist/assets/index-Cs5xePn_.js +0 -40
- package/dist/assets/index-RAZRO3YN.css +0 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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-${
|
|
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-${
|
|
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={
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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>
|