@tonyclaw/agent-inspector 2.1.15 → 2.1.17
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-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
- package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
- package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
- package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
- package/.output/public/assets/index-Blqvndcn.js +1 -0
- package/.output/public/assets/index-Cs79WRQj.css +1 -0
- package/.output/public/assets/{index-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
- package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
- package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +255 -201
- package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
- package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
- package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
- package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
- package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- 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 +535 -163
- package/src/components/ProxyViewerContainer.tsx +77 -67
- package/src/components/alerts/AlertsDialog.tsx +2 -1
- package/src/components/clients/ClientLogo.tsx +1 -1
- package/src/components/groups/GroupsDialog.tsx +19 -1
- package/src/components/providers/ProviderCard.tsx +133 -32
- package/src/components/providers/ProviderForm.tsx +61 -29
- package/src/components/providers/ProviderLogo.tsx +6 -1
- package/src/components/providers/ProvidersPanel.tsx +7 -7
- package/src/components/providers/SettingsDialog.tsx +13 -6
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
- package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
- package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
- package/src/components/proxy-viewer/ConversationHeader.tsx +153 -120
- package/src/components/proxy-viewer/LogEntry.tsx +130 -43
- package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
- package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
- package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
- package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
- package/src/components/proxy-viewer/bodyHydration.ts +56 -0
- package/src/components/proxy-viewer/viewerState.ts +0 -4
- package/src/components/ui/icon-trigger.ts +2 -0
- package/src/components/ui/transient-toast.tsx +104 -0
- package/src/lib/providerTestContract.ts +56 -10
- package/src/lib/providerTestPrompt.ts +23 -1
- package/src/lib/sessionInfoContract.ts +4 -0
- package/src/lib/upstreamUrl.ts +105 -1
- package/src/mcp/server.ts +71 -27
- package/src/mcp/toolHandlers.ts +24 -5
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
- package/src/proxy/sessionInfo.ts +31 -2
- package/src/proxy/sessionSupervisor.ts +20 -1
- package/src/proxy/store.ts +1 -1
- package/src/routes/api/providers.$providerId.test.log.ts +93 -14
- package/styles/globals.css +180 -180
- package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
- package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
- package/.output/public/assets/index-CI1-G8ua.js +0 -1
- package/.output/public/assets/index-DdhFqPsI.css +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ChangeEvent,
|
|
3
3
|
type JSX,
|
|
4
|
+
type ReactNode,
|
|
4
5
|
useCallback,
|
|
5
6
|
useEffect,
|
|
6
7
|
useMemo,
|
|
@@ -24,8 +25,10 @@ import {
|
|
|
24
25
|
Download,
|
|
25
26
|
FileJson,
|
|
26
27
|
Gauge,
|
|
28
|
+
Flag,
|
|
27
29
|
Plus,
|
|
28
30
|
Siren,
|
|
31
|
+
Timer,
|
|
29
32
|
Trash2,
|
|
30
33
|
Upload,
|
|
31
34
|
} from "lucide-react";
|
|
@@ -37,6 +40,7 @@ import { copyTextToClipboard } from "../lib/clipboard";
|
|
|
37
40
|
import type { InspectorGroupEvidence, InspectorGroupMember } from "../lib/groupContract";
|
|
38
41
|
import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
|
|
39
42
|
import { formatTimestampRange } from "../lib/timeDisplay";
|
|
43
|
+
import { extractStopReason, type StopReason } from "../lib/stopReason";
|
|
40
44
|
import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
|
|
41
45
|
import { useProviders } from "../lib/useProviders";
|
|
42
46
|
import packageJson from "../../package.json";
|
|
@@ -47,6 +51,7 @@ import { crabVariants } from "./ui/crab-variants";
|
|
|
47
51
|
import { McpLogo } from "./ui/mcp-logo";
|
|
48
52
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
49
53
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
54
|
+
import type { TransientToastInput } from "./ui/transient-toast";
|
|
50
55
|
import { AlertsDialog } from "./alerts/AlertsDialog";
|
|
51
56
|
import { GroupsDialog } from "./groups/GroupsDialog";
|
|
52
57
|
import { SettingsDialog } from "./providers/SettingsDialog";
|
|
@@ -54,7 +59,7 @@ import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
|
54
59
|
import { LazyCompareDrawer } from "./proxy-viewer/lazy";
|
|
55
60
|
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
56
61
|
import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
|
|
57
|
-
import { buildValidPredecessors } from "./proxy-viewer/viewerState";
|
|
62
|
+
import { buildTurnGroups, buildValidPredecessors } from "./proxy-viewer/viewerState";
|
|
58
63
|
import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
|
|
59
64
|
import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
|
|
60
65
|
import type {
|
|
@@ -169,16 +174,34 @@ function getFirstUserAgent(logs: CapturedLog[]): string | null {
|
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
type SessionSlateStatus = "rolling" | "clear" | "watch" | "failed";
|
|
177
|
+
type SessionSlateMetricTone = "neutral" | "cool" | "watch" | "danger";
|
|
178
|
+
|
|
179
|
+
type ConversationLogs = {
|
|
180
|
+
logs: CapturedLog[];
|
|
181
|
+
};
|
|
172
182
|
|
|
173
183
|
type SessionSlateStats = {
|
|
174
184
|
status: SessionSlateStatus;
|
|
175
185
|
statusLabel: string;
|
|
176
186
|
modelLabel: string;
|
|
187
|
+
providerLabel: string;
|
|
177
188
|
requests: number;
|
|
189
|
+
turns: number;
|
|
178
190
|
failures: number;
|
|
191
|
+
pending: number;
|
|
179
192
|
slow: number;
|
|
193
|
+
lengthStops: number;
|
|
180
194
|
timeRange: string | null;
|
|
181
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;
|
|
182
205
|
};
|
|
183
206
|
|
|
184
207
|
function hasLogFailure(log: CapturedLog): boolean {
|
|
@@ -199,25 +222,237 @@ function getModelLabel(logs: readonly CapturedLog[]): string {
|
|
|
199
222
|
return `${sorted.length} models`;
|
|
200
223
|
}
|
|
201
224
|
|
|
202
|
-
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({
|
|
203
409
|
logs,
|
|
410
|
+
conversationGroups,
|
|
204
411
|
isLoading,
|
|
205
412
|
timeDisplayFormat,
|
|
206
413
|
slowResponseThresholdSeconds,
|
|
207
414
|
}: {
|
|
208
415
|
logs: CapturedLog[];
|
|
416
|
+
conversationGroups: readonly ConversationLogs[];
|
|
209
417
|
isLoading: boolean;
|
|
210
418
|
timeDisplayFormat: TimeDisplayFormat;
|
|
211
419
|
slowResponseThresholdSeconds: number;
|
|
212
420
|
}): SessionSlateStats {
|
|
213
421
|
let failures = 0;
|
|
422
|
+
let pending = 0;
|
|
214
423
|
let slow = 0;
|
|
424
|
+
let lengthStops = 0;
|
|
215
425
|
let totalIn = 0;
|
|
216
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;
|
|
217
436
|
const slowThresholdMs = slowResponseThresholdSeconds * 1000;
|
|
218
437
|
|
|
219
|
-
for (
|
|
220
|
-
|
|
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;
|
|
221
456
|
if (
|
|
222
457
|
slowResponseThresholdSeconds > 0 &&
|
|
223
458
|
log.elapsedMs !== null &&
|
|
@@ -227,15 +462,30 @@ function buildSessionSlateStats({
|
|
|
227
462
|
}
|
|
228
463
|
if (log.inputTokens !== null) totalIn += log.inputTokens;
|
|
229
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
|
+
}
|
|
230
479
|
}
|
|
231
480
|
|
|
232
|
-
const status: SessionSlateStatus =
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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";
|
|
239
489
|
const statusLabel =
|
|
240
490
|
status === "rolling"
|
|
241
491
|
? "Rolling"
|
|
@@ -244,87 +494,169 @@ function buildSessionSlateStats({
|
|
|
244
494
|
: status === "watch"
|
|
245
495
|
? "Watch"
|
|
246
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
|
+
});
|
|
247
509
|
|
|
248
510
|
return {
|
|
249
511
|
status,
|
|
250
512
|
statusLabel,
|
|
251
513
|
modelLabel: getModelLabel(logs),
|
|
514
|
+
providerLabel: getProviderLabel(logs),
|
|
252
515
|
requests: logs.length,
|
|
516
|
+
turns: countSessionTurns(logs, conversationGroups),
|
|
253
517
|
failures,
|
|
518
|
+
pending,
|
|
254
519
|
slow,
|
|
520
|
+
lengthStops,
|
|
255
521
|
timeRange: formatTimeRange(logs, timeDisplayFormat),
|
|
256
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)}`,
|
|
257
535
|
};
|
|
258
536
|
}
|
|
259
537
|
|
|
260
|
-
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 {
|
|
261
552
|
switch (status) {
|
|
262
553
|
case "rolling":
|
|
263
|
-
return "
|
|
554
|
+
return "cool";
|
|
264
555
|
case "failed":
|
|
265
|
-
return "
|
|
556
|
+
return "danger";
|
|
266
557
|
case "watch":
|
|
267
|
-
return "
|
|
558
|
+
return "watch";
|
|
268
559
|
case "clear":
|
|
269
|
-
return "
|
|
560
|
+
return "neutral";
|
|
270
561
|
}
|
|
271
562
|
}
|
|
272
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
|
+
|
|
273
597
|
function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Element | null {
|
|
274
598
|
if (stats.requests === 0) return null;
|
|
275
599
|
|
|
276
600
|
return (
|
|
277
|
-
<div className="
|
|
278
|
-
<div className="
|
|
279
|
-
<div className="
|
|
280
|
-
<div className="
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
{stats.timeRange ?? "Timeline pending"}
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
<div className="border-r border-b border-border/70 px-4 py-3 md:border-b-0">
|
|
292
|
-
<div className="text-[10px] uppercase text-muted-foreground">Status</div>
|
|
293
|
-
<div
|
|
294
|
-
className={cn(
|
|
295
|
-
"mt-2 inline-flex h-7 items-center rounded-md border px-2.5 font-mono text-xs font-semibold",
|
|
296
|
-
slateStatusClass(stats.status),
|
|
297
|
-
)}
|
|
298
|
-
>
|
|
299
|
-
{stats.statusLabel}
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
<div className="border-b border-border/70 px-4 py-3 md:border-b-0 md:border-r">
|
|
303
|
-
<div className="text-[10px] uppercase text-muted-foreground">Requests</div>
|
|
304
|
-
<div className="mt-2 font-mono text-xl font-semibold text-foreground">
|
|
305
|
-
{stats.requests}
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
<div className="border-r border-border/70 px-4 py-3 md:border-r">
|
|
309
|
-
<div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
|
|
310
|
-
<Siren className="size-3 text-red-300" />
|
|
311
|
-
Failure
|
|
312
|
-
</div>
|
|
313
|
-
<div className="mt-2 font-mono text-xl font-semibold text-red-100">{stats.failures}</div>
|
|
314
|
-
</div>
|
|
315
|
-
<div className="px-4 py-3">
|
|
316
|
-
<div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
|
|
317
|
-
<Gauge className="size-3 text-amber-200" />
|
|
318
|
-
Tempo
|
|
319
|
-
</div>
|
|
320
|
-
<div className="mt-2 font-mono text-sm font-semibold text-foreground">
|
|
321
|
-
{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>
|
|
322
612
|
</div>
|
|
323
|
-
<div className="
|
|
324
|
-
{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>
|
|
325
616
|
</div>
|
|
326
617
|
</div>
|
|
327
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>
|
|
328
660
|
</div>
|
|
329
661
|
);
|
|
330
662
|
}
|
|
@@ -411,7 +743,7 @@ function sessionRiskBarClass(risk: ContextRiskLevel): string {
|
|
|
411
743
|
case "unknown":
|
|
412
744
|
return "bg-muted-foreground/50";
|
|
413
745
|
case "ok":
|
|
414
|
-
return "bg-
|
|
746
|
+
return "bg-cyan-300";
|
|
415
747
|
}
|
|
416
748
|
}
|
|
417
749
|
|
|
@@ -470,7 +802,7 @@ function evidenceChipClass(value: string): string {
|
|
|
470
802
|
normalized.includes("complete") ||
|
|
471
803
|
normalized.includes("accept")
|
|
472
804
|
) {
|
|
473
|
-
return "border-
|
|
805
|
+
return "border-cyan-300/25 bg-cyan-400/10 text-cyan-100";
|
|
474
806
|
}
|
|
475
807
|
return "border-border/70 bg-background/70 text-muted-foreground";
|
|
476
808
|
}
|
|
@@ -513,11 +845,11 @@ function SessionMembershipPanel({
|
|
|
513
845
|
);
|
|
514
846
|
|
|
515
847
|
return (
|
|
516
|
-
<div className="
|
|
848
|
+
<div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] text-xs">
|
|
517
849
|
<button
|
|
518
850
|
type="button"
|
|
519
851
|
onClick={() => setOpen((value) => !value)}
|
|
520
|
-
className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-
|
|
852
|
+
className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-white/[0.04]"
|
|
521
853
|
aria-expanded={open}
|
|
522
854
|
>
|
|
523
855
|
<div className="min-w-0 flex-1">
|
|
@@ -555,7 +887,7 @@ function SessionMembershipPanel({
|
|
|
555
887
|
)}
|
|
556
888
|
</button>
|
|
557
889
|
{open && (
|
|
558
|
-
<div className="space-y-2 border-t border-
|
|
890
|
+
<div className="space-y-2 border-t border-white/10 px-3 py-2">
|
|
559
891
|
{memberships.map((membership) => {
|
|
560
892
|
const member = membership.member;
|
|
561
893
|
const metadataRows = getMemberMetadataRows(member);
|
|
@@ -595,7 +927,7 @@ function SessionMembershipPanel({
|
|
|
595
927
|
{metadataRows.map((row) => (
|
|
596
928
|
<div
|
|
597
929
|
key={row.key}
|
|
598
|
-
className="min-w-0 rounded
|
|
930
|
+
className="border border-border bg-muted/40 min-w-0 rounded-md px-2 py-1"
|
|
599
931
|
>
|
|
600
932
|
<div className="font-mono text-[10px] text-muted-foreground">{row.key}</div>
|
|
601
933
|
<div
|
|
@@ -653,7 +985,7 @@ function LogPaginationButton({
|
|
|
653
985
|
type="button"
|
|
654
986
|
onClick={onClick}
|
|
655
987
|
disabled={disabled}
|
|
656
|
-
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:
|
|
988
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-45"
|
|
657
989
|
title={title}
|
|
658
990
|
>
|
|
659
991
|
{icon}
|
|
@@ -679,8 +1011,8 @@ function LogPaginationBar({
|
|
|
679
1011
|
className={cn(
|
|
680
1012
|
"flex min-h-9 flex-col gap-2 px-1 py-1 sm:flex-row sm:items-center",
|
|
681
1013
|
embedded
|
|
682
|
-
? "border-t border-
|
|
683
|
-
: "
|
|
1014
|
+
? "border-t border-white/10 pt-2"
|
|
1015
|
+
: "border border-border bg-card shadow-sm mb-3 rounded-[8px] px-2",
|
|
684
1016
|
)}
|
|
685
1017
|
>
|
|
686
1018
|
<div className="w-full min-w-0 px-1 font-mono text-[11px] text-muted-foreground sm:flex-1">
|
|
@@ -738,7 +1070,7 @@ function sessionRiskClass(risk: ContextRiskLevel): string {
|
|
|
738
1070
|
case "unknown":
|
|
739
1071
|
return "text-muted-foreground";
|
|
740
1072
|
case "ok":
|
|
741
|
-
return "text-
|
|
1073
|
+
return "text-cyan-200";
|
|
742
1074
|
}
|
|
743
1075
|
}
|
|
744
1076
|
|
|
@@ -749,7 +1081,7 @@ function sessionHealthClass(level: ContextHealthLevel): string {
|
|
|
749
1081
|
case "optimizable":
|
|
750
1082
|
return "border-amber-400/25 bg-amber-400/8 text-amber-100";
|
|
751
1083
|
case "ok":
|
|
752
|
-
return "border-
|
|
1084
|
+
return "border-cyan-300/25 bg-cyan-400/8 text-cyan-100";
|
|
753
1085
|
}
|
|
754
1086
|
}
|
|
755
1087
|
|
|
@@ -812,12 +1144,12 @@ function CopyableCommand({ command }: { command: string }): JSX.Element {
|
|
|
812
1144
|
}, [command]);
|
|
813
1145
|
|
|
814
1146
|
return (
|
|
815
|
-
<div className="inline-flex items-center gap-2
|
|
816
|
-
<pre className="
|
|
1147
|
+
<div className="border border-border bg-muted/40 inline-flex max-w-full items-center gap-2 rounded-md px-3 py-2">
|
|
1148
|
+
<pre className="m-0 overflow-x-auto font-mono text-sm text-cyan-200">{command}</pre>
|
|
817
1149
|
<button
|
|
818
1150
|
type="button"
|
|
819
1151
|
onClick={handleCopy}
|
|
820
|
-
className="
|
|
1152
|
+
className="shrink-0 cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
|
|
821
1153
|
aria-label="Copy command"
|
|
822
1154
|
>
|
|
823
1155
|
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
|
@@ -831,8 +1163,8 @@ function McpReadyBadge(): JSX.Element {
|
|
|
831
1163
|
<TooltipProvider>
|
|
832
1164
|
<Tooltip>
|
|
833
1165
|
<TooltipTrigger asChild>
|
|
834
|
-
<span className="inline-flex h-7 items-center gap-2 rounded-md
|
|
835
|
-
<span className="size-1.5 rounded-full bg-
|
|
1166
|
+
<span className="border border-border bg-muted/40 inline-flex h-7 items-center gap-2 rounded-md px-2.5 font-mono text-[11px] font-medium text-cyan-200">
|
|
1167
|
+
<span className="size-1.5 rounded-full bg-cyan-200 shadow-[0_0_8px_rgba(125,211,252,0.75)]" />
|
|
836
1168
|
MCP Ready
|
|
837
1169
|
<span className="hidden text-cyan-200/70 sm:inline">/api/mcp</span>
|
|
838
1170
|
</span>
|
|
@@ -846,6 +1178,40 @@ function McpReadyBadge(): JSX.Element {
|
|
|
846
1178
|
);
|
|
847
1179
|
}
|
|
848
1180
|
|
|
1181
|
+
function SidebarPanel({
|
|
1182
|
+
label,
|
|
1183
|
+
children,
|
|
1184
|
+
className,
|
|
1185
|
+
}: {
|
|
1186
|
+
label: string;
|
|
1187
|
+
children: ReactNode;
|
|
1188
|
+
className?: string;
|
|
1189
|
+
}): JSX.Element {
|
|
1190
|
+
return (
|
|
1191
|
+
<section className={cn("border border-border bg-card shadow-sm rounded-[8px]", className)}>
|
|
1192
|
+
<div className="border-b border-white/10 px-3 py-2">
|
|
1193
|
+
<div className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-white/40">
|
|
1194
|
+
{label}
|
|
1195
|
+
</div>
|
|
1196
|
+
</div>
|
|
1197
|
+
<div className="p-3">{children}</div>
|
|
1198
|
+
</section>
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function SidebarMetric({ label, value }: { label: string; value: string | number }): JSX.Element {
|
|
1203
|
+
return (
|
|
1204
|
+
<div className="border border-border bg-muted/40 min-w-0 rounded-md px-2 py-2">
|
|
1205
|
+
<div className="truncate font-mono text-lg font-semibold leading-6 text-slate-100">
|
|
1206
|
+
{value}
|
|
1207
|
+
</div>
|
|
1208
|
+
<div className="mt-0.5 truncate font-mono text-[10px] uppercase tracking-[0.12em] text-white/35">
|
|
1209
|
+
{label}
|
|
1210
|
+
</div>
|
|
1211
|
+
</div>
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
849
1215
|
const CRAB_VARIANT_COLORS = [
|
|
850
1216
|
"text-amber-500",
|
|
851
1217
|
"text-rose-500",
|
|
@@ -909,7 +1275,7 @@ function BrandHeader({
|
|
|
909
1275
|
}): JSX.Element {
|
|
910
1276
|
if (compact) {
|
|
911
1277
|
return (
|
|
912
|
-
<div className="
|
|
1278
|
+
<div className="border border-border bg-card shadow-sm rounded-[8px] p-3">
|
|
913
1279
|
<div className="flex items-start justify-between gap-3">
|
|
914
1280
|
<div className="flex min-w-0 items-start gap-2">
|
|
915
1281
|
<CrabFamily compact={true} entrancePhase={crabEntrancePhase} />
|
|
@@ -972,7 +1338,7 @@ function SessionContextLogButton({ label, logId }: { label: string; logId: numbe
|
|
|
972
1338
|
<button
|
|
973
1339
|
type="button"
|
|
974
1340
|
onClick={() => openLogContext(logId)}
|
|
975
|
-
className="inline-flex h-5 items-center rounded px-1 font-mono text-[10px] text-muted-foreground transition-colors hover:bg-
|
|
1341
|
+
className="inline-flex h-5 items-center rounded px-1 font-mono text-[10px] text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground"
|
|
976
1342
|
title={`Open Context for ${formatLogReference(logId)}`}
|
|
977
1343
|
>
|
|
978
1344
|
{label} {formatLogReference(logId)}
|
|
@@ -1009,7 +1375,7 @@ function SessionContextProgressBar({
|
|
|
1009
1375
|
title="Open latest Context details"
|
|
1010
1376
|
>
|
|
1011
1377
|
<span
|
|
1012
|
-
className="relative block h-2.5 overflow-hidden rounded-full border border-
|
|
1378
|
+
className="relative block h-2.5 overflow-hidden rounded-full border border-white/10 bg-black/25 transition-colors group-hover:border-cyan-200/35"
|
|
1013
1379
|
role="meter"
|
|
1014
1380
|
aria-label={ariaLabel}
|
|
1015
1381
|
aria-valuemin={0}
|
|
@@ -1044,7 +1410,7 @@ function SessionModelContextRow({ summary }: { summary: SessionModelContextSumma
|
|
|
1044
1410
|
};
|
|
1045
1411
|
|
|
1046
1412
|
return (
|
|
1047
|
-
<div className="grid gap-x-4 gap-y-2 border-t border-
|
|
1413
|
+
<div className="grid gap-x-4 gap-y-2 border-t border-white/10 py-2 first:border-t-0 lg:grid-cols-[minmax(180px,1fr)_minmax(260px,1fr)_minmax(260px,1.4fr)]">
|
|
1048
1414
|
<div className="min-w-0">
|
|
1049
1415
|
<div className="flex min-w-0 items-center gap-2">
|
|
1050
1416
|
<button
|
|
@@ -1099,7 +1465,7 @@ function SessionModelContextRow({ summary }: { summary: SessionModelContextSumma
|
|
|
1099
1465
|
<TrendIcon
|
|
1100
1466
|
className={cn(
|
|
1101
1467
|
"size-3",
|
|
1102
|
-
trend !== null && trend > 0 ? "text-amber-300" : "text-
|
|
1468
|
+
trend !== null && trend > 0 ? "text-amber-300" : "text-cyan-300",
|
|
1103
1469
|
)}
|
|
1104
1470
|
/>
|
|
1105
1471
|
)}
|
|
@@ -1137,7 +1503,7 @@ function SessionContextSummaryPanel({
|
|
|
1137
1503
|
}): JSX.Element | null {
|
|
1138
1504
|
if (summary.models.length === 0) return null;
|
|
1139
1505
|
return (
|
|
1140
|
-
<div className="mt-3 border-t border-
|
|
1506
|
+
<div className="mt-3 border-t border-white/10 pt-2">
|
|
1141
1507
|
<div className="mb-1 flex items-center justify-between gap-2">
|
|
1142
1508
|
<div className="text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">
|
|
1143
1509
|
Session Context
|
|
@@ -1205,12 +1571,12 @@ function SessionContextBar({
|
|
|
1205
1571
|
}, []);
|
|
1206
1572
|
|
|
1207
1573
|
return (
|
|
1208
|
-
<div className="
|
|
1574
|
+
<div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] px-3 py-2 text-xs">
|
|
1209
1575
|
<div className="flex flex-wrap items-center gap-2">
|
|
1210
1576
|
{showBackLink && (
|
|
1211
1577
|
<a
|
|
1212
1578
|
href="/"
|
|
1213
|
-
className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:
|
|
1579
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground"
|
|
1214
1580
|
aria-label="Back to all sessions"
|
|
1215
1581
|
title="Back to all sessions"
|
|
1216
1582
|
>
|
|
@@ -1247,7 +1613,7 @@ function SessionContextBar({
|
|
|
1247
1613
|
<button
|
|
1248
1614
|
type="button"
|
|
1249
1615
|
onClick={() => setDetailsOpen((value) => !value)}
|
|
1250
|
-
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md
|
|
1616
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md px-2.5 font-mono text-[11px] text-muted-foreground transition-colors hover:text-foreground"
|
|
1251
1617
|
aria-expanded={detailsOpen}
|
|
1252
1618
|
title={detailsOpen ? "Hide context details" : "Show context details"}
|
|
1253
1619
|
>
|
|
@@ -1262,7 +1628,7 @@ function SessionContextBar({
|
|
|
1262
1628
|
<button
|
|
1263
1629
|
type="button"
|
|
1264
1630
|
onClick={handleCopyLink}
|
|
1265
|
-
className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:
|
|
1631
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground"
|
|
1266
1632
|
aria-label={copied ? "Copied session link" : "Copy session link"}
|
|
1267
1633
|
title={copied ? "Copied session link" : "Copy session link"}
|
|
1268
1634
|
>
|
|
@@ -1286,6 +1652,8 @@ export type ProxyViewerProps = {
|
|
|
1286
1652
|
/** Clear only the logs whose ids are passed. Called by the per-group
|
|
1287
1653
|
* Clear button on each conversation header. */
|
|
1288
1654
|
onClearGroup: (ids: number[]) => void;
|
|
1655
|
+
/** Surface short-lived action feedback without adding sticky page chrome. */
|
|
1656
|
+
onNotify?: (notice: TransientToastInput) => void;
|
|
1289
1657
|
onImportLogs?: (file: File) => Promise<ImportLogsResponse>;
|
|
1290
1658
|
viewMode: "simple" | "full";
|
|
1291
1659
|
captureMode: CaptureMode;
|
|
@@ -1319,6 +1687,7 @@ export function ProxyViewer({
|
|
|
1319
1687
|
onModelChange,
|
|
1320
1688
|
onClearAll,
|
|
1321
1689
|
onClearGroup,
|
|
1690
|
+
onNotify,
|
|
1322
1691
|
onImportLogs,
|
|
1323
1692
|
viewMode,
|
|
1324
1693
|
captureMode,
|
|
@@ -1375,13 +1744,17 @@ export function ProxyViewer({
|
|
|
1375
1744
|
try {
|
|
1376
1745
|
const result = await exportLogsAsZip(logs, mode);
|
|
1377
1746
|
if (!result.ok) {
|
|
1378
|
-
|
|
1747
|
+
if (onNotify !== undefined) {
|
|
1748
|
+
onNotify({ message: result.message, tone: "error" });
|
|
1749
|
+
} else {
|
|
1750
|
+
setExportError(result.message);
|
|
1751
|
+
}
|
|
1379
1752
|
}
|
|
1380
1753
|
} finally {
|
|
1381
1754
|
setExporting(null);
|
|
1382
1755
|
}
|
|
1383
1756
|
},
|
|
1384
|
-
[logs],
|
|
1757
|
+
[logs, onNotify],
|
|
1385
1758
|
);
|
|
1386
1759
|
const handleImportClick = useCallback(() => {
|
|
1387
1760
|
importInputRef.current?.click();
|
|
@@ -1399,18 +1772,26 @@ export function ProxyViewer({
|
|
|
1399
1772
|
void onImportLogs(file)
|
|
1400
1773
|
.then((result) => {
|
|
1401
1774
|
const skipped = result.skipped > 0 ? `, skipped ${String(result.skipped)}` : "";
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1775
|
+
const message = `Imported ${String(result.imported)} logs as ${result.sessionId}${skipped}.`;
|
|
1776
|
+
if (onNotify !== undefined) {
|
|
1777
|
+
onNotify({ message, tone: "success" });
|
|
1778
|
+
} else {
|
|
1779
|
+
setImportStatus(message);
|
|
1780
|
+
}
|
|
1405
1781
|
})
|
|
1406
1782
|
.catch((err: unknown) => {
|
|
1407
|
-
|
|
1783
|
+
const message = err instanceof Error ? err.message : "Failed to import logs";
|
|
1784
|
+
if (onNotify !== undefined) {
|
|
1785
|
+
onNotify({ message, tone: "error" });
|
|
1786
|
+
} else {
|
|
1787
|
+
setImportError(message);
|
|
1788
|
+
}
|
|
1408
1789
|
})
|
|
1409
1790
|
.finally(() => {
|
|
1410
1791
|
setImporting(false);
|
|
1411
1792
|
});
|
|
1412
1793
|
},
|
|
1413
|
-
[onImportLogs],
|
|
1794
|
+
[onImportLogs, onNotify],
|
|
1414
1795
|
);
|
|
1415
1796
|
// Close the compare drawer when the user changes the session or model
|
|
1416
1797
|
// filter, since the predecessor relationship may no longer be meaningful.
|
|
@@ -1452,11 +1833,12 @@ export function ProxyViewer({
|
|
|
1452
1833
|
() =>
|
|
1453
1834
|
buildSessionSlateStats({
|
|
1454
1835
|
logs,
|
|
1836
|
+
conversationGroups: groups,
|
|
1455
1837
|
isLoading,
|
|
1456
1838
|
timeDisplayFormat,
|
|
1457
1839
|
slowResponseThresholdSeconds,
|
|
1458
1840
|
}),
|
|
1459
|
-
[isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
|
|
1841
|
+
[groups, isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
|
|
1460
1842
|
);
|
|
1461
1843
|
const handleCompareWithPrevious = useCallback(
|
|
1462
1844
|
(log: CapturedLog) => {
|
|
@@ -1467,38 +1849,30 @@ export function ProxyViewer({
|
|
|
1467
1849
|
);
|
|
1468
1850
|
|
|
1469
1851
|
return (
|
|
1470
|
-
<div className="min-h-screen w-full
|
|
1852
|
+
<div className="bg-background min-h-screen w-full text-foreground">
|
|
1471
1853
|
<div className="grid min-h-screen w-full grid-cols-1 lg:grid-cols-[340px_minmax(0,1fr)] xl:grid-cols-[360px_minmax(0,1fr)]">
|
|
1472
|
-
<aside className="
|
|
1854
|
+
<aside className="bg-background border-b lg:sticky lg:top-0 lg:h-screen lg:overflow-y-auto lg:border-b-0 lg:border-r">
|
|
1473
1855
|
<div className="space-y-4 p-4">
|
|
1474
1856
|
<BrandHeader compact={true} crabEntrancePhase={crabEntrancePhase} />
|
|
1475
1857
|
|
|
1476
|
-
<
|
|
1477
|
-
<div className="
|
|
1478
|
-
<
|
|
1479
|
-
<
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
<div className="min-w-0">
|
|
1488
|
-
<div className="font-mono text-lg font-semibold text-foreground">
|
|
1489
|
-
{formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
|
|
1490
|
-
</div>
|
|
1491
|
-
<div className="text-[10px] uppercase text-muted-foreground">Output</div>
|
|
1858
|
+
<SidebarPanel label="Session">
|
|
1859
|
+
<div className="grid grid-cols-3 gap-2">
|
|
1860
|
+
<SidebarMetric label="Requests" value={logs.length} />
|
|
1861
|
+
<SidebarMetric
|
|
1862
|
+
label="Input"
|
|
1863
|
+
value={formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
|
|
1864
|
+
/>
|
|
1865
|
+
<SidebarMetric
|
|
1866
|
+
label="Output"
|
|
1867
|
+
value={formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
|
|
1868
|
+
/>
|
|
1492
1869
|
</div>
|
|
1493
|
-
</
|
|
1870
|
+
</SidebarPanel>
|
|
1494
1871
|
|
|
1495
|
-
<
|
|
1496
|
-
<div className="
|
|
1497
|
-
Workspace
|
|
1498
|
-
</div>
|
|
1499
|
-
<div className="rounded-md border border-border bg-[#0c0d0f] px-3 py-2">
|
|
1872
|
+
<SidebarPanel label="Workspace">
|
|
1873
|
+
<div className="border border-border bg-muted/40 rounded-md px-3 py-2">
|
|
1500
1874
|
<div
|
|
1501
|
-
className="font-mono text-xs font-semibold text-
|
|
1875
|
+
className="truncate font-mono text-xs font-semibold text-cyan-100"
|
|
1502
1876
|
title={activeScopeLabel}
|
|
1503
1877
|
>
|
|
1504
1878
|
{activeScopeLabel}
|
|
@@ -1507,7 +1881,7 @@ export function ProxyViewer({
|
|
|
1507
1881
|
{isPinnedSessionPage ? "Session workspace" : "Live workspace"}
|
|
1508
1882
|
</div>
|
|
1509
1883
|
</div>
|
|
1510
|
-
</
|
|
1884
|
+
</SidebarPanel>
|
|
1511
1885
|
|
|
1512
1886
|
{hasSessionContext && sessionContextScope !== null && (
|
|
1513
1887
|
<SessionContextBar
|
|
@@ -1527,44 +1901,43 @@ export function ProxyViewer({
|
|
|
1527
1901
|
<SessionMembershipPanel memberships={sessionMemberships} />
|
|
1528
1902
|
)}
|
|
1529
1903
|
|
|
1530
|
-
<
|
|
1531
|
-
<div className="
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
<
|
|
1904
|
+
<SidebarPanel label="Filters">
|
|
1905
|
+
<div className="space-y-2">
|
|
1906
|
+
{!hideSessionFilter && (
|
|
1907
|
+
<Select value={selectedSession} onValueChange={onSessionChange}>
|
|
1908
|
+
<SelectTrigger className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 w-full text-xs">
|
|
1909
|
+
<SelectValue placeholder="All sessions" />
|
|
1910
|
+
</SelectTrigger>
|
|
1911
|
+
<SelectContent>
|
|
1912
|
+
<SelectItem value="__all__">All sessions</SelectItem>
|
|
1913
|
+
{sessions.map((s) => (
|
|
1914
|
+
<SelectItem key={s} value={s}>
|
|
1915
|
+
{truncateSessionId(s)}
|
|
1916
|
+
</SelectItem>
|
|
1917
|
+
))}
|
|
1918
|
+
</SelectContent>
|
|
1919
|
+
</Select>
|
|
1920
|
+
)}
|
|
1921
|
+
<Select value={selectedModel} onValueChange={onModelChange}>
|
|
1922
|
+
<SelectTrigger className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 w-full text-xs">
|
|
1923
|
+
<SelectValue placeholder="All models" />
|
|
1538
1924
|
</SelectTrigger>
|
|
1539
1925
|
<SelectContent>
|
|
1540
|
-
<SelectItem value="__all__">All
|
|
1541
|
-
{
|
|
1542
|
-
<SelectItem key={
|
|
1543
|
-
{
|
|
1926
|
+
<SelectItem value="__all__">All models</SelectItem>
|
|
1927
|
+
{models.map((m) => (
|
|
1928
|
+
<SelectItem key={m} value={m}>
|
|
1929
|
+
{m}
|
|
1544
1930
|
</SelectItem>
|
|
1545
1931
|
))}
|
|
1546
1932
|
</SelectContent>
|
|
1547
1933
|
</Select>
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
<SelectTrigger className="h-9 w-full text-xs">
|
|
1551
|
-
<SelectValue placeholder="All models" />
|
|
1552
|
-
</SelectTrigger>
|
|
1553
|
-
<SelectContent>
|
|
1554
|
-
<SelectItem value="__all__">All models</SelectItem>
|
|
1555
|
-
{models.map((m) => (
|
|
1556
|
-
<SelectItem key={m} value={m}>
|
|
1557
|
-
{m}
|
|
1558
|
-
</SelectItem>
|
|
1559
|
-
))}
|
|
1560
|
-
</SelectContent>
|
|
1561
|
-
</Select>
|
|
1562
|
-
</div>
|
|
1934
|
+
</div>
|
|
1935
|
+
</SidebarPanel>
|
|
1563
1936
|
</div>
|
|
1564
1937
|
</aside>
|
|
1565
1938
|
|
|
1566
1939
|
<main className="min-w-0">
|
|
1567
|
-
<div className="sticky top-0 z-30 border-b
|
|
1940
|
+
<div className="border border-border bg-background/95 shadow-sm sticky top-0 z-30 border-b px-4 py-3 lg:px-5">
|
|
1568
1941
|
<div className="flex flex-wrap items-center gap-2">
|
|
1569
1942
|
<div className="min-w-[180px] flex-1">
|
|
1570
1943
|
<div className="text-sm font-semibold text-foreground">Logs</div>
|
|
@@ -1585,7 +1958,7 @@ export function ProxyViewer({
|
|
|
1585
1958
|
type="button"
|
|
1586
1959
|
onClick={handleImportClick}
|
|
1587
1960
|
disabled={importing}
|
|
1588
|
-
className="inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:
|
|
1961
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
1589
1962
|
title="Import Agent Inspector export"
|
|
1590
1963
|
>
|
|
1591
1964
|
{importing ? (
|
|
@@ -1606,7 +1979,7 @@ export function ProxyViewer({
|
|
|
1606
1979
|
void handleExport("redacted");
|
|
1607
1980
|
}}
|
|
1608
1981
|
disabled={exporting !== null}
|
|
1609
|
-
className="inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:
|
|
1982
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
1610
1983
|
title="Export redacted logs as JSON ZIP"
|
|
1611
1984
|
>
|
|
1612
1985
|
{exporting === "redacted" ? (
|
|
@@ -1626,7 +1999,7 @@ export function ProxyViewer({
|
|
|
1626
1999
|
void handleExport("raw");
|
|
1627
2000
|
}}
|
|
1628
2001
|
disabled={exporting !== null}
|
|
1629
|
-
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:
|
|
2002
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
1630
2003
|
title="Export raw logs without redaction"
|
|
1631
2004
|
>
|
|
1632
2005
|
{exporting === "raw" ? (
|
|
@@ -1642,7 +2015,7 @@ export function ProxyViewer({
|
|
|
1642
2015
|
<button
|
|
1643
2016
|
type="button"
|
|
1644
2017
|
onClick={onClearAll}
|
|
1645
|
-
className="inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:
|
|
2018
|
+
className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
|
1646
2019
|
title="Clear all logs"
|
|
1647
2020
|
>
|
|
1648
2021
|
<Trash2 className="size-3.5" />
|
|
@@ -1650,18 +2023,18 @@ export function ProxyViewer({
|
|
|
1650
2023
|
</button>
|
|
1651
2024
|
</div>
|
|
1652
2025
|
<LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
|
|
1653
|
-
{exportError !== null && (
|
|
2026
|
+
{onNotify === undefined && exportError !== null && (
|
|
1654
2027
|
<div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
1655
2028
|
{exportError}
|
|
1656
2029
|
</div>
|
|
1657
2030
|
)}
|
|
1658
|
-
{importError !== null && (
|
|
2031
|
+
{onNotify === undefined && importError !== null && (
|
|
1659
2032
|
<div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
1660
2033
|
{importError}
|
|
1661
2034
|
</div>
|
|
1662
2035
|
)}
|
|
1663
|
-
{importStatus !== null && importError === null && (
|
|
1664
|
-
<div className="mt-2 rounded-md border border-
|
|
2036
|
+
{onNotify === undefined && importStatus !== null && importError === null && (
|
|
2037
|
+
<div className="mt-2 rounded-md border border-cyan-400/25 bg-cyan-500/10 px-3 py-2 text-xs text-cyan-100">
|
|
1665
2038
|
{importStatus}
|
|
1666
2039
|
</div>
|
|
1667
2040
|
)}
|
|
@@ -1672,7 +2045,7 @@ export function ProxyViewer({
|
|
|
1672
2045
|
{logs.length === 0 ? (
|
|
1673
2046
|
selectedSession !== "__all__" ? (
|
|
1674
2047
|
<div className="py-16 text-center text-muted-foreground">
|
|
1675
|
-
<div className="mx-auto max-w-xl space-y-4 rounded-
|
|
2048
|
+
<div className="border border-border bg-card shadow-sm mx-auto max-w-xl space-y-4 rounded-[8px] px-4 py-8">
|
|
1676
2049
|
<p className="text-sm font-medium">
|
|
1677
2050
|
{isLoading ? "Loading session data..." : "Session not found"}
|
|
1678
2051
|
</p>
|
|
@@ -1706,7 +2079,7 @@ export function ProxyViewer({
|
|
|
1706
2079
|
</div>
|
|
1707
2080
|
) : (
|
|
1708
2081
|
<div className="py-16 text-center text-muted-foreground">
|
|
1709
|
-
<div className="mx-auto max-w-2xl space-y-4 rounded-
|
|
2082
|
+
<div className="border border-border bg-card shadow-sm mx-auto max-w-2xl space-y-4 rounded-[8px] px-4 py-8">
|
|
1710
2083
|
<p className="text-sm">No requests captured yet.</p>
|
|
1711
2084
|
<div className="flex flex-col items-center gap-2">
|
|
1712
2085
|
<CopyableCommand command="ANTHROPIC_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
@@ -1736,8 +2109,7 @@ export function ProxyViewer({
|
|
|
1736
2109
|
onCompareWithPrevious={handleCompareWithPrevious}
|
|
1737
2110
|
comparisonPredecessors={comparisonPredecessors}
|
|
1738
2111
|
onClearGroup={onClearGroup}
|
|
1739
|
-
|
|
1740
|
-
hasPinnedSessionContext={hasSessionContext}
|
|
2112
|
+
defaultExpanded={groups.length === 1}
|
|
1741
2113
|
timeDisplayFormat={timeDisplayFormat}
|
|
1742
2114
|
/>
|
|
1743
2115
|
</div>
|