@tonyclaw/agent-inspector 2.1.16 → 2.1.18
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/.output/cli.js +57 -13
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-Ccrrcx1j.js → CompareDrawer-Fqqw5Lci.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CP_RzwW9.js +106 -0
- package/.output/public/assets/{ReplayDialog-C7axhr-l.js → ReplayDialog-hYhz_i6W.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-WOptg8j_.js → RequestAnatomy-CTuWbtwC.js} +1 -1
- package/.output/public/assets/{ResponseView-CSAcxh_M.js → ResponseView-CsZ7S5Gv.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-DJS5KhPx.js → StreamingChunkSequence-PIbJMotU.js} +1 -1
- package/.output/public/assets/_sessionId-CUGA9qU3.js +1 -0
- package/.output/public/assets/{index-DMPNh46t.js → index-F7mynwuH.js} +1 -1
- package/.output/public/assets/index-G4dTUobw.js +1 -0
- package/.output/public/assets/index-gvYkgMPF.css +1 -0
- package/.output/public/assets/{json-viewer-inX5QSa3.js → json-viewer-DCXtNmmD.js} +1 -1
- package/.output/public/assets/{main-aCAKYGSD.js → main-BJZf7_Ib.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +220 -202
- package/.output/server/{_sessionId-C0fhvQnn.mjs → _sessionId-DjSHDNh8.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CO-Ti5sg.mjs → CompareDrawer-Dal52IFo.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-N4J7uBCX.mjs → ProxyViewerContainer-CVqFZGff.mjs} +1833 -1366
- package/.output/server/_ssr/{ReplayDialog-J1VxC0In.mjs → ReplayDialog-BOEgdMes.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-DBH-F3rq.mjs → RequestAnatomy-gch7_vCV.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-DLFwV7Hd.mjs → ResponseView-lEWPy-H4.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-BywmuND0.mjs → StreamingChunkSequence-DnudbJ1O.mjs} +2 -2
- package/.output/server/_ssr/{index-CStymIc0.mjs → index-Cza8zDrW.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-DlQu0N1x.mjs → json-viewer-C2lIVEX3.mjs} +3 -3
- package/.output/server/_ssr/{router-CQ4Zxhmr.mjs → router-bF9uzd1k.mjs} +939 -113
- package/.output/server/{_tanstack-start-manifest_v-BHeJBGfo.mjs → _tanstack-start-manifest_v-ErvW-mtr.mjs} +1 -1
- package/.output/server/index.mjs +74 -74
- package/README.md +57 -2
- package/package.json +1 -1
- package/src/cli/startupOutput.ts +16 -0
- package/src/cli.ts +61 -15
- package/src/components/ProxyViewer.tsx +395 -64
- package/src/components/providers/ProviderCard.tsx +132 -31
- package/src/components/providers/ProviderForm.tsx +43 -11
- package/src/components/providers/ProvidersPanel.tsx +1 -1
- package/src/components/proxy-viewer/ConversationHeader.tsx +133 -112
- package/src/components/proxy-viewer/LogEntry.tsx +109 -26
- package/src/components/proxy-viewer/LogEntryHeader.tsx +339 -329
- package/src/components/proxy-viewer/ThreadConnector.tsx +49 -46
- package/src/components/proxy-viewer/TurnGroup.tsx +22 -62
- package/src/components/proxy-viewer/bodyHydration.ts +56 -0
- package/src/lib/providerTestContract.ts +56 -10
- package/src/lib/providerTestPrompt.ts +23 -1
- package/src/lib/upstreamUrl.ts +105 -1
- package/src/mcp/server.ts +275 -0
- package/src/mcp/toolHandlers.ts +381 -4
- package/src/proxy/logSearch.ts +3 -0
- package/src/proxy/store.ts +40 -15
- package/src/routes/api/logs.ts +8 -2
- package/src/routes/api/providers.$providerId.test.log.ts +85 -10
- package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +0 -106
- package/.output/public/assets/_sessionId-DUSalzKH.js +0 -1
- package/.output/public/assets/index-BPpA21dY.css +0 -1
- package/.output/public/assets/index-Bt0Az2I2.js +0 -1
|
@@ -25,8 +25,10 @@ import {
|
|
|
25
25
|
Download,
|
|
26
26
|
FileJson,
|
|
27
27
|
Gauge,
|
|
28
|
+
Flag,
|
|
28
29
|
Plus,
|
|
29
30
|
Siren,
|
|
31
|
+
Timer,
|
|
30
32
|
Trash2,
|
|
31
33
|
Upload,
|
|
32
34
|
} from "lucide-react";
|
|
@@ -38,6 +40,7 @@ import { copyTextToClipboard } from "../lib/clipboard";
|
|
|
38
40
|
import type { InspectorGroupEvidence, InspectorGroupMember } from "../lib/groupContract";
|
|
39
41
|
import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
|
|
40
42
|
import { formatTimestampRange } from "../lib/timeDisplay";
|
|
43
|
+
import { extractStopReason, type StopReason } from "../lib/stopReason";
|
|
41
44
|
import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
|
|
42
45
|
import { useProviders } from "../lib/useProviders";
|
|
43
46
|
import packageJson from "../../package.json";
|
|
@@ -56,7 +59,7 @@ import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
|
56
59
|
import { LazyCompareDrawer } from "./proxy-viewer/lazy";
|
|
57
60
|
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
58
61
|
import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
|
|
59
|
-
import { buildValidPredecessors } from "./proxy-viewer/viewerState";
|
|
62
|
+
import { buildTurnGroups, buildValidPredecessors } from "./proxy-viewer/viewerState";
|
|
60
63
|
import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
|
|
61
64
|
import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
|
|
62
65
|
import type {
|
|
@@ -171,16 +174,34 @@ function getFirstUserAgent(logs: CapturedLog[]): string | null {
|
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
type SessionSlateStatus = "rolling" | "clear" | "watch" | "failed";
|
|
177
|
+
type SessionSlateMetricTone = "neutral" | "cool" | "watch" | "danger";
|
|
178
|
+
|
|
179
|
+
type ConversationLogs = {
|
|
180
|
+
logs: CapturedLog[];
|
|
181
|
+
};
|
|
174
182
|
|
|
175
183
|
type SessionSlateStats = {
|
|
176
184
|
status: SessionSlateStatus;
|
|
177
185
|
statusLabel: string;
|
|
178
186
|
modelLabel: string;
|
|
187
|
+
providerLabel: string;
|
|
179
188
|
requests: number;
|
|
189
|
+
turns: number;
|
|
180
190
|
failures: number;
|
|
191
|
+
pending: number;
|
|
181
192
|
slow: number;
|
|
193
|
+
lengthStops: number;
|
|
182
194
|
timeRange: string | null;
|
|
183
195
|
tokenLabel: string;
|
|
196
|
+
finishLabel: string;
|
|
197
|
+
finishDetail: string;
|
|
198
|
+
finishTone: SessionSlateMetricTone;
|
|
199
|
+
outcomeLabel: string;
|
|
200
|
+
outcomeDetail: string;
|
|
201
|
+
outcomeTone: SessionSlateMetricTone;
|
|
202
|
+
slowestLabel: string;
|
|
203
|
+
slowestDetail: string;
|
|
204
|
+
heaviestDetail: string;
|
|
184
205
|
};
|
|
185
206
|
|
|
186
207
|
function hasLogFailure(log: CapturedLog): boolean {
|
|
@@ -201,25 +222,237 @@ function getModelLabel(logs: readonly CapturedLog[]): string {
|
|
|
201
222
|
return `${sorted.length} models`;
|
|
202
223
|
}
|
|
203
224
|
|
|
204
|
-
function
|
|
225
|
+
function getProviderLabel(logs: readonly CapturedLog[]): string {
|
|
226
|
+
const providers = new Set<string>();
|
|
227
|
+
for (const log of logs) {
|
|
228
|
+
if (
|
|
229
|
+
log.providerName !== null &&
|
|
230
|
+
log.providerName !== undefined &&
|
|
231
|
+
log.providerName.length > 0
|
|
232
|
+
) {
|
|
233
|
+
providers.add(log.providerName);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const sorted = [...providers].sort();
|
|
237
|
+
if (sorted.length === 0) return "Provider unknown";
|
|
238
|
+
if (sorted.length === 1) return sorted[0] ?? "Provider unknown";
|
|
239
|
+
return `${sorted.length} providers`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatElapsedMs(ms: number): string {
|
|
243
|
+
if (ms < 1000) return `${ms}ms`;
|
|
244
|
+
if (ms < 60_000) {
|
|
245
|
+
const seconds = ms / 1000;
|
|
246
|
+
return `${seconds < 10 ? seconds.toFixed(1) : seconds.toFixed(0)}s`;
|
|
247
|
+
}
|
|
248
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
249
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
250
|
+
const seconds = totalSeconds % 60;
|
|
251
|
+
return `${minutes}m ${seconds}s`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function formatTokenPair(inputTokens: number, outputTokens: number): string {
|
|
255
|
+
return `${formatTokens(inputTokens)} in / ${formatTokens(outputTokens)} out`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function countSessionTurns(
|
|
259
|
+
logs: CapturedLog[],
|
|
260
|
+
conversationGroups: readonly ConversationLogs[],
|
|
261
|
+
): number {
|
|
262
|
+
if (logs.length === 0) return 0;
|
|
263
|
+
if (conversationGroups.length === 0) return buildTurnGroups(logs).length;
|
|
264
|
+
|
|
265
|
+
let turns = 0;
|
|
266
|
+
for (const group of conversationGroups) {
|
|
267
|
+
turns += buildTurnGroups(group.logs).length;
|
|
268
|
+
}
|
|
269
|
+
return turns;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
type FinishSummary = {
|
|
273
|
+
label: string;
|
|
274
|
+
detail: string;
|
|
275
|
+
tone: SessionSlateMetricTone;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
type SessionOutcomeSummary = {
|
|
279
|
+
label: string;
|
|
280
|
+
detail: string;
|
|
281
|
+
tone: SessionSlateMetricTone;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
type FailureSummary = {
|
|
285
|
+
logNumber: number;
|
|
286
|
+
status: number | null;
|
|
287
|
+
error: string | null;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
function summarizeFinishReason({
|
|
291
|
+
stopReason,
|
|
292
|
+
pending,
|
|
293
|
+
lastFailureLogNumber,
|
|
294
|
+
}: {
|
|
295
|
+
stopReason: StopReason;
|
|
296
|
+
pending: number;
|
|
297
|
+
lastFailureLogNumber: number | null;
|
|
298
|
+
}): FinishSummary {
|
|
299
|
+
if (pending > 0) {
|
|
300
|
+
return {
|
|
301
|
+
label: "In flight",
|
|
302
|
+
detail: `${pending} request${pending === 1 ? "" : "s"} pending`,
|
|
303
|
+
tone: "cool",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (lastFailureLogNumber !== null && stopReason === null) {
|
|
308
|
+
return {
|
|
309
|
+
label: "Error",
|
|
310
|
+
detail: `Last failed request #${lastFailureLogNumber}`,
|
|
311
|
+
tone: "danger",
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
switch (stopReason) {
|
|
316
|
+
case "end_turn":
|
|
317
|
+
return { label: "End turn", detail: "Assistant completed the turn", tone: "neutral" };
|
|
318
|
+
case "stop":
|
|
319
|
+
return { label: "Stop", detail: "Model reached a stop condition", tone: "neutral" };
|
|
320
|
+
case "length":
|
|
321
|
+
return { label: "Output limit", detail: "Turn ended at max output tokens", tone: "watch" };
|
|
322
|
+
case "tool_use":
|
|
323
|
+
return { label: "Tool call", detail: "Assistant requested a tool", tone: "cool" };
|
|
324
|
+
case null:
|
|
325
|
+
return { label: "Unknown", detail: "No finish reason captured", tone: "neutral" };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isClientAbortError(error: string | null): boolean {
|
|
330
|
+
if (error === null) return false;
|
|
331
|
+
const lower = error.toLowerCase();
|
|
332
|
+
return (
|
|
333
|
+
lower.includes("abort") ||
|
|
334
|
+
lower.includes("cancel") ||
|
|
335
|
+
lower.includes("client disconnected") ||
|
|
336
|
+
lower.includes("stream disconnected") ||
|
|
337
|
+
lower.includes("connection closed")
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function summarizeSessionOutcome({
|
|
342
|
+
pending,
|
|
343
|
+
failures,
|
|
344
|
+
lengthStops,
|
|
345
|
+
stopReason,
|
|
346
|
+
lastFailure,
|
|
347
|
+
}: {
|
|
348
|
+
pending: number;
|
|
349
|
+
failures: number;
|
|
350
|
+
lengthStops: number;
|
|
351
|
+
stopReason: StopReason;
|
|
352
|
+
lastFailure: FailureSummary | null;
|
|
353
|
+
}): SessionOutcomeSummary {
|
|
354
|
+
if (
|
|
355
|
+
lastFailure !== null &&
|
|
356
|
+
(lastFailure.status === 499 || isClientAbortError(lastFailure.error))
|
|
357
|
+
) {
|
|
358
|
+
return {
|
|
359
|
+
label: "Client aborted",
|
|
360
|
+
detail: `Request #${lastFailure.logNumber} ended before completion`,
|
|
361
|
+
tone: "watch",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (pending > 0) {
|
|
366
|
+
return {
|
|
367
|
+
label: "In flight",
|
|
368
|
+
detail: `${pending} request${pending === 1 ? "" : "s"} still running`,
|
|
369
|
+
tone: "cool",
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (lastFailure !== null && lastFailure.status !== null && lastFailure.status >= 500) {
|
|
374
|
+
return {
|
|
375
|
+
label: "Provider error",
|
|
376
|
+
detail: `Request #${lastFailure.logNumber} returned HTTP ${lastFailure.status}`,
|
|
377
|
+
tone: "danger",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (failures > 0 && lastFailure !== null) {
|
|
382
|
+
return {
|
|
383
|
+
label: "Failed",
|
|
384
|
+
detail: `Request #${lastFailure.logNumber} needs attention`,
|
|
385
|
+
tone: "danger",
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (lengthStops > 0 || stopReason === "length") {
|
|
390
|
+
return {
|
|
391
|
+
label: "Output limited",
|
|
392
|
+
detail: "Model stopped at the output token limit",
|
|
393
|
+
tone: "watch",
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
switch (stopReason) {
|
|
398
|
+
case "end_turn":
|
|
399
|
+
case "stop":
|
|
400
|
+
return { label: "Completed", detail: "Assistant completed normally", tone: "neutral" };
|
|
401
|
+
case "tool_use":
|
|
402
|
+
return { label: "Tool requested", detail: "Assistant handed off to a tool", tone: "cool" };
|
|
403
|
+
case null:
|
|
404
|
+
return { label: "Unknown", detail: "No terminal signal captured", tone: "neutral" };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function buildSessionSlateStats({
|
|
205
409
|
logs,
|
|
410
|
+
conversationGroups,
|
|
206
411
|
isLoading,
|
|
207
412
|
timeDisplayFormat,
|
|
208
413
|
slowResponseThresholdSeconds,
|
|
209
414
|
}: {
|
|
210
415
|
logs: CapturedLog[];
|
|
416
|
+
conversationGroups: readonly ConversationLogs[];
|
|
211
417
|
isLoading: boolean;
|
|
212
418
|
timeDisplayFormat: TimeDisplayFormat;
|
|
213
419
|
slowResponseThresholdSeconds: number;
|
|
214
420
|
}): SessionSlateStats {
|
|
215
421
|
let failures = 0;
|
|
422
|
+
let pending = 0;
|
|
216
423
|
let slow = 0;
|
|
424
|
+
let lengthStops = 0;
|
|
217
425
|
let totalIn = 0;
|
|
218
426
|
let totalOut = 0;
|
|
427
|
+
let terminalStopReason: StopReason = null;
|
|
428
|
+
let lastFailureLogNumber: number | null = null;
|
|
429
|
+
let lastFailure: FailureSummary | null = null;
|
|
430
|
+
let slowestMs: number | null = null;
|
|
431
|
+
let slowestLogNumber: number | null = null;
|
|
432
|
+
let heaviestTokens = 0;
|
|
433
|
+
let heaviestInputTokens = 0;
|
|
434
|
+
let heaviestOutputTokens = 0;
|
|
435
|
+
let heaviestLogNumber: number | null = null;
|
|
219
436
|
const slowThresholdMs = slowResponseThresholdSeconds * 1000;
|
|
220
437
|
|
|
221
|
-
for (
|
|
222
|
-
|
|
438
|
+
for (let index = 0; index < logs.length; index += 1) {
|
|
439
|
+
const log = logs[index];
|
|
440
|
+
if (log === undefined) continue;
|
|
441
|
+
const logNumber = index + 1;
|
|
442
|
+
const stopReason = extractStopReason(log);
|
|
443
|
+
if (stopReason !== null) terminalStopReason = stopReason;
|
|
444
|
+
if (stopReason === "length") lengthStops += 1;
|
|
445
|
+
|
|
446
|
+
if (hasLogFailure(log)) {
|
|
447
|
+
failures += 1;
|
|
448
|
+
lastFailureLogNumber = logNumber;
|
|
449
|
+
lastFailure = {
|
|
450
|
+
logNumber,
|
|
451
|
+
status: log.responseStatus,
|
|
452
|
+
error: log.error ?? null,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (log.responseStatus === null) pending += 1;
|
|
223
456
|
if (
|
|
224
457
|
slowResponseThresholdSeconds > 0 &&
|
|
225
458
|
log.elapsedMs !== null &&
|
|
@@ -229,15 +462,30 @@ function buildSessionSlateStats({
|
|
|
229
462
|
}
|
|
230
463
|
if (log.inputTokens !== null) totalIn += log.inputTokens;
|
|
231
464
|
if (log.outputTokens !== null) totalOut += log.outputTokens;
|
|
465
|
+
if (log.elapsedMs !== null && (slowestMs === null || log.elapsedMs > slowestMs)) {
|
|
466
|
+
slowestMs = log.elapsedMs;
|
|
467
|
+
slowestLogNumber = logNumber;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const inputTokens = log.inputTokens ?? 0;
|
|
471
|
+
const outputTokens = log.outputTokens ?? 0;
|
|
472
|
+
const requestTokens = inputTokens + outputTokens;
|
|
473
|
+
if (requestTokens > heaviestTokens) {
|
|
474
|
+
heaviestTokens = requestTokens;
|
|
475
|
+
heaviestInputTokens = inputTokens;
|
|
476
|
+
heaviestOutputTokens = outputTokens;
|
|
477
|
+
heaviestLogNumber = logNumber;
|
|
478
|
+
}
|
|
232
479
|
}
|
|
233
480
|
|
|
234
|
-
const status: SessionSlateStatus =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
481
|
+
const status: SessionSlateStatus =
|
|
482
|
+
isLoading || (pending > 0 && failures === 0)
|
|
483
|
+
? "rolling"
|
|
484
|
+
: failures > 0
|
|
485
|
+
? "failed"
|
|
486
|
+
: slow > 0 || lengthStops > 0
|
|
487
|
+
? "watch"
|
|
488
|
+
: "clear";
|
|
241
489
|
const statusLabel =
|
|
242
490
|
status === "rolling"
|
|
243
491
|
? "Rolling"
|
|
@@ -246,87 +494,169 @@ function buildSessionSlateStats({
|
|
|
246
494
|
: status === "watch"
|
|
247
495
|
? "Watch"
|
|
248
496
|
: "Clear";
|
|
497
|
+
const finish = summarizeFinishReason({
|
|
498
|
+
stopReason: terminalStopReason,
|
|
499
|
+
pending,
|
|
500
|
+
lastFailureLogNumber,
|
|
501
|
+
});
|
|
502
|
+
const outcome = summarizeSessionOutcome({
|
|
503
|
+
pending,
|
|
504
|
+
failures,
|
|
505
|
+
lengthStops,
|
|
506
|
+
stopReason: terminalStopReason,
|
|
507
|
+
lastFailure,
|
|
508
|
+
});
|
|
249
509
|
|
|
250
510
|
return {
|
|
251
511
|
status,
|
|
252
512
|
statusLabel,
|
|
253
513
|
modelLabel: getModelLabel(logs),
|
|
514
|
+
providerLabel: getProviderLabel(logs),
|
|
254
515
|
requests: logs.length,
|
|
516
|
+
turns: countSessionTurns(logs, conversationGroups),
|
|
255
517
|
failures,
|
|
518
|
+
pending,
|
|
256
519
|
slow,
|
|
520
|
+
lengthStops,
|
|
257
521
|
timeRange: formatTimeRange(logs, timeDisplayFormat),
|
|
258
522
|
tokenLabel: `${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`,
|
|
523
|
+
finishLabel: finish.label,
|
|
524
|
+
finishDetail: finish.detail,
|
|
525
|
+
finishTone: finish.tone,
|
|
526
|
+
outcomeLabel: outcome.label,
|
|
527
|
+
outcomeDetail: outcome.detail,
|
|
528
|
+
outcomeTone: outcome.tone,
|
|
529
|
+
slowestLabel: slowestMs === null ? "n/a" : formatElapsedMs(slowestMs),
|
|
530
|
+
slowestDetail: slowestLogNumber === null ? "Latency pending" : `Request #${slowestLogNumber}`,
|
|
531
|
+
heaviestDetail:
|
|
532
|
+
heaviestLogNumber === null
|
|
533
|
+
? "Tokens pending"
|
|
534
|
+
: `#${heaviestLogNumber} ${formatTokenPair(heaviestInputTokens, heaviestOutputTokens)}`,
|
|
259
535
|
};
|
|
260
536
|
}
|
|
261
537
|
|
|
262
|
-
function
|
|
538
|
+
function slateMetricToneClass(tone: SessionSlateMetricTone): string {
|
|
539
|
+
switch (tone) {
|
|
540
|
+
case "cool":
|
|
541
|
+
return "text-cyan-100";
|
|
542
|
+
case "danger":
|
|
543
|
+
return "text-red-100";
|
|
544
|
+
case "watch":
|
|
545
|
+
return "text-amber-100";
|
|
546
|
+
case "neutral":
|
|
547
|
+
return "text-foreground";
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function statusMetricTone(status: SessionSlateStatus): SessionSlateMetricTone {
|
|
263
552
|
switch (status) {
|
|
264
553
|
case "rolling":
|
|
265
|
-
return "
|
|
554
|
+
return "cool";
|
|
266
555
|
case "failed":
|
|
267
|
-
return "
|
|
556
|
+
return "danger";
|
|
268
557
|
case "watch":
|
|
269
|
-
return "
|
|
558
|
+
return "watch";
|
|
270
559
|
case "clear":
|
|
271
|
-
return "
|
|
560
|
+
return "neutral";
|
|
272
561
|
}
|
|
273
562
|
}
|
|
274
563
|
|
|
564
|
+
function requestHealthDetail(stats: SessionSlateStats): string {
|
|
565
|
+
if (stats.pending > 0) return `${stats.pending} pending / ${stats.failures} failed`;
|
|
566
|
+
if (stats.lengthStops > 0) return `${stats.lengthStops} output limit / ${stats.failures} failed`;
|
|
567
|
+
return `${stats.failures} failed / ${stats.slow} slow`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function SessionSlateMetric({
|
|
571
|
+
label,
|
|
572
|
+
value,
|
|
573
|
+
detail,
|
|
574
|
+
icon,
|
|
575
|
+
tone = "neutral",
|
|
576
|
+
}: {
|
|
577
|
+
label: string;
|
|
578
|
+
value: string;
|
|
579
|
+
detail: string;
|
|
580
|
+
icon?: ReactNode;
|
|
581
|
+
tone?: SessionSlateMetricTone;
|
|
582
|
+
}): JSX.Element {
|
|
583
|
+
return (
|
|
584
|
+
<div className="min-w-0 border-t border-white/10 px-4 py-3">
|
|
585
|
+
<div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
|
|
586
|
+
{icon}
|
|
587
|
+
{label}
|
|
588
|
+
</div>
|
|
589
|
+
<div className={cn("mt-2 font-mono text-sm font-semibold", slateMetricToneClass(tone))}>
|
|
590
|
+
{value}
|
|
591
|
+
</div>
|
|
592
|
+
<div className="mt-1 font-mono text-[11px] leading-snug text-muted-foreground">{detail}</div>
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
275
597
|
function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Element | null {
|
|
276
598
|
if (stats.requests === 0) return null;
|
|
277
599
|
|
|
278
600
|
return (
|
|
279
|
-
<div className="border border-border bg-card shadow-sm mb-4 rounded-[8px]">
|
|
280
|
-
<div className="
|
|
281
|
-
<div className="
|
|
282
|
-
<div className="
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
{stats.timeRange ?? "Timeline pending"}
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
<div className="border-r border-b border-white/10 px-4 py-3 md:border-b-0">
|
|
294
|
-
<div className="text-[10px] uppercase text-muted-foreground">Status</div>
|
|
295
|
-
<div
|
|
296
|
-
className={cn(
|
|
297
|
-
"mt-2 inline-flex h-7 items-center rounded-md border px-2.5 font-mono text-xs font-semibold",
|
|
298
|
-
slateStatusClass(stats.status),
|
|
299
|
-
)}
|
|
300
|
-
>
|
|
301
|
-
{stats.statusLabel}
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
304
|
-
<div className="border-b border-white/10 px-4 py-3 md:border-b-0 md:border-r">
|
|
305
|
-
<div className="text-[10px] uppercase text-muted-foreground">Requests</div>
|
|
306
|
-
<div className="mt-2 font-mono text-xl font-semibold text-foreground">
|
|
307
|
-
{stats.requests}
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
310
|
-
<div className="border-r border-white/10 px-4 py-3 md:border-r">
|
|
311
|
-
<div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
|
|
312
|
-
<Siren className="size-3 text-red-300" />
|
|
313
|
-
Failure
|
|
314
|
-
</div>
|
|
315
|
-
<div className="mt-2 font-mono text-xl font-semibold text-red-100">{stats.failures}</div>
|
|
316
|
-
</div>
|
|
317
|
-
<div className="px-4 py-3">
|
|
318
|
-
<div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
|
|
319
|
-
<Gauge className="size-3 text-amber-200" />
|
|
320
|
-
Tempo
|
|
321
|
-
</div>
|
|
322
|
-
<div className="mt-2 font-mono text-sm font-semibold text-foreground">
|
|
323
|
-
{stats.slow} slow
|
|
601
|
+
<div className="border border-border bg-card shadow-sm mb-4 overflow-hidden rounded-[8px]">
|
|
602
|
+
<div className="px-4 py-3">
|
|
603
|
+
<div className="flex flex-wrap items-end justify-between gap-3">
|
|
604
|
+
<div className="min-w-0">
|
|
605
|
+
<div className="flex items-center gap-2 text-[10px] font-semibold uppercase text-muted-foreground">
|
|
606
|
+
<Clapperboard className="size-3.5 text-slate-300" />
|
|
607
|
+
Flight Recorder
|
|
608
|
+
</div>
|
|
609
|
+
<div className="mt-2 min-w-0 font-mono text-sm font-semibold text-foreground">
|
|
610
|
+
{stats.modelLabel}
|
|
611
|
+
</div>
|
|
324
612
|
</div>
|
|
325
|
-
<div className="
|
|
326
|
-
{stats.
|
|
613
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-muted-foreground">
|
|
614
|
+
<span className="min-w-0 font-mono">{stats.providerLabel}</span>
|
|
615
|
+
<span className="min-w-0 font-mono">{stats.timeRange ?? "Timeline pending"}</span>
|
|
327
616
|
</div>
|
|
328
617
|
</div>
|
|
329
618
|
</div>
|
|
619
|
+
<div className="grid grid-cols-2 border-t border-white/10 md:grid-cols-3 2xl:grid-cols-6">
|
|
620
|
+
<SessionSlateMetric
|
|
621
|
+
label="Status"
|
|
622
|
+
value={stats.statusLabel}
|
|
623
|
+
detail={requestHealthDetail(stats)}
|
|
624
|
+
tone={statusMetricTone(stats.status)}
|
|
625
|
+
/>
|
|
626
|
+
<SessionSlateMetric
|
|
627
|
+
label="Turns"
|
|
628
|
+
value={String(stats.turns)}
|
|
629
|
+
detail={stats.finishLabel}
|
|
630
|
+
icon={<Flag className="size-3 text-muted-foreground" />}
|
|
631
|
+
tone={stats.finishTone}
|
|
632
|
+
/>
|
|
633
|
+
<SessionSlateMetric
|
|
634
|
+
label="Requests"
|
|
635
|
+
value={String(stats.requests)}
|
|
636
|
+
detail={`${stats.failures} failed / ${stats.slow} slow`}
|
|
637
|
+
icon={<Siren className="size-3 text-muted-foreground" />}
|
|
638
|
+
tone={stats.failures > 0 ? "danger" : "neutral"}
|
|
639
|
+
/>
|
|
640
|
+
<SessionSlateMetric
|
|
641
|
+
label="Peak"
|
|
642
|
+
value={stats.slowestLabel}
|
|
643
|
+
detail={stats.slowestDetail}
|
|
644
|
+
icon={<Timer className="size-3 text-muted-foreground" />}
|
|
645
|
+
tone={stats.slow > 0 ? "watch" : "neutral"}
|
|
646
|
+
/>
|
|
647
|
+
<SessionSlateMetric
|
|
648
|
+
label="Tokens"
|
|
649
|
+
value={stats.tokenLabel}
|
|
650
|
+
detail={stats.heaviestDetail}
|
|
651
|
+
icon={<Gauge className="size-3 text-muted-foreground" />}
|
|
652
|
+
/>
|
|
653
|
+
<SessionSlateMetric
|
|
654
|
+
label="Outcome"
|
|
655
|
+
value={stats.outcomeLabel}
|
|
656
|
+
detail={stats.outcomeDetail}
|
|
657
|
+
tone={stats.outcomeTone}
|
|
658
|
+
/>
|
|
659
|
+
</div>
|
|
330
660
|
</div>
|
|
331
661
|
);
|
|
332
662
|
}
|
|
@@ -1503,11 +1833,12 @@ export function ProxyViewer({
|
|
|
1503
1833
|
() =>
|
|
1504
1834
|
buildSessionSlateStats({
|
|
1505
1835
|
logs,
|
|
1836
|
+
conversationGroups: groups,
|
|
1506
1837
|
isLoading,
|
|
1507
1838
|
timeDisplayFormat,
|
|
1508
1839
|
slowResponseThresholdSeconds,
|
|
1509
1840
|
}),
|
|
1510
|
-
[isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
|
|
1841
|
+
[groups, isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
|
|
1511
1842
|
);
|
|
1512
1843
|
const handleCompareWithPrevious = useCallback(
|
|
1513
1844
|
(log: CapturedLog) => {
|