@tonyclaw/agent-inspector 3.0.28 → 3.0.30

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 (156) hide show
  1. package/.output/cli.js +791 -191
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/CompareDrawer-B6cXJohL.js +1 -0
  4. package/.output/public/assets/InspectorPet-mAzeGmEE.js +4108 -0
  5. package/.output/public/assets/ProxyViewerContainer-CkFWv_Nm.js +126 -0
  6. package/.output/public/assets/{ReplayDialog-CZMApaF4.js → ReplayDialog-DodiTL6E.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-DNWccR7O.js → RequestAnatomy-B3IauToI.js} +1 -1
  8. package/.output/public/assets/ResponseView-Hn-5ordK.js +3 -0
  9. package/.output/public/assets/StreamingChunkSequence-vLDOVQM_.js +1 -0
  10. package/.output/public/assets/_sessionId-BFH4TOaI.js +1 -0
  11. package/.output/public/assets/{_sessionId-DRfvuE2k.js → _sessionId-Cj_HIFZx.js} +1 -1
  12. package/.output/public/assets/{index-D4HkS7Ct.js → index-Bmj2lcWE.js} +1 -1
  13. package/.output/public/assets/index-CgJj-HBC.css +1 -0
  14. package/.output/public/assets/index-Ch7uqnCT.js +1 -0
  15. package/.output/public/assets/index-DkDdKvLl.js +76 -0
  16. package/.output/public/assets/{index-B3aAuqWq.js → index-ZlhqCrSg.js} +6 -6
  17. package/.output/public/assets/{json-viewer-wuJSKaed.js → json-viewer-BdrnVQu-.js} +1 -1
  18. package/.output/public/assets/jszip.min-CS_nt_o1.js +2 -0
  19. package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
  20. package/.output/server/_libs/jszip.mjs +35 -9
  21. package/.output/server/_libs/lucide-react.mjs +86 -86
  22. package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
  23. package/.output/server/_libs/tanstack__react-router.mjs +1 -1
  24. package/.output/server/{_sessionId-BJTNiP_O.mjs → _sessionId-C5S3pGZd.mjs} +15 -15
  25. package/.output/server/_sessionId-g6D69lKq.mjs +81 -0
  26. package/.output/server/_ssr/{CompareDrawer-BMRH3N97.mjs → CompareDrawer-CqUmGHal.mjs} +138 -103
  27. package/.output/server/_ssr/InspectorPet-DI0UJd01.mjs +925 -0
  28. package/.output/server/_ssr/{ProxyViewerContainer-AFwcHAIR.mjs → ProxyViewerContainer-CvsUmbJ3.mjs} +1216 -1659
  29. package/.output/server/_ssr/{ReplayDialog-CbWvRHiN.mjs → ReplayDialog-DS48dpFG.mjs} +18 -18
  30. package/.output/server/_ssr/{RequestAnatomy-C4kcPyqN.mjs → RequestAnatomy-DxYVQU1C.mjs} +58 -58
  31. package/.output/server/_ssr/{ResponseView-D8VYt-gZ.mjs → ResponseView-BLXW73eq.mjs} +60 -60
  32. package/.output/server/_ssr/{StreamingChunkSequence-BUUo6vU0.mjs → StreamingChunkSequence-DD0iFuy0.mjs} +16 -16
  33. package/.output/server/_ssr/{index-DTwMEEXO.mjs → index-B10LIaK0.mjs} +15 -15
  34. package/.output/server/_ssr/index-RHVJnU2p.mjs +81 -0
  35. package/.output/server/_ssr/index.mjs +2 -2
  36. package/.output/server/_ssr/{json-viewer-BqF1pSQD.mjs → json-viewer-LDu2XBaK.mjs} +58 -58
  37. package/.output/server/_ssr/{router-C8BOns7y.mjs → router-GKvrwifJ.mjs} +9005 -6488
  38. package/.output/server/_tanstack-start-manifest_v-Df2n4mbT.mjs +4 -0
  39. package/.output/server/index.mjs +105 -77
  40. package/.output/workers/logFinalizer.worker.js +16908 -0
  41. package/.output/workers/sessionWorkerEntry.js +16904 -0
  42. package/README.md +21 -5
  43. package/package.json +6 -2
  44. package/src/assets/logos/qwen.webp +0 -0
  45. package/src/cli.ts +85 -10
  46. package/src/components/OnboardingBanner.tsx +41 -21
  47. package/src/components/ProxyViewer.tsx +198 -269
  48. package/src/components/ProxyViewerContainer.tsx +128 -17
  49. package/src/components/clients/ClientLogo.tsx +9 -6
  50. package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
  51. package/src/components/errors/SafeErrorBoundary.tsx +201 -0
  52. package/src/components/pi-agent/PiAgentPanel.tsx +4 -1
  53. package/src/components/providers/ImportWizardDialog.tsx +18 -42
  54. package/src/components/providers/ProviderLogo.tsx +1 -1
  55. package/src/components/providers/ProvidersPanel.tsx +8 -5
  56. package/src/components/providers/SettingsDialog.tsx +34 -11
  57. package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
  58. package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
  59. package/src/components/proxy-viewer/ConversationGroupList.tsx +49 -34
  60. package/src/components/proxy-viewer/ConversationHeader.tsx +22 -18
  61. package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
  62. package/src/components/proxy-viewer/LogEntry.tsx +283 -275
  63. package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
  64. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
  65. package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
  66. package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
  67. package/src/components/proxy-viewer/accessibility.ts +8 -0
  68. package/src/components/proxy-viewer/lazy.ts +4 -0
  69. package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
  70. package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
  71. package/src/components/proxy-viewer/viewerState.ts +2 -6
  72. package/src/components/ui/select.tsx +1 -1
  73. package/src/components/ui/tabs.tsx +3 -3
  74. package/src/components/ui/transient-toast.tsx +1 -1
  75. package/src/lib/apiClient.ts +17 -2
  76. package/src/lib/ecosystemContract.ts +34 -0
  77. package/src/lib/export-logs.ts +1 -1
  78. package/src/lib/providerContract.ts +70 -4
  79. package/src/lib/providerImportContract.ts +27 -0
  80. package/src/lib/providerModelMetadata.ts +16 -7
  81. package/src/lib/resourceLimits.ts +152 -0
  82. package/src/lib/safeDiagnostic.ts +38 -0
  83. package/src/lib/useProviders.ts +4 -4
  84. package/src/lib/utils.ts +5 -1
  85. package/src/mcp/server.ts +39 -6
  86. package/src/mcp/toolHandlers.ts +131 -4
  87. package/src/proxy/chunkStorage.ts +20 -6
  88. package/src/proxy/config.ts +20 -6
  89. package/src/proxy/dataDir.ts +3 -0
  90. package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
  91. package/src/proxy/ecosystemExecutionRoute.ts +116 -0
  92. package/src/proxy/ecosystemTasks.ts +48 -0
  93. package/src/proxy/evidenceExporter.ts +23 -9
  94. package/src/proxy/groupEvidenceExporter.ts +26 -5
  95. package/src/proxy/groupStore.ts +13 -3
  96. package/src/proxy/handler.ts +103 -20
  97. package/src/proxy/identityProxy.ts +333 -14
  98. package/src/proxy/logFinalizer.ts +73 -6
  99. package/src/proxy/logImportUpload.ts +128 -0
  100. package/src/proxy/logImporter.ts +321 -70
  101. package/src/proxy/logIndex.ts +16 -6
  102. package/src/proxy/logger.ts +294 -41
  103. package/src/proxy/privateDataPath.ts +183 -0
  104. package/src/proxy/providerScanStore.ts +87 -0
  105. package/src/proxy/providerSecretStore.ts +58 -19
  106. package/src/proxy/providers.ts +175 -58
  107. package/src/proxy/rawStreamCapture.ts +66 -5
  108. package/src/proxy/runStore.ts +13 -3
  109. package/src/proxy/runtimeAdmission.ts +52 -0
  110. package/src/proxy/runtimeHealth.ts +206 -0
  111. package/src/proxy/runtimeShutdown.ts +75 -0
  112. package/src/proxy/sessionArchive.ts +15 -2
  113. package/src/proxy/sessionProcess.ts +19 -0
  114. package/src/proxy/sessionRuntime.ts +7 -0
  115. package/src/proxy/shutdownCoordinator.ts +90 -0
  116. package/src/proxy/sqliteLogIndex.ts +18 -2
  117. package/src/proxy/store.ts +18 -2
  118. package/src/routes/__root.tsx +18 -1
  119. package/src/routes/api/alerts.summary.ts +12 -8
  120. package/src/routes/api/alerts.ts +27 -9
  121. package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
  122. package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
  123. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
  124. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
  125. package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
  126. package/src/routes/api/health.ts +9 -2
  127. package/src/routes/api/knowledge.project-context.ts +11 -0
  128. package/src/routes/api/knowledge.search.ts +33 -2
  129. package/src/routes/api/logs.$id.body.ts +16 -2
  130. package/src/routes/api/logs.import.ts +7 -18
  131. package/src/routes/api/logs.stream.ts +147 -69
  132. package/src/routes/api/logs.ts +55 -14
  133. package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
  134. package/src/routes/api/providers.$providerId.ts +37 -6
  135. package/src/routes/api/providers.export.ts +34 -16
  136. package/src/routes/api/providers.import.ts +42 -8
  137. package/src/routes/api/providers.scan.ts +13 -8
  138. package/src/routes/api/providers.ts +36 -5
  139. package/src/routes/api/runs.ts +12 -12
  140. package/src/routes/api/sessions.ts +15 -8
  141. package/src/routes/index.tsx +6 -0
  142. package/src/routes/livez.ts +13 -0
  143. package/src/routes/readyz.ts +18 -0
  144. package/src/routes/session/$sessionId.tsx +6 -0
  145. package/styles/globals.css +19 -3
  146. package/.output/public/assets/CompareDrawer-TghZr5t1.js +0 -1
  147. package/.output/public/assets/ProxyViewerContainer-fShBAZjj.js +0 -4234
  148. package/.output/public/assets/ResponseView-d25MdFhY.js +0 -3
  149. package/.output/public/assets/StreamingChunkSequence-DIFZEnWe.js +0 -1
  150. package/.output/public/assets/index-BsxAGqMA.css +0 -1
  151. package/.output/public/assets/index-DzARL2Fi.js +0 -76
  152. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  153. package/.output/server/_tanstack-start-manifest_v-BBf2nRhb.mjs +0 -4
  154. package/src/assets/logos/mcp.png +0 -0
  155. package/src/assets/logos/qwen.png +0 -0
  156. package/src/components/ui/mcp-logo.tsx +0 -20
