@tonyclaw/agent-inspector 3.0.23 → 3.0.24

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 (47) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-CFedvZHU.js → CompareDrawer-BC52FYwn.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-WuChOrSS.js +127 -0
  4. package/.output/public/assets/{ReplayDialog-X-fRSKAu.js → ReplayDialog-BcKsarKt.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-S8qB5nVg.js → RequestAnatomy-DR0uGpCj.js} +1 -1
  6. package/.output/public/assets/{ResponseView-_rcOWSwJ.js → ResponseView-kxAAPfmn.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-Bc15AVpN.js → StreamingChunkSequence-YQR0PjQj.js} +1 -1
  8. package/.output/public/assets/{_sessionId-Cz-HW2Wf.js → _sessionId-DRl-Y9om.js} +1 -1
  9. package/.output/public/assets/{index-CEgwWY3E.js → index-CQbrc-hi.js} +71 -7
  10. package/.output/public/assets/{index-CXYBU0j8.js → index-CcSIBI4L.js} +1 -1
  11. package/.output/public/assets/index-DQeXi4Rv.css +1 -0
  12. package/.output/public/assets/{index-ClKXooQd.js → index-ZrMgyEZV.js} +1 -1
  13. package/.output/public/assets/{json-viewer-QfU4qGdd.js → json-viewer-CS5pwnEA.js} +1 -1
  14. package/.output/server/_libs/lucide-react.mjs +266 -234
  15. package/.output/server/{_sessionId-9FAmYZVf.mjs → _sessionId-BLk2xPkR.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-BLKjOzwD.mjs → CompareDrawer-Cw7Dseo3.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-Dcs2Mq8l.mjs → ProxyViewerContainer-D8SB0zyQ.mjs} +1124 -877
  18. package/.output/server/_ssr/{ReplayDialog-oyiF4LIo.mjs → ReplayDialog-B-YMhZkE.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-DkyQXSuj.mjs → RequestAnatomy-a9d8UTYZ.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-3NTDEiDh.mjs → ResponseView-PzaZS4CN.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-DRq-5Bgn.mjs → StreamingChunkSequence-C21_TCXk.mjs} +2 -2
  22. package/.output/server/_ssr/{index-dtMiNUa1.mjs → index-CrCeNP7H.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-BhUwicvp.mjs → json-viewer-Di2gxs9H.mjs} +3 -3
  25. package/.output/server/_ssr/{router-DO0OntUK.mjs → router-BJzdUbcH.mjs} +296 -91
  26. package/.output/server/{_tanstack-start-manifest_v-DO5v3BHY.mjs → _tanstack-start-manifest_v-CGBTdAYC.mjs} +1 -1
  27. package/.output/server/index.mjs +76 -76
  28. package/package.json +3 -2
  29. package/src/components/ProxyViewer.tsx +136 -460
  30. package/src/components/ProxyViewerContainer.tsx +5 -47
  31. package/src/components/alerts/AlertsDialog.tsx +6 -2
  32. package/src/components/providers/ProviderCard.tsx +20 -579
  33. package/src/components/providers/ProviderTestResultsView.tsx +732 -0
  34. package/src/components/proxy-viewer/LogEntry.tsx +5 -0
  35. package/src/components/proxy-viewer/LogEntryHeader.tsx +7 -0
  36. package/src/components/proxy-viewer/ToolTraceEvents.tsx +33 -36
  37. package/src/components/proxy-viewer/TurnGroup.tsx +1 -5
  38. package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +46 -0
  39. package/src/components/proxy-viewer/proxyViewerLogic.ts +469 -0
  40. package/src/lib/themeMode.ts +45 -0
  41. package/src/lib/uiScale.ts +112 -0
  42. package/src/lib/useThemeMode.ts +40 -0
  43. package/src/lib/useUiScale.ts +52 -0
  44. package/src/routes/__root.tsx +78 -1
  45. package/styles/globals.css +305 -23
  46. package/.output/public/assets/ProxyViewerContainer-BlCrBvdm.js +0 -127
  47. package/.output/public/assets/index-B_SIQ47i.css +0 -1
