@tonyclaw/agent-inspector 3.0.2 → 3.0.4

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.
Files changed (42) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Dqpwaku6.js → CompareDrawer-Bs2kzUSp.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-BUnlrfiS.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-eaMk-WFG.js → ReplayDialog-B6xy7pl5.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-DCnoqam_.js → RequestAnatomy-BSvSmpCO.js} +1 -1
  6. package/.output/public/assets/{ResponseView-Q7HW0fAd.js → ResponseView-BFnUva7Z.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-K7qKD9VX.js → StreamingChunkSequence-u7ZaJOqn.js} +1 -1
  8. package/.output/public/assets/{_sessionId-CHwYmqKk.js → _sessionId-CH4nsMOA.js} +1 -1
  9. package/.output/public/assets/{index-EI9mxK7O.js → index-B5HS6AxR.js} +1 -1
  10. package/.output/public/assets/index-BqVL284D.js +1 -0
  11. package/.output/public/assets/index-DJW6qbnH.css +1 -0
  12. package/.output/public/assets/{json-viewer-Dbu2fh6-.js → json-viewer-5Tj5vkqe.js} +1 -1
  13. package/.output/public/assets/{main-CLNurSfs.js → main-ChxZWG8K.js} +2 -2
  14. package/.output/server/{_sessionId-1KajCUiB.mjs → _sessionId-B3r-VBdM.mjs} +2 -2
  15. package/.output/server/_ssr/{CompareDrawer-iM778CsC.mjs → CompareDrawer-DAYrKHKe.mjs} +2 -2
  16. package/.output/server/_ssr/{ProxyViewerContainer-DzVBZMbn.mjs → ProxyViewerContainer-CYmNmQK9.mjs} +357 -214
  17. package/.output/server/_ssr/{ReplayDialog-BxPm6uxq.mjs → ReplayDialog-CdKvdXxk.mjs} +3 -3
  18. package/.output/server/_ssr/{RequestAnatomy-D5aE8Qas.mjs → RequestAnatomy-BtmTUq7O.mjs} +2 -2
  19. package/.output/server/_ssr/{ResponseView-D0mzqj7J.mjs → ResponseView-DuD_z5hd.mjs} +2 -2
  20. package/.output/server/_ssr/{StreamingChunkSequence-BbeKs1PZ.mjs → StreamingChunkSequence-DpIV2wt_.mjs} +2 -2
  21. package/.output/server/_ssr/{index-BIrW_uL8.mjs → index-B4_8vH8k.mjs} +2 -2
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{json-viewer-DVAdcOds.mjs → json-viewer-gcNJEB7y.mjs} +2 -2
  24. package/.output/server/_ssr/{router-DtMA9SLr.mjs → router-CYjIsElo.mjs} +4 -4
  25. package/.output/server/{_tanstack-start-manifest_v-Rq5BNh_J.mjs → _tanstack-start-manifest_v-BRdOxY6q.mjs} +1 -1
  26. package/.output/server/index.mjs +72 -72
  27. package/package.json +1 -1
  28. package/src/assets/IDE/claudecode-color.svg +1 -0
  29. package/src/assets/IDE/codex-color.svg +1 -0
  30. package/src/assets/IDE/opencode.svg +1 -0
  31. package/src/assets/IDE/xiaomimimo.svg +1 -0
  32. package/src/assets/logos/opencode.svg +4 -4
  33. package/src/components/ProxyViewer.tsx +92 -25
  34. package/src/components/ProxyViewerContainer.tsx +154 -55
  35. package/src/components/clients/ClientLogo.tsx +14 -52
  36. package/src/components/providers/ProviderCard.tsx +83 -36
  37. package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
  38. package/src/components/proxy-viewer/ConversationHeader.tsx +4 -5
  39. package/src/components/proxy-viewer/LogEntryHeader.tsx +1 -7
  40. package/.output/public/assets/ProxyViewerContainer-tu_yG20v.js +0 -106
  41. package/.output/public/assets/index-CZnCJkDN.js +0 -1
  42. package/.output/public/assets/index-Tq5s9NV0.css +0 -1