@@ -22,7 +22,7 @@ import { ProviderLogoStack } from "./ProviderLogoStack";
22
22
  import { resolveLogFormat } from "./log-formats";
23
23
 
24
24
  const HEADER_ACTION_BUTTON_CLASS =
25
- "bg-white/[0.045] hover:bg-white/[0.075] inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground";
25
+ "inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md bg-[var(--inspector-surface-low)] text-muted-foreground transition-colors hover:bg-[var(--inspector-surface-medium)] hover:text-foreground";
26
26
 
27
27
  export function isSyntheticSessionId(conversationId: string): boolean {
28
28
  return (
@@ -34,7 +34,7 @@ export function isSyntheticSessionId(conversationId: string): boolean {
34
34
  }
35
35
 
36
36
  export function compactSessionLabel(conversationId: string): string {
37
- if (isSyntheticSessionId(conversationId)) return "Sessionless";
37
+ if (isSyntheticSessionId(conversationId)) return "No session ID";
38
38
  return conversationId.length > 24
39
39
  ? `${conversationId.slice(0, 12)}...${conversationId.slice(-12)}`
40
40
  : conversationId;
@@ -54,7 +54,7 @@ function HeaderMetric({
54
54
  return (
55
55
  <span
56
56
  className={cn(
57
- "bg-white/[0.045] inline-flex h-7 min-w-0 items-center gap-1.5 rounded-md px-2 text-xs text-muted-foreground",
57
+ "inline-flex h-7 min-w-0 items-center gap-1.5 rounded-md bg-[var(--inspector-surface-low)] px-2 text-xs text-muted-foreground",
58
58
  className,
59
59
  )}
60
60
  title={title}
@@ -68,7 +68,7 @@ function HeaderMetric({
68
68
  function PendingTurnBadge(): JSX.Element {
69
69
  return (
70
70
  <span
71
- className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-amber-300/[0.10] px-1.5 font-mono text-[10px] uppercase text-amber-100/90 ring-1 ring-amber-200/10"
71
+ className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-md bg-[var(--inspector-surface-medium)] px-1.5 font-mono text-[10px] uppercase text-[var(--inspector-status-watch)] ring-1 ring-border/50"
72
72
  title="This session has a turn that has not finished yet"
73
73
  >
74
74
  <Loader2 className="size-3 animate-spin" />
@@ -174,11 +174,11 @@ export function ConversationHeader({
174
174
  data-nav-id={`conv-${conversationId}`}
175
175
  data-nav-action={expanded ? "collapse" : "expand"}
176
176
  className={cn(
177
- "bg-black/[0.10] relative flex cursor-pointer items-start gap-2.5 overflow-hidden rounded-[8px] px-3 py-2.5 transition-colors",
178
- "hover:bg-black/[0.16]",
177
+ "relative flex cursor-pointer items-start gap-2.5 overflow-hidden rounded-md bg-[var(--inspector-surface-low)] px-3 py-2.5 transition-colors",
178
+ "hover:bg-[var(--inspector-surface-medium)]",
179
179
  "select-none",
180
180
  "sticky top-0 z-10 mb-2",
181
- "focus-visible:ring-1 focus-visible:ring-cyan-300/35 focus-visible:outline-none",
181
+ "focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
182
182
  )}
183
183
  onClick={onToggle}
184
184
  onKeyDown={(e) => {
@@ -198,20 +198,20 @@ export function ConversationHeader({
198
198
 
199
199
  <span className="min-w-0 flex-1">
200
200
  <span className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
201
- <span className="shrink-0 rounded bg-white/[0.04] px-1.5 py-0.5 font-mono text-[9px] uppercase text-muted-foreground/70">
201
+ <span className="shrink-0 rounded bg-[var(--inspector-surface-medium)] px-1.5 py-0.5 font-mono text-[9px] uppercase text-muted-foreground">
202
202
  Session
203
203
  </span>
204
204
  {isFallbackConversation && showProcessMetadata ? (
205
205
  clientPid !== null && clientPid !== undefined ? (
206
206
  <span
207
- className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-slate-100"
207
+ className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-foreground"
208
208
  title={`PID ${clientPid}`}
209
209
  >
210
210
  PID {clientPid}
211
211
  </span>
212
212
  ) : (
213
213
  <span
214
- className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-slate-100"
214
+ className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-foreground"
215
215
  title={conversationId}
216
216
  >
217
217
  {conversationId}
@@ -219,7 +219,7 @@ export function ConversationHeader({
219
219
  )
220
220
  ) : (
221
221
  <span
222
- className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-slate-100"
222
+ className="min-w-0 max-w-full truncate font-mono text-xs font-semibold text-foreground"
223
223
  title={conversationId}
224
224
  >
225
225
  {sessionLabel}
@@ -231,7 +231,7 @@ export function ConversationHeader({
231
231
  clientPid !== null &&
232
232
  clientPid !== undefined && (
233
233
  <span
234
- className="bg-white/[0.045] shrink-0 rounded px-1.5 py-0.5 font-mono text-xs tabular-nums text-muted-foreground"
234
+ className="shrink-0 rounded bg-[var(--inspector-surface-medium)] px-1.5 py-0.5 font-mono text-xs tabular-nums text-muted-foreground"
235
235
  title={`Client process ID ${clientPid}`}
236
236
  >
237
237
  PID {clientPid}
@@ -253,7 +253,7 @@ export function ConversationHeader({
253
253
 
254
254
  {showClientIdentity && clientDisplayLabel !== null && clientDisplayLabel !== "" && (
255
255
  <span
256
- className="bg-white/[0.045] inline-flex min-h-6 shrink-0 flex-wrap items-center gap-1 rounded-md px-1.5 py-0.5 text-xs text-muted-foreground"
256
+ className="inline-flex min-h-6 shrink-0 flex-wrap items-center gap-1 rounded-md bg-[var(--inspector-surface-medium)] px-1.5 py-0.5 text-xs text-muted-foreground"
257
257
  title={clientTooltip ?? clientDisplayLabel}
258
258
  >
259
259
  <ClientLogo client={clientApp} className="size-4 shrink-0" decorative={true} />
@@ -272,9 +272,13 @@ export function ConversationHeader({
272
272
  {callsLabel}
273
273
  </HeaderMetric>
274
274
  <HeaderMetric icon={<Zap className="size-3 shrink-0" />} className="max-w-full">
275
- <span className="text-blue-400">{formatTokens(totalInputTokens)}</span>
275
+ <span className="text-[var(--inspector-status-info)]">
276
+ {formatTokens(totalInputTokens)}
277
+ </span>
276
278
  {" / "}
277
- <span className="text-amber-400">{formatTokens(totalOutputTokens)}</span>
279
+ <span className="text-[var(--inspector-status-watch)]">
280
+ {formatTokens(totalOutputTokens)}
281
+ </span>
278
282
  </HeaderMetric>
279
283
  </span>
280
284
  </span>
@@ -311,7 +315,7 @@ export function ConversationHeader({
311
315
 
312
316
  {/* Expand chevron - shows spinner when collapsed and group has pending logs */}
313
317
  <span
314
- className="bg-white/[0.045] hover:bg-white/[0.075] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-slate-200"
318
+ className="inline-flex size-8 shrink-0 items-center justify-center rounded-md bg-[var(--inspector-surface-low)] text-muted-foreground transition-colors hover:bg-[var(--inspector-surface-medium)] hover:text-foreground"
315
319
  aria-hidden="true"
316
320
  >
317
321
  {expanded ? (
@@ -326,10 +330,10 @@ export function ConversationHeader({
326
330
 
327
331
  {isLoading && (
328
332
  <span
329
- className="pointer-events-none absolute inset-x-3 bottom-0 h-px overflow-hidden rounded-full bg-white/[0.06]"
333
+ className="pointer-events-none absolute inset-x-3 bottom-0 h-px overflow-hidden rounded-full bg-[var(--inspector-surface-medium)]"
330
334
  aria-hidden="true"
331
335
  >
332
- <span className="absolute inset-y-0 left-0 w-1/3 animate-pulse rounded-full bg-amber-200/65 shadow-[0_0_14px_rgba(252,211,77,0.32)]" />
336
+ <span className="absolute inset-y-0 left-0 w-1/3 animate-pulse rounded-full bg-[var(--inspector-status-watch)] opacity-70" />
333
337
  </span>
334
338
  )}
335
339
 
@@ -0,0 +1,186 @@
1
+ import { type JSX } from "react";
2
+ import { CircleStop, RefreshCw } from "lucide-react";
3
+ import { cn } from "../../lib/utils";
4
+ import type { LiveConnectionPhase, LiveConnectionState } from "./liveConnectionState";
5
+
6
+ type ConnectionPresentation = {
7
+ label: string;
8
+ description: string;
9
+ toneClass: string;
10
+ dotClass: string;
11
+ canRetry: boolean;
12
+ canStop: boolean;
13
+ };
14
+
15
+ function formatLastSuccess(lastSuccessAt: number | null): string | null {
16
+ if (lastSuccessAt === null) return null;
17
+ return new Date(lastSuccessAt).toLocaleTimeString([], {
18
+ hour: "2-digit",
19
+ minute: "2-digit",
20
+ second: "2-digit",
21
+ });
22
+ }
23
+
24
+ function buildDescription(state: LiveConnectionState, message: string, hasLogs: boolean): string {
25
+ const lastSuccess = formatLastSuccess(state.lastSuccessAt);
26
+ const retry = state.retryCount > 0 ? ` Retry ${state.retryCount.toString()}.` : "";
27
+ const retained = hasLogs ? " Captured logs remain visible." : "";
28
+ const lastUpdate = lastSuccess === null ? "" : ` Last update ${lastSuccess}.`;
29
+ return `${message}${lastUpdate}${retry}${retained}`;
30
+ }
31
+
32
+ function presentationForStatus(
33
+ status: LiveConnectionPhase,
34
+ state: LiveConnectionState,
35
+ hasLogs: boolean,
36
+ ): ConnectionPresentation {
37
+ switch (status) {
38
+ case "connecting":
39
+ return {
40
+ label: "Connecting",
41
+ description: buildDescription(state, "Waiting for the first live update.", hasLogs),
42
+ toneClass: "border-cyan-400/20 bg-cyan-500/8 text-cyan-100",
43
+ dotClass: "bg-cyan-300 animate-pulse motion-reduce:animate-none",
44
+ canRetry: false,
45
+ canStop: false,
46
+ };
47
+ case "live":
48
+ return {
49
+ label: "Live",
50
+ description: buildDescription(state, "Live updates are current.", hasLogs),
51
+ toneClass: "border-emerald-400/20 bg-emerald-500/8 text-emerald-100",
52
+ dotClass: "bg-emerald-300",
53
+ canRetry: false,
54
+ canStop: false,
55
+ };
56
+ case "reconnecting":
57
+ return {
58
+ label: "Reconnecting",
59
+ description: buildDescription(state, "Live updates were interrupted.", hasLogs),
60
+ toneClass: "border-amber-400/20 bg-amber-500/8 text-amber-100",
61
+ dotClass: "bg-amber-300 animate-pulse motion-reduce:animate-none",
62
+ canRetry: true,
63
+ canStop: true,
64
+ };
65
+ case "stale":
66
+ return {
67
+ label: "Stale data",
68
+ description: buildDescription(state, "Live updates have not resumed.", hasLogs),
69
+ toneClass: "border-orange-400/25 bg-orange-500/10 text-orange-100",
70
+ dotClass: "bg-orange-300",
71
+ canRetry: true,
72
+ canStop: true,
73
+ };
74
+ case "offline":
75
+ return {
76
+ label: "Offline",
77
+ description: buildDescription(
78
+ state,
79
+ state.errorMessage ?? "Live updates are paused.",
80
+ hasLogs,
81
+ ),
82
+ toneClass: "border-slate-400/20 bg-slate-500/10 text-slate-200",
83
+ dotClass: "bg-slate-400",
84
+ canRetry: true,
85
+ canStop: false,
86
+ };
87
+ case "error":
88
+ return {
89
+ label: "Connection error",
90
+ description: buildDescription(
91
+ state,
92
+ state.errorMessage ?? "The live stream reported a recoverable error.",
93
+ hasLogs,
94
+ ),
95
+ toneClass: "border-red-400/25 bg-red-500/10 text-red-100",
96
+ dotClass: "bg-red-300",
97
+ canRetry: true,
98
+ canStop: true,
99
+ };
100
+ }
101
+ }
102
+
103
+ export function LiveConnectionBadge({ state }: { state: LiveConnectionState }): JSX.Element {
104
+ const presentation = presentationForStatus(state.status, state, false);
105
+ const lastSuccess = formatLastSuccess(state.lastSuccessAt);
106
+ const retryLabel = state.retryCount > 0 ? `Retry ${state.retryCount.toString()}` : null;
107
+
108
+ return (
109
+ <span
110
+ role="status"
111
+ aria-label={presentation.description}
112
+ data-connection-state={state.status}
113
+ className={cn(
114
+ "inline-flex h-7 max-w-full items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium",
115
+ presentation.toneClass,
116
+ )}
117
+ >
118
+ <span className={cn("size-1.5 shrink-0 rounded-full", presentation.dotClass)} />
119
+ <span className="shrink-0">{presentation.label}</span>
120
+ {retryLabel !== null && (
121
+ <span className="hidden truncate text-current/70 sm:inline">{retryLabel}</span>
122
+ )}
123
+ {lastSuccess !== null && (
124
+ <span className="hidden truncate font-mono text-[10px] text-current/65 md:inline">
125
+ Last {lastSuccess}
126
+ </span>
127
+ )}
128
+ </span>
129
+ );
130
+ }
131
+
132
+ export function LiveConnectionStatus({
133
+ state,
134
+ hasLogs,
135
+ onRetry,
136
+ onStop,
137
+ }: {
138
+ state: LiveConnectionState;
139
+ hasLogs: boolean;
140
+ onRetry: () => void;
141
+ onStop: () => void;
142
+ }): JSX.Element {
143
+ const presentation = presentationForStatus(state.status, state, hasLogs);
144
+
145
+ return (
146
+ <div
147
+ role="status"
148
+ aria-live="polite"
149
+ data-connection-state={state.status}
150
+ className={cn(
151
+ "mt-2 flex flex-wrap items-center gap-x-3 gap-y-2 rounded-md border px-3 py-2 text-xs",
152
+ presentation.toneClass,
153
+ )}
154
+ >
155
+ <div className="flex min-w-0 flex-1 items-center gap-2">
156
+ <span className={cn("size-2 shrink-0 rounded-full", presentation.dotClass)} />
157
+ <span className="shrink-0 font-semibold">{presentation.label}</span>
158
+ <span className="min-w-0 text-current/75">{presentation.description}</span>
159
+ </div>
160
+ {(presentation.canRetry || presentation.canStop) && (
161
+ <div className="flex shrink-0 items-center gap-1.5">
162
+ {presentation.canRetry && (
163
+ <button
164
+ type="button"
165
+ onClick={onRetry}
166
+ className="inline-flex h-7 items-center gap-1 rounded-md bg-black/15 px-2 font-medium transition-colors hover:bg-black/25 focus-visible:ring-1 focus-visible:ring-current focus-visible:outline-none"
167
+ >
168
+ <RefreshCw className="size-3" />
169
+ Retry now
170
+ </button>
171
+ )}
172
+ {presentation.canStop && (
173
+ <button
174
+ type="button"
175
+ onClick={onStop}
176
+ className="inline-flex h-7 items-center gap-1 rounded-md px-2 text-current/70 transition-colors hover:bg-black/15 hover:text-current focus-visible:ring-1 focus-visible:ring-current focus-visible:outline-none"
177
+ >
178
+ <CircleStop className="size-3" />
179
+ Stop retrying
180
+ </button>
181
+ )}
182
+ </div>
183
+ )}
184
+ </div>
185
+ );
186
+ }