@@ -1,4 +1,4 @@
1
- import { type JSX, type ReactNode, useState } from "react";
1
+ import { type JSX, useState } from "react";
2
2
  import { Button } from "../ui/button";
3
3
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
4
4
  import {
@@ -8,15 +8,7 @@ import {
8
8
  Trash2,
9
9
  RotateCw,
10
10
  CheckCircle,
11
- Minus,
12
11
  ExternalLink,
13
- AlertCircle,
14
- Wifi,
15
- WifiOff,
16
- Lock,
17
- Gauge,
18
- Server,
19
- HelpCircle,
20
12
  Clock,
21
13
  Copy,
22
14
  Check,
@@ -31,13 +23,12 @@ import {
31
23
  providerHasContextMetadata,
32
24
  } from "../../lib/providerModelMetadata";
33
25
  import { formatContextWindowTokens } from "../../lib/utils";
34
- import type {
35
- ProviderFormatTestResults,
36
- ProviderTestErrorType as ErrorType,
37
- ProviderTestResult as TestResult,
38
- ProviderTestResults as TestResults,
39
- ProviderTestState,
40
- } from "../../lib/providerTestContract";
26
+ import type { ProviderTestResults as TestResults } from "../../lib/providerTestContract";
27
+ import {
28
+ ProviderModelTestResultsView,
29
+ ProviderTestMatrix,
30
+ ProviderTestPill,
31
+ } from "./ProviderTestResultsView";
41
32
 
42
33
  // Known provider documentation links (fallback when apiDocsUrl is not configured)
43
34
  const KNOWN_PROVIDER_DOCS: Record<string, string> = {
@@ -57,218 +48,14 @@ type ProviderCardProps = {
57
48
  isRefreshingMetadata?: boolean;
58
49
  };
59
50
 
60
- function hasSuccessField(result: ProviderTestState): result is TestResult {
61
- return Object.prototype.hasOwnProperty.call(result, "success");
62
- }
63
-
64
- // Using Object.prototype.hasOwnProperty.call to avoid 'in' operator
65
- function isNotConfiguredState(result: ProviderTestState): result is { notConfigured: true } {
66
- return Object.prototype.hasOwnProperty.call(result, "notConfigured");
67
- }
68
-
69
- function hasText(value: string | undefined): value is string {
70
- return value !== undefined && value.trim() !== "";
71
- }
72
-
73
- function formatHeaders(headers: Record<string, string> | undefined): string | undefined {
74
- if (headers === undefined) {
75
- return undefined;
76
- }
77
-
78
- const lines = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
79
- if (lines.length === 0) {
80
- return undefined;
81
- }
82
-
83
- return lines.join("\n");
84
- }
85
-
86
- function hasHeaderDetails(headers: Record<string, string> | undefined): boolean {
87
- return formatHeaders(headers) !== undefined;
88
- }
89
-
90
- function formatElapsed(ms: number): string {
91
- if (ms < 1000) return `${ms}ms`;
92
- return `${(ms / 1000).toFixed(1)}s`;
93
- }
94
-
95
- function formatTokenRate(tokensPerSecond: number): string {
96
- if (tokensPerSecond >= 10) return `${tokensPerSecond.toFixed(0)}/s`;
97
- return `${tokensPerSecond.toFixed(1)}/s`;
98
- }
99
-
100
- function hasDiagnosticDetails(result: TestResult): boolean {
101
- const debug = result.debug;
102
- return (
103
- hasText(result.error?.hint) ||
104
- hasText(result.error?.details) ||
105
- hasText(result.rawResponse) ||
106
- hasText(debug?.requestUrl) ||
107
- hasText(debug?.requestBody) ||
108
- hasText(debug?.responseBody) ||
109
- hasHeaderDetails(debug?.requestHeaders) ||
110
- hasHeaderDetails(debug?.responseHeaders) ||
111
- debug?.responseStatus !== undefined
112
- );
113
- }
114
-
115
- function DiagnosticSection({
116
- title,
117
- children,
118
- }: {
119
- title: string;
120
- children: ReactNode;
121
- }): JSX.Element {
122
- return (
123
- <div className="space-y-1">
124
- <div className="font-medium text-foreground/80">{title}</div>
125
- {children}
126
- </div>
127
- );
128
- }
129
-
130
- function DiagnosticPre({ value }: { value: string }): JSX.Element {
131
- return (
132
- <pre className="max-h-40 overflow-auto whitespace-pre-wrap break-all rounded-md bg-background/80 p-2 font-mono text-[11px] leading-relaxed text-foreground/80">
133
- {value}
134
- </pre>
135
- );
136
- }
137
-
138
- function ProviderTestDetails({ result }: { result: TestResult }): JSX.Element {
139
- const debug = result.debug;
140
- const requestUrl = debug?.requestUrl;
141
- const requestMethod = debug?.requestMethod ?? "POST";
142
- const requestHeaders = formatHeaders(debug?.requestHeaders);
143
- const responseHeaders = formatHeaders(debug?.responseHeaders);
144
- const responseStatus =
145
- debug?.responseStatus !== undefined
146
- ? `${debug.responseStatus}${
147
- hasText(debug.responseStatusText) ? ` ${debug.responseStatusText}` : ""
148
- }`
149
- : undefined;
150
- const responseBody = debug?.responseBody ?? result.rawResponse;
151
-
152
- return (
153
- <div className="max-h-80 max-w-full space-y-2 overflow-auto rounded-md border bg-muted/50 p-2 text-xs text-muted-foreground shadow-sm sm:w-[520px]">
154
- {hasText(result.error?.hint) && (
155
- <DiagnosticSection title="Hint">
156
- <div>{result.error.hint}</div>
157
- </DiagnosticSection>
158
- )}
159
- {hasText(result.error?.details) && (
160
- <DiagnosticSection title="Error">
161
- <DiagnosticPre value={result.error.details} />
162
- </DiagnosticSection>
163
- )}
164
- {hasText(requestUrl) && (
165
- <DiagnosticSection title="Request">
166
- <div className="rounded-md bg-background/80 p-2 font-mono text-[11px] break-all text-foreground/80">
167
- {requestMethod} {requestUrl}
168
- </div>
169
- </DiagnosticSection>
170
- )}
171
- {requestHeaders !== undefined && (
172
- <DiagnosticSection title="Request Headers">
173
- <DiagnosticPre value={requestHeaders} />
174
- </DiagnosticSection>
175
- )}
176
- {hasText(debug?.requestBody) && (
177
- <DiagnosticSection title="Request Body">
178
- <DiagnosticPre value={debug.requestBody} />
179
- </DiagnosticSection>
180
- )}
181
- {hasText(responseStatus) && (
182
- <DiagnosticSection title="Response Status">
183
- <div className="rounded-md bg-background/80 p-2 font-mono text-[11px] text-foreground/80">
184
- {responseStatus}
185
- </div>
186
- </DiagnosticSection>
187
- )}
188
- {responseHeaders !== undefined && (
189
- <DiagnosticSection title="Response Headers">
190
- <DiagnosticPre value={responseHeaders} />
191
- </DiagnosticSection>
192
- )}
193
- {hasText(responseBody) && (
194
- <DiagnosticSection title="Response Body">
195
- <DiagnosticPre value={responseBody} />
196
- </DiagnosticSection>
197
- )}
198
- </div>
199
- );
200
- }
201
-
202
- function ProviderFormatTestStatus({
203
- results,
204
- }: {
205
- results: ProviderFormatTestResults;
206
- }): JSX.Element {
207
- return (
208
- <div className="grid min-w-0 gap-2 sm:grid-cols-2">
209
- <div className="min-w-0 rounded-md border border-border/60 bg-muted/20 px-2 py-1.5">
210
- <span className="text-[10px] text-muted-foreground">Non-stream</span>
211
- <TestStatus result={results.nonStreaming} />
212
- </div>
213
- <div className="min-w-0 rounded-md border border-border/60 bg-muted/20 px-2 py-1.5">
214
- <span className="text-[10px] text-muted-foreground">Stream</span>
215
- <TestStatus result={results.streaming} />
216
- </div>
217
- </div>
218
- );
219
- }
220
-
221
- function ProviderFormatStatusBlock({
222
- label,
223
- children,
224
- }: {
225
- label: string;
226
- children?: ReactNode;
227
- }): JSX.Element {
228
- return (
229
- <div className="min-w-0 space-y-2 rounded-md border border-border/60 bg-muted/15 p-2">
230
- <div className="text-xs font-medium text-foreground/80">{label}</div>
231
- {children}
232
- </div>
233
- );
234
- }
235
-
236
51
  function providerHasEndpoint(value: string | undefined): value is string {
237
52
  return value !== undefined && value !== "";
238
53
  }
239
54
 
240
- function providerTestTargets(provider: ProviderConfig): string[] {
241
- const targets: string[] = [];
242
- if (providerHasEndpoint(provider.anthropicBaseUrl)) {
243
- targets.push("Anthropic");
244
- }
245
- if (providerHasEndpoint(provider.openaiBaseUrl)) {
246
- targets.push("Chat");
247
- }
248
- if (providerHasEndpoint(provider.openaiResponsesBaseUrl)) {
249
- targets.push("Responses");
250
- }
251
- return targets;
252
- }
253
-
254
- type CapabilityChipTone = "active" | "quiet";
255
-
256
- function CapabilityChip({
257
- label,
258
- enabled,
259
- tone,
260
- }: {
261
- label: string;
262
- enabled: boolean;
263
- tone?: CapabilityChipTone;
264
- }): JSX.Element {
265
- const resolvedTone: CapabilityChipTone = tone ?? (enabled ? "active" : "quiet");
266
- const className = enabled
267
- ? "border-border/70 bg-muted/25 text-foreground/80"
268
- : "border-border/50 bg-transparent text-muted-foreground/70";
55
+ function CapabilityChip({ label }: { label: string }): JSX.Element {
269
56
  return (
270
- <span className={`inline-flex items-center gap-1 rounded border px-1.5 py-0.5 ${className}`}>
271
- {enabled ? <CheckCircle className="size-3" /> : <Minus className="size-3" />}
57
+ <span className="inline-flex items-center gap-1 rounded border border-border/70 bg-muted/25 px-1.5 py-0.5 text-foreground/80">
58
+ <CheckCircle className="size-3" />
272
59
  {label}
273
60
  </span>
274
61
  );
@@ -289,123 +76,13 @@ function ProviderCapabilitySummary({ provider }: { provider: ProviderConfig }):
289
76
 
290
77
  return (
291
78
  <div className="flex min-w-0 flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
292
- {hasAnthropic && <CapabilityChip label="Anthropic" enabled={true} />}
293
- {hasChat && <CapabilityChip label="OpenAI Chat" enabled={true} />}
294
- {hasResponses && <CapabilityChip label="OpenAI Responses" enabled={true} />}
79
+ {hasAnthropic && <CapabilityChip label="Anthropic" />}
80
+ {hasChat && <CapabilityChip label="OpenAI Chat" />}
81
+ {hasResponses && <CapabilityChip label="OpenAI Responses" />}
295
82
  </div>
296
83
  );
297
84
  }
298
85
 
299
- type ProviderTestSummary = {
300
- total: number;
301
- passed: number;
302
- failed: number;
303
- testing: number;
304
- };
305
-
306
- function configuredFormatResults(
307
- provider: ProviderConfig,
308
- testResults: TestResults | undefined,
309
- ): ProviderFormatTestResults[] {
310
- if (testResults === undefined) return [];
311
- const results: ProviderFormatTestResults[] = [];
312
- if (providerHasEndpoint(provider.anthropicBaseUrl)) {
313
- results.push(testResults.anthropic);
314
- }
315
- if (providerHasEndpoint(provider.openaiBaseUrl)) {
316
- results.push(testResults.openaiChat);
317
- }
318
- if (providerHasEndpoint(provider.openaiResponsesBaseUrl)) {
319
- results.push(testResults.openaiResponses);
320
- }
321
- return results;
322
- }
323
-
324
- function summarizeTestResults(
325
- provider: ProviderConfig,
326
- testResults: TestResults | undefined,
327
- ): ProviderTestSummary {
328
- const summary: ProviderTestSummary = { total: 0, passed: 0, failed: 0, testing: 0 };
329
- for (const results of configuredFormatResults(provider, testResults)) {
330
- for (const state of [results.nonStreaming, results.streaming]) {
331
- if (isNotConfiguredState(state)) continue;
332
- summary.total += 1;
333
- if (hasSuccessField(state)) {
334
- if (state.success) {
335
- summary.passed += 1;
336
- } else {
337
- summary.failed += 1;
338
- }
339
- } else {
340
- summary.testing += 1;
341
- }
342
- }
343
- }
344
- return summary;
345
- }
346
-
347
- function ProviderTestPill({
348
- provider,
349
- testResults,
350
- isTesting,
351
- }: {
352
- provider: ProviderConfig;
353
- testResults: TestResults | undefined;
354
- isTesting: boolean;
355
- }): JSX.Element {
356
- const targets = providerTestTargets(provider);
357
- const summary = summarizeTestResults(provider, testResults);
358
-
359
- if (isTesting) {
360
- return (
361
- <span className="inline-flex items-center gap-1 rounded border border-cyan-300/25 bg-cyan-300/[0.08] px-2 py-1 text-xs text-cyan-100">
362
- <RotateCw className="size-3 animate-spin" />
363
- Testing
364
- </span>
365
- );
366
- }
367
-
368
- if (targets.length === 0) {
369
- return (
370
- <span className="rounded border border-border/70 bg-muted/20 px-2 py-1 text-xs text-muted-foreground">
371
- No endpoint
372
- </span>
373
- );
374
- }
375
-
376
- if (testResults === undefined || summary.total === 0) {
377
- return (
378
- <span className="rounded border border-border/70 bg-muted/20 px-2 py-1 text-xs text-muted-foreground">
379
- Not tested
380
- </span>
381
- );
382
- }
383
-
384
- if (summary.failed > 0) {
385
- return (
386
- <span className="inline-flex items-center gap-1 rounded border border-red-400/25 bg-red-500/[0.08] px-2 py-1 text-xs text-red-200">
387
- <AlertCircle className="size-3" />
388
- {summary.passed}/{summary.total} passed
389
- </span>
390
- );
391
- }
392
-
393
- if (summary.testing > 0 || summary.passed < summary.total) {
394
- return (
395
- <span className="rounded border border-amber-300/25 bg-amber-400/[0.08] px-2 py-1 text-xs text-amber-100">
396
- Partial {summary.passed}/{summary.total}
397
- </span>
398
- );
399
- }
400
-
401
- return (
402
- <span className="inline-flex items-center gap-1 rounded border border-cyan-300/25 bg-cyan-300/[0.08] px-2 py-1 text-xs text-cyan-100">
403
- <CheckCircle className="size-3" />
404
- OK {summary.passed}/{summary.total}
405
- </span>
406
- );
407
- }
408
-
409
86
  function compactModelLabel(provider: ProviderConfig): string {
410
87
  const models = provider.models ?? [];
411
88
  if (models.length === 0) return "No models configured";
@@ -447,139 +124,6 @@ function ModelChip({ provider, model }: { provider: ProviderConfig; model: strin
447
124
  );
448
125
  }
449
126
 
450
- function getErrorIcon(type: ErrorType): JSX.Element {
451
- const iconProps = { className: "size-3", strokeWidth: 2 };
452
- switch (type) {
453
- case "timeout":
454
- return <Clock {...iconProps} />;
455
- case "network_unreachable":
456
- return <WifiOff {...iconProps} />;
457
- case "connection_refused":
458
- return <Wifi {...iconProps} />;
459
- case "auth_failed":
460
- return <Lock {...iconProps} />;
461
- case "rate_limited":
462
- return <Gauge {...iconProps} />;
463
- case "server_error":
464
- return <Server {...iconProps} />;
465
- case "invalid_response":
466
- return <HelpCircle {...iconProps} />;
467
- case "unknown":
468
- return <AlertCircle {...iconProps} />;
469
- default:
470
- return <AlertCircle {...iconProps} />;
471
- }
472
- }
473
-
474
- function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
475
- const [showDetails, setShowDetails] = useState(false);
476
-
477
- if (!hasSuccessField(result)) {
478
- // Not TestResult - check if NotConfigured or Testing
479
- if (isNotConfiguredState(result)) {
480
- return (
481
- <div className="flex items-center gap-1 text-xs text-muted-foreground shrink-0">
482
- <Minus className="size-3" />
483
- <span>Not configured</span>
484
- </div>
485
- );
486
- }
487
- // Must be Testing state
488
- return (
489
- <div className="flex items-center gap-1 text-xs text-muted-foreground shrink-0">
490
- <RotateCw className="size-3 animate-spin" />
491
- <span>Testing...</span>
492
- </div>
493
- );
494
- }
495
- if (result.success) {
496
- const tokenParts: ReactNode[] = [];
497
- const timingParts: ReactNode[] = [];
498
- if (result.inputTokens !== undefined) {
499
- tokenParts.push(<span key="in">{result.inputTokens} in</span>);
500
- }
501
- if (result.outputTokens !== undefined) {
502
- tokenParts.push(<span key="out">{result.outputTokens} out</span>);
503
- }
504
- if (result.cacheCreationInputTokens !== undefined && result.cacheCreationInputTokens > 0) {
505
- tokenParts.push(
506
- <span key="cache-create" className="font-mono tabular-nums text-emerald-400">
507
- KV Cache +{result.cacheCreationInputTokens}
508
- </span>,
509
- );
510
- }
511
- if (result.cacheReadInputTokens !== undefined && result.cacheReadInputTokens > 0) {
512
- tokenParts.push(
513
- <span key="cache-read" className="font-mono tabular-nums text-purple-400">
514
- KV Cache ~{result.cacheReadInputTokens}
515
- </span>,
516
- );
517
- }
518
- if (result.firstChunkMs !== undefined && result.firstChunkMs !== null) {
519
- timingParts.push(<span key="first">first {formatElapsed(result.firstChunkMs)}</span>);
520
- }
521
- if (result.totalStreamMs !== undefined && result.totalStreamMs !== null) {
522
- timingParts.push(<span key="total">total {formatElapsed(result.totalStreamMs)}</span>);
523
- }
524
- if (
525
- result.tokensPerSecond !== undefined &&
526
- result.tokensPerSecond !== null &&
527
- result.tokensPerSecond > 0
528
- ) {
529
- timingParts.push(<span key="rate">{formatTokenRate(result.tokensPerSecond)} tok/s</span>);
530
- }
531
- const displayTokens: ReactNode[] = [];
532
- for (let i = 0; i < tokenParts.length; i++) {
533
- if (i > 0) displayTokens.push(", ");
534
- displayTokens.push(tokenParts[i]);
535
- }
536
- const displayTiming: ReactNode[] = [];
537
- for (let i = 0; i < timingParts.length; i++) {
538
- if (i > 0) displayTiming.push(", ");
539
- displayTiming.push(timingParts[i]);
540
- }
541
- return (
542
- <div className="flex shrink-0 items-center gap-1 text-xs text-cyan-100">
543
- <CheckCircle className="size-3 text-cyan-300" />
544
- <span>Connected</span>
545
- {tokenParts.length > 0 && <span className="text-muted-foreground">({displayTokens})</span>}
546
- {timingParts.length > 0 && (
547
- <span className="hidden text-muted-foreground lg:inline">({displayTiming})</span>
548
- )}
549
- </div>
550
- );
551
- }
552
-
553
- const error = result.error;
554
- const errorMessage = error?.message ?? "Connection failed";
555
- const errorType = error?.type ?? "unknown";
556
- const hasDetails = hasDiagnosticDetails(result);
557
-
558
- return (
559
- <div className="flex flex-col gap-1 shrink-0">
560
- <div className="flex items-center gap-1 text-xs text-red-600 max-w-[200px]">
561
- {getErrorIcon(errorType)}
562
- <span className="truncate">{errorMessage}</span>
563
- {hasDetails && (
564
- <button
565
- type="button"
566
- onClick={() => setShowDetails((value) => !value)}
567
- aria-expanded={showDetails}
568
- aria-label={
569
- showDetails ? "Hide provider test diagnostics" : "Show provider test diagnostics"
570
- }
571
- className="inline-flex h-5 shrink-0 items-center gap-1 rounded border border-border/70 bg-background/70 px-1.5 text-[10px] text-muted-foreground transition-colors hover:text-foreground"
572
- >
573
- {showDetails ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
574
- <span>{showDetails ? "Hide" : "Details"}</span>
575
- </button>
576
- )}
577
- </div>
578
- {showDetails && hasDetails && <ProviderTestDetails result={result} />}
579
- </div>
580
- );
581
- }
582
-
583
127
  function formatTimeAgo(isoString: string): string {
584
128
  const now = Date.now();
585
129
  const then = new Date(isoString).getTime();
@@ -633,7 +177,6 @@ export function ProviderCard({
633
177
  const [showApiKey, setShowApiKey] = useState(false);
634
178
  const [copied, setCopied] = useState(false);
635
179
  const [showDetails, setShowDetails] = useState(false);
636
- const [showModelResults, setShowModelResults] = useState(false);
637
180
 
638
181
  function handleCopy() {
639
182
  void copyTextToClipboard(provider.apiKey).then((success) => {
@@ -666,6 +209,7 @@ export function ProviderCard({
666
209
 
667
210
  return (
668
211
  <div
212
+ data-provider-id={provider.id}
669
213
  className={`flex flex-col gap-3 rounded-[8px] border border-border bg-card p-4 shadow-sm transition-all duration-500 ${isHighlighted === true ? "ring-2 ring-primary shadow-md" : ""}`}
670
214
  >
671
215
  <div className="flex flex-wrap items-start justify-between gap-2">
@@ -771,116 +315,13 @@ export function ProviderCard({
771
315
  </div>
772
316
  )}
773
317
 
774
- {(providerHasEndpoint(provider.anthropicBaseUrl) ||
775
- providerHasEndpoint(provider.openaiBaseUrl) ||
776
- providerHasEndpoint(provider.openaiResponsesBaseUrl)) && (
777
- <div className="grid gap-2 lg:grid-cols-3">
778
- {providerHasEndpoint(provider.anthropicBaseUrl) && (
779
- <ProviderFormatStatusBlock label="Anthropic">
780
- {testResults !== undefined && (
781
- <ProviderFormatTestStatus results={testResults.anthropic} />
782
- )}
783
- {testResults === undefined && (
784
- <div className="text-xs text-muted-foreground">Not tested yet</div>
785
- )}
786
- </ProviderFormatStatusBlock>
787
- )}
788
-
789
- {providerHasEndpoint(provider.openaiBaseUrl) && (
790
- <ProviderFormatStatusBlock label="OpenAI Chat">
791
- {testResults !== undefined && (
792
- <ProviderFormatTestStatus results={testResults.openaiChat} />
793
- )}
794
- {testResults === undefined && (
795
- <div className="text-xs text-muted-foreground">Not tested yet</div>
796
- )}
797
- </ProviderFormatStatusBlock>
798
- )}
799
-
800
- {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
801
- <ProviderFormatStatusBlock label="OpenAI Responses">
802
- {testResults !== undefined && (
803
- <ProviderFormatTestStatus results={testResults.openaiResponses} />
804
- )}
805
- {testResults === undefined && (
806
- <div className="text-xs text-muted-foreground">Not tested yet</div>
807
- )}
808
- </ProviderFormatStatusBlock>
809
- )}
318
+ <section className="space-y-2">
319
+ <h4 className="text-xs font-medium text-foreground/80">Connection probes</h4>
320
+ <ProviderTestMatrix provider={provider} results={testResults} />
321
+ </section>
810
322
 
811
- {!providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
812
- <div className="min-w-0 rounded-md border border-border/50 bg-transparent p-2 text-xs text-muted-foreground/75">
813
- OpenAI Responses not configured
814
- </div>
815
- )}
816
- </div>
817
- )}
818
-
819
- {testResults?.models !== undefined && Object.keys(testResults.models).length > 0 && (
820
- <div>
821
- <button
822
- type="button"
823
- onClick={() => setShowModelResults((value) => !value)}
824
- className="flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
825
- >
826
- {showModelResults ? (
827
- <ChevronDown className="size-3" />
828
- ) : (
829
- <ChevronRight className="size-3" />
830
- )}
831
- Model Test Results ({Object.keys(testResults.models).length})
832
- </button>
833
- {showModelResults && (
834
- <div className="mt-2 overflow-x-auto">
835
- <table className="w-full border-collapse text-xs">
836
- <thead>
837
- <tr className="border-b border-border">
838
- <th className="px-2 py-1 text-left font-medium text-muted-foreground">
839
- Model
840
- </th>
841
- {providerHasEndpoint(provider.anthropicBaseUrl) && (
842
- <th className="px-2 py-1 text-left font-medium text-muted-foreground">
843
- Anthropic
844
- </th>
845
- )}
846
- {providerHasEndpoint(provider.openaiBaseUrl) && (
847
- <th className="px-2 py-1 text-left font-medium text-muted-foreground">
848
- Chat
849
- </th>
850
- )}
851
- {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
852
- <th className="px-2 py-1 text-left font-medium text-muted-foreground">
853
- Responses
854
- </th>
855
- )}
856
- </tr>
857
- </thead>
858
- <tbody>
859
- {Object.entries(testResults.models).map(([modelName, modelResult]) => (
860
- <tr key={modelName} className="border-b border-border/50 last:border-0">
861
- <td className="px-2 py-1 font-medium">{modelName}</td>
862
- {providerHasEndpoint(provider.anthropicBaseUrl) && (
863
- <td className="px-2 py-1 align-top">
864
- <ProviderFormatTestStatus results={modelResult.anthropic} />
865
- </td>
866
- )}
867
- {providerHasEndpoint(provider.openaiBaseUrl) && (
868
- <td className="px-2 py-1 align-top">
869
- <ProviderFormatTestStatus results={modelResult.openaiChat} />
870
- </td>
871
- )}
872
- {providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
873
- <td className="px-2 py-1 align-top">
874
- <ProviderFormatTestStatus results={modelResult.openaiResponses} />
875
- </td>
876
- )}
877
- </tr>
878
- ))}
879
- </tbody>
880
- </table>
881
- </div>
882
- )}
883
- </div>
323
+ {testResults !== undefined && (
324
+ <ProviderModelTestResultsView provider={provider} testResults={testResults} />
884
325
  )}
885
326
  </div>
886
327
  )}