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/README.md +32 -4
- package/dist/assets/index-B1tQFDjw.js +40 -0
- package/dist/assets/index-BFRahC0d.css +1 -0
- package/dist/index.html +3 -3
- package/package.json +2 -2
- package/src/App.tsx +486 -214
- package/src/app-payload.test.ts +108 -1
- package/src/server/api.test.ts +28 -7
- package/src/server/api.ts +1 -1
- package/src/server/dashboard.test.ts +41 -0
- package/src/server/dashboard.ts +79 -0
- package/src/server/dev.ts +1 -1
- package/src/server/start.ts +1 -1
- package/src/styles.css +58 -0
- package/src/time-series-ui.test.tsx +111 -0
- package/src/timeseries-stacked.test.ts +36 -24
- package/src/timeseries-stacked.ts +21 -5
- package/dist/assets/index--GqzhA4-.css +0 -1
- package/dist/assets/index-CiC6k4Yg.js +0 -40
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
|
-
|
|
710
|
-
const existing =
|
|
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 =
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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);
|