oh-my-opencode-dashboard 0.1.0 → 0.1.2
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 +18 -3
- package/dist/assets/index-BUGGDzan.js +40 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/App.tsx +261 -212
- package/src/server/api.test.ts +9 -1
- 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-BsLpOGvG.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;
|
|
@@ -732,15 +992,6 @@ export default function App() {
|
|
|
732
992
|
const liveLabel = connected ? "Live" : "Disconnected";
|
|
733
993
|
const liveTone = connected ? "teal" : "sand";
|
|
734
994
|
|
|
735
|
-
const timeSeriesById = React.useMemo(() => {
|
|
736
|
-
const map = new Map<TimeSeriesSeriesId, TimeSeriesSeries>();
|
|
737
|
-
for (const s of data.timeSeries.series) {
|
|
738
|
-
if (s && typeof s.id === "string") {
|
|
739
|
-
map.set(s.id, s);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return map;
|
|
743
|
-
}, [data.timeSeries.series]);
|
|
744
995
|
|
|
745
996
|
const fetchToolCalls = React.useCallback(async (sessionId: string, opts: { force: boolean }) => {
|
|
746
997
|
const existing = toolCallsBySessionRef.current.get(sessionId);
|
|
@@ -871,14 +1122,6 @@ export default function App() {
|
|
|
871
1122
|
}
|
|
872
1123
|
}, [connected, data.backgroundTasks, data.mainSessionTasks, expandedBgTaskIds, expandedMainTaskIds, fetchToolCalls]);
|
|
873
1124
|
|
|
874
|
-
const buckets = Math.max(1, data.timeSeries.buckets);
|
|
875
|
-
const bucketMs = Math.max(1, data.timeSeries.bucketMs);
|
|
876
|
-
const viewBox = `0 0 ${buckets} 28`;
|
|
877
|
-
const minuteStep = Math.max(1, Math.round(60_000 / bucketMs));
|
|
878
|
-
const bucketStartMs = data.timeSeries.anchorMs - (buckets - 1) * bucketMs;
|
|
879
|
-
|
|
880
|
-
const overallValues = timeSeriesById.get("overall-main")?.values ?? [];
|
|
881
|
-
|
|
882
1125
|
return (
|
|
883
1126
|
<div className="page">
|
|
884
1127
|
<div className="container">
|
|
@@ -923,201 +1166,7 @@ export default function App() {
|
|
|
923
1166
|
</header>
|
|
924
1167
|
|
|
925
1168
|
<main className="stack">
|
|
926
|
-
<
|
|
927
|
-
<div className="timeSeriesHeader">
|
|
928
|
-
<h2 className="timeSeriesTitle">Time-series activity</h2>
|
|
929
|
-
<p className="timeSeriesSub">Last 5 minutes</p>
|
|
930
|
-
</div>
|
|
931
|
-
|
|
932
|
-
<div className="timeSeriesAxisTop" aria-hidden="true">
|
|
933
|
-
<div />
|
|
934
|
-
<div className="timeSeriesAxisTopLabels">
|
|
935
|
-
<span className="timeSeriesAxisTopLabel">-5m</span>
|
|
936
|
-
<span className="timeSeriesAxisTopLabel">-4m</span>
|
|
937
|
-
<span className="timeSeriesAxisTopLabel">-3m</span>
|
|
938
|
-
<span className="timeSeriesAxisTopLabel">-1m</span>
|
|
939
|
-
</div>
|
|
940
|
-
</div>
|
|
941
|
-
|
|
942
|
-
<div className="timeSeriesRows">
|
|
943
|
-
{(
|
|
944
|
-
[
|
|
945
|
-
{
|
|
946
|
-
kind: "main-agents" as const,
|
|
947
|
-
label: "Main agents" as const,
|
|
948
|
-
},
|
|
949
|
-
{
|
|
950
|
-
kind: "single" as const,
|
|
951
|
-
label: "background tasks (total)",
|
|
952
|
-
tone: "muted" as const,
|
|
953
|
-
overlayId: "background-total" as const,
|
|
954
|
-
baseline: false,
|
|
955
|
-
},
|
|
956
|
-
] as const
|
|
957
|
-
).map((row) => {
|
|
958
|
-
const H = 28;
|
|
959
|
-
const padTop = 2;
|
|
960
|
-
const padBottom = 2;
|
|
961
|
-
const chartHeight = H - padTop - padBottom;
|
|
962
|
-
const baselineY = H - padBottom;
|
|
963
|
-
const barW = 0.85;
|
|
964
|
-
const barInset = (1 - barW) / 2;
|
|
965
|
-
|
|
966
|
-
if (row.kind === "main-agents") {
|
|
967
|
-
const sisyphusValues = timeSeriesById.get("agent:sisyphus")?.values ?? [];
|
|
968
|
-
const prometheusValues = timeSeriesById.get("agent:prometheus")?.values ?? [];
|
|
969
|
-
const atlasValues = timeSeriesById.get("agent:atlas")?.values ?? [];
|
|
970
|
-
|
|
971
|
-
let sumMax = 0;
|
|
972
|
-
for (let i = 0; i < buckets; i++) {
|
|
973
|
-
const rawSis = sisyphusValues[i];
|
|
974
|
-
const rawPro = prometheusValues[i];
|
|
975
|
-
const rawAtl = atlasValues[i];
|
|
976
|
-
const sis = typeof rawSis === "number" && Number.isFinite(rawSis) ? Math.max(0, rawSis) : 0;
|
|
977
|
-
const pro = typeof rawPro === "number" && Number.isFinite(rawPro) ? Math.max(0, rawPro) : 0;
|
|
978
|
-
const atl = typeof rawAtl === "number" && Number.isFinite(rawAtl) ? Math.max(0, rawAtl) : 0;
|
|
979
|
-
const s = sis + pro + atl;
|
|
980
|
-
if (s > sumMax) sumMax = s;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
const scaleMax = Math.max(1, sumMax || 1);
|
|
984
|
-
|
|
985
|
-
return (
|
|
986
|
-
<div key="main-agents" className="timeSeriesRow">
|
|
987
|
-
<div className="timeSeriesRowLabel">{row.label}</div>
|
|
988
|
-
<div className="timeSeriesSvgWrap">
|
|
989
|
-
<svg className="timeSeriesSvg" viewBox={viewBox} preserveAspectRatio="none" aria-hidden="true">
|
|
990
|
-
{Array.from({ length: Math.floor(buckets / minuteStep) + 1 }, (_, idx) => {
|
|
991
|
-
const x = idx * minuteStep;
|
|
992
|
-
if (x < 0 || x > buckets) return null;
|
|
993
|
-
return (
|
|
994
|
-
<line
|
|
995
|
-
key={`g-${bucketStartMs + x * bucketMs}`}
|
|
996
|
-
className="timeSeriesGridline"
|
|
997
|
-
x1={x}
|
|
998
|
-
x2={x}
|
|
999
|
-
y1={0}
|
|
1000
|
-
y2={H}
|
|
1001
|
-
/>
|
|
1002
|
-
);
|
|
1003
|
-
})}
|
|
1004
|
-
|
|
1005
|
-
{Array.from({ length: buckets }, (_, i) => {
|
|
1006
|
-
const bucketMsAt = bucketStartMs + i * bucketMs;
|
|
1007
|
-
const barX = i + barInset;
|
|
1008
|
-
const rawSis = sisyphusValues[i];
|
|
1009
|
-
const rawPro = prometheusValues[i];
|
|
1010
|
-
const rawAtl = atlasValues[i];
|
|
1011
|
-
const sis = typeof rawSis === "number" && Number.isFinite(rawSis) ? Math.max(0, rawSis) : 0;
|
|
1012
|
-
const pro = typeof rawPro === "number" && Number.isFinite(rawPro) ? Math.max(0, rawPro) : 0;
|
|
1013
|
-
const atl = typeof rawAtl === "number" && Number.isFinite(rawAtl) ? Math.max(0, rawAtl) : 0;
|
|
1014
|
-
const segments = computeStackedSegments(
|
|
1015
|
-
{
|
|
1016
|
-
sisyphus: sis,
|
|
1017
|
-
prometheus: pro,
|
|
1018
|
-
atlas: atl,
|
|
1019
|
-
},
|
|
1020
|
-
scaleMax,
|
|
1021
|
-
chartHeight
|
|
1022
|
-
);
|
|
1023
|
-
|
|
1024
|
-
if (segments.length === 0) return null;
|
|
1025
|
-
return segments.map((seg) => (
|
|
1026
|
-
<rect
|
|
1027
|
-
key={`main-agents-${bucketMsAt}-${seg.tone}`}
|
|
1028
|
-
className={`timeSeriesBar timeSeriesBar--${seg.tone}`}
|
|
1029
|
-
x={barX}
|
|
1030
|
-
y={padTop + seg.y}
|
|
1031
|
-
width={barW}
|
|
1032
|
-
height={seg.height}
|
|
1033
|
-
/>
|
|
1034
|
-
));
|
|
1035
|
-
})}
|
|
1036
|
-
</svg>
|
|
1037
|
-
</div>
|
|
1038
|
-
</div>
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const overlayValues = timeSeriesById.get(row.overlayId)?.values ?? [];
|
|
1043
|
-
const baselineMax = row.baseline ? maxCount(overallValues) : 0;
|
|
1044
|
-
const overlayMax = maxCount(overlayValues);
|
|
1045
|
-
const scaleMax = Math.max(1, row.baseline ? Math.max(baselineMax, overlayMax) : overlayMax || 1);
|
|
1046
|
-
|
|
1047
|
-
return (
|
|
1048
|
-
<div key={row.overlayId} className="timeSeriesRow" data-tone={row.tone}>
|
|
1049
|
-
<div className="timeSeriesRowLabel">{row.label}</div>
|
|
1050
|
-
<div className="timeSeriesSvgWrap">
|
|
1051
|
-
<svg className="timeSeriesSvg" viewBox={viewBox} preserveAspectRatio="none" aria-hidden="true">
|
|
1052
|
-
{Array.from({ length: Math.floor(buckets / minuteStep) + 1 }, (_, idx) => {
|
|
1053
|
-
const x = idx * minuteStep;
|
|
1054
|
-
if (x < 0 || x > buckets) return null;
|
|
1055
|
-
return (
|
|
1056
|
-
<line
|
|
1057
|
-
key={`g-${bucketStartMs + x * bucketMs}`}
|
|
1058
|
-
className="timeSeriesGridline"
|
|
1059
|
-
x1={x}
|
|
1060
|
-
x2={x}
|
|
1061
|
-
y1={0}
|
|
1062
|
-
y2={H}
|
|
1063
|
-
/>
|
|
1064
|
-
);
|
|
1065
|
-
})}
|
|
1066
|
-
|
|
1067
|
-
{row.baseline
|
|
1068
|
-
? overallValues.slice(0, buckets).map((v, i) => {
|
|
1069
|
-
const h = barHeight(v ?? 0, scaleMax, chartHeight);
|
|
1070
|
-
if (!h) return null;
|
|
1071
|
-
const barX = i + barInset;
|
|
1072
|
-
const bucketMsAt = bucketStartMs + i * bucketMs;
|
|
1073
|
-
return (
|
|
1074
|
-
<rect
|
|
1075
|
-
key={`b-${bucketMsAt}`}
|
|
1076
|
-
className="timeSeriesBarBaseline"
|
|
1077
|
-
x={barX}
|
|
1078
|
-
y={baselineY - h}
|
|
1079
|
-
width={barW}
|
|
1080
|
-
height={h}
|
|
1081
|
-
/>
|
|
1082
|
-
);
|
|
1083
|
-
})
|
|
1084
|
-
: null}
|
|
1085
|
-
|
|
1086
|
-
{overlayValues.slice(0, buckets).map((v, i) => {
|
|
1087
|
-
const h = barHeight(v ?? 0, scaleMax, chartHeight);
|
|
1088
|
-
if (!h) return null;
|
|
1089
|
-
const barX = i + barInset;
|
|
1090
|
-
const bucketMsAt = bucketStartMs + i * bucketMs;
|
|
1091
|
-
return (
|
|
1092
|
-
<rect
|
|
1093
|
-
key={`${row.overlayId}-${bucketMsAt}`}
|
|
1094
|
-
className="timeSeriesBar"
|
|
1095
|
-
x={barX}
|
|
1096
|
-
y={baselineY - h}
|
|
1097
|
-
width={barW}
|
|
1098
|
-
height={h}
|
|
1099
|
-
/>
|
|
1100
|
-
);
|
|
1101
|
-
})}
|
|
1102
|
-
</svg>
|
|
1103
|
-
</div>
|
|
1104
|
-
</div>
|
|
1105
|
-
);
|
|
1106
|
-
})}
|
|
1107
|
-
</div>
|
|
1108
|
-
|
|
1109
|
-
<div className="timeSeriesAxisBottom" aria-hidden="true">
|
|
1110
|
-
<div />
|
|
1111
|
-
<div className="timeSeriesAxisBottomLabels">
|
|
1112
|
-
<span className="timeSeriesAxisBottomLabel">-5m</span>
|
|
1113
|
-
<span className="timeSeriesAxisBottomLabel">-4m</span>
|
|
1114
|
-
<span className="timeSeriesAxisBottomLabel">-3m</span>
|
|
1115
|
-
<span className="timeSeriesAxisBottomLabel">-2m</span>
|
|
1116
|
-
<span className="timeSeriesAxisBottomLabel">-1m</span>
|
|
1117
|
-
<span className="timeSeriesAxisBottomLabel">Now</span>
|
|
1118
|
-
</div>
|
|
1119
|
-
</div>
|
|
1120
|
-
</section>
|
|
1169
|
+
<TimeSeriesActivitySection timeSeries={data.timeSeries} />
|
|
1121
1170
|
|
|
1122
1171
|
<section className="grid2">
|
|
1123
1172
|
<article className="card">
|
package/src/server/api.test.ts
CHANGED
|
@@ -79,7 +79,15 @@ function hasSensitiveKeys(value: unknown): boolean {
|
|
|
79
79
|
|
|
80
80
|
const createStore = (): DashboardStore => ({
|
|
81
81
|
getSnapshot: (): DashboardPayload => ({
|
|
82
|
-
mainSession: {
|
|
82
|
+
mainSession: {
|
|
83
|
+
agent: "x",
|
|
84
|
+
currentModel: null,
|
|
85
|
+
currentTool: "-",
|
|
86
|
+
lastUpdatedLabel: "never",
|
|
87
|
+
session: "s",
|
|
88
|
+
sessionId: null,
|
|
89
|
+
statusPill: "idle",
|
|
90
|
+
},
|
|
83
91
|
planProgress: { name: "p", completed: 0, total: 0, path: "", statusPill: "not started", steps: [] as PlanStep[] },
|
|
84
92
|
backgroundTasks: [],
|
|
85
93
|
mainSessionTasks: [],
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
TimeSeriesActivitySection,
|
|
7
|
+
computeMainAgentsScaleMax,
|
|
8
|
+
computeOtherMainAgentsCount,
|
|
9
|
+
} from "./App";
|
|
10
|
+
|
|
11
|
+
type TimeSeriesProps = React.ComponentProps<typeof TimeSeriesActivitySection>;
|
|
12
|
+
|
|
13
|
+
function mkTimeSeries(override?: Partial<TimeSeriesProps["timeSeries"]>): TimeSeriesProps["timeSeries"] {
|
|
14
|
+
const buckets = 3;
|
|
15
|
+
const mkSeries = (id: TimeSeriesProps["timeSeries"]["series"][number]["id"], values: number[]) => ({
|
|
16
|
+
id,
|
|
17
|
+
label: id,
|
|
18
|
+
tone: "muted" as const,
|
|
19
|
+
values,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
windowMs: 300_000,
|
|
24
|
+
buckets,
|
|
25
|
+
bucketMs: 2_000,
|
|
26
|
+
anchorMs: 0,
|
|
27
|
+
serverNowMs: 0,
|
|
28
|
+
series: [
|
|
29
|
+
mkSeries("overall-main", [0, 0, 0]),
|
|
30
|
+
mkSeries("agent:sisyphus", [0, 0, 0]),
|
|
31
|
+
mkSeries("agent:prometheus", [0, 0, 0]),
|
|
32
|
+
mkSeries("agent:atlas", [0, 0, 0]),
|
|
33
|
+
mkSeries("background-total", [0, 0, 0]),
|
|
34
|
+
],
|
|
35
|
+
...override,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("TimeSeriesActivitySection (SSR)", () => {
|
|
40
|
+
it("should not render top axis and should keep bottom axis", () => {
|
|
41
|
+
// #given
|
|
42
|
+
const timeSeries = mkTimeSeries();
|
|
43
|
+
|
|
44
|
+
// #when
|
|
45
|
+
const html = renderToStaticMarkup(<TimeSeriesActivitySection timeSeries={timeSeries} />);
|
|
46
|
+
|
|
47
|
+
// #then
|
|
48
|
+
expect(html).not.toContain("timeSeriesAxisTop");
|
|
49
|
+
expect(html).toContain("timeSeriesAxisBottom");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should render sand bars for other main agents when derived otherMain is non-zero", () => {
|
|
53
|
+
// #given
|
|
54
|
+
const timeSeries = mkTimeSeries({
|
|
55
|
+
series: [
|
|
56
|
+
{ id: "overall-main", label: "Overall", tone: "muted", values: [10, 0, 0] },
|
|
57
|
+
{ id: "agent:sisyphus", label: "Sisyphus", tone: "teal", values: [0, 0, 0] },
|
|
58
|
+
{ id: "agent:prometheus", label: "Prometheus", tone: "red", values: [0, 0, 0] },
|
|
59
|
+
{ id: "agent:atlas", label: "Atlas", tone: "green", values: [0, 0, 0] },
|
|
60
|
+
{ id: "background-total", label: "Background", tone: "muted", values: [0, 0, 0] },
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// #when
|
|
65
|
+
const html = renderToStaticMarkup(<TimeSeriesActivitySection timeSeries={timeSeries} />);
|
|
66
|
+
|
|
67
|
+
// #then
|
|
68
|
+
expect(html).toContain("timeSeriesBar--sand");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("time-series helpers", () => {
|
|
73
|
+
it("computeOtherMainAgentsCount should clamp to >= 0 and ignore invalid numbers", () => {
|
|
74
|
+
// #given
|
|
75
|
+
const value = computeOtherMainAgentsCount({
|
|
76
|
+
overall: 10,
|
|
77
|
+
background: 3,
|
|
78
|
+
sisyphus: 2,
|
|
79
|
+
prometheus: 1,
|
|
80
|
+
atlas: 0,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// #then
|
|
84
|
+
expect(value).toBe(4);
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
computeOtherMainAgentsCount({
|
|
88
|
+
overall: NaN,
|
|
89
|
+
background: Infinity,
|
|
90
|
+
sisyphus: -1,
|
|
91
|
+
prometheus: 0,
|
|
92
|
+
atlas: 0,
|
|
93
|
+
})
|
|
94
|
+
).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("computeMainAgentsScaleMax should include otherMain in the max", () => {
|
|
98
|
+
// #given
|
|
99
|
+
const scaleMax = computeMainAgentsScaleMax({
|
|
100
|
+
buckets: 1,
|
|
101
|
+
overallValues: [10],
|
|
102
|
+
backgroundValues: [0],
|
|
103
|
+
sisyphusValues: [0],
|
|
104
|
+
prometheusValues: [0],
|
|
105
|
+
atlasValues: [0],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// #then
|
|
109
|
+
expect(scaleMax).toBe(10);
|
|
110
|
+
});
|
|
111
|
+
});
|