@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.
Files changed (73) hide show
  1. package/.output/cli.js +57 -13
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
  7. package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
  9. package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
  10. package/.output/public/assets/index-Blqvndcn.js +1 -0
  11. package/.output/public/assets/index-Cs79WRQj.css +1 -0
  12. package/.output/public/assets/{index-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
  13. package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
  14. package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +255 -201
  16. package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
  17. package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
  19. package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
  20. package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
  21. package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
  23. package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
  26. package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
  27. package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
  28. package/.output/server/index.mjs +63 -63
  29. package/README.md +57 -2
  30. package/package.json +1 -1
  31. package/src/cli/startupOutput.ts +16 -0
  32. package/src/cli.ts +61 -15
  33. package/src/components/ProxyViewer.tsx +535 -163
  34. package/src/components/ProxyViewerContainer.tsx +77 -67
  35. package/src/components/alerts/AlertsDialog.tsx +2 -1
  36. package/src/components/clients/ClientLogo.tsx +1 -1
  37. package/src/components/groups/GroupsDialog.tsx +19 -1
  38. package/src/components/providers/ProviderCard.tsx +133 -32
  39. package/src/components/providers/ProviderForm.tsx +61 -29
  40. package/src/components/providers/ProviderLogo.tsx +6 -1
  41. package/src/components/providers/ProvidersPanel.tsx +7 -7
  42. package/src/components/providers/SettingsDialog.tsx +13 -6
  43. package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
  44. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  45. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  46. package/src/components/proxy-viewer/ConversationHeader.tsx +153 -120
  47. package/src/components/proxy-viewer/LogEntry.tsx +130 -43
  48. package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
  49. package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
  50. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  51. package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
  52. package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
  53. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  54. package/src/components/proxy-viewer/bodyHydration.ts +56 -0
  55. package/src/components/proxy-viewer/viewerState.ts +0 -4
  56. package/src/components/ui/icon-trigger.ts +2 -0
  57. package/src/components/ui/transient-toast.tsx +104 -0
  58. package/src/lib/providerTestContract.ts +56 -10
  59. package/src/lib/providerTestPrompt.ts +23 -1
  60. package/src/lib/sessionInfoContract.ts +4 -0
  61. package/src/lib/upstreamUrl.ts +105 -1
  62. package/src/mcp/server.ts +71 -27
  63. package/src/mcp/toolHandlers.ts +24 -5
  64. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  65. package/src/proxy/sessionInfo.ts +31 -2
  66. package/src/proxy/sessionSupervisor.ts +20 -1
  67. package/src/proxy/store.ts +1 -1
  68. package/src/routes/api/providers.$providerId.test.log.ts +93 -14
  69. package/styles/globals.css +180 -180
  70. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
  71. package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
  72. package/.output/public/assets/index-CI1-G8ua.js +0 -1
  73. package/.output/public/assets/index-DdhFqPsI.css +0 -1
@@ -12,6 +12,7 @@ import {
12
12
  type SessionMembershipEvidence,
13
13
  } from "./ProxyViewer";
14
14
  import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
15
+ import { TransientToast, useTransientToast } from "./ui/transient-toast";
15
16
 
16
17
  type SSEUpdate =
17
18
  | {
@@ -185,7 +186,7 @@ export function ProxyViewerContainer({
185
186
  const [allLogs, setAllLogs] = useState<CapturedLog[]>([]);
186
187
  const [selectedSession, setSelectedSession] = useState(initialSessionId ?? "__all__");
187
188
  const [selectedModel, setSelectedModel] = useState("__all__");
188
- const [error, setError] = useState<string | null>(null);
189
+ const { notice, showToast, dismissToast } = useTransientToast();
189
190
  const [streamInitialized, setStreamInitialized] = useState(initialSessionId === undefined);
190
191
  const [logPage, setLogPage] = useState<LogCursorPage | null>(null);
191
192
  const [sessionPageLoading, setSessionPageLoading] = useState(initialSessionId !== undefined);
@@ -202,6 +203,12 @@ export function ProxyViewerContainer({
202
203
  // Debounce buffer for SSE updates
203
204
  const pendingUpdatesRef = useRef<CapturedLog[]>([]);
204
205
  const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
206
+ const showErrorToast = useCallback(
207
+ (message: string) => {
208
+ showToast({ message, tone: "error" });
209
+ },
210
+ [showToast],
211
+ );
205
212
 
206
213
  // Derived view: the logs the user actually sees. Recomputed only when the
207
214
  // underlying set or filter changes — never on every SSE message.
@@ -269,7 +276,7 @@ export function ProxyViewerContainer({
269
276
  const parsed: unknown = JSON.parse(rawData);
270
277
  const updateResult = SSEUpdateSchema.safeParse(parsed);
271
278
  if (!updateResult.success) {
272
- setError("Failed to parse SSE data");
279
+ showErrorToast("Failed to parse SSE data");
273
280
  return;
274
281
  }
275
282
  const update = updateResult.data;
@@ -285,17 +292,17 @@ export function ProxyViewerContainer({
285
292
  logIndexRef.current = buildLogIndex(nextLogs);
286
293
  setAllLogs(nextLogs);
287
294
  setStreamInitialized(true);
288
- setError(null);
295
+ dismissToast();
289
296
  } else if (update.type === "update") {
290
297
  scheduleUpdate(update.log);
291
298
  }
292
299
  } catch {
293
- setError("Failed to parse SSE data");
300
+ showErrorToast("Failed to parse SSE data");
294
301
  }
295
302
  };
296
303
 
297
304
  es.onerror = () => {
298
- setError("SSE connection lost, reconnecting...");
305
+ showErrorToast("SSE connection lost, reconnecting...");
299
306
  setStreamInitialized(true);
300
307
  es.close();
301
308
  if (reconnectTimeoutRef.current !== null) {
@@ -303,7 +310,7 @@ export function ProxyViewerContainer({
303
310
  }
304
311
  reconnectTimeoutRef.current = setTimeout(connectSSE, 3000);
305
312
  };
306
- }, [initialSessionId, scheduleUpdate]);
313
+ }, [dismissToast, initialSessionId, scheduleUpdate, showErrorToast]);
307
314
 
308
315
  const loadSessionPage = useCallback(
309
316
  (request: SessionPageRequest) => {
@@ -312,7 +319,7 @@ export function ProxyViewerContainer({
312
319
  const requestId = sessionPageRequestIdRef.current + 1;
313
320
  sessionPageRequestIdRef.current = requestId;
314
321
  setSessionPageLoading(true);
315
- setError(null);
322
+ dismissToast();
316
323
 
317
324
  void fetchJson(buildSessionLogsPageUrl(initialSessionId, request), LogCursorPageSchema)
318
325
  .then((page) => {
@@ -323,17 +330,17 @@ export function ProxyViewerContainer({
323
330
  setLogPage(page);
324
331
  setSelectedSession(initialSessionId);
325
332
  setSessionPageLoading(false);
326
- setError(null);
333
+ dismissToast();
327
334
  })
328
335
  .catch((err: unknown) => {
329
336
  if (sessionPageRequestIdRef.current !== requestId) return;
330
337
  setAllLogs([]);
331
338
  setLogPage(null);
332
339
  setSessionPageLoading(false);
333
- setError(err instanceof Error ? err.message : "Failed to load session logs");
340
+ showErrorToast(err instanceof Error ? err.message : "Failed to load session logs");
334
341
  });
335
342
  },
336
- [initialSessionId],
343
+ [dismissToast, initialSessionId, showErrorToast],
337
344
  );
338
345
 
339
346
  useEffect(() => {
@@ -458,67 +465,73 @@ export function ProxyViewerContainer({
458
465
  }),
459
466
  });
460
467
  if (!res.ok) {
461
- setError("Failed to clear logs");
468
+ showErrorToast("Failed to clear logs");
462
469
  return;
463
470
  }
464
471
  logIndexRef.current.clear();
465
472
  setAllLogs([]);
466
- setError(null);
473
+ dismissToast();
467
474
  } catch (err) {
468
- setError(err instanceof Error ? err.message : "Unknown error clearing logs");
475
+ showErrorToast(err instanceof Error ? err.message : "Unknown error clearing logs");
469
476
  }
470
477
  })();
471
- }, [allLogs, initialSessionId]);
472
-
473
- const handleClearGroup = useCallback((ids: number[]) => {
474
- if (ids.length === 0) return;
475
- void (async () => {
476
- try {
477
- const res = await fetch("/api/logs", {
478
- method: "DELETE",
479
- headers: { "Content-Type": "application/json" },
480
- body: JSON.stringify({ ids }),
481
- });
482
- if (!res.ok) {
483
- setError("Failed to clear group");
484
- return;
478
+ }, [allLogs, dismissToast, initialSessionId, showErrorToast]);
479
+
480
+ const handleClearGroup = useCallback(
481
+ (ids: number[]) => {
482
+ if (ids.length === 0) return;
483
+ void (async () => {
484
+ try {
485
+ const res = await fetch("/api/logs", {
486
+ method: "DELETE",
487
+ headers: { "Content-Type": "application/json" },
488
+ body: JSON.stringify({ ids }),
489
+ });
490
+ if (!res.ok) {
491
+ showErrorToast("Failed to clear group");
492
+ return;
493
+ }
494
+ const idSet = new Set(ids);
495
+ setAllLogs((prev) => {
496
+ const remaining = prev.filter((l) => !idSet.has(l.id));
497
+ logIndexRef.current = buildLogIndex(remaining);
498
+ return remaining;
499
+ });
500
+ dismissToast();
501
+ } catch (err) {
502
+ showErrorToast(err instanceof Error ? err.message : "Unknown error clearing group");
485
503
  }
486
- const idSet = new Set(ids);
487
- setAllLogs((prev) => {
488
- const remaining = prev.filter((l) => !idSet.has(l.id));
489
- logIndexRef.current = buildLogIndex(remaining);
490
- return remaining;
491
- });
492
- setError(null);
493
- } catch (err) {
494
- setError(err instanceof Error ? err.message : "Unknown error clearing group");
495
- }
496
- })();
497
- }, []);
498
-
499
- const handleImportLogs = useCallback(async (file: File): Promise<ImportLogsResponse> => {
500
- const formData = new FormData();
501
- formData.set("file", file);
502
- const result = await fetchJson("/api/logs/import", ImportLogsResponseSchema, {
503
- method: "POST",
504
- body: formData,
505
- });
504
+ })();
505
+ },
506
+ [dismissToast, showErrorToast],
507
+ );
506
508
 
507
- setAllLogs((prev) => {
508
- const importedIds = new Set(result.logs.map((log) => log.id));
509
- const next = [...prev.filter((log) => !importedIds.has(log.id)), ...result.logs].sort(
510
- (left, right) => left.id - right.id,
511
- );
512
- const trimmed = trimClientLogs(next);
513
- logIndexRef.current = buildLogIndex(trimmed);
514
- return trimmed;
515
- });
516
- setSelectedSession(result.sessionId);
517
- setSelectedModel("__all__");
518
- setLogPage(null);
519
- setError(null);
520
- return result;
521
- }, []);
509
+ const handleImportLogs = useCallback(
510
+ async (file: File): Promise<ImportLogsResponse> => {
511
+ const formData = new FormData();
512
+ formData.set("file", file);
513
+ const result = await fetchJson("/api/logs/import", ImportLogsResponseSchema, {
514
+ method: "POST",
515
+ body: formData,
516
+ });
517
+
518
+ setAllLogs((prev) => {
519
+ const importedIds = new Set(result.logs.map((log) => log.id));
520
+ const next = [...prev.filter((log) => !importedIds.has(log.id)), ...result.logs].sort(
521
+ (left, right) => left.id - right.id,
522
+ );
523
+ const trimmed = trimClientLogs(next);
524
+ logIndexRef.current = buildLogIndex(trimmed);
525
+ return trimmed;
526
+ });
527
+ setSelectedSession(result.sessionId);
528
+ setSelectedModel("__all__");
529
+ setLogPage(null);
530
+ dismissToast();
531
+ return result;
532
+ },
533
+ [dismissToast],
534
+ );
522
535
 
523
536
  // Read the strip config once at the container so the virtualized list does
524
537
  // not need N independent SWR subscriptions per row.
@@ -569,11 +582,7 @@ export function ProxyViewerContainer({
569
582
 
570
583
  return (
571
584
  <>
572
- {error !== null && (
573
- <div className="fixed top-4 right-4 bg-destructive text-destructive-foreground px-4 py-2 rounded-md text-sm z-50">
574
- {error}
575
- </div>
576
- )}
585
+ <TransientToast notice={notice} onDismiss={dismissToast} />
577
586
  <OnboardingBanner />
578
587
  <ProxyViewer
579
588
  logs={logs}
@@ -585,6 +594,7 @@ export function ProxyViewerContainer({
585
594
  onModelChange={setSelectedModel}
586
595
  onClearAll={handleClearAll}
587
596
  onClearGroup={handleClearGroup}
597
+ onNotify={showToast}
588
598
  onImportLogs={initialSessionId === undefined ? handleImportLogs : undefined}
589
599
  isLoading={initialSessionId === undefined ? !streamInitialized : sessionPageLoading}
590
600
  pagination={pagination}
@@ -5,6 +5,7 @@ import { type AlertListFilters, useAlerts, useAlertSummary } from "../../lib/use
5
5
  import { cn } from "../../lib/utils";
6
6
  import { Badge } from "../ui/badge";
7
7
  import { Button } from "../ui/button";
8
+ import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
8
9
  import {
9
10
  Dialog,
10
11
  DialogContent,
@@ -477,7 +478,7 @@ export function AlertsDialog(): JSX.Element {
477
478
  type="button"
478
479
  variant="ghost"
479
480
  size="icon"
480
- className="relative size-8"
481
+ className={INSPECTOR_ICON_TRIGGER_CLASS}
481
482
  aria-label="Alerts"
482
483
  title="Alerts"
483
484
  >
@@ -117,7 +117,7 @@ export function ClientLogo({
117
117
  return (
118
118
  <span
119
119
  className={cn(
120
- "inline-flex size-6 shrink-0 items-center justify-center rounded-md border border-white/10 bg-black/20 p-1 text-cyan-100 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]",
120
+ "border border-border bg-muted/40 inline-flex size-6 shrink-0 items-center justify-center rounded-md p-1 text-cyan-100",
121
121
  className,
122
122
  )}
123
123
  aria-label={label}
@@ -14,6 +14,7 @@ import {
14
14
  import { Button } from "../ui/button";
15
15
  import { Badge } from "../ui/badge";
16
16
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
17
+ import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
17
18
  import { cn } from "../../lib/utils";
18
19
  import { parseJsonResponse, readApiError } from "../../lib/apiClient";
19
20
  import {
@@ -29,6 +30,7 @@ import {
29
30
  import type { EvidenceReport } from "../../lib/runContract";
30
31
  import type { SessionLogSummary } from "../../lib/sessionInfoContract";
31
32
  import { getSessionPath } from "../../lib/sessionUrl";
33
+ import { ClientLogo, clientAppLabel, detectClientApp } from "../clients/ClientLogo";
32
34
  import { useGroupEvidence } from "../../lib/useGroupEvidence";
33
35
  import { useGroups } from "../../lib/useGroups";
34
36
  import { useCopyFeedback } from "../proxy-viewer/useCopyFeedback";
@@ -195,6 +197,13 @@ function firstNonEmpty(...values: Array<string | null>): string {
195
197
  return "-";
196
198
  }
197
199
 
200
+ function firstOptionalText(...values: Array<string | null | undefined>): string | null {
201
+ for (const value of values) {
202
+ if (value !== null && value !== undefined && value.trim().length > 0) return value;
203
+ }
204
+ return null;
205
+ }
206
+
198
207
  function providerModel(member: InspectorGroupMember): string {
199
208
  const provider = member.provider;
200
209
  const model = member.model;
@@ -400,7 +409,7 @@ export function GroupsDialog(): JSX.Element {
400
409
  type="button"
401
410
  variant="ghost"
402
411
  size="icon"
403
- className="relative size-8"
412
+ className={INSPECTOR_ICON_TRIGGER_CLASS}
404
413
  aria-label="Evaluation groups"
405
414
  title="Evaluation groups"
406
415
  >
@@ -981,6 +990,14 @@ function MemberRow({
981
990
  const tokens = session?.tokenUsage.total ?? null;
982
991
  const firstChunkMs = log?.firstChunkMs ?? null;
983
992
  const sessionLabel = firstNonEmpty(member.label, member.sessionId);
993
+ const clientUserAgent = firstOptionalText(session?.clientUserAgent, log?.userAgent, member.agent);
994
+ const clientProjectFolder = session?.clientProjectFolder ?? null;
995
+ const clientApp = detectClientApp({
996
+ userAgent: clientUserAgent,
997
+ conversationId: member.sessionId,
998
+ clientProjectFolder,
999
+ });
1000
+ const clientLabel = clientAppLabel(clientApp);
984
1001
 
985
1002
  return (
986
1003
  <tr className="border-b border-border last:border-0 hover:bg-muted/20">
@@ -1003,6 +1020,7 @@ function MemberRow({
1003
1020
  <ChevronRight className="size-3.5" />
1004
1021
  )}
1005
1022
  </Button>
1023
+ {clientLabel !== null && <ClientLogo client={clientApp} className="size-5" />}
1006
1024
  {canOpenLiveSession ? (
1007
1025
  <a
1008
1026
  href={sessionPath}
@@ -31,7 +31,15 @@ import {
31
31
  providerHasContextMetadata,
32
32
  } from "../../lib/providerModelMetadata";
33
33
  import { formatContextWindowTokens } from "../../lib/utils";
34
+ import {
35
+ PATH_V1_CHAT_COMPLETIONS,
36
+ PATH_V1_MESSAGES,
37
+ PATH_V1_RESPONSES,
38
+ previewUpstreamUrl,
39
+ type UpstreamUrlDiagnostic,
40
+ } from "../../lib/upstreamUrl";
34
41
  import type {
42
+ ProviderFormatTestResults,
35
43
  ProviderTestErrorType as ErrorType,
36
44
  ProviderTestResult as TestResult,
37
45
  ProviderTestResults as TestResults,
@@ -65,8 +73,6 @@ function isNotConfiguredState(result: ProviderTestState): result is { notConfigu
65
73
  return Object.prototype.hasOwnProperty.call(result, "notConfigured");
66
74
  }
67
75
 
68
- type ProviderFormatTestResults = TestResults["anthropic"];
69
-
70
76
  function hasText(value: string | undefined): value is string {
71
77
  return value !== undefined && value.trim() !== "";
72
78
  }
@@ -206,12 +212,12 @@ function ProviderFormatTestStatus({
206
212
  results: ProviderFormatTestResults;
207
213
  }): JSX.Element {
208
214
  return (
209
- <div className="flex min-w-0 flex-col gap-2">
210
- <div className="flex min-w-0 flex-col gap-1">
215
+ <div className="grid min-w-0 gap-2 sm:grid-cols-2">
216
+ <div className="min-w-0 rounded-md border border-border/60 bg-muted/20 px-2 py-1.5">
211
217
  <span className="text-[10px] text-muted-foreground">Non-stream</span>
212
218
  <TestStatus result={results.nonStreaming} />
213
219
  </div>
214
- <div className="flex min-w-0 flex-col gap-1">
220
+ <div className="min-w-0 rounded-md border border-border/60 bg-muted/20 px-2 py-1.5">
215
221
  <span className="text-[10px] text-muted-foreground">Stream</span>
216
222
  <TestStatus result={results.streaming} />
217
223
  </div>
@@ -219,6 +225,56 @@ function ProviderFormatTestStatus({
219
225
  );
220
226
  }
221
227
 
228
+ function EndpointDiagnosticLine({
229
+ diagnostic,
230
+ }: {
231
+ diagnostic: UpstreamUrlDiagnostic;
232
+ }): JSX.Element {
233
+ return (
234
+ <div className={diagnostic.severity === "warning" ? "text-amber-200" : "text-muted-foreground"}>
235
+ {diagnostic.message}
236
+ </div>
237
+ );
238
+ }
239
+
240
+ function ProviderEndpointSummary({
241
+ label,
242
+ baseUrl,
243
+ endpoint,
244
+ providerName,
245
+ children,
246
+ }: {
247
+ label: string;
248
+ baseUrl: string;
249
+ endpoint: string;
250
+ providerName: string;
251
+ children?: ReactNode;
252
+ }): JSX.Element {
253
+ const preview = previewUpstreamUrl(baseUrl, endpoint, { providerName });
254
+
255
+ return (
256
+ <div className="flex min-w-0 flex-col gap-2 border-t border-border/60 pt-2 first:border-t-0 first:pt-0">
257
+ <div className="grid min-w-0 gap-1 text-xs sm:grid-cols-[104px_minmax(0,1fr)]">
258
+ <span className="font-medium text-muted-foreground">{label}</span>
259
+ <code className="min-w-0 break-all font-mono text-foreground/80">{baseUrl}</code>
260
+ <span className="text-[10px] uppercase text-muted-foreground/80">Final</span>
261
+ <code className="min-w-0 break-all font-mono text-foreground">{preview.finalUrl}</code>
262
+ </div>
263
+ {preview.diagnostics.length > 0 && (
264
+ <div className="space-y-1 text-xs">
265
+ {preview.diagnostics.map((diagnostic) => (
266
+ <EndpointDiagnosticLine
267
+ key={`${diagnostic.severity}:${diagnostic.message}`}
268
+ diagnostic={diagnostic}
269
+ />
270
+ ))}
271
+ </div>
272
+ )}
273
+ {children}
274
+ </div>
275
+ );
276
+ }
277
+
222
278
  function getErrorIcon(type: ErrorType): JSX.Element {
223
279
  const iconProps = { className: "size-3", strokeWidth: 2 };
224
280
  switch (type) {
@@ -314,8 +370,8 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
314
370
  <TooltipProvider>
315
371
  <Tooltip>
316
372
  <TooltipTrigger asChild>
317
- <div className="flex items-center gap-1 text-xs text-green-600 shrink-0">
318
- <CheckCircle className="size-3" />
373
+ <div className="flex items-center gap-1 text-xs text-cyan-100 shrink-0">
374
+ <CheckCircle className="size-3 text-cyan-300" />
319
375
  <span>Connected</span>
320
376
  {tokenParts.length > 0 && (
321
377
  <span className="text-muted-foreground">({displayTokens})</span>
@@ -440,15 +496,40 @@ export function ProviderCard({
440
496
  const canRefreshMetadata =
441
497
  provider.modelMetadataUrl !== undefined && provider.modelMetadataUrl.trim() !== "";
442
498
  const metadataImportLabel = canRefreshMetadata ? "Refresh" : "Import defaults";
499
+ const providerSourceBadge =
500
+ provider.source === "company" ? (
501
+ <TooltipProvider>
502
+ <Tooltip>
503
+ <TooltipTrigger asChild>
504
+ <span className="shrink-0 rounded border border-cyan-300/20 bg-cyan-300/[0.08] px-1.5 py-0.5 text-xs text-cyan-100">
505
+ Company
506
+ </span>
507
+ </TooltipTrigger>
508
+ <TooltipContent>Company-provided API key</TooltipContent>
509
+ </Tooltip>
510
+ </TooltipProvider>
511
+ ) : provider.source === "personal" ? (
512
+ <TooltipProvider>
513
+ <Tooltip>
514
+ <TooltipTrigger asChild>
515
+ <span className="shrink-0 rounded border border-violet-300/20 bg-violet-300/[0.08] px-1.5 py-0.5 text-xs text-violet-100">
516
+ Personal
517
+ </span>
518
+ </TooltipTrigger>
519
+ <TooltipContent>Your personal API key</TooltipContent>
520
+ </Tooltip>
521
+ </TooltipProvider>
522
+ ) : null;
443
523
 
444
524
  return (
445
525
  <div
446
- className={`border rounded-lg p-4 flex flex-col gap-3 bg-card transition-all duration-500 ${isHighlighted === true ? "ring-2 ring-primary shadow-md" : ""}`}
526
+ className={`border border-border bg-card shadow-sm flex flex-col gap-3 rounded-[8px] p-4 transition-all duration-500 ${isHighlighted === true ? "ring-2 ring-primary shadow-md" : ""}`}
447
527
  >
448
528
  <div className="flex items-start justify-between gap-2">
449
529
  <div className="flex items-center gap-2 min-w-0">
450
530
  <span className="font-medium truncate">{provider.name}</span>
451
- {provider.source === "company" && (
531
+ {providerSourceBadge}
532
+ {providerSourceBadge === null && provider.source === "company" && (
452
533
  <TooltipProvider>
453
534
  <Tooltip>
454
535
  <TooltipTrigger asChild>
@@ -460,7 +541,7 @@ export function ProviderCard({
460
541
  </Tooltip>
461
542
  </TooltipProvider>
462
543
  )}
463
- {provider.source === "personal" && (
544
+ {providerSourceBadge === null && provider.source === "personal" && (
464
545
  <TooltipProvider>
465
546
  <Tooltip>
466
547
  <TooltipTrigger asChild>
@@ -575,39 +656,47 @@ export function ProviderCard({
575
656
  className="text-muted-foreground hover:text-foreground transition-colors p-1"
576
657
  aria-label="Copy API key"
577
658
  >
578
- {copied ? <Check className="size-4 text-green-500" /> : <Copy className="size-4" />}
659
+ {copied ? <Check className="size-4 text-cyan-300" /> : <Copy className="size-4" />}
579
660
  </button>
580
661
  </div>
581
662
 
582
663
  {provider.anthropicBaseUrl !== undefined && provider.anthropicBaseUrl !== "" && (
583
- <div className="flex min-w-0 flex-col gap-2">
584
- <div className="min-w-0 text-xs text-muted-foreground">
585
- <span className="font-medium">Anthropic:</span>{" "}
586
- <span className="truncate">{provider.anthropicBaseUrl}</span>
587
- </div>
664
+ <ProviderEndpointSummary
665
+ label="Anthropic"
666
+ baseUrl={provider.anthropicBaseUrl}
667
+ endpoint={PATH_V1_MESSAGES}
668
+ providerName={provider.name}
669
+ >
588
670
  {testResults !== undefined && (
589
671
  <ProviderFormatTestStatus results={testResults.anthropic} />
590
672
  )}
591
- </div>
673
+ </ProviderEndpointSummary>
592
674
  )}
593
675
 
594
676
  {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
595
- <div className="flex min-w-0 flex-col gap-2">
596
- <div className="min-w-0 text-xs text-muted-foreground">
597
- <span className="font-medium">OpenAI Chat:</span>{" "}
598
- <span className="truncate">{provider.openaiBaseUrl}</span>
599
- </div>
600
- {testResults !== undefined && <ProviderFormatTestStatus results={testResults.openai} />}
601
- </div>
677
+ <ProviderEndpointSummary
678
+ label="OpenAI Chat"
679
+ baseUrl={provider.openaiBaseUrl}
680
+ endpoint={PATH_V1_CHAT_COMPLETIONS}
681
+ providerName={provider.name}
682
+ >
683
+ {testResults !== undefined && (
684
+ <ProviderFormatTestStatus results={testResults.openaiChat} />
685
+ )}
686
+ </ProviderEndpointSummary>
602
687
  )}
603
688
 
604
689
  {provider.openaiResponsesBaseUrl !== undefined && provider.openaiResponsesBaseUrl !== "" && (
605
- <div className="flex min-w-0 flex-col gap-2">
606
- <div className="min-w-0 text-xs text-muted-foreground">
607
- <span className="font-medium">OpenAI Responses:</span>{" "}
608
- <span className="truncate">{provider.openaiResponsesBaseUrl}</span>
609
- </div>
610
- </div>
690
+ <ProviderEndpointSummary
691
+ label="OpenAI Responses"
692
+ baseUrl={provider.openaiResponsesBaseUrl}
693
+ endpoint={PATH_V1_RESPONSES}
694
+ providerName={provider.name}
695
+ >
696
+ {testResults !== undefined && (
697
+ <ProviderFormatTestStatus results={testResults.openaiResponses} />
698
+ )}
699
+ </ProviderEndpointSummary>
611
700
  )}
612
701
 
613
702
  {testResults?.testedAt !== undefined && (
@@ -644,9 +733,15 @@ export function ProviderCard({
644
733
  )}
645
734
  {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
646
735
  <th className="text-left py-1 px-2 font-medium text-muted-foreground">
647
- OpenAI
736
+ Chat
648
737
  </th>
649
738
  )}
739
+ {provider.openaiResponsesBaseUrl !== undefined &&
740
+ provider.openaiResponsesBaseUrl !== "" && (
741
+ <th className="text-left py-1 px-2 font-medium text-muted-foreground">
742
+ Responses
743
+ </th>
744
+ )}
650
745
  </tr>
651
746
  </thead>
652
747
  <tbody>
@@ -661,9 +756,15 @@ export function ProviderCard({
661
756
  )}
662
757
  {provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
663
758
  <td className="py-1 px-2 align-top">
664
- <ProviderFormatTestStatus results={modelResult.openai} />
759
+ <ProviderFormatTestStatus results={modelResult.openaiChat} />
665
760
  </td>
666
761
  )}
762
+ {provider.openaiResponsesBaseUrl !== undefined &&
763
+ provider.openaiResponsesBaseUrl !== "" && (
764
+ <td className="py-1 px-2 align-top">
765
+ <ProviderFormatTestStatus results={modelResult.openaiResponses} />
766
+ </td>
767
+ )}
667
768
  </tr>
668
769
  ))}
669
770
  </tbody>