@@ -47,6 +47,8 @@ const LogCursorPageSchema = z.object({
47
47
 
48
48
  type LogCursorPage = z.infer<typeof LogCursorPageSchema>;
49
49
 
50
+ const SessionsResponseSchema = z.array(z.string());
51
+
50
52
  type SessionPageRequest =
51
53
  | { kind: "newest" }
52
54
  | { kind: "oldest" }
@@ -114,6 +116,8 @@ const DEBOUNCE_MS = 100;
114
116
  const HASH_SCROLL_ATTEMPTS = 12;
115
117
  const HASH_HIGHLIGHT_MS = 1800;
116
118
  const SESSION_PAGE_LIMIT = 100;
119
+ const SESSION_RELOAD_PAGE_LIMIT = 500;
120
+ const MAX_SESSION_RELOAD_PAGES = 200;
117
121
 
118
122
  function buildLogsStreamUrl(sessionId: string | undefined): string {
119
123
  const params = new URLSearchParams();
@@ -125,11 +129,15 @@ function buildLogsStreamUrl(sessionId: string | undefined): string {
125
129
  return query.length > 0 ? `/api/logs/stream?${query}` : "/api/logs/stream";
126
130
  }
127
131
 
128
- function buildSessionLogsPageUrl(sessionId: string, request: SessionPageRequest): string {
132
+ function buildSessionLogsPageUrl(
133
+ sessionId: string,
134
+ request: SessionPageRequest,
135
+ limit = SESSION_PAGE_LIMIT,
136
+ ): string {
129
137
  const params = new URLSearchParams({
130
138
  cursor: "1",
131
139
  compact: "1",
132
- limit: String(SESSION_PAGE_LIMIT),
140
+ limit: String(limit),
133
141
  sessionId,
134
142
  });
135
143
 
@@ -174,6 +182,20 @@ export function mergeLogsById(
174
182
  return [...byId.values()].sort((left, right) => left.id - right.id);
175
183
  }
176
184
 
185
+ export function mergeSessionIds(
186
+ previousSessions: readonly string[],
187
+ incomingSessions: readonly string[],
188
+ ): string[] {
189
+ const sessions = new Set<string>();
190
+ for (const session of previousSessions) {
191
+ if (session !== "") sessions.add(session);
192
+ }
193
+ for (const session of incomingSessions) {
194
+ if (session !== "") sessions.add(session);
195
+ }
196
+ return [...sessions].sort();
197
+ }
198
+
177
199
  export function ProxyViewerContainer({
178
200
  initialSessionId,
179
201
  }: {
@@ -195,6 +217,7 @@ export function ProxyViewerContainer({
195
217
  const [allLogs, setAllLogs] = useState<CapturedLog[]>([]);
196
218
  const [selectedSession, setSelectedSession] = useState(initialSessionId ?? "__all__");
197
219
  const [selectedModel, setSelectedModel] = useState("__all__");
220
+ const [knownSessions, setKnownSessions] = useState<string[]>([]);
198
221
  const { notice, showToast, dismissToast } = useTransientToast();
199
222
  const [streamInitialized, setStreamInitialized] = useState(initialSessionId === undefined);
200
223
  const [logPage, setLogPage] = useState<LogCursorPage | null>(null);
@@ -225,9 +248,22 @@ export function ProxyViewerContainer({
225
248
  () => filterLogs(allLogs, selectedSession, selectedModel),
226
249
  [allLogs, selectedSession, selectedModel],
227
250
  );
228
- const sessions = useMemo(() => extractSessions(allLogs), [allLogs]);
251
+ const sessions = useMemo(
252
+ () => mergeSessionIds(knownSessions, extractSessions(allLogs)),
253
+ [allLogs, knownSessions],
254
+ );
229
255
  const models = useMemo(() => extractModels(allLogs), [allLogs]);
230
256
 
257
+ const refreshSessions = useCallback(() => {
258
+ void fetchJson("/api/sessions", SessionsResponseSchema)
259
+ .then((sessionIds) => {
260
+ setKnownSessions((prev) => mergeSessionIds(prev, sessionIds));
261
+ })
262
+ .catch(() => {
263
+ // Session discovery is best-effort. Live SSE and session reloads still populate this list.
264
+ });
265
+ }, []);
266
+
231
267
  const flushUpdates = useCallback(() => {
232
268
  flushTimerRef.current = null;
233
269
  const updates = pendingUpdatesRef.current;
@@ -286,9 +322,14 @@ export function ProxyViewerContainer({
286
322
  logIndexRef.current = buildLogIndex(nextLogs);
287
323
  return nextLogs;
288
324
  });
325
+ setKnownSessions((prev) => mergeSessionIds(prev, extractSessions(update.logs)));
289
326
  setStreamInitialized(true);
290
327
  dismissToast();
291
328
  } else if (update.type === "update") {
329
+ setKnownSessions((prev) => {
330
+ if (update.log.sessionId === null || update.log.sessionId === "") return prev;
331
+ return mergeSessionIds(prev, [update.log.sessionId]);
332
+ });
292
333
  scheduleUpdate(update.log);
293
334
  }
294
335
  } catch {
@@ -338,6 +379,64 @@ export function ProxyViewerContainer({
338
379
  [dismissToast, initialSessionId, showErrorToast],
339
380
  );
340
381
 
382
+ const reloadSessionLogs = useCallback(
383
+ (sessionId: string) => {
384
+ const requestId = sessionPageRequestIdRef.current + 1;
385
+ sessionPageRequestIdRef.current = requestId;
386
+ setSessionPageLoading(true);
387
+ dismissToast();
388
+
389
+ void (async () => {
390
+ const collected: CapturedLog[] = [];
391
+ let page = await fetchJson(
392
+ buildSessionLogsPageUrl(sessionId, { kind: "oldest" }, SESSION_RELOAD_PAGE_LIMIT),
393
+ LogCursorPageSchema,
394
+ );
395
+ collected.push(...page.logs);
396
+
397
+ let loadedPages = 1;
398
+ while (
399
+ page.hasNewer &&
400
+ page.newestLogId !== null &&
401
+ loadedPages < MAX_SESSION_RELOAD_PAGES
402
+ ) {
403
+ page = await fetchJson(
404
+ buildSessionLogsPageUrl(
405
+ sessionId,
406
+ { kind: "newer", afterLogId: page.newestLogId },
407
+ SESSION_RELOAD_PAGE_LIMIT,
408
+ ),
409
+ LogCursorPageSchema,
410
+ );
411
+ collected.push(...page.logs);
412
+ loadedPages += 1;
413
+ }
414
+
415
+ if (sessionPageRequestIdRef.current !== requestId) return;
416
+ setAllLogs((prev) => {
417
+ const next = mergeLogsById(prev, collected);
418
+ logIndexRef.current = buildLogIndex(next);
419
+ return next;
420
+ });
421
+ setKnownSessions((prev) => mergeSessionIds(prev, [sessionId]));
422
+ setSessionPageLoading(false);
423
+ if (page.hasNewer) {
424
+ showToast({
425
+ message: `Loaded ${String(collected.length)} logs for this session. More history is available.`,
426
+ tone: "success",
427
+ });
428
+ } else {
429
+ dismissToast();
430
+ }
431
+ })().catch((err: unknown) => {
432
+ if (sessionPageRequestIdRef.current !== requestId) return;
433
+ setSessionPageLoading(false);
434
+ showErrorToast(err instanceof Error ? err.message : "Failed to reload session logs");
435
+ });
436
+ },
437
+ [dismissToast, showErrorToast, showToast],
438
+ );
439
+
341
440
  useEffect(() => {
342
441
  if (initialSessionId === undefined) {
343
442
  setSessionMemberships([]);
@@ -365,11 +464,21 @@ export function ProxyViewerContainer({
365
464
  };
366
465
  }, [initialSessionId]);
367
466
 
467
+ useEffect(() => {
468
+ refreshSessions();
469
+ }, [refreshSessions]);
470
+
368
471
  useEffect(() => {
369
472
  if (initialSessionId === undefined) return;
370
473
  loadSessionPage({ kind: "newest" });
371
474
  }, [initialSessionId, loadSessionPage]);
372
475
 
476
+ useEffect(() => {
477
+ if (initialSessionId !== undefined) return;
478
+ if (selectedSession === "__all__") return;
479
+ reloadSessionLogs(selectedSession);
480
+ }, [initialSessionId, reloadSessionLogs, selectedSession]);
481
+
373
482
  useEffect(() => {
374
483
  if (initialSessionId !== undefined) return undefined;
375
484
  connectSSE();
@@ -443,64 +552,45 @@ export function ProxyViewerContainer({
443
552
  }, [logs.length]);
444
553
 
445
554
  const handleClearAll = useCallback(() => {
446
- if (initialSessionId !== undefined && allLogs.length === 0) return;
447
- void (async () => {
448
- try {
449
- const body =
450
- initialSessionId === undefined
451
- ? undefined
452
- : JSON.stringify({ ids: allLogs.map((log) => log.id) });
453
- const res = await fetch("/api/logs", {
454
- method: "DELETE",
455
- ...(body === undefined
456
- ? {}
457
- : {
458
- headers: { "Content-Type": "application/json" },
459
- body,
460
- }),
461
- });
462
- if (!res.ok) {
463
- showErrorToast("Failed to clear logs");
464
- return;
465
- }
466
- logIndexRef.current.clear();
467
- setAllLogs([]);
468
- dismissToast();
469
- } catch (err) {
470
- showErrorToast(err instanceof Error ? err.message : "Unknown error clearing logs");
471
- }
472
- })();
473
- }, [allLogs, dismissToast, initialSessionId, showErrorToast]);
555
+ const clearedSessions = extractSessions(allLogs);
556
+ logIndexRef.current.clear();
557
+ setAllLogs([]);
558
+ setLogPage(null);
559
+ setKnownSessions((prev) => mergeSessionIds(prev, clearedSessions));
560
+ showToast({
561
+ message: "Cleared the current view. Stored logs are still reloadable.",
562
+ tone: "success",
563
+ });
564
+ }, [allLogs, showToast]);
474
565
 
475
566
  const handleClearGroup = useCallback(
476
567
  (ids: number[]) => {
477
568
  if (ids.length === 0) return;
478
- void (async () => {
479
- try {
480
- const res = await fetch("/api/logs", {
481
- method: "DELETE",
482
- headers: { "Content-Type": "application/json" },
483
- body: JSON.stringify({ ids }),
484
- });
485
- if (!res.ok) {
486
- showErrorToast("Failed to clear group");
487
- return;
488
- }
489
- const idSet = new Set(ids);
490
- setAllLogs((prev) => {
491
- const remaining = prev.filter((l) => !idSet.has(l.id));
492
- logIndexRef.current = buildLogIndex(remaining);
493
- return remaining;
494
- });
495
- dismissToast();
496
- } catch (err) {
497
- showErrorToast(err instanceof Error ? err.message : "Unknown error clearing group");
498
- }
499
- })();
569
+ const idSet = new Set(ids);
570
+ setAllLogs((prev) => {
571
+ const removed = prev.filter((log) => idSet.has(log.id));
572
+ const remaining = prev.filter((log) => !idSet.has(log.id));
573
+ setKnownSessions((known) => mergeSessionIds(known, extractSessions(removed)));
574
+ logIndexRef.current = buildLogIndex(remaining);
575
+ return remaining;
576
+ });
577
+ showToast({ message: "Cleared this group from the current view.", tone: "success" });
500
578
  },
501
- [dismissToast, showErrorToast],
579
+ [showToast],
502
580
  );
503
581
 
582
+ const handleReloadSession = useCallback(() => {
583
+ if (initialSessionId !== undefined) {
584
+ loadSessionPage({ kind: "newest" });
585
+ return;
586
+ }
587
+ if (selectedSession === "__all__") {
588
+ refreshSessions();
589
+ return;
590
+ }
591
+ reloadSessionLogs(selectedSession);
592
+ }, [initialSessionId, loadSessionPage, refreshSessions, reloadSessionLogs, selectedSession]);
593
+
504
594
  const handleImportLogs = useCallback(
505
595
  async (file: File): Promise<ImportLogsResponse> => {
506
596
  const formData = new FormData();
@@ -588,10 +678,19 @@ export function ProxyViewerContainer({
588
678
  onSessionChange={setSelectedSession}
589
679
  onModelChange={setSelectedModel}
590
680
  onClearAll={handleClearAll}
681
+ onReloadSession={
682
+ initialSessionId !== undefined || selectedSession !== "__all__"
683
+ ? handleReloadSession
684
+ : undefined
685
+ }
591
686
  onClearGroup={handleClearGroup}
592
687
  onNotify={showToast}
593
688
  onImportLogs={initialSessionId === undefined ? handleImportLogs : undefined}
594
- isLoading={initialSessionId === undefined ? !streamInitialized : sessionPageLoading}
689
+ isLoading={
690
+ initialSessionId === undefined
691
+ ? !streamInitialized || sessionPageLoading
692
+ : sessionPageLoading
693
+ }
595
694
  pagination={pagination}
596
695
  sessionMemberships={sessionMemberships}
597
696
  viewMode={viewMode}
@@ -1,5 +1,9 @@
1
1
  import type { JSX } from "react";
2
2
  import { cn } from "../../lib/utils";
3
+ import ClaudeCodeLogoSvg from "../../assets/IDE/claudecode-color.svg";
4
+ import CodexLogoSvg from "../../assets/IDE/codex-color.svg";
5
+ import OpenCodeLogoSvg from "../../assets/IDE/opencode.svg";
6
+ import MiMoCodeLogoSvg from "../../assets/IDE/xiaomimimo.svg";
3
7
 
4
8
  export type ClientApp = "codex" | "opencode" | "mimo-code" | "claude-code" | "unknown";
5
9
 
@@ -56,53 +60,12 @@ export function clientAppLabel(client: ClientApp): string | null {
56
60
  }
57
61
  }
58
62
 
59
- function CodexMark(): JSX.Element {
60
- return (
61
- <svg viewBox="0 0 24 24" className="size-full" aria-hidden="true">
62
- <circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" strokeWidth="2.2" />
63
- <path
64
- d="M14.8 8.6a4.8 4.8 0 1 0 0 6.8"
65
- fill="none"
66
- stroke="currentColor"
67
- strokeLinecap="round"
68
- strokeWidth="2.2"
69
- />
70
- </svg>
71
- );
72
- }
73
-
74
- function OpenCodeMark(): JSX.Element {
75
- return (
76
- <svg viewBox="0 0 24 24" className="size-full" aria-hidden="true">
77
- <path d="M7 4h10v16H7z" fill="none" stroke="currentColor" strokeWidth="2.2" />
78
- <path d="M10 8h4M10 12h4M10 16h2" stroke="currentColor" strokeLinecap="round" />
79
- </svg>
80
- );
81
- }
82
-
83
- function MiMoCodeMark(): JSX.Element {
84
- return (
85
- <svg viewBox="0 0 24 24" className="size-full" aria-hidden="true">
86
- <path
87
- d="M5 18V6h3.2l3.8 6.1L15.8 6H19v12h-3V11.3l-3.1 5h-1.8L8 11.3V18z"
88
- fill="currentColor"
89
- />
90
- </svg>
91
- );
92
- }
93
-
94
- function ClaudeCodeMark(): JSX.Element {
95
- return (
96
- <svg viewBox="0 0 24 24" className="size-full" aria-hidden="true">
97
- <path
98
- fill="currentColor"
99
- fillRule="evenodd"
100
- clipRule="evenodd"
101
- d="M21 10.95h3v3.1h-3v3.03h-1.49V20H18v-2.92h-1.49V20H15v-2.92H9V20H7.49v-2.92H6V20H4.49v-2.92H3v-3.03H0v-3.1h3V5h18v5.95zM6 10.95h1.49V8.1H6v2.85zm10.51 0H18V8.1h-1.49v2.85z"
102
- />
103
- </svg>
104
- );
105
- }
63
+ const CLIENT_LOGOS: Record<Exclude<ClientApp, "unknown">, string> = {
64
+ codex: CodexLogoSvg,
65
+ opencode: OpenCodeLogoSvg,
66
+ "mimo-code": MiMoCodeLogoSvg,
67
+ "claude-code": ClaudeCodeLogoSvg,
68
+ };
106
69
 
107
70
  export function ClientLogo({
108
71
  client,
@@ -111,22 +74,21 @@ export function ClientLogo({
111
74
  client: ClientApp;
112
75
  className?: string;
113
76
  }): JSX.Element | null {
77
+ if (client === "unknown") return null;
114
78
  const label = clientAppLabel(client);
115
79
  if (label === null) return null;
80
+ const logoSrc = CLIENT_LOGOS[client];
116
81
 
117
82
  return (
118
83
  <span
119
84
  className={cn(
120
- "border border-border bg-muted/40 inline-flex size-6 shrink-0 items-center justify-center rounded-md p-1 text-cyan-100",
85
+ "border border-border bg-muted/40 inline-flex size-6 shrink-0 items-center justify-center rounded-md p-0.5",
121
86
  className,
122
87
  )}
123
88
  aria-label={label}
124
89
  title={label}
125
90
  >
126
- {client === "codex" && <CodexMark />}
127
- {client === "opencode" && <OpenCodeMark />}
128
- {client === "mimo-code" && <MiMoCodeMark />}
129
- {client === "claude-code" && <ClaudeCodeMark />}
91
+ <img src={logoSrc} alt="" className="size-full object-contain" aria-hidden="true" />
130
92
  </span>
131
93
  );
132
94
  }
@@ -275,6 +275,50 @@ function ProviderEndpointSummary({
275
275
  );
276
276
  }
277
277
 
278
+ function providerHasEndpoint(value: string | undefined): value is string {
279
+ return value !== undefined && value !== "";
280
+ }
281
+
282
+ function providerTestTargets(provider: ProviderConfig): string[] {
283
+ const targets: string[] = [];
284
+ if (providerHasEndpoint(provider.anthropicBaseUrl)) {
285
+ targets.push("Anthropic");
286
+ }
287
+ if (providerHasEndpoint(provider.openaiBaseUrl)) {
288
+ targets.push("Chat");
289
+ }
290
+ if (providerHasEndpoint(provider.openaiResponsesBaseUrl)) {
291
+ targets.push("Responses");
292
+ }
293
+ return targets;
294
+ }
295
+
296
+ function ProviderTestCoverage({ provider }: { provider: ProviderConfig }): JSX.Element {
297
+ const targets = providerTestTargets(provider);
298
+ if (targets.length === 0) {
299
+ return (
300
+ <div className="text-xs text-muted-foreground">
301
+ Test requires at least one configured endpoint.
302
+ </div>
303
+ );
304
+ }
305
+
306
+ return (
307
+ <div className="flex min-w-0 flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
308
+ <span>Test covers</span>
309
+ {targets.map((target) => (
310
+ <span
311
+ key={target}
312
+ className="rounded border border-border/70 bg-muted/25 px-1.5 py-0.5 text-foreground/80"
313
+ >
314
+ {target}
315
+ </span>
316
+ ))}
317
+ <span>non-stream + stream</span>
318
+ </div>
319
+ );
320
+ }
321
+
278
322
  function getErrorIcon(type: ErrorType): JSX.Element {
279
323
  const iconProps = { className: "size-3", strokeWidth: 2 };
280
324
  switch (type) {
@@ -660,9 +704,9 @@ export function ProviderCard({
660
704
  </button>
661
705
  </div>
662
706
 
663
- {provider.anthropicBaseUrl !== undefined && provider.anthropicBaseUrl !== "" && (
707
+ {providerHasEndpoint(provider.anthropicBaseUrl) && (
664
708
  <ProviderEndpointSummary
665
- label="Anthropic"
709
+ label="Test: Anthropic"
666
710
  baseUrl={provider.anthropicBaseUrl}
667
711
  endpoint={PATH_V1_MESSAGES}
668
712
  providerName={provider.name}
@@ -673,9 +717,9 @@ export function ProviderCard({
673
717
  </ProviderEndpointSummary>
674
718
  )}
675
719
 
676
- {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
720
+ {providerHasEndpoint(provider.openaiBaseUrl) && (
677
721
  <ProviderEndpointSummary
678
- label="OpenAI Chat"
722
+ label="Test: Chat"
679
723
  baseUrl={provider.openaiBaseUrl}
680
724
  endpoint={PATH_V1_CHAT_COMPLETIONS}
681
725
  providerName={provider.name}
@@ -686,9 +730,9 @@ export function ProviderCard({
686
730
  </ProviderEndpointSummary>
687
731
  )}
688
732
 
689
- {provider.openaiResponsesBaseUrl !== undefined && provider.openaiResponsesBaseUrl !== "" && (
733
+ {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
690
734
  <ProviderEndpointSummary
691
- label="OpenAI Responses"
735
+ label="Test: Responses"
692
736
  baseUrl={provider.openaiResponsesBaseUrl}
693
737
  endpoint={PATH_V1_RESPONSES}
694
738
  providerName={provider.name}
@@ -774,43 +818,46 @@ export function ProviderCard({
774
818
  </div>
775
819
  )}
776
820
 
777
- <div className="flex gap-2 pt-1 border-t">
778
- {onTest !== undefined && (
821
+ <div className="flex flex-col gap-2 pt-1 border-t sm:flex-row sm:items-center sm:justify-between">
822
+ <ProviderTestCoverage provider={provider} />
823
+ <div className="flex gap-2">
824
+ {onTest !== undefined && (
825
+ <Button
826
+ variant="outline"
827
+ size="sm"
828
+ onClick={() => onTest(provider.id)}
829
+ className="text-xs h-7 gap-1"
830
+ disabled={isTesting ?? false}
831
+ >
832
+ <RotateCw className={`size-3 ${(isTesting ?? false) ? "animate-spin" : ""}`} />
833
+ {(isTesting ?? false)
834
+ ? testingTimeLeft !== undefined
835
+ ? `Testing (${testingTimeLeft}s)`
836
+ : "Testing..."
837
+ : "Test"}
838
+ </Button>
839
+ )}
779
840
  <Button
780
841
  variant="outline"
781
842
  size="sm"
782
- onClick={() => onTest(provider.id)}
843
+ onClick={() => onEdit(provider)}
783
844
  className="text-xs h-7 gap-1"
784
845
  disabled={isTesting ?? false}
785
846
  >
786
- <RotateCw className={`size-3 ${(isTesting ?? false) ? "animate-spin" : ""}`} />
787
- {(isTesting ?? false)
788
- ? testingTimeLeft !== undefined
789
- ? `Testing (${testingTimeLeft}s)`
790
- : "Testing..."
791
- : "Test"}
847
+ <Pencil className="size-3" />
848
+ Edit
792
849
  </Button>
793
- )}
794
- <Button
795
- variant="outline"
796
- size="sm"
797
- onClick={() => onEdit(provider)}
798
- className="text-xs h-7 gap-1"
799
- disabled={isTesting ?? false}
800
- >
801
- <Pencil className="size-3" />
802
- Edit
803
- </Button>
804
- <Button
805
- variant="outline"
806
- size="sm"
807
- onClick={() => onDelete(provider.id)}
808
- className="text-xs h-7 gap-1 text-destructive hover:text-destructive"
809
- disabled={isTesting ?? false}
810
- >
811
- <Trash2 className="size-3" />
812
- Delete
813
- </Button>
850
+ <Button
851
+ variant="outline"
852
+ size="sm"
853
+ onClick={() => onDelete(provider.id)}
854
+ className="text-xs h-7 gap-1 text-destructive hover:text-destructive"
855
+ disabled={isTesting ?? false}
856
+ >
857
+ <Trash2 className="size-3" />
858
+ Delete
859
+ </Button>
860
+ </div>
814
861
  </div>
815
862
  </div>
816
863
  );
@@ -29,7 +29,7 @@ export type ConversationGroupProps = {
29
29
  defaultExpanded?: boolean;
30
30
  /** Controls whether timestamps render as compact local time or full ISO strings. */
31
31
  timeDisplayFormat: TimeDisplayFormat;
32
- /** Clear all logs that belong to this group. */
32
+ /** Clear all logs that belong to this group from the current view. */
33
33
  onClearGroup: (ids: number[]) => void;
34
34
  /** Show IDE identity in the session header. */
35
35
  showClientIdentity?: boolean;
@@ -96,8 +96,7 @@ export type ConversationHeaderProps = {
96
96
  showProcessMetadata?: boolean;
97
97
  /** Controls whether timestamps render as compact local time or full ISO strings. */
98
98
  timeDisplayFormat: TimeDisplayFormat;
99
- /** Clear all logs in this group. After confirmation the parent removes them
100
- * from the in-memory store; this header is then unmounted. */
99
+ /** Clear all logs in this group from the current view; this header is then unmounted. */
101
100
  onClear?: () => void;
102
101
  };
103
102
 
@@ -287,7 +286,7 @@ export function ConversationHeader({
287
286
  type="button"
288
287
  onClick={handleClearClick}
289
288
  aria-label={`Clear group (${totalCalls} request${totalCalls !== 1 ? "s" : ""})`}
290
- title="Clear this group"
289
+ title="Clear this group from the current view"
291
290
  className={HEADER_ACTION_BUTTON_CLASS}
292
291
  >
293
292
  <Trash2 className="size-3.5" />
@@ -321,8 +320,8 @@ export function ConversationHeader({
321
320
  <ConfirmDialog
322
321
  open={confirmOpen}
323
322
  onOpenChange={setConfirmOpen}
324
- title="Clear this group?"
325
- description={`This will remove ${totalCalls} request${totalCalls !== 1 ? "s" : ""} from this conversation. This action cannot be undone.`}
323
+ title="Clear this group from the current view?"
324
+ description={`This will hide ${totalCalls} request${totalCalls !== 1 ? "s" : ""} from the current view. Stored logs remain reloadable.`}
326
325
  confirmLabel="Clear"
327
326
  variant="destructive"
328
327
  onConfirm={() => {
@@ -44,24 +44,19 @@ const STATUS_CUE_CLASSES: Record<
44
44
  StatusCategory,
45
45
  {
46
46
  frame: string;
47
- dot: string;
48
47
  }
49
48
  > = {
50
49
  success: {
51
50
  frame: "border-cyan-200/15 bg-white/[0.04] text-slate-100",
52
- dot: "bg-cyan-200/80 shadow-[0_0_14px_rgba(125,211,252,0.65)]",
53
51
  },
54
52
  client_error: {
55
53
  frame: "border-amber-300/25 bg-amber-300/[0.08] text-amber-100",
56
- dot: "bg-amber-200 shadow-[0_0_14px_rgba(252,211,77,0.8)]",
57
54
  },
58
55
  server_error: {
59
56
  frame: "border-rose-300/25 bg-rose-300/[0.08] text-rose-100",
60
- dot: "bg-rose-300 shadow-[0_0_14px_rgba(253,164,175,0.8)]",
61
57
  },
62
58
  pending: {
63
59
  frame: "border-cyan-300/25 bg-cyan-300/[0.08] text-cyan-100",
64
- dot: "bg-cyan-200 shadow-[0_0_14px_rgba(103,232,249,0.8)]",
65
60
  },
66
61
  };
67
62
 
@@ -247,11 +242,10 @@ export const LogEntryHeader = memo(function ({
247
242
  {/* Request ID */}
248
243
  <span
249
244
  className={cn(
250
- "inline-flex h-8 shrink-0 items-center gap-2 rounded-md px-2 font-mono",
245
+ "inline-flex h-8 shrink-0 items-center rounded-md px-2 font-mono",
251
246
  statusCue.frame,
252
247
  )}
253
248
  >
254
- <span className={cn("size-1.5 rounded-full", statusCue.dot)} aria-hidden="true" />
255
249
  <span className="text-xs font-semibold tabular-nums" title={`Log ID ${String(log.id)}`}>
256
250
  #{displayNumber}
257
251
  </span>