@tonyclaw/agent-inspector 4.0.1 → 4.0.2

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 (373) hide show
  1. package/.output/backend/nitro.json +1 -1
  2. package/.output/cli.js +192 -109
  3. package/.output/server/_ssr/index.mjs +1 -1
  4. package/.output/server/_ssr/{router-CTHqmroT.mjs → router-BBY_zWRK.mjs} +91 -3
  5. package/.output/server/index.mjs +1 -1
  6. package/.output/ui/assets/{CompareDrawer-L3n4J-Or.js → CompareDrawer-rtNElN-B.js} +1 -1
  7. package/.output/ui/assets/{InspectorPet-D5o_3OZS.js → InspectorPet-QaAiuF3d.js} +1 -1
  8. package/.output/ui/assets/{ProxyViewerContainer-Ocagx75S.js → ProxyViewerContainer-6twNZHwl.js} +4 -4
  9. package/.output/ui/assets/{ReplayDialog-Bk7x1h-y.js → ReplayDialog-B7KlziO2.js} +1 -1
  10. package/.output/ui/assets/{RequestAnatomy-U4lzL7Sc.js → RequestAnatomy-2XVFBrW4.js} +1 -1
  11. package/.output/ui/assets/{ResponseView-LOWfvBLK.js → ResponseView-BULgzLVB.js} +1 -1
  12. package/.output/ui/assets/{StreamingChunkSequence-DPfsrrXQ.js → StreamingChunkSequence-DSkvkbns.js} +1 -1
  13. package/.output/ui/assets/{_sessionId-OdX2Jvt1.js → _sessionId-D2dmJi3G.js} +1 -1
  14. package/.output/ui/assets/{_sessionId-0MlWS-CO.js → _sessionId-DcS0s1Mr.js} +1 -1
  15. package/.output/ui/assets/{index-Du4qNOOt.js → index-B0TN1KKO.js} +2 -2
  16. package/.output/ui/assets/{index-gvFj_9K6.js → index-Bva1zHEs.js} +1 -1
  17. package/.output/ui/assets/{index-Ea-c8hPg.js → index-BzBExOxu.js} +1 -1
  18. package/.output/ui/assets/{index-zVVS53Hd.js → index-k6sRQz55.js} +1 -1
  19. package/.output/ui/assets/{json-viewer-C1WAMzg3.js → json-viewer-CWM4tY-j.js} +1 -1
  20. package/.output/ui/assets/{jszip.min-B8AKu2zm.js → jszip.min-BkWvRFlo.js} +1 -1
  21. package/.output/ui/index.html +1 -1
  22. package/package.json +5 -10
  23. package/src/assets/IDE/claudecode-color.svg +0 -1
  24. package/src/assets/IDE/codex-color.svg +0 -1
  25. package/src/assets/IDE/opencode.svg +0 -1
  26. package/src/assets/IDE/xiaomimimo.svg +0 -1
  27. package/src/assets/favicon.svg +0 -21
  28. package/src/assets/logos/alibaba.svg +0 -1
  29. package/src/assets/logos/anthropic.svg +0 -1
  30. package/src/assets/logos/claude-code.svg +0 -4
  31. package/src/assets/logos/deepseek.svg +0 -1
  32. package/src/assets/logos/minimax.jpeg +0 -0
  33. package/src/assets/logos/openai.svg +0 -1
  34. package/src/assets/logos/opencode.svg +0 -4
  35. package/src/assets/logos/qwen.webp +0 -0
  36. package/src/assets/logos/zhipuai.svg +0 -219
  37. package/src/backend/router.tsx +0 -36
  38. package/src/backend/routes/$.ts +0 -26
  39. package/src/backend/routes/__root.ts +0 -4
  40. package/src/backend/routes/api/-instances.ts +0 -7
  41. package/src/backend/routes/api/alerts.summary.ts +0 -28
  42. package/src/backend/routes/api/alerts.ts +0 -70
  43. package/src/backend/routes/api/config.paths.ts +0 -14
  44. package/src/backend/routes/api/config.ts +0 -73
  45. package/src/backend/routes/api/ecosystem.packages.$packageId.help.ts +0 -11
  46. package/src/backend/routes/api/ecosystem.packages.$packageId.install.ts +0 -11
  47. package/src/backend/routes/api/ecosystem.packages.$packageId.runner-presets.ts +0 -11
  48. package/src/backend/routes/api/ecosystem.packages.$packageId.upgrade.ts +0 -11
  49. package/src/backend/routes/api/ecosystem.packages.ts +0 -21
  50. package/src/backend/routes/api/ecosystem.recipes.$recipeId.run.ts +0 -11
  51. package/src/backend/routes/api/ecosystem.tasks.$taskId.ts +0 -16
  52. package/src/backend/routes/api/ecosystem.tasks.ts +0 -12
  53. package/src/backend/routes/api/groups.$groupId.evidence.ts +0 -55
  54. package/src/backend/routes/api/groups.$groupId.sessions.ts +0 -50
  55. package/src/backend/routes/api/groups.$groupId.ts +0 -93
  56. package/src/backend/routes/api/groups.ts +0 -79
  57. package/src/backend/routes/api/health.ts +0 -22
  58. package/src/backend/routes/api/instances.$name.connection.ts +0 -12
  59. package/src/backend/routes/api/instances.$name.restart.ts +0 -23
  60. package/src/backend/routes/api/instances.$name.start.ts +0 -18
  61. package/src/backend/routes/api/instances.$name.stop.ts +0 -23
  62. package/src/backend/routes/api/instances.$name.ts +0 -12
  63. package/src/backend/routes/api/instances.ts +0 -16
  64. package/src/backend/routes/api/knowledge.candidates.$candidateId.promote.ts +0 -32
  65. package/src/backend/routes/api/knowledge.candidates.$candidateId.ts +0 -50
  66. package/src/backend/routes/api/knowledge.candidates.ts +0 -10
  67. package/src/backend/routes/api/knowledge.project-context.ts +0 -29
  68. package/src/backend/routes/api/knowledge.search.ts +0 -62
  69. package/src/backend/routes/api/knowledge.sessions.$sessionId.candidates.ts +0 -26
  70. package/src/backend/routes/api/logs.$id.body.ts +0 -58
  71. package/src/backend/routes/api/logs.$id.chunks.ts +0 -37
  72. package/src/backend/routes/api/logs.$id.replay.ts +0 -297
  73. package/src/backend/routes/api/logs.$id.ts +0 -23
  74. package/src/backend/routes/api/logs.import.ts +0 -39
  75. package/src/backend/routes/api/logs.stream.ts +0 -174
  76. package/src/backend/routes/api/logs.ts +0 -214
  77. package/src/backend/routes/api/mcp.ts +0 -25
  78. package/src/backend/routes/api/models.ts +0 -10
  79. package/src/backend/routes/api/pi-agent.ts +0 -29
  80. package/src/backend/routes/api/providers.$providerId.model-metadata.ts +0 -173
  81. package/src/backend/routes/api/providers.$providerId.test.log.ts +0 -342
  82. package/src/backend/routes/api/providers.$providerId.ts +0 -111
  83. package/src/backend/routes/api/providers.export.ts +0 -44
  84. package/src/backend/routes/api/providers.import.ts +0 -81
  85. package/src/backend/routes/api/providers.scan.ts +0 -28
  86. package/src/backend/routes/api/providers.ts +0 -102
  87. package/src/backend/routes/api/runs.$runId.evidence.ts +0 -55
  88. package/src/backend/routes/api/runs.$runId.ts +0 -50
  89. package/src/backend/routes/api/runs.ts +0 -62
  90. package/src/backend/routes/api/runtime.ts +0 -46
  91. package/src/backend/routes/api/sessions.ts +0 -51
  92. package/src/backend/routes/api/storage.ts +0 -56
  93. package/src/backend/routes/livez.ts +0 -13
  94. package/src/backend/routes/metrics.ts +0 -14
  95. package/src/backend/routes/proxy/$.ts +0 -15
  96. package/src/backend/routes/readyz.ts +0 -18
  97. package/src/backend/start.ts +0 -10
  98. package/src/cli/alias.ts +0 -431
  99. package/src/cli/detect-tools.ts +0 -123
  100. package/src/cli/doctor.ts +0 -836
  101. package/src/cli/instance.ts +0 -1071
  102. package/src/cli/instanceApi.ts +0 -213
  103. package/src/cli/instanceArgs.ts +0 -301
  104. package/src/cli/instanceControl.ts +0 -191
  105. package/src/cli/instanceModel.ts +0 -276
  106. package/src/cli/instanceRegistry.ts +0 -731
  107. package/src/cli/networkHints.ts +0 -170
  108. package/src/cli/onboard.ts +0 -1668
  109. package/src/cli/onboardTarget.ts +0 -228
  110. package/src/cli/rustBackendPackage.ts +0 -458
  111. package/src/cli/startupOutput.ts +0 -16
  112. package/src/cli/templates/codex-skill-onboard.ts +0 -320
  113. package/src/cli/templates/command-onboard.ts +0 -25
  114. package/src/cli/templates/skill-onboard.ts +0 -625
  115. package/src/cli.ts +0 -1433
  116. package/src/components/OnboardingBanner.tsx +0 -115
  117. package/src/components/ProxyViewer.tsx +0 -2259
  118. package/src/components/ProxyViewerContainer.tsx +0 -1076
  119. package/src/components/alerts/AlertsDialog.tsx +0 -615
  120. package/src/components/clients/ClientLogo.tsx +0 -175
  121. package/src/components/ecosystem/AgentLabDialog.tsx +0 -947
  122. package/src/components/errors/SafeErrorBoundary.tsx +0 -201
  123. package/src/components/groups/GroupsDialog.tsx +0 -1304
  124. package/src/components/inspector-pet/InspectorPet.tsx +0 -398
  125. package/src/components/inspector-pet/InspectorPetCrab3D.tsx +0 -455
  126. package/src/components/inspector-pet/inspectorPetLogic.ts +0 -272
  127. package/src/components/pi-agent/PiAgentPanel.tsx +0 -446
  128. package/src/components/pi-agent/piAgentChatLogic.ts +0 -273
  129. package/src/components/providers/ImportWizardDialog.tsx +0 -281
  130. package/src/components/providers/ProviderCard.tsx +0 -347
  131. package/src/components/providers/ProviderForm.tsx +0 -1183
  132. package/src/components/providers/ProviderLogo.tsx +0 -122
  133. package/src/components/providers/ProviderTestResultsView.tsx +0 -734
  134. package/src/components/providers/ProvidersPanel.tsx +0 -714
  135. package/src/components/providers/SettingsDialog.tsx +0 -1042
  136. package/src/components/providers/providerFormCapabilities.ts +0 -36
  137. package/src/components/proxy-viewer/AgentTraceSummary.tsx +0 -902
  138. package/src/components/proxy-viewer/AnswerMarkdown.tsx +0 -51
  139. package/src/components/proxy-viewer/ApplicationBar.tsx +0 -123
  140. package/src/components/proxy-viewer/CompareDrawer.tsx +0 -984
  141. package/src/components/proxy-viewer/ConversationGroup.tsx +0 -208
  142. package/src/components/proxy-viewer/ConversationGroupList.tsx +0 -652
  143. package/src/components/proxy-viewer/ConversationHeader.tsx +0 -429
  144. package/src/components/proxy-viewer/LiveConnectionStatus.tsx +0 -186
  145. package/src/components/proxy-viewer/LogEntry.tsx +0 -1419
  146. package/src/components/proxy-viewer/LogEntryHeader.tsx +0 -596
  147. package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -57
  148. package/src/components/proxy-viewer/RawExportWarning.tsx +0 -77
  149. package/src/components/proxy-viewer/ReplayDialog.tsx +0 -636
  150. package/src/components/proxy-viewer/RequestToolsPanel.tsx +0 -299
  151. package/src/components/proxy-viewer/ResponseView.tsx +0 -202
  152. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +0 -61
  153. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +0 -265
  154. package/src/components/proxy-viewer/ThreadConnector.tsx +0 -165
  155. package/src/components/proxy-viewer/ToolTraceEvents.tsx +0 -64
  156. package/src/components/proxy-viewer/TurnGroup.tsx +0 -383
  157. package/src/components/proxy-viewer/TurnGroupList.tsx +0 -156
  158. package/src/components/proxy-viewer/accessibility.ts +0 -8
  159. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +0 -887
  160. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +0 -222
  161. package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +0 -737
  162. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +0 -268
  163. package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +0 -53
  164. package/src/components/proxy-viewer/anatomy/types.ts +0 -41
  165. package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +0 -114
  166. package/src/components/proxy-viewer/bodyHydration.ts +0 -123
  167. package/src/components/proxy-viewer/cacheTrend.ts +0 -50
  168. package/src/components/proxy-viewer/diff/DiffView.tsx +0 -321
  169. package/src/components/proxy-viewer/diff/computeDiff.ts +0 -178
  170. package/src/components/proxy-viewer/diff/index.ts +0 -3
  171. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +0 -194
  172. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +0 -81
  173. package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +0 -21
  174. package/src/components/proxy-viewer/formats/index.tsx +0 -47
  175. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +0 -442
  176. package/src/components/proxy-viewer/index.ts +0 -9
  177. package/src/components/proxy-viewer/lazy.ts +0 -41
  178. package/src/components/proxy-viewer/liveConnectionState.ts +0 -158
  179. package/src/components/proxy-viewer/log-formats/anthropic.ts +0 -191
  180. package/src/components/proxy-viewer/log-formats/index.ts +0 -23
  181. package/src/components/proxy-viewer/log-formats/openai.ts +0 -256
  182. package/src/components/proxy-viewer/log-formats/types.ts +0 -40
  183. package/src/components/proxy-viewer/log-formats/unknown.ts +0 -18
  184. package/src/components/proxy-viewer/logEntryVisibility.ts +0 -73
  185. package/src/components/proxy-viewer/logFocus.ts +0 -54
  186. package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +0 -124
  187. package/src/components/proxy-viewer/proxyViewerLogic.ts +0 -488
  188. package/src/components/proxy-viewer/replayComparison.ts +0 -131
  189. package/src/components/proxy-viewer/requestDiff.ts +0 -277
  190. package/src/components/proxy-viewer/requestTools.ts +0 -220
  191. package/src/components/proxy-viewer/useCopyFeedback.ts +0 -38
  192. package/src/components/proxy-viewer/useKeyboardNavigation.ts +0 -232
  193. package/src/components/proxy-viewer/viewerState.ts +0 -176
  194. package/src/components/ui/badge.tsx +0 -47
  195. package/src/components/ui/button.tsx +0 -47
  196. package/src/components/ui/collapsible.tsx +0 -21
  197. package/src/components/ui/confirm-dialog.tsx +0 -51
  198. package/src/components/ui/crab-logo.tsx +0 -45
  199. package/src/components/ui/crab-variants.tsx +0 -467
  200. package/src/components/ui/dialog.tsx +0 -130
  201. package/src/components/ui/icon-trigger.ts +0 -2
  202. package/src/components/ui/json-viewer-bulk.ts +0 -132
  203. package/src/components/ui/json-viewer.tsx +0 -508
  204. package/src/components/ui/scroll-area.tsx +0 -54
  205. package/src/components/ui/select.tsx +0 -178
  206. package/src/components/ui/separator.tsx +0 -28
  207. package/src/components/ui/tabs.tsx +0 -88
  208. package/src/components/ui/tooltip.tsx +0 -51
  209. package/src/components/ui/transient-toast.tsx +0 -105
  210. package/src/contracts/anthropic.ts +0 -230
  211. package/src/contracts/index.ts +0 -59
  212. package/src/contracts/json.ts +0 -37
  213. package/src/contracts/log.ts +0 -139
  214. package/src/contracts/openai.ts +0 -267
  215. package/src/index.css +0 -11
  216. package/src/knowledge/candidateStore.ts +0 -94
  217. package/src/knowledge/distiller.ts +0 -98
  218. package/src/knowledge/openclawClient.ts +0 -161
  219. package/src/knowledge/openclawGatewayClient.ts +0 -249
  220. package/src/knowledge/openclawMarkdown.ts +0 -146
  221. package/src/knowledge/redactor.ts +0 -80
  222. package/src/knowledge/types.ts +0 -83
  223. package/src/lib/alertContract.ts +0 -76
  224. package/src/lib/apiClient.ts +0 -166
  225. package/src/lib/apiFormat.ts +0 -29
  226. package/src/lib/backendImplementation.ts +0 -16
  227. package/src/lib/basePath.ts +0 -107
  228. package/src/lib/browserRuntimeContract.ts +0 -124
  229. package/src/lib/browserRuntimeResolver.ts +0 -333
  230. package/src/lib/chunkLoadRecovery.ts +0 -83
  231. package/src/lib/claudeCodeStrip.ts +0 -98
  232. package/src/lib/clipboard.ts +0 -67
  233. package/src/lib/codingAgentEndpoints.ts +0 -54
  234. package/src/lib/ecosystemContract.ts +0 -130
  235. package/src/lib/export-logs.ts +0 -287
  236. package/src/lib/groupContract.ts +0 -166
  237. package/src/lib/instanceContract.ts +0 -239
  238. package/src/lib/jsonFile.ts +0 -46
  239. package/src/lib/logImportContract.ts +0 -12
  240. package/src/lib/managedInstance.ts +0 -112
  241. package/src/lib/mask.ts +0 -4
  242. package/src/lib/objectUtils.ts +0 -22
  243. package/src/lib/piAgentContract.ts +0 -102
  244. package/src/lib/providerContract.ts +0 -125
  245. package/src/lib/providerImportContract.ts +0 -27
  246. package/src/lib/providerModelMetadata.ts +0 -458
  247. package/src/lib/providerTestContract.ts +0 -170
  248. package/src/lib/providerTestPrompt.ts +0 -100
  249. package/src/lib/publicBasePathContext.ts +0 -137
  250. package/src/lib/resourceLimits.ts +0 -345
  251. package/src/lib/runContract.ts +0 -164
  252. package/src/lib/runtimeConfig.ts +0 -43
  253. package/src/lib/safeDiagnostic.ts +0 -38
  254. package/src/lib/safeFetch.ts +0 -508
  255. package/src/lib/serverPort.ts +0 -47
  256. package/src/lib/sessionInfoContract.ts +0 -78
  257. package/src/lib/sessionUrl.ts +0 -46
  258. package/src/lib/ssrfGuard.ts +0 -133
  259. package/src/lib/stopReason.ts +0 -130
  260. package/src/lib/themeMode.ts +0 -41
  261. package/src/lib/timeDisplay.ts +0 -22
  262. package/src/lib/toolTrace.ts +0 -290
  263. package/src/lib/uiScale.ts +0 -117
  264. package/src/lib/unknown.ts +0 -27
  265. package/src/lib/upstreamUrl.ts +0 -150
  266. package/src/lib/useAlerts.ts +0 -82
  267. package/src/lib/useGroupEvidence.ts +0 -37
  268. package/src/lib/useGroups.ts +0 -32
  269. package/src/lib/useOnboarding.ts +0 -93
  270. package/src/lib/useProviders.ts +0 -30
  271. package/src/lib/useStripConfig.ts +0 -154
  272. package/src/lib/useThemeMode.ts +0 -40
  273. package/src/lib/useUiScale.ts +0 -52
  274. package/src/lib/utils.ts +0 -78
  275. package/src/mcp/currentContext.ts +0 -65
  276. package/src/mcp/instanceHandlers.ts +0 -151
  277. package/src/mcp/loopback.ts +0 -88
  278. package/src/mcp/mode.ts +0 -75
  279. package/src/mcp/previewExtractor.ts +0 -233
  280. package/src/mcp/server.ts +0 -2274
  281. package/src/mcp/toolHandlers.ts +0 -1533
  282. package/src/proxy/alerts.ts +0 -664
  283. package/src/proxy/chunkStorage.ts +0 -123
  284. package/src/proxy/config.ts +0 -218
  285. package/src/proxy/constants.ts +0 -66
  286. package/src/proxy/dataDir.ts +0 -147
  287. package/src/proxy/ecosystemExecutionConfirmation.ts +0 -114
  288. package/src/proxy/ecosystemExecutionRoute.ts +0 -116
  289. package/src/proxy/ecosystemRegistry.ts +0 -296
  290. package/src/proxy/ecosystemTasks.ts +0 -415
  291. package/src/proxy/evidenceAnalysis.ts +0 -526
  292. package/src/proxy/evidenceExporter.ts +0 -235
  293. package/src/proxy/formats/anthropic/anthropicProvider.ts +0 -75
  294. package/src/proxy/formats/anthropic/handler.ts +0 -75
  295. package/src/proxy/formats/anthropic/index.ts +0 -13
  296. package/src/proxy/formats/anthropic/register.ts +0 -4
  297. package/src/proxy/formats/anthropic/schemas.ts +0 -6
  298. package/src/proxy/formats/anthropic/stream.ts +0 -205
  299. package/src/proxy/formats/handler.ts +0 -46
  300. package/src/proxy/formats/index.ts +0 -13
  301. package/src/proxy/formats/jsonSchema.ts +0 -2
  302. package/src/proxy/formats/openai/alibabaProvider.ts +0 -34
  303. package/src/proxy/formats/openai/codexMetadata.ts +0 -61
  304. package/src/proxy/formats/openai/handler.ts +0 -133
  305. package/src/proxy/formats/openai/index.ts +0 -33
  306. package/src/proxy/formats/openai/provider.ts +0 -50
  307. package/src/proxy/formats/openai/register.ts +0 -4
  308. package/src/proxy/formats/openai/schemas.ts +0 -25
  309. package/src/proxy/formats/openai/stream.ts +0 -600
  310. package/src/proxy/formats/openai/zhipuProvider.ts +0 -31
  311. package/src/proxy/formats/providerAdapter.ts +0 -50
  312. package/src/proxy/formats/providerRegistry.ts +0 -51
  313. package/src/proxy/formats/providers/index.ts +0 -3
  314. package/src/proxy/formats/registry.ts +0 -65
  315. package/src/proxy/groupEvidenceExporter.ts +0 -371
  316. package/src/proxy/groupStore.ts +0 -336
  317. package/src/proxy/handler.ts +0 -729
  318. package/src/proxy/identityProxy.ts +0 -1842
  319. package/src/proxy/jsonlRecovery.ts +0 -554
  320. package/src/proxy/logBodyChunks.ts +0 -91
  321. package/src/proxy/logFinalizer.ts +0 -777
  322. package/src/proxy/logFinalizer.worker.ts +0 -24
  323. package/src/proxy/logImportUpload.ts +0 -128
  324. package/src/proxy/logImporter.ts +0 -813
  325. package/src/proxy/logIndex.ts +0 -663
  326. package/src/proxy/logSearch.ts +0 -122
  327. package/src/proxy/logger.ts +0 -537
  328. package/src/proxy/openaiOrphanToolStrip.ts +0 -142
  329. package/src/proxy/platformCommands.ts +0 -258
  330. package/src/proxy/privateDataPath.ts +0 -419
  331. package/src/proxy/providerImporters.ts +0 -578
  332. package/src/proxy/providerScanStore.ts +0 -87
  333. package/src/proxy/providerSecretStore.ts +0 -216
  334. package/src/proxy/providers.ts +0 -998
  335. package/src/proxy/rawStreamCapture.ts +0 -342
  336. package/src/proxy/runFailures.ts +0 -94
  337. package/src/proxy/runStore.ts +0 -214
  338. package/src/proxy/runtimeAdmission.ts +0 -55
  339. package/src/proxy/runtimeHealth.ts +0 -221
  340. package/src/proxy/runtimeMetrics.ts +0 -149
  341. package/src/proxy/runtimeShutdown.ts +0 -157
  342. package/src/proxy/schemas.ts +0 -112
  343. package/src/proxy/sessionArchive.ts +0 -479
  344. package/src/proxy/sessionInfo.ts +0 -266
  345. package/src/proxy/sessionProcess.ts +0 -194
  346. package/src/proxy/sessionRuntime.ts +0 -121
  347. package/src/proxy/sessionSupervisor.ts +0 -305
  348. package/src/proxy/sessionWorkerEntry.ts +0 -26
  349. package/src/proxy/shutdownCoordinator.ts +0 -90
  350. package/src/proxy/socketTracker.ts +0 -639
  351. package/src/proxy/sqliteLogIndex.ts +0 -733
  352. package/src/proxy/storageLifecycle.ts +0 -432
  353. package/src/proxy/store.ts +0 -1773
  354. package/src/proxy/streamTiming.ts +0 -31
  355. package/src/proxy/toolSchemaWarnings.ts +0 -529
  356. package/src/proxy/uiAssetServer.ts +0 -106
  357. package/src/proxy/upstream.ts +0 -127
  358. package/src/services/alerts.ts +0 -12
  359. package/src/services/groups.ts +0 -119
  360. package/src/services/piAgent.ts +0 -421
  361. package/src/services/piAgentRetrieval.ts +0 -1180
  362. package/src/services/result.ts +0 -17
  363. package/src/services/runs.ts +0 -76
  364. package/src/ui/bootstrap.ts +0 -167
  365. package/src/ui/index.html +0 -38
  366. package/src/ui/logsStreamClient.ts +0 -20
  367. package/src/ui/main.tsx +0 -13
  368. package/src/ui/public/runtime-config.js +0 -2
  369. package/src/ui/router.tsx +0 -13
  370. package/src/ui/routes/__root.tsx +0 -69
  371. package/src/ui/routes/index.tsx +0 -35
  372. package/src/ui/routes/session/$sessionId.tsx +0 -50
  373. package/src/ui/runtimeDiscovery.ts +0 -50
@@ -1,2259 +0,0 @@
1
- import {
2
- type ChangeEvent,
3
- type JSX,
4
- type PointerEvent as ReactPointerEvent,
5
- type ReactNode,
6
- useCallback,
7
- useEffect,
8
- useMemo,
9
- useRef,
10
- useState,
11
- Suspense,
12
- } from "react";
13
- import {
14
- ALargeSmall,
15
- ArrowDownRight,
16
- ArrowLeft,
17
- ArrowUpRight,
18
- Clapperboard,
19
- Check,
20
- ChevronDown,
21
- ChevronLeft,
22
- ChevronRight,
23
- ChevronUp,
24
- ChevronsLeft,
25
- ChevronsRight,
26
- Copy,
27
- Download,
28
- Eye,
29
- FileJson,
30
- Gauge,
31
- Flag,
32
- PanelRightClose,
33
- PanelRightOpen,
34
- RefreshCw,
35
- Siren,
36
- Moon,
37
- Sun,
38
- Timer,
39
- Trash2,
40
- Upload,
41
- X,
42
- } from "lucide-react";
43
-
44
- import type { CapturedLog } from "../contracts";
45
- import { withBasePath } from "../lib/basePath";
46
- import { exportLogsAsZip, type ExportMode } from "../lib/export-logs";
47
- import {
48
- subscribeCodingAgentEndpoints,
49
- type CodingAgentEndpoints,
50
- } from "../lib/codingAgentEndpoints";
51
- import type { ImportLogsResponse } from "../lib/logImportContract";
52
- import { copyTextToClipboard } from "../lib/clipboard";
53
- import type { InspectorGroupEvidence, InspectorGroupMember } from "../lib/groupContract";
54
- import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
55
- import { formatTimestampRange } from "../lib/timeDisplay";
56
- import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
57
- import { useThemeMode } from "../lib/useThemeMode";
58
- import { formatUiScale, normalizeUiScalePreference, type UiScalePreference } from "../lib/uiScale";
59
- import { useUiScale } from "../lib/useUiScale";
60
- import { useProviders } from "../lib/useProviders";
61
- import { groupLogsByConversation, type ConversationGroupData } from "./proxy-viewer";
62
- import {
63
- buildSessionSlateStats,
64
- buildHomeConnectionCommands,
65
- COPYABLE_COMMAND_CONTAINER_CLASS_NAME,
66
- COPYABLE_COMMAND_TEXT_CLASS_NAME,
67
- exportRequiresConfirmation,
68
- formatPaginationStatus,
69
- getExportActionVisibility,
70
- resolveSessionContextScope,
71
- type SessionContextScope,
72
- type SessionSlateMetricTone,
73
- type SessionSlateStats,
74
- type SessionSlateStatus,
75
- } from "./proxy-viewer/proxyViewerLogic";
76
-
77
- import { CrabLogo } from "./ui/crab-logo";
78
- import { Button } from "./ui/button";
79
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
80
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
81
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
82
- import type { TransientToastInput } from "./ui/transient-toast";
83
- import { AlertsDialog } from "./alerts/AlertsDialog";
84
- import { GroupsDialog } from "./groups/GroupsDialog";
85
- import { SettingsDialog } from "./providers/SettingsDialog";
86
- import { AgentLabDialog } from "./ecosystem/AgentLabDialog";
87
- import { PiAgentPanel, type PiAgentSearchResult } from "./pi-agent/PiAgentPanel";
88
- import { LazyFeatureBoundary } from "./errors/SafeErrorBoundary";
89
- import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
90
- import { LazyCompareDrawer, LazyInspectorPet } from "./proxy-viewer/lazy";
91
- import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
92
- import { ApplicationBar } from "./proxy-viewer/ApplicationBar";
93
- import {
94
- SessionLoadProgressBar,
95
- type SessionLoadProgress,
96
- } from "./proxy-viewer/SessionLoadProgressBar";
97
- import { browserPrefersReducedMotion, preferredScrollBehavior } from "./proxy-viewer/accessibility";
98
- import { LiveConnectionStatus } from "./proxy-viewer/LiveConnectionStatus";
99
- import {
100
- getLiveEmptyStateCopy,
101
- resolveLiveEmptyState,
102
- type LiveConnectionState,
103
- } from "./proxy-viewer/liveConnectionState";
104
- import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
105
- import { RAW_EXPORT_DIALOG_CLASS_NAME, RawExportWarning } from "./proxy-viewer/RawExportWarning";
106
- import { buildValidPredecessors } from "./proxy-viewer/viewerState";
107
- import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
108
- import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
109
- import type {
110
- ContextHealthLevel,
111
- ContextRiskLevel,
112
- } from "./proxy-viewer/anatomy/contextIntelligence";
113
- import {
114
- buildSessionContextSummary,
115
- type SessionContextSummary,
116
- type SessionModelContextSummary,
117
- } from "./proxy-viewer/anatomy/sessionContextSummary";
118
-
119
- function truncateSessionId(id: string): string {
120
- if (id.length <= 30) return id;
121
- return `${id.slice(0, 12)}...${id.slice(-12)}`;
122
- }
123
-
124
- function buildDisplayNumberByLogId(groups: readonly ConversationGroupData[]): Map<number, number> {
125
- const result = new Map<number, number>();
126
- for (const group of groups) {
127
- for (let index = 0; index < group.logs.length; index += 1) {
128
- const log = group.logs[index];
129
- if (log === undefined) continue;
130
- result.set(log.id, index + 1);
131
- }
132
- }
133
- return result;
134
- }
135
-
136
- function displayNumberForLog(
137
- log: CapturedLog,
138
- displayNumberByLogId: ReadonlyMap<number, number>,
139
- ): number {
140
- return displayNumberByLogId.get(log.id) ?? log.id;
141
- }
142
-
143
- function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut: number } {
144
- let totalIn = 0;
145
- let totalOut = 0;
146
- for (const log of logs) {
147
- if (log.inputTokens !== null) totalIn += log.inputTokens;
148
- if (log.outputTokens !== null) totalOut += log.outputTokens;
149
- }
150
- return { totalIn, totalOut };
151
- }
152
-
153
- export type SessionMembershipEvidence = {
154
- groupId: string;
155
- groupTitle: string;
156
- groupKind: string;
157
- groupStatus: string;
158
- groupTask: string | null;
159
- groupProject: string | null;
160
- groupEvidence: InspectorGroupEvidence | null;
161
- member: InspectorGroupMember;
162
- };
163
-
164
- export type LogPaginationControls = {
165
- isLoading: boolean;
166
- loadProgress?: SessionLoadProgress;
167
- total: number | null;
168
- pageSize: number;
169
- hasOlder: boolean;
170
- hasNewer: boolean;
171
- oldestLogId: number | null;
172
- newestLogId: number | null;
173
- onOldest: () => void;
174
- onOlder: () => void;
175
- onNewer: () => void;
176
- onNewest: () => void;
177
- };
178
-
179
- function getFirstUserAgent(logs: CapturedLog[]): string | null {
180
- for (const log of logs) {
181
- if (log.userAgent !== null && log.userAgent !== undefined && log.userAgent !== "") {
182
- return log.userAgent;
183
- }
184
- }
185
- return null;
186
- }
187
-
188
- function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
189
- const first = logs[0];
190
- const last = logs[logs.length - 1];
191
- if (first === undefined || last === undefined) return null;
192
- return formatTimestampRange(first.timestamp, last.timestamp, timeDisplayFormat);
193
- }
194
-
195
- function slateMetricToneClass(tone: SessionSlateMetricTone): string {
196
- switch (tone) {
197
- case "cool":
198
- return "text-status-cool";
199
- case "danger":
200
- return "text-status-danger";
201
- case "watch":
202
- return "text-status-watch";
203
- case "neutral":
204
- return "text-foreground";
205
- }
206
- }
207
-
208
- function statusMetricIcon(status: SessionSlateStatus): ReactNode {
209
- switch (status) {
210
- case "rolling":
211
- return <RefreshCw className="size-3 text-status-cool" />;
212
- case "failed":
213
- return <Siren className="size-3 text-status-danger" />;
214
- case "watch":
215
- return <Timer className="size-3 text-status-watch" />;
216
- case "clear":
217
- return <Check className="size-3 text-status-success" />;
218
- }
219
- }
220
-
221
- function statusMetricTone(status: SessionSlateStatus): SessionSlateMetricTone {
222
- switch (status) {
223
- case "rolling":
224
- return "cool";
225
- case "failed":
226
- return "danger";
227
- case "watch":
228
- return "watch";
229
- case "clear":
230
- return "neutral";
231
- }
232
- }
233
-
234
- function requestHealthDetail(stats: SessionSlateStats): string {
235
- if (stats.pending > 0) return `${stats.pending} pending / ${stats.failures} failed`;
236
- if (stats.lengthStops > 0) return `${stats.lengthStops} output limit / ${stats.failures} failed`;
237
- return `${stats.failures} failed / ${stats.slow} slow`;
238
- }
239
-
240
- function SessionSlateMetric({
241
- label,
242
- value,
243
- detail,
244
- icon,
245
- tone = "neutral",
246
- }: {
247
- label: string;
248
- value: string;
249
- detail: string;
250
- icon?: ReactNode;
251
- tone?: SessionSlateMetricTone;
252
- }): JSX.Element {
253
- return (
254
- <div className="min-w-0 border-t border-border/60 px-4 py-3">
255
- <div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
256
- {icon}
257
- {label}
258
- </div>
259
- <div className={cn("mt-2 font-mono text-sm font-semibold", slateMetricToneClass(tone))}>
260
- {value}
261
- </div>
262
- <div className="mt-1 font-mono text-[11px] leading-snug text-muted-foreground">{detail}</div>
263
- </div>
264
- );
265
- }
266
-
267
- function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Element | null {
268
- if (stats.requests === 0) return null;
269
-
270
- return (
271
- <div className="mb-4 overflow-hidden rounded-[8px] border border-border bg-card">
272
- <div className="px-4 py-3">
273
- <div className="flex flex-wrap items-end justify-between gap-3">
274
- <div className="min-w-0">
275
- <div className="flex items-center gap-2 text-[10px] font-semibold uppercase text-muted-foreground">
276
- <Clapperboard className="size-3.5 text-slate-300" />
277
- Flight Recorder
278
- </div>
279
- <div className="mt-2 min-w-0 font-mono text-sm font-semibold text-foreground">
280
- {stats.modelLabel}
281
- </div>
282
- </div>
283
- <div className="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-muted-foreground">
284
- <span className="min-w-0 font-mono">{stats.providerLabel}</span>
285
- <span className="min-w-0 font-mono">{stats.timeRange ?? "Timeline pending"}</span>
286
- </div>
287
- </div>
288
- </div>
289
- <div className="grid grid-cols-2 border-t border-border/60 md:grid-cols-3 2xl:grid-cols-6">
290
- <SessionSlateMetric
291
- label="Status"
292
- value={stats.statusLabel}
293
- detail={requestHealthDetail(stats)}
294
- icon={statusMetricIcon(stats.status)}
295
- tone={statusMetricTone(stats.status)}
296
- />
297
- <SessionSlateMetric
298
- label="Turns"
299
- value={String(stats.turns)}
300
- detail={stats.finishLabel}
301
- icon={<Flag className="size-3 text-muted-foreground" />}
302
- tone={stats.finishTone}
303
- />
304
- <SessionSlateMetric
305
- label="Requests"
306
- value={String(stats.requests)}
307
- detail={`${stats.failures} failed / ${stats.slow} slow`}
308
- icon={<Siren className="size-3 text-muted-foreground" />}
309
- tone={stats.failures > 0 ? "danger" : "neutral"}
310
- />
311
- <SessionSlateMetric
312
- label="Peak"
313
- value={stats.slowestLabel}
314
- detail={stats.slowestDetail}
315
- icon={<Timer className="size-3 text-muted-foreground" />}
316
- tone={stats.slow > 0 ? "watch" : "neutral"}
317
- />
318
- <SessionSlateMetric
319
- label="Tokens"
320
- value={stats.tokenLabel}
321
- detail={stats.heaviestDetail}
322
- icon={<Gauge className="size-3 text-muted-foreground" />}
323
- />
324
- <SessionSlateMetric
325
- label="Outcome"
326
- value={stats.outcomeLabel}
327
- detail={stats.outcomeDetail}
328
- tone={stats.outcomeTone}
329
- />
330
- </div>
331
- </div>
332
- );
333
- }
334
-
335
- function formatPercent(value: number | null): string {
336
- if (value === null) return "Unknown";
337
- const percent = value * 100;
338
- if (percent >= 10) return `${percent.toFixed(0)}%`;
339
- if (percent >= 1) return `${percent.toFixed(1)}%`;
340
- if (percent > 0) return "<1%";
341
- return "0%";
342
- }
343
-
344
- function formatTrend(value: number | null): string {
345
- if (value === null) return "n/a";
346
- if (Math.abs(value) < 0.1) return "0%";
347
- const sign = value > 0 ? "+" : "";
348
- return `${sign}${value.toFixed(1)}%`;
349
- }
350
-
351
- function progressPercent(value: number | null): number {
352
- if (value === null || !Number.isFinite(value)) return 0;
353
- return Math.min(Math.max(value * 100, 0), 100);
354
- }
355
-
356
- function peakMarkerPercent(
357
- latestPercent: number | null,
358
- peakPercent: number | null,
359
- ): number | null {
360
- if (peakPercent === null) return null;
361
- const latest = progressPercent(latestPercent);
362
- const peak = progressPercent(peakPercent);
363
- if (peak <= 0) return null;
364
- if (Math.abs(peak - latest) < 0.5) return null;
365
- return peak;
366
- }
367
-
368
- function sessionRiskBarClass(risk: ContextRiskLevel): string {
369
- switch (risk) {
370
- case "danger":
371
- return "bg-red-400";
372
- case "watch":
373
- return "bg-amber-300";
374
- case "unknown":
375
- return "bg-muted-foreground/50";
376
- case "ok":
377
- return "bg-cyan-300";
378
- }
379
- }
380
-
381
- function formatContextWindow(summary: SessionModelContextSummary): string {
382
- const tokens = summary.latest.contextWindow.tokens;
383
- if (tokens === null) return "Context window unknown";
384
- return `Context window ${formatContextWindowTokens(tokens)}`;
385
- }
386
-
387
- const MEMBER_METADATA_KEYS: string[] = [
388
- "score",
389
- "decision",
390
- "final_status",
391
- "validation_pass_ratio",
392
- "semantic_memory_score",
393
- "opencode_exit",
394
- "actual_model",
395
- "run_dir",
396
- "workspace",
397
- "project_dir",
398
- "work_dir",
399
- ];
400
-
401
- function formatMetadataValue(value: InspectorGroupMember["metadata"][string]): string | null {
402
- if (typeof value === "string") return value.length > 0 ? value : null;
403
- if (typeof value === "number" || typeof value === "boolean") return String(value);
404
- if (value === null) return null;
405
- return JSON.stringify(value);
406
- }
407
-
408
- function formatMemberMetadata(member: InspectorGroupMember | null, key: string): string | null {
409
- if (member === null) return null;
410
- const value = member.metadata[key];
411
- if (value === undefined) return null;
412
- return formatMetadataValue(value);
413
- }
414
-
415
- function evidenceChipClass(value: string): string {
416
- const normalized = value.toLowerCase();
417
- if (
418
- normalized.includes("reject") ||
419
- normalized.includes("fail") ||
420
- normalized.includes("error")
421
- ) {
422
- return "border-red-400/35 bg-red-500/10 text-red-100";
423
- }
424
- if (
425
- normalized.includes("partial") ||
426
- normalized.includes("pending") ||
427
- normalized.includes("slow")
428
- ) {
429
- return "border-amber-300/35 bg-amber-400/10 text-amber-100";
430
- }
431
- if (
432
- normalized.includes("pass") ||
433
- normalized.includes("complete") ||
434
- normalized.includes("accept")
435
- ) {
436
- return "border-cyan-300/25 bg-cyan-400/10 text-cyan-100";
437
- }
438
- return "border-border/70 bg-background/70 text-muted-foreground";
439
- }
440
-
441
- function getMemberMetadataRows(member: InspectorGroupMember): { key: string; value: string }[] {
442
- const rows: { key: string; value: string }[] = [];
443
- for (const key of MEMBER_METADATA_KEYS) {
444
- const value = member.metadata[key];
445
- if (value === undefined) continue;
446
- const formatted = formatMetadataValue(value);
447
- if (formatted !== null) rows.push({ key, value: formatted });
448
- }
449
- return rows;
450
- }
451
-
452
- function SessionMembershipPanel({
453
- memberships,
454
- defaultOpen = false,
455
- }: {
456
- memberships: SessionMembershipEvidence[];
457
- defaultOpen?: boolean;
458
- }): JSX.Element | null {
459
- const [open, setOpen] = useState(defaultOpen);
460
- if (memberships.length === 0) return null;
461
- const primary = memberships[0];
462
- const primaryMember = primary?.member ?? null;
463
- const score = formatMemberMetadata(primaryMember, "score");
464
- const decision = formatMemberMetadata(primaryMember, "decision");
465
- const finalStatus = formatMemberMetadata(primaryMember, "final_status");
466
- const summaryChips = [
467
- score === null ? null : { label: `score ${score}`, value: score },
468
- decision === null ? null : { label: decision, value: decision },
469
- finalStatus === null ? null : { label: finalStatus, value: finalStatus },
470
- primaryMember?.model === null || primaryMember?.model === undefined
471
- ? null
472
- : { label: primaryMember.model, value: primaryMember.model },
473
- ].filter(
474
- (value): value is { label: string; value: string } =>
475
- value !== null && value.label.length > 0 && value.value.length > 0,
476
- );
477
-
478
- return (
479
- <div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] text-xs">
480
- <button
481
- type="button"
482
- onClick={() => setOpen((value) => !value)}
483
- className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-white/[0.04]"
484
- aria-expanded={open}
485
- >
486
- <div className="min-w-0 flex-1">
487
- <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
488
- <span className="font-semibold text-foreground">Case File</span>
489
- {primary !== undefined && (
490
- <span className="truncate text-muted-foreground" title={primary.groupTitle}>
491
- {primary.groupTitle}
492
- </span>
493
- )}
494
- <span className="font-mono text-[10px] uppercase text-muted-foreground">
495
- {memberships.length} group{memberships.length === 1 ? "" : "s"}
496
- </span>
497
- </div>
498
- {summaryChips.length > 0 && (
499
- <div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
500
- {summaryChips.slice(0, 4).map((chip) => (
501
- <span
502
- key={chip.label}
503
- className={cn(
504
- "rounded border px-1.5 py-0.5 font-mono text-[10px]",
505
- evidenceChipClass(chip.value),
506
- )}
507
- >
508
- {chip.label}
509
- </span>
510
- ))}
511
- </div>
512
- )}
513
- </div>
514
- {open ? (
515
- <ChevronUp className="size-4 shrink-0 text-muted-foreground" />
516
- ) : (
517
- <ChevronDown className="size-4 shrink-0 text-muted-foreground" />
518
- )}
519
- </button>
520
- {open && (
521
- <div className="space-y-2 border-t border-white/10 px-3 py-2">
522
- {memberships.map((membership) => {
523
- const member = membership.member;
524
- const metadataRows = getMemberMetadataRows(member);
525
- return (
526
- <div key={`${membership.groupId}:${member.id}`} className="min-w-0">
527
- <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
528
- <span
529
- className="truncate font-medium text-foreground"
530
- title={membership.groupTitle}
531
- >
532
- {membership.groupTitle}
533
- </span>
534
- <span className="font-mono text-[10px] uppercase text-muted-foreground">
535
- {membership.groupStatus}
536
- </span>
537
- {member.status !== null && (
538
- <span className="font-mono text-[10px] text-muted-foreground">
539
- member {member.status}
540
- </span>
541
- )}
542
- </div>
543
- <div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
544
- <span className="font-mono" title={member.sessionId}>
545
- {truncateSessionId(member.sessionId)}
546
- </span>
547
- {member.label !== null && <span>{member.label}</span>}
548
- {member.provider !== null && <span>{member.provider}</span>}
549
- {member.model !== null && <span className="font-mono">{member.model}</span>}
550
- {member.runId !== null && (
551
- <span className="font-mono" title={member.runId}>
552
- run {truncateSessionId(member.runId)}
553
- </span>
554
- )}
555
- </div>
556
- {metadataRows.length > 0 && (
557
- <div className="mt-2 grid gap-1 sm:grid-cols-2 xl:grid-cols-3">
558
- {metadataRows.map((row) => (
559
- <div
560
- key={row.key}
561
- className="border border-border bg-muted/40 min-w-0 rounded-md px-2 py-1"
562
- >
563
- <div className="font-mono text-[10px] text-muted-foreground">{row.key}</div>
564
- <div
565
- className="truncate font-mono text-[11px] text-foreground"
566
- title={row.value}
567
- >
568
- {row.value}
569
- </div>
570
- </div>
571
- ))}
572
- </div>
573
- )}
574
- </div>
575
- );
576
- })}
577
- </div>
578
- )}
579
- </div>
580
- );
581
- }
582
-
583
- function LogPaginationButton({
584
- label,
585
- title,
586
- icon,
587
- disabled,
588
- onClick,
589
- }: {
590
- label: string;
591
- title: string;
592
- icon: JSX.Element;
593
- disabled: boolean;
594
- onClick: () => void;
595
- }): JSX.Element {
596
- return (
597
- <button
598
- type="button"
599
- onClick={onClick}
600
- disabled={disabled}
601
- className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-45"
602
- title={title}
603
- >
604
- {icon}
605
- <span>{label}</span>
606
- </button>
607
- );
608
- }
609
-
610
- function LogPaginationBar({
611
- logs,
612
- pagination,
613
- embedded = false,
614
- }: {
615
- logs: readonly CapturedLog[];
616
- pagination: LogPaginationControls | undefined;
617
- embedded?: boolean;
618
- }): JSX.Element | null {
619
- if (pagination === undefined) return null;
620
- const disabled = pagination.isLoading;
621
-
622
- return (
623
- <nav
624
- aria-label="Session log pagination"
625
- className={cn(
626
- "flex min-h-9 flex-col gap-2 px-1 py-1 sm:flex-row sm:items-center",
627
- embedded
628
- ? "border-t border-white/10 pt-2"
629
- : "border border-border bg-card shadow-sm mb-3 rounded-[8px] px-2",
630
- )}
631
- >
632
- <div className="w-full min-w-0 px-1 font-mono text-[11px] text-muted-foreground sm:flex-1">
633
- <div role="status" aria-live="polite" aria-atomic="true">
634
- {formatPaginationStatus({ logs, pagination })}
635
- {pagination.isLoading && logs.length > 0 && <span className="ml-2">Loading...</span>}
636
- </div>
637
- {pagination.loadProgress !== undefined && (
638
- <SessionLoadProgressBar progress={pagination.loadProgress} />
639
- )}
640
- </div>
641
- <div className="flex w-full items-center gap-1 overflow-x-auto sm:w-auto">
642
- <LogPaginationButton
643
- label="Oldest"
644
- title="Load the oldest logs in this session"
645
- icon={<ChevronsLeft className="size-3.5" />}
646
- disabled={disabled || !pagination.hasOlder}
647
- onClick={pagination.onOldest}
648
- />
649
- <LogPaginationButton
650
- label="Older"
651
- title="Load older logs"
652
- icon={<ChevronLeft className="size-3.5" />}
653
- disabled={disabled || !pagination.hasOlder}
654
- onClick={pagination.onOlder}
655
- />
656
- <LogPaginationButton
657
- label="Newer"
658
- title="Load newer logs"
659
- icon={<ChevronRight className="size-3.5" />}
660
- disabled={disabled || !pagination.hasNewer}
661
- onClick={pagination.onNewer}
662
- />
663
- <LogPaginationButton
664
- label="Newest"
665
- title="Load the newest logs in this session"
666
- icon={<ChevronsRight className="size-3.5" />}
667
- disabled={disabled || !pagination.hasNewer}
668
- onClick={pagination.onNewest}
669
- />
670
- </div>
671
- </nav>
672
- );
673
- }
674
-
675
- function formatRequestCount(count: number): string {
676
- return `${count} ${count === 1 ? "request" : "requests"}`;
677
- }
678
-
679
- function formatLogReference(logId: number): string {
680
- return `#${String(logId)}`;
681
- }
682
-
683
- function sessionRiskClass(risk: ContextRiskLevel): string {
684
- switch (risk) {
685
- case "danger":
686
- return "text-red-300";
687
- case "watch":
688
- return "text-amber-200";
689
- case "unknown":
690
- return "text-muted-foreground";
691
- case "ok":
692
- return "text-cyan-200";
693
- }
694
- }
695
-
696
- function sessionHealthClass(level: ContextHealthLevel): string {
697
- switch (level) {
698
- case "risk":
699
- return "border-red-400/30 bg-red-500/10 text-red-100";
700
- case "optimizable":
701
- return "border-amber-400/25 bg-amber-400/8 text-amber-100";
702
- case "ok":
703
- return "border-cyan-300/25 bg-cyan-400/8 text-cyan-100";
704
- }
705
- }
706
-
707
- function sessionHealthLabel(level: ContextHealthLevel): string {
708
- switch (level) {
709
- case "risk":
710
- return "Risk";
711
- case "optimizable":
712
- return "Optimizable";
713
- case "ok":
714
- return "OK";
715
- }
716
- }
717
-
718
- const LOG_FOCUS_RETRY_LIMIT = 10;
719
- const LOG_FOCUS_RETRY_DELAY_MS = 80;
720
-
721
- function highlightLogTarget(target: HTMLElement): void {
722
- target.setAttribute("data-deep-link-highlight", "true");
723
- window.setTimeout(() => {
724
- target.removeAttribute("data-deep-link-highlight");
725
- }, 1800);
726
- }
727
-
728
- function focusLogTarget(logId: number): boolean {
729
- const target = document.getElementById(`log-${String(logId)}`);
730
- if (!(target instanceof HTMLElement)) return false;
731
- target.scrollIntoView({
732
- block: "center",
733
- behavior: preferredScrollBehavior(browserPrefersReducedMotion()),
734
- });
735
- target.focus({ preventScroll: true });
736
- highlightLogTarget(target);
737
- return true;
738
- }
739
-
740
- function openLogContext(logId: number): void {
741
- dispatchLogFocusRequest({ logId, tab: "anatomy" });
742
-
743
- let attempts = 0;
744
- const tryFocus = (): void => {
745
- dispatchLogFocusRequest({ logId, tab: "anatomy" });
746
- if (focusLogTarget(logId)) return;
747
-
748
- attempts += 1;
749
- if (attempts < LOG_FOCUS_RETRY_LIMIT) {
750
- window.setTimeout(tryFocus, LOG_FOCUS_RETRY_DELAY_MS);
751
- }
752
- };
753
-
754
- window.setTimeout(tryFocus, 0);
755
- }
756
-
757
- function CopyableCommand({ command }: { command: string }): JSX.Element {
758
- const [copied, setCopied] = useState(false);
759
-
760
- const handleCopy = useCallback(() => {
761
- void copyTextToClipboard(command).then((success) => {
762
- if (!success) return;
763
- setCopied(true);
764
- setTimeout(() => setCopied(false), 2000);
765
- });
766
- }, [command]);
767
-
768
- return (
769
- <div data-command-example="true" className={COPYABLE_COMMAND_CONTAINER_CLASS_NAME}>
770
- <pre className={COPYABLE_COMMAND_TEXT_CLASS_NAME}>{command}</pre>
771
- <button
772
- type="button"
773
- onClick={handleCopy}
774
- className="shrink-0 cursor-pointer rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
775
- aria-label="Copy command"
776
- >
777
- {copied ? <Check className="size-4" /> : <Copy className="size-4" />}
778
- </button>
779
- </div>
780
- );
781
- }
782
-
783
- function SidebarPanel({
784
- label,
785
- children,
786
- className,
787
- }: {
788
- label: string;
789
- children: ReactNode;
790
- className?: string;
791
- }): JSX.Element {
792
- return (
793
- <section className={cn("rounded-[8px] border border-border/60 bg-card/55", className)}>
794
- <div className="px-3 pt-3">
795
- <div className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-text-subtle">
796
- {label}
797
- </div>
798
- </div>
799
- <div className="p-3 pt-2">{children}</div>
800
- </section>
801
- );
802
- }
803
-
804
- function SidebarMetric({ label, value }: { label: string; value: string | number }): JSX.Element {
805
- return (
806
- <div className="min-w-0 rounded-md border border-border/50 bg-muted/30 px-2 py-2">
807
- <div className="truncate font-mono text-lg font-semibold leading-6 text-text-strong">
808
- {value}
809
- </div>
810
- <div className="mt-0.5 truncate font-mono text-[10px] uppercase tracking-[0.12em] text-text-subtle">
811
- {label}
812
- </div>
813
- </div>
814
- );
815
- }
816
-
817
- function ThemeModeToggle(): JSX.Element {
818
- const { themeMode, setThemeMode } = useThemeMode();
819
-
820
- return (
821
- <div
822
- className="inline-flex h-9 items-center rounded-md border border-border/70 bg-muted/70 p-0.5"
823
- aria-label="Theme mode"
824
- role="group"
825
- >
826
- <button
827
- type="button"
828
- onClick={() => setThemeMode("light")}
829
- className={cn(
830
- "inline-flex size-8 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none max-lg:size-11",
831
- themeMode === "light" && "bg-background text-foreground shadow-sm",
832
- )}
833
- aria-label="Use light mode"
834
- aria-pressed={themeMode === "light"}
835
- title="Light mode"
836
- >
837
- <Sun className="size-3.5" />
838
- </button>
839
- <button
840
- type="button"
841
- onClick={() => setThemeMode("eye-care")}
842
- className={cn(
843
- "inline-flex size-8 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none max-lg:size-11",
844
- themeMode === "eye-care" && "bg-background text-foreground shadow-sm",
845
- )}
846
- aria-label="Use eye care mode"
847
- aria-pressed={themeMode === "eye-care"}
848
- title="Eye care mode"
849
- >
850
- <Eye className="size-3.5" />
851
- </button>
852
- <button
853
- type="button"
854
- onClick={() => setThemeMode("dark")}
855
- className={cn(
856
- "inline-flex size-8 items-center justify-center rounded-[6px] text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none max-lg:size-11",
857
- themeMode === "dark" && "bg-background text-foreground shadow-sm",
858
- )}
859
- aria-label="Use dark mode"
860
- aria-pressed={themeMode === "dark"}
861
- title="Dark mode"
862
- >
863
- <Moon className="size-3.5" />
864
- </button>
865
- </div>
866
- );
867
- }
868
-
869
- function PetVisibilityToggle({
870
- visible,
871
- onToggle,
872
- }: {
873
- visible: boolean;
874
- onToggle: () => void;
875
- }): JSX.Element {
876
- const label = visible ? "Hide Inspector pet" : "Summon Inspector pet";
877
-
878
- return (
879
- <button
880
- type="button"
881
- onClick={onToggle}
882
- className={cn(
883
- "border-border/70 bg-muted/70 inline-flex size-8 shrink-0 items-center justify-center rounded-md border text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
884
- visible && "bg-background text-amber-500 shadow-sm",
885
- )}
886
- aria-label={label}
887
- aria-pressed={visible}
888
- title={label}
889
- >
890
- <CrabLogo className={cn("size-4 transition-opacity", !visible && "opacity-45")} />
891
- </button>
892
- );
893
- }
894
-
895
- function UiScaleControl({
896
- preference,
897
- resolvedScale,
898
- onChange,
899
- }: {
900
- preference: UiScalePreference;
901
- resolvedScale: number;
902
- onChange: (preference: UiScalePreference) => void;
903
- }): JSX.Element {
904
- const resolvedLabel = formatUiScale(resolvedScale);
905
- const currentLabel = preference === "auto" ? `Auto (${resolvedLabel})` : `${preference}%`;
906
-
907
- return (
908
- <Select
909
- value={preference}
910
- onValueChange={(value) => onChange(normalizeUiScalePreference(value))}
911
- >
912
- <SelectTrigger
913
- size="sm"
914
- className="h-9 min-w-[4.5rem] justify-center gap-1 border-border/70 bg-muted/70 px-2 py-0 text-muted-foreground hover:bg-muted max-lg:h-11 [&>svg:last-child]:hidden"
915
- aria-label={`Interface size: ${currentLabel}`}
916
- >
917
- <ALargeSmall className="size-3.5" />
918
- <span className="font-mono text-[10px] font-semibold tabular-nums">{resolvedLabel}</span>
919
- </SelectTrigger>
920
- <SelectContent align="end">
921
- <SelectItem value="auto">Auto</SelectItem>
922
- <SelectItem value="100">100%</SelectItem>
923
- <SelectItem value="125">125%</SelectItem>
924
- <SelectItem value="150">150%</SelectItem>
925
- </SelectContent>
926
- </Select>
927
- );
928
- }
929
-
930
- const DETAILS_PANEL_COLLAPSED_STORAGE_KEY = "agent-inspector.detailsPanelCollapsed";
931
- const DETAILS_PANEL_WIDTH_STORAGE_KEY = "agent-inspector.detailsPanelWidth";
932
- const INSPECTOR_PET_VISIBLE_STORAGE_KEY = "agent-inspector.pet.visible";
933
- const DETAILS_PANEL_DEFAULT_WIDTH = 360;
934
- const DETAILS_PANEL_MIN_WIDTH = 288;
935
- const DETAILS_PANEL_MAX_WIDTH = 520;
936
-
937
- type DetailsPanelResizeState = {
938
- startX: number;
939
- startWidth: number;
940
- pointerId: number;
941
- };
942
-
943
- function clampDetailsPanelWidth(width: number): number {
944
- return Math.min(Math.max(width, DETAILS_PANEL_MIN_WIDTH), DETAILS_PANEL_MAX_WIDTH);
945
- }
946
-
947
- function readStoredDetailsPanelCollapsed(): boolean {
948
- if (typeof window === "undefined") return true;
949
- try {
950
- const stored = window.localStorage.getItem(DETAILS_PANEL_COLLAPSED_STORAGE_KEY);
951
- return stored === null || stored === "1";
952
- } catch {
953
- return true;
954
- }
955
- }
956
-
957
- function readStoredDetailsPanelWidth(): number {
958
- if (typeof window === "undefined") return DETAILS_PANEL_DEFAULT_WIDTH;
959
- try {
960
- const stored = window.localStorage.getItem(DETAILS_PANEL_WIDTH_STORAGE_KEY);
961
- if (stored === null) return DETAILS_PANEL_DEFAULT_WIDTH;
962
- const parsed = Number.parseInt(stored, 10);
963
- return Number.isFinite(parsed) ? clampDetailsPanelWidth(parsed) : DETAILS_PANEL_DEFAULT_WIDTH;
964
- } catch {
965
- return DETAILS_PANEL_DEFAULT_WIDTH;
966
- }
967
- }
968
-
969
- function readStoredInspectorPetVisible(): boolean {
970
- if (typeof window === "undefined") return true;
971
- try {
972
- return window.localStorage.getItem(INSPECTOR_PET_VISIBLE_STORAGE_KEY) !== "0";
973
- } catch {
974
- return true;
975
- }
976
- }
977
-
978
- function SessionContextLogButton({ label, logId }: { label: string; logId: number }): JSX.Element {
979
- return (
980
- <button
981
- type="button"
982
- onClick={() => openLogContext(logId)}
983
- className="inline-flex h-5 items-center rounded px-1 font-mono text-[10px] text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground"
984
- title={`Open Context for ${formatLogReference(logId)}`}
985
- >
986
- {label} {formatLogReference(logId)}
987
- </button>
988
- );
989
- }
990
-
991
- function SessionContextProgressBar({
992
- latestPercent,
993
- peakPercent,
994
- risk,
995
- onOpenContext,
996
- }: {
997
- latestPercent: number | null;
998
- peakPercent: number | null;
999
- risk: ContextRiskLevel;
1000
- onOpenContext: () => void;
1001
- }): JSX.Element {
1002
- const latest = progressPercent(latestPercent);
1003
- const peakMarker = peakMarkerPercent(latestPercent, peakPercent);
1004
- const ariaLabel =
1005
- peakPercent === null
1006
- ? `Latest context window usage ${formatPercent(latestPercent)}`
1007
- : `Latest context window usage ${formatPercent(latestPercent)}, peak ${formatPercent(
1008
- peakPercent,
1009
- )}`;
1010
-
1011
- return (
1012
- <button
1013
- type="button"
1014
- onClick={onOpenContext}
1015
- className="group block w-full rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
1016
- aria-label={`${ariaLabel}. Open latest Context details`}
1017
- title="Open latest Context details"
1018
- >
1019
- <span
1020
- className="relative block h-2.5 overflow-hidden rounded-full border border-white/10 bg-black/25 transition-colors group-hover:border-cyan-200/35"
1021
- role="meter"
1022
- aria-label={ariaLabel}
1023
- aria-valuemin={0}
1024
- aria-valuemax={100}
1025
- aria-valuenow={Math.round(latest)}
1026
- >
1027
- <span
1028
- className={cn("block h-full rounded-full transition-[width]", sessionRiskBarClass(risk))}
1029
- style={{ width: `${latest}%` }}
1030
- />
1031
- {peakMarker !== null && (
1032
- <span
1033
- className="absolute top-0 h-full w-0.5 rounded-full bg-amber-100/75 shadow-[0_0_0_1px_rgba(0,0,0,0.25)]"
1034
- style={{ left: `calc(${peakMarker}% - 0.5px)` }}
1035
- aria-hidden="true"
1036
- />
1037
- )}
1038
- </span>
1039
- </button>
1040
- );
1041
- }
1042
-
1043
- function SessionModelContextRow({ summary }: { summary: SessionModelContextSummary }): JSX.Element {
1044
- const trend = summary.trendPercent;
1045
- const TrendIcon =
1046
- trend === null || Math.abs(trend) < 0.1 ? null : trend > 0 ? ArrowUpRight : ArrowDownRight;
1047
- const diagnosticLabel = summary.latest.diagnostics[0] ?? null;
1048
- const latestUsage = formatPercent(summary.latest.usagePercent);
1049
- const peakUsage = formatPercent(summary.peak.usagePercent);
1050
- const openLatestContext = (): void => {
1051
- openLogContext(summary.latest.logId);
1052
- };
1053
-
1054
- return (
1055
- <div className="grid gap-x-4 gap-y-2 border-t border-white/10 py-2 first:border-t-0 lg:grid-cols-[minmax(180px,1fr)_minmax(260px,1fr)_minmax(260px,1.4fr)]">
1056
- <div className="min-w-0">
1057
- <div className="flex min-w-0 items-center gap-2">
1058
- <button
1059
- type="button"
1060
- onClick={openLatestContext}
1061
- className="min-w-0 truncate rounded-sm text-left font-mono font-semibold text-foreground transition-colors hover:text-text-link focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
1062
- title={`Open latest Context for ${summary.model}`}
1063
- >
1064
- {summary.model}
1065
- </button>
1066
- <span
1067
- className={cn(
1068
- "inline-flex h-5 shrink-0 items-center rounded border px-1.5 font-mono text-[10px]",
1069
- sessionHealthClass(summary.latest.health.level),
1070
- )}
1071
- >
1072
- {sessionHealthLabel(summary.latest.health.level)}
1073
- </span>
1074
- </div>
1075
- <div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
1076
- <span>{formatRequestCount(summary.requestCount)}</span>
1077
- <span>{formatContextWindow(summary)}</span>
1078
- </div>
1079
- </div>
1080
-
1081
- <div className="min-w-0 space-y-2 text-xs">
1082
- <SessionContextProgressBar
1083
- latestPercent={summary.latest.usagePercent}
1084
- peakPercent={summary.peak.usagePercent}
1085
- risk={summary.latest.riskLevel}
1086
- onOpenContext={openLatestContext}
1087
- />
1088
- <div className="grid grid-cols-3 gap-2">
1089
- <div className="min-w-0">
1090
- <div className="text-[10px] uppercase text-muted-foreground">Latest</div>
1091
- <div className={cn("mt-0.5 font-mono", sessionRiskClass(summary.latest.riskLevel))}>
1092
- {latestUsage}
1093
- </div>
1094
- <SessionContextLogButton label="Log" logId={summary.latest.logId} />
1095
- </div>
1096
- <div className="min-w-0">
1097
- <div className="text-[10px] uppercase text-muted-foreground">Peak</div>
1098
- <div className={cn("mt-0.5 font-mono", sessionRiskClass(summary.peak.riskLevel))}>
1099
- {peakUsage}
1100
- </div>
1101
- <SessionContextLogButton label="Log" logId={summary.peak.logId} />
1102
- </div>
1103
- <div className="min-w-0">
1104
- <div className="text-[10px] uppercase text-muted-foreground">Trend</div>
1105
- <div className="mt-0.5 flex items-center gap-1 font-mono text-foreground">
1106
- {TrendIcon !== null && (
1107
- <TrendIcon
1108
- className={cn(
1109
- "size-3",
1110
- trend !== null && trend > 0 ? "text-amber-300" : "text-cyan-300",
1111
- )}
1112
- />
1113
- )}
1114
- {formatTrend(trend)}
1115
- </div>
1116
- <div className="h-5" aria-hidden="true" />
1117
- </div>
1118
- </div>
1119
- </div>
1120
-
1121
- <div className="min-w-0 space-y-1.5">
1122
- <SegmentBar
1123
- segments={summary.latest.roleSegments}
1124
- totalTokens={summary.latest.roleTotalTokens}
1125
- showLabels={false}
1126
- />
1127
- <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
1128
- <span className="font-mono">
1129
- ~{formatTokens(summary.latest.estimatedInputTokens)} input
1130
- </span>
1131
- {summary.latest.windowUsedTokens !== null && (
1132
- <span className="font-mono">~{formatTokens(summary.latest.windowUsedTokens)} used</span>
1133
- )}
1134
- {diagnosticLabel !== null && <span className="truncate">{diagnosticLabel}</span>}
1135
- </div>
1136
- </div>
1137
- </div>
1138
- );
1139
- }
1140
-
1141
- function SessionContextSummaryPanel({
1142
- summary,
1143
- }: {
1144
- summary: SessionContextSummary;
1145
- }): JSX.Element | null {
1146
- if (summary.models.length === 0) return null;
1147
- return (
1148
- <div className="mt-3 border-t border-white/10 pt-2">
1149
- <div className="mb-1 flex items-center justify-between gap-2">
1150
- <div className="text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">
1151
- Session Context
1152
- </div>
1153
- <div className="font-mono text-[10px] text-muted-foreground">
1154
- {summary.models.length} model{summary.models.length === 1 ? "" : "s"}
1155
- </div>
1156
- </div>
1157
- <div className="max-h-52 overflow-y-auto pr-1">
1158
- {summary.models.map((modelSummary) => (
1159
- <SessionModelContextRow key={modelSummary.model} summary={modelSummary} />
1160
- ))}
1161
- </div>
1162
- </div>
1163
- );
1164
- }
1165
-
1166
- function formatSessionContextOverview(summary: SessionContextSummary): string {
1167
- const riskCount = summary.models.filter((model) => model.latest.health.level === "risk").length;
1168
- if (riskCount > 0) return `${riskCount} risk`;
1169
-
1170
- const optimizableCount = summary.models.filter(
1171
- (model) => model.latest.health.level === "optimizable",
1172
- ).length;
1173
- if (optimizableCount > 0) return `${optimizableCount} optimizable`;
1174
-
1175
- return `${summary.models.length} model${summary.models.length === 1 ? "" : "s"}`;
1176
- }
1177
-
1178
- function SessionContextBar({
1179
- label,
1180
- logs,
1181
- totalIn,
1182
- totalOut,
1183
- timeDisplayFormat,
1184
- contextSummary,
1185
- showBackLink,
1186
- }: {
1187
- label: string;
1188
- logs: CapturedLog[];
1189
- totalIn: number;
1190
- totalOut: number;
1191
- timeDisplayFormat: TimeDisplayFormat;
1192
- contextSummary: SessionContextSummary;
1193
- showBackLink: boolean;
1194
- }): JSX.Element {
1195
- const [copied, setCopied] = useState(false);
1196
- const [detailsOpen, setDetailsOpen] = useState(false);
1197
- const timeRange = useMemo(
1198
- () => formatTimeRange(logs, timeDisplayFormat),
1199
- [logs, timeDisplayFormat],
1200
- );
1201
- const userAgent = useMemo(() => getFirstUserAgent(logs), [logs]);
1202
- const contextOverview = useMemo(
1203
- () => formatSessionContextOverview(contextSummary),
1204
- [contextSummary],
1205
- );
1206
-
1207
- const handleCopyLink = useCallback(() => {
1208
- void copyTextToClipboard(window.location.href).then((success) => {
1209
- if (!success) return;
1210
- setCopied(true);
1211
- setTimeout(() => setCopied(false), 2000);
1212
- });
1213
- }, []);
1214
-
1215
- return (
1216
- <div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] px-3 py-2 text-xs">
1217
- <div className="flex flex-wrap items-center gap-2">
1218
- {showBackLink && (
1219
- <a
1220
- href={withBasePath("/")}
1221
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground"
1222
- aria-label="Back to all sessions"
1223
- title="Back to all sessions"
1224
- >
1225
- <ArrowLeft className="size-3.5" />
1226
- </a>
1227
- )}
1228
- <div className="min-w-[180px] flex-1">
1229
- <div className="flex min-w-0 items-center gap-2">
1230
- <span className="font-mono font-semibold text-purple-400/90 truncate" title={label}>
1231
- {truncateSessionId(label)}
1232
- </span>
1233
- {userAgent !== null && (
1234
- <span
1235
- className="font-mono text-muted-foreground truncate max-w-[220px]"
1236
- title={userAgent}
1237
- >
1238
- {userAgent}
1239
- </span>
1240
- )}
1241
- </div>
1242
- <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
1243
- <span>
1244
- {logs.length} request{logs.length !== 1 ? "s" : ""}
1245
- </span>
1246
- {timeRange !== null && <span>{timeRange}</span>}
1247
- {(totalIn > 0 || totalOut > 0) && (
1248
- <span className="font-mono">
1249
- {formatTokens(totalIn)} in / {formatTokens(totalOut)} out
1250
- </span>
1251
- )}
1252
- </div>
1253
- </div>
1254
- {contextSummary.models.length > 0 && (
1255
- <button
1256
- type="button"
1257
- onClick={() => setDetailsOpen((value) => !value)}
1258
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md px-2.5 font-mono text-[11px] text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground"
1259
- aria-expanded={detailsOpen}
1260
- title={detailsOpen ? "Hide context details" : "Show context details"}
1261
- >
1262
- <span>{contextOverview}</span>
1263
- {detailsOpen ? (
1264
- <ChevronUp className="size-3.5" />
1265
- ) : (
1266
- <ChevronDown className="size-3.5" />
1267
- )}
1268
- </button>
1269
- )}
1270
- <button
1271
- type="button"
1272
- onClick={handleCopyLink}
1273
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground"
1274
- aria-label={copied ? "Copied session link" : "Copy session link"}
1275
- title={copied ? "Copied session link" : "Copy session link"}
1276
- >
1277
- {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
1278
- </button>
1279
- </div>
1280
- {detailsOpen && <SessionContextSummaryPanel summary={contextSummary} />}
1281
- </div>
1282
- );
1283
- }
1284
-
1285
- export type ProxyViewerProps = {
1286
- logs: CapturedLog[];
1287
- sessions: string[];
1288
- models: string[];
1289
- selectedSession: string;
1290
- selectedModel: string;
1291
- onSessionChange: (session: string) => void;
1292
- onModelChange: (model: string) => void;
1293
- onClearAll: () => void;
1294
- /** Delete persisted storage for the selected session, or all storage from the all-sessions view. */
1295
- onDeleteCurrentView?: () => Promise<void>;
1296
- /** Reload the selected/pinned session from stored logs without requiring a page refresh. */
1297
- onReloadSession?: () => void;
1298
- /** Clear only the logs whose ids are passed. Called by the per-group
1299
- * Clear button on each conversation header. */
1300
- onClearGroup: (ids: number[]) => void;
1301
- /** Surface short-lived action feedback without adding sticky page chrome. */
1302
- onNotify?: (notice: TransientToastInput) => void;
1303
- onImportLogs?: (file: File) => Promise<ImportLogsResponse>;
1304
- viewMode: "simple" | "full";
1305
- captureMode: CaptureMode;
1306
- /** Live strip-Claude-Code-billing-header flag, sourced once at the container. */
1307
- strip: boolean;
1308
- /** Slow-response threshold in seconds. `0` disables the warning indicator. */
1309
- slowResponseThresholdSeconds: number;
1310
- /** Controls whether timestamps render as compact local time or full ISO strings. */
1311
- timeDisplayFormat: TimeDisplayFormat;
1312
- /** True while a pinned session page is waiting for its initial log scan. */
1313
- isLoading?: boolean;
1314
- /** Current truth for the all-sessions live stream. Omitted on stored session pages. */
1315
- liveConnection?: LiveConnectionState;
1316
- /** Retry the live stream immediately without discarding displayed logs. */
1317
- onRetryLiveConnection?: () => void;
1318
- /** Stop automatic live-stream retries while keeping displayed logs available. */
1319
- onStopLiveConnection?: () => void;
1320
- /** Cursor controls for history pages. Undefined keeps the live stream UI compact. */
1321
- pagination?: LogPaginationControls;
1322
- /** Active session log loading progress, shown near load/reload controls. */
1323
- sessionLoadProgress?: SessionLoadProgress;
1324
- /** Recoverable session history load failure; existing logs remain visible. */
1325
- sessionLoadError?: string;
1326
- /** Group/run member metadata attached to the pinned session id. */
1327
- sessionMemberships?: SessionMembershipEvidence[];
1328
- /** Hide the session filter dropdown. Used on `/session/$id` routes where
1329
- * the session is already pinned by the URL and the dropdown would just
1330
- * fight the URL state. */
1331
- hideSessionFilter?: boolean;
1332
- /** Session id pinned by a `/session/$id` route. Enables session-page chrome. */
1333
- pinnedSessionId?: string;
1334
- };
1335
-
1336
- export function ProxyViewer({
1337
- logs,
1338
- sessions,
1339
- selectedSession,
1340
- selectedModel,
1341
- onSessionChange,
1342
- onClearAll,
1343
- onDeleteCurrentView,
1344
- onReloadSession,
1345
- onClearGroup,
1346
- onNotify,
1347
- onImportLogs,
1348
- viewMode,
1349
- captureMode,
1350
- strip,
1351
- slowResponseThresholdSeconds,
1352
- timeDisplayFormat,
1353
- isLoading = false,
1354
- liveConnection,
1355
- onRetryLiveConnection,
1356
- onStopLiveConnection,
1357
- pagination,
1358
- sessionLoadProgress,
1359
- sessionLoadError,
1360
- sessionMemberships = [],
1361
- hideSessionFilter = false,
1362
- pinnedSessionId,
1363
- }: ProxyViewerProps): JSX.Element {
1364
- const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
1365
- const { providers } = useProviders();
1366
- const { uiScalePreference, resolvedUiScale, setUiScalePreference } = useUiScale();
1367
- const [exporting, setExporting] = useState<ExportMode | null>(null);
1368
- const [exportError, setExportError] = useState<string | null>(null);
1369
- const [rawExportDialogOpen, setRawExportDialogOpen] = useState(false);
1370
- const [importing, setImporting] = useState(false);
1371
- const [importStatus, setImportStatus] = useState<string | null>(null);
1372
- const [importError, setImportError] = useState<string | null>(null);
1373
- const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
1374
- const [deletingCurrentView, setDeletingCurrentView] = useState(false);
1375
- const [detailsPanelCollapsed, setDetailsPanelCollapsed] = useState(true);
1376
- const [detailsPanelWidth, setDetailsPanelWidth] = useState(DETAILS_PANEL_DEFAULT_WIDTH);
1377
- const [detailsPanelResizing, setDetailsPanelResizing] = useState(false);
1378
- const [detailsPanelStorageLoaded, setDetailsPanelStorageLoaded] = useState(false);
1379
- const [petVisible, setPetVisible] = useState(true);
1380
- const [petVisibilityStorageLoaded, setPetVisibilityStorageLoaded] = useState(false);
1381
- const [assistantSearchResult, setAssistantSearchResult] = useState<PiAgentSearchResult | null>(
1382
- null,
1383
- );
1384
- const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
1385
- const logListRef = useRef<HTMLDivElement>(null);
1386
- const logListWrapperRef = useRef<HTMLDivElement>(null);
1387
- const importInputRef = useRef<HTMLInputElement>(null);
1388
- const exportAbortRef = useRef<AbortController | null>(null);
1389
- const exportRequestIdRef = useRef(0);
1390
- const detailsPanelResizeRef = useRef<DetailsPanelResizeState | null>(null);
1391
- useKeyboardNavigation(logListRef, logListWrapperRef, { pageWide: true });
1392
- const [connectionEndpoints, setConnectionEndpoints] = useState<CodingAgentEndpoints | null>(null);
1393
-
1394
- useEffect(() => subscribeCodingAgentEndpoints(setConnectionEndpoints), []);
1395
-
1396
- const cancelTransientTasks = useCallback(() => {
1397
- exportRequestIdRef.current += 1;
1398
- exportAbortRef.current?.abort();
1399
- exportAbortRef.current = null;
1400
- setExporting(null);
1401
- setExportError(null);
1402
- }, []);
1403
-
1404
- useEffect(
1405
- () => () => {
1406
- exportRequestIdRef.current += 1;
1407
- exportAbortRef.current?.abort();
1408
- exportAbortRef.current = null;
1409
- },
1410
- [],
1411
- );
1412
-
1413
- useEffect(() => {
1414
- setDetailsPanelCollapsed(readStoredDetailsPanelCollapsed());
1415
- setDetailsPanelWidth(readStoredDetailsPanelWidth());
1416
- setDetailsPanelStorageLoaded(true);
1417
- }, []);
1418
-
1419
- useEffect(() => {
1420
- if (!detailsPanelStorageLoaded) return;
1421
- window.localStorage.setItem(
1422
- DETAILS_PANEL_COLLAPSED_STORAGE_KEY,
1423
- detailsPanelCollapsed ? "1" : "0",
1424
- );
1425
- }, [detailsPanelCollapsed, detailsPanelStorageLoaded]);
1426
-
1427
- useEffect(() => {
1428
- if (!detailsPanelStorageLoaded) return;
1429
- try {
1430
- window.localStorage.setItem(DETAILS_PANEL_WIDTH_STORAGE_KEY, String(detailsPanelWidth));
1431
- } catch {
1432
- // Local storage can be unavailable in hardened browser profiles.
1433
- }
1434
- }, [detailsPanelStorageLoaded, detailsPanelWidth]);
1435
-
1436
- useEffect(() => {
1437
- setPetVisible(readStoredInspectorPetVisible());
1438
- setPetVisibilityStorageLoaded(true);
1439
- }, []);
1440
-
1441
- useEffect(() => {
1442
- if (!petVisibilityStorageLoaded) return;
1443
- try {
1444
- window.localStorage.setItem(INSPECTOR_PET_VISIBLE_STORAGE_KEY, petVisible ? "1" : "0");
1445
- } catch {
1446
- // Local storage can be unavailable in hardened browser profiles.
1447
- }
1448
- }, [petVisibilityStorageLoaded, petVisible]);
1449
-
1450
- useEffect(() => {
1451
- if (pinnedSessionId === undefined) {
1452
- document.title = "Agent Inspector";
1453
- return;
1454
- }
1455
- const requestLabel = logs.length === 1 ? "1 req" : `${logs.length} req`;
1456
- document.title = `${truncateSessionId(pinnedSessionId)} - ${requestLabel} - Agent Inspector`;
1457
- }, [logs.length, pinnedSessionId]);
1458
-
1459
- const handleExport = useCallback(
1460
- async (mode: ExportMode) => {
1461
- exportAbortRef.current?.abort();
1462
- const controller = new AbortController();
1463
- exportAbortRef.current = controller;
1464
- const requestId = exportRequestIdRef.current + 1;
1465
- exportRequestIdRef.current = requestId;
1466
- setExporting(mode);
1467
- setExportError(null);
1468
- try {
1469
- const result = await exportLogsAsZip(logs, mode, { signal: controller.signal });
1470
- if (exportRequestIdRef.current !== requestId) return;
1471
- if (!result.ok) {
1472
- if (result.canceled === true) return;
1473
- if (onNotify !== undefined) {
1474
- onNotify({ message: result.message, tone: "error" });
1475
- } else {
1476
- setExportError(result.message);
1477
- }
1478
- }
1479
- } finally {
1480
- if (exportRequestIdRef.current === requestId) {
1481
- exportAbortRef.current = null;
1482
- setExporting(null);
1483
- }
1484
- }
1485
- },
1486
- [logs, onNotify],
1487
- );
1488
- const handleExportRequest = useCallback(
1489
- (mode: ExportMode) => {
1490
- if (exportRequiresConfirmation(mode)) {
1491
- setRawExportDialogOpen(true);
1492
- return;
1493
- }
1494
- void handleExport(mode);
1495
- },
1496
- [handleExport],
1497
- );
1498
- const handleExportRedactedInstead = useCallback(() => {
1499
- setRawExportDialogOpen(false);
1500
- void handleExport("redacted");
1501
- }, [handleExport]);
1502
- const handleConfirmRawExport = useCallback(() => {
1503
- setRawExportDialogOpen(false);
1504
- void handleExport("raw");
1505
- }, [handleExport]);
1506
- const handleImportClick = useCallback(() => {
1507
- importInputRef.current?.click();
1508
- }, []);
1509
-
1510
- const handleImportFileChange = useCallback(
1511
- (event: ChangeEvent<HTMLInputElement>) => {
1512
- const file = event.currentTarget.files?.[0];
1513
- event.currentTarget.value = "";
1514
- if (file === undefined || onImportLogs === undefined) return;
1515
-
1516
- setImporting(true);
1517
- setImportError(null);
1518
- setImportStatus(null);
1519
- void onImportLogs(file)
1520
- .then((result) => {
1521
- const skipped = result.skipped > 0 ? `, skipped ${String(result.skipped)}` : "";
1522
- const message = `Imported ${String(result.imported)} logs as ${result.sessionId}${skipped}.`;
1523
- if (onNotify !== undefined) {
1524
- onNotify({ message, tone: "success" });
1525
- } else {
1526
- setImportStatus(message);
1527
- }
1528
- })
1529
- .catch((err: unknown) => {
1530
- const message = err instanceof Error ? err.message : "Failed to import logs";
1531
- if (onNotify !== undefined) {
1532
- onNotify({ message, tone: "error" });
1533
- } else {
1534
- setImportError(message);
1535
- }
1536
- })
1537
- .finally(() => {
1538
- setImporting(false);
1539
- });
1540
- },
1541
- [onImportLogs, onNotify],
1542
- );
1543
- // Close the compare drawer when the user changes the session or model
1544
- // filter, since the predecessor relationship may no longer be meaningful.
1545
- useEffect(() => {
1546
- cancelTransientTasks();
1547
- setComparePair(null);
1548
- setAssistantSearchResult(null);
1549
- }, [cancelTransientTasks, selectedSession, selectedModel]);
1550
-
1551
- const handleClearCurrentView = useCallback(() => {
1552
- cancelTransientTasks();
1553
- setImportStatus(null);
1554
- setImportError(null);
1555
- setComparePair(null);
1556
- onClearAll();
1557
- }, [cancelTransientTasks, onClearAll]);
1558
-
1559
- const handleRequestDeleteCurrentView = useCallback(() => {
1560
- setDeleteDialogOpen(true);
1561
- }, []);
1562
-
1563
- const handleConfirmDeleteCurrentView = useCallback(() => {
1564
- cancelTransientTasks();
1565
- setImportStatus(null);
1566
- setImportError(null);
1567
- setComparePair(null);
1568
-
1569
- if (onDeleteCurrentView === undefined) return;
1570
-
1571
- setDeletingCurrentView(true);
1572
- void onDeleteCurrentView()
1573
- .then(() => {
1574
- setDeleteDialogOpen(false);
1575
- })
1576
- .catch((err: unknown) => {
1577
- const message = err instanceof Error ? err.message : "Failed to delete stored logs";
1578
- if (onNotify !== undefined) {
1579
- onNotify({ message, tone: "error" });
1580
- } else {
1581
- setImportError(message);
1582
- }
1583
- })
1584
- .finally(() => {
1585
- setDeletingCurrentView(false);
1586
- });
1587
- }, [cancelTransientTasks, onDeleteCurrentView, onNotify]);
1588
-
1589
- const handleReloadCurrentSession = useCallback(() => {
1590
- if (onReloadSession === undefined) return;
1591
- cancelTransientTasks();
1592
- setImportStatus(null);
1593
- setImportError(null);
1594
- setComparePair(null);
1595
- onReloadSession();
1596
- }, [cancelTransientTasks, onReloadSession]);
1597
-
1598
- const closeCompare = useCallback(() => {
1599
- setComparePair(null);
1600
- }, []);
1601
-
1602
- const toggleDetailsPanelCollapsed = useCallback(() => {
1603
- setDetailsPanelCollapsed((value) => !value);
1604
- }, []);
1605
- const handleDetailsPanelResizeStart = useCallback(
1606
- (event: ReactPointerEvent<HTMLButtonElement>) => {
1607
- detailsPanelResizeRef.current = {
1608
- startX: event.clientX,
1609
- startWidth: detailsPanelWidth,
1610
- pointerId: event.pointerId,
1611
- };
1612
- event.currentTarget.setPointerCapture(event.pointerId);
1613
- setDetailsPanelResizing(true);
1614
- },
1615
- [detailsPanelWidth],
1616
- );
1617
- const handleDetailsPanelResizeMove = useCallback(
1618
- (event: ReactPointerEvent<HTMLButtonElement>) => {
1619
- const resize = detailsPanelResizeRef.current;
1620
- if (resize === null || resize.pointerId !== event.pointerId) return;
1621
- setDetailsPanelWidth(
1622
- clampDetailsPanelWidth(resize.startWidth + resize.startX - event.clientX),
1623
- );
1624
- },
1625
- [],
1626
- );
1627
- const handleDetailsPanelResizeEnd = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
1628
- const resize = detailsPanelResizeRef.current;
1629
- if (resize === null || resize.pointerId !== event.pointerId) return;
1630
- if (event.currentTarget.hasPointerCapture(event.pointerId)) {
1631
- event.currentTarget.releasePointerCapture(event.pointerId);
1632
- }
1633
- detailsPanelResizeRef.current = null;
1634
- setDetailsPanelResizing(false);
1635
- }, []);
1636
- const resetDetailsPanelWidth = useCallback(() => {
1637
- setDetailsPanelWidth(DETAILS_PANEL_DEFAULT_WIDTH);
1638
- }, []);
1639
- const togglePetVisibility = useCallback(() => {
1640
- setPetVisible((value) => !value);
1641
- }, []);
1642
-
1643
- const assistantResultLogIds = useMemo(
1644
- () => (assistantSearchResult === null ? null : new Set<number>(assistantSearchResult.logIds)),
1645
- [assistantSearchResult],
1646
- );
1647
- const displayedLogs = useMemo(
1648
- () =>
1649
- assistantResultLogIds === null
1650
- ? logs
1651
- : logs.filter((log) => assistantResultLogIds.has(log.id)),
1652
- [assistantResultLogIds, logs],
1653
- );
1654
- const groups = useMemo(() => groupLogsByConversation(displayedLogs), [displayedLogs]);
1655
- const displayNumberByLogId = useMemo(() => buildDisplayNumberByLogId(groups), [groups]);
1656
- const sessionContextScope = useMemo(
1657
- () => resolveSessionContextScope({ pinnedSessionId, selectedSession, sessions, logs }),
1658
- [logs, pinnedSessionId, selectedSession, sessions],
1659
- );
1660
- const sessionContextLogs = sessionContextScope?.logs ?? [];
1661
- const hasSessionContext = sessionContextScope !== null && sessionContextLogs.length > 0;
1662
- const { totalIn: sessionContextTotalIn, totalOut: sessionContextTotalOut } = useMemo(
1663
- () => computeTokenSummary(sessionContextLogs),
1664
- [sessionContextLogs],
1665
- );
1666
- const sessionContextSummary = useMemo(
1667
- () => buildSessionContextSummary(sessionContextLogs, providers),
1668
- [sessionContextLogs, providers],
1669
- );
1670
- const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
1671
- const comparisonPredecessors = useMemo(() => buildValidPredecessors(groups), [groups]);
1672
- const exportActionVisibility = getExportActionVisibility(captureMode, logs.length);
1673
- const isPinnedSessionPage = pinnedSessionId !== undefined;
1674
- const canClearCurrentView = logs.length > 0;
1675
- const canDeleteCurrentView =
1676
- onDeleteCurrentView !== undefined && (logs.length > 0 || selectedSession !== "__all__");
1677
- const deleteTargetLabel = selectedSession === "__all__" ? "all sessions" : "this session";
1678
- const deleteTargetDescription =
1679
- selectedSession === "__all__"
1680
- ? "Deletes every stored log, streaming chunk, and session archive entry across all sessions."
1681
- : "Deletes every stored log, streaming chunk, and session archive entry for this session, including history that is not currently loaded.";
1682
- const reloadSessionLabel = logs.length === 0 ? "Load" : "Reload";
1683
- const activeScopeLabel =
1684
- hasSessionContext && sessionContextScope !== null
1685
- ? truncateSessionId(sessionContextScope.label)
1686
- : selectedSession === "__all__"
1687
- ? "All sessions"
1688
- : truncateSessionId(selectedSession);
1689
- const liveEmptyState = resolveLiveEmptyState(liveConnection);
1690
- const liveEmptyStateCopy = getLiveEmptyStateCopy(liveEmptyState);
1691
- const slateStats = useMemo(
1692
- () =>
1693
- buildSessionSlateStats({
1694
- logs: displayedLogs,
1695
- conversationGroups: groups,
1696
- isLoading,
1697
- timeDisplayFormat,
1698
- slowResponseThresholdSeconds,
1699
- }),
1700
- [displayedLogs, groups, isLoading, slowResponseThresholdSeconds, timeDisplayFormat],
1701
- );
1702
- const handleCompareWithPrevious = useCallback(
1703
- (log: CapturedLog) => {
1704
- const predecessor = comparisonPredecessors.get(log.id);
1705
- if (predecessor !== undefined) setComparePair([predecessor, log]);
1706
- },
1707
- [comparisonPredecessors],
1708
- );
1709
- const labSessionId =
1710
- pinnedSessionId !== undefined
1711
- ? pinnedSessionId
1712
- : selectedSession === "__all__"
1713
- ? null
1714
- : selectedSession;
1715
-
1716
- return (
1717
- <div className="bg-background min-h-screen w-full overflow-x-clip text-foreground lg:h-screen lg:flex lg:flex-col lg:overflow-hidden">
1718
- <ApplicationBar
1719
- liveConnection={liveConnection}
1720
- alertsAction={<AlertsDialog />}
1721
- settingsAction={<SettingsDialog />}
1722
- />
1723
- <button
1724
- type="button"
1725
- onClick={toggleDetailsPanelCollapsed}
1726
- className={cn(
1727
- "group fixed top-1/2 right-3 z-50 inline-flex h-14 w-8 -translate-y-1/2 items-center justify-center rounded-l-md border border-border/70 bg-background/90 text-muted-foreground shadow-lg backdrop-blur transition-colors motion-reduce:transition-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
1728
- )}
1729
- aria-label={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
1730
- title={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
1731
- >
1732
- <span className="absolute bottom-2 left-1 top-2 w-px rounded-full bg-border transition-colors group-hover:bg-foreground/35" />
1733
- {detailsPanelCollapsed ? (
1734
- <PanelRightOpen className="size-3.5" />
1735
- ) : (
1736
- <PanelRightClose className="size-3.5" />
1737
- )}
1738
- </button>
1739
- {petVisibilityStorageLoaded && petVisible && (
1740
- <LazyFeatureBoundary feature="Inspector Pet">
1741
- <Suspense fallback={null}>
1742
- <LazyInspectorPet logs={logs} />
1743
- </Suspense>
1744
- </LazyFeatureBoundary>
1745
- )}
1746
- <div
1747
- className={cn(
1748
- "flex w-full min-w-0 flex-col lg:min-h-0 lg:flex-1 lg:flex-row lg:overflow-hidden",
1749
- detailsPanelResizing && "cursor-col-resize select-none",
1750
- )}
1751
- >
1752
- {!detailsPanelCollapsed && (
1753
- <>
1754
- <button
1755
- type="button"
1756
- className="fixed inset-0 z-30 bg-background/70 backdrop-blur-sm lg:hidden"
1757
- onClick={toggleDetailsPanelCollapsed}
1758
- aria-label="Close details panel"
1759
- />
1760
- <aside
1761
- className="inspector-scrollbar fixed inset-x-3 bottom-3 top-24 z-40 order-1 overflow-y-auto rounded-lg border border-border bg-background/98 shadow-2xl max-lg:!w-auto lg:relative lg:inset-auto lg:order-2 lg:h-full lg:shrink-0 lg:rounded-none lg:border-y-0 lg:border-r-0 lg:shadow-none"
1762
- style={{ width: detailsPanelWidth }}
1763
- >
1764
- <button
1765
- type="button"
1766
- className="absolute bottom-0 left-0 top-0 z-30 hidden w-2 -translate-x-1/2 cursor-col-resize touch-none items-center justify-center lg:flex"
1767
- onPointerDown={handleDetailsPanelResizeStart}
1768
- onPointerMove={handleDetailsPanelResizeMove}
1769
- onPointerUp={handleDetailsPanelResizeEnd}
1770
- onPointerCancel={handleDetailsPanelResizeEnd}
1771
- onDoubleClick={resetDetailsPanelWidth}
1772
- aria-label="Resize details panel"
1773
- title="Drag to resize; double-click to reset"
1774
- >
1775
- <span className="h-12 w-1 rounded-full bg-border transition-colors hover:bg-primary" />
1776
- </button>
1777
- <div className="flex min-h-full flex-col gap-4 p-4">
1778
- <div className="sticky top-0 z-20 -mx-4 -mt-4 grid gap-2 border-b border-border/60 bg-background/95 px-4 pb-3 pt-4 backdrop-blur">
1779
- <SidebarPanel label="View">
1780
- <div className="flex flex-wrap items-center gap-1.5">
1781
- <ThemeModeToggle />
1782
- <UiScaleControl
1783
- preference={uiScalePreference}
1784
- resolvedScale={resolvedUiScale}
1785
- onChange={setUiScalePreference}
1786
- />
1787
- </div>
1788
- </SidebarPanel>
1789
- <SidebarPanel label="Workspace">
1790
- <div className="flex flex-wrap items-center gap-2">
1791
- <GroupsDialog labelledTrigger />
1792
- <AgentLabDialog
1793
- currentSessionId={labSessionId}
1794
- logCount={logs.length}
1795
- labelledTrigger
1796
- />
1797
- </div>
1798
- </SidebarPanel>
1799
- </div>
1800
-
1801
- <SidebarPanel label="Current scope">
1802
- <div className="mb-2 rounded-md border border-border/50 bg-muted/25 px-3 py-2">
1803
- <div
1804
- className="truncate font-mono text-xs font-semibold text-text-link"
1805
- title={activeScopeLabel}
1806
- >
1807
- {activeScopeLabel}
1808
- </div>
1809
- <div className="mt-1 text-[11px] text-muted-foreground">
1810
- {isPinnedSessionPage ? "Pinned session" : "Live Inspector data"}
1811
- </div>
1812
- </div>
1813
- <div className="grid grid-cols-2 gap-2">
1814
- <SidebarMetric
1815
- label={assistantSearchResult === null ? "Requests" : "Results"}
1816
- value={displayedLogs.length}
1817
- />
1818
- <SidebarMetric label="Sessions" value={sessions.length} />
1819
- <SidebarMetric
1820
- label="Input"
1821
- value={formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
1822
- />
1823
- <SidebarMetric
1824
- label="Output"
1825
- value={formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
1826
- />
1827
- </div>
1828
- {sessionLoadProgress !== undefined && (
1829
- <div className="mt-2 rounded-md border border-border/50 bg-muted/25 px-2 py-2">
1830
- <SessionLoadProgressBar progress={sessionLoadProgress} />
1831
- </div>
1832
- )}
1833
- </SidebarPanel>
1834
-
1835
- {hasSessionContext && sessionContextScope !== null && (
1836
- <SessionContextBar
1837
- label={sessionContextScope.label}
1838
- logs={sessionContextLogs}
1839
- totalIn={sessionContextTotalIn}
1840
- totalOut={sessionContextTotalOut}
1841
- timeDisplayFormat={timeDisplayFormat}
1842
- contextSummary={sessionContextSummary}
1843
- showBackLink={sessionContextScope.showBackLink}
1844
- />
1845
- )}
1846
-
1847
- {hasSessionContext &&
1848
- pinnedSessionId !== undefined &&
1849
- sessionMemberships.length > 0 && (
1850
- <SessionMembershipPanel memberships={sessionMemberships} />
1851
- )}
1852
-
1853
- <div className="flex-1" />
1854
-
1855
- <PiAgentPanel
1856
- currentSessionId={labSessionId}
1857
- logCount={logs.length}
1858
- onResultChange={setAssistantSearchResult}
1859
- />
1860
- </div>
1861
- </aside>
1862
- </>
1863
- )}
1864
-
1865
- <main className="inspector-scrollbar order-2 min-w-0 lg:order-1 lg:h-full lg:flex-1 lg:overflow-y-auto">
1866
- <div className="bg-background/95 shadow-[0_12px_36px_rgba(0,0,0,0.18),inset_0_-1px_0_rgba(255,255,255,0.045)] sticky top-0 z-30 px-4 py-3 lg:px-5">
1867
- <div className="flex flex-wrap items-center gap-2">
1868
- <div className="min-w-[180px] flex-1">
1869
- <div className="text-sm font-semibold text-foreground">Logs</div>
1870
- <div className="mt-0.5 font-mono text-[11px] text-muted-foreground">
1871
- {activeScopeLabel}
1872
- </div>
1873
- </div>
1874
- <PetVisibilityToggle visible={petVisible} onToggle={togglePetVisibility} />
1875
- {onImportLogs !== undefined && (
1876
- <>
1877
- <input
1878
- ref={importInputRef}
1879
- type="file"
1880
- accept=".zip,.json,.jsonl,application/zip,application/json"
1881
- className="hidden"
1882
- onChange={handleImportFileChange}
1883
- />
1884
- <button
1885
- type="button"
1886
- onClick={handleImportClick}
1887
- disabled={importing}
1888
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1889
- title="Import Agent Inspector export"
1890
- >
1891
- {importing ? (
1892
- <span>Importing...</span>
1893
- ) : (
1894
- <>
1895
- <Upload className="size-3.5" />
1896
- <span>Import</span>
1897
- </>
1898
- )}
1899
- </button>
1900
- </>
1901
- )}
1902
- {exportActionVisibility.redacted && (
1903
- <button
1904
- type="button"
1905
- onClick={() => {
1906
- handleExportRequest("redacted");
1907
- }}
1908
- disabled={exporting !== null}
1909
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1910
- title="Export redacted logs as JSON ZIP"
1911
- >
1912
- {exporting === "redacted" ? (
1913
- <span>Exporting...</span>
1914
- ) : (
1915
- <>
1916
- <Download className="size-3.5" />
1917
- <span>Export</span>
1918
- </>
1919
- )}
1920
- </button>
1921
- )}
1922
- {exportActionVisibility.raw && (
1923
- <button
1924
- type="button"
1925
- onClick={() => {
1926
- handleExportRequest("raw");
1927
- }}
1928
- disabled={exporting !== null}
1929
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1930
- title="Export raw logs without redaction"
1931
- >
1932
- {exporting === "raw" ? (
1933
- <span>Exporting...</span>
1934
- ) : (
1935
- <>
1936
- <FileJson className="size-3.5" />
1937
- <span>Raw</span>
1938
- </>
1939
- )}
1940
- </button>
1941
- )}
1942
- {onReloadSession !== undefined && (
1943
- <button
1944
- type="button"
1945
- onClick={handleReloadCurrentSession}
1946
- disabled={isLoading}
1947
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1948
- title="Reload this session from stored logs"
1949
- >
1950
- <RefreshCw
1951
- className={cn(
1952
- "size-3.5",
1953
- isLoading && "animate-spin motion-reduce:animate-none",
1954
- )}
1955
- />
1956
- <span>{reloadSessionLabel}</span>
1957
- </button>
1958
- )}
1959
- <button
1960
- type="button"
1961
- onClick={handleClearCurrentView}
1962
- disabled={!canClearCurrentView}
1963
- className="bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1964
- title="Clear the current view. Stored logs remain reloadable."
1965
- >
1966
- <Trash2 className="size-3.5" />
1967
- <span>Clear</span>
1968
- </button>
1969
- {onDeleteCurrentView !== undefined && (
1970
- <button
1971
- type="button"
1972
- onClick={handleRequestDeleteCurrentView}
1973
- disabled={!canDeleteCurrentView || deletingCurrentView}
1974
- className="bg-red-500/[0.07] hover:bg-red-500/[0.12] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-red-200 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.14)] transition-colors hover:text-red-100 focus-visible:ring-1 focus-visible:ring-red-300 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1975
- title="Delete persisted history for the selected session."
1976
- >
1977
- <Trash2 className="size-3.5" />
1978
- <span>Delete</span>
1979
- </button>
1980
- )}
1981
- </div>
1982
- {pagination === undefined && sessionLoadProgress !== undefined && (
1983
- <div className="mt-2 rounded-md bg-black/20 px-2 py-2 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
1984
- <SessionLoadProgressBar progress={sessionLoadProgress} />
1985
- </div>
1986
- )}
1987
- {sessionLoadError !== undefined && (
1988
- <div
1989
- role="alert"
1990
- className="mt-2 flex flex-wrap items-center gap-2 rounded-md border border-red-400/25 bg-red-500/10 px-3 py-2 text-xs text-red-100"
1991
- >
1992
- <span className="min-w-0 flex-1">
1993
- Session logs could not be loaded. {sessionLoadError} Existing logs remain visible.
1994
- </span>
1995
- {onReloadSession !== undefined && (
1996
- <button
1997
- type="button"
1998
- onClick={handleReloadCurrentSession}
1999
- 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"
2000
- >
2001
- <RefreshCw className="size-3" />
2002
- Retry
2003
- </button>
2004
- )}
2005
- </div>
2006
- )}
2007
- {assistantSearchResult !== null && (
2008
- <div className="mt-2 flex items-center gap-3 rounded-md bg-cyan-400/[0.07] px-3 py-2 shadow-[inset_2px_0_0_rgba(103,232,249,0.35),inset_0_0_0_1px_rgba(103,232,249,0.08)]">
2009
- <div className="min-w-0 flex-1">
2010
- <div className="truncate text-xs font-medium text-cyan-50">
2011
- {assistantSearchResult.question}
2012
- </div>
2013
- <div className="mt-0.5 text-[10px] text-muted-foreground">
2014
- Showing {String(displayedLogs.length)} loaded logs from{" "}
2015
- {String(assistantSearchResult.logIds.length)} linked results
2016
- </div>
2017
- </div>
2018
- <button
2019
- type="button"
2020
- onClick={() => setAssistantSearchResult(null)}
2021
- className="inline-flex h-7 shrink-0 items-center gap-1 rounded-md bg-black/20 px-2 text-[11px] text-muted-foreground transition-colors hover:bg-black/30 hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
2022
- >
2023
- <X className="size-3" />
2024
- All logs
2025
- </button>
2026
- </div>
2027
- )}
2028
- {assistantSearchResult === null && (
2029
- <LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
2030
- )}
2031
- {onNotify === undefined && exportError !== null && (
2032
- <div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
2033
- {exportError}
2034
- </div>
2035
- )}
2036
- {onNotify === undefined && importError !== null && (
2037
- <div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
2038
- {importError}
2039
- </div>
2040
- )}
2041
- {onNotify === undefined && importStatus !== null && importError === null && (
2042
- <div className="mt-2 rounded-md border border-cyan-400/25 bg-cyan-500/10 px-3 py-2 text-xs text-cyan-100">
2043
- {importStatus}
2044
- </div>
2045
- )}
2046
- {liveConnection !== undefined &&
2047
- liveConnection.status !== "live" &&
2048
- onRetryLiveConnection !== undefined &&
2049
- onStopLiveConnection !== undefined && (
2050
- <LiveConnectionStatus
2051
- state={liveConnection}
2052
- hasLogs={logs.length > 0}
2053
- onRetry={onRetryLiveConnection}
2054
- onStop={onStopLiveConnection}
2055
- />
2056
- )}
2057
- </div>
2058
-
2059
- <div className="px-4 py-4 lg:px-5">
2060
- <SessionOpeningSlate stats={slateStats} />
2061
- {assistantSearchResult !== null && displayedLogs.length === 0 ? (
2062
- <div className="py-16 text-center text-muted-foreground">
2063
- <div className="mx-auto max-w-xl rounded-[8px] bg-white/[0.035] px-5 py-8 shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)]">
2064
- <p className="text-sm font-medium text-foreground">No linked logs are loaded</p>
2065
- <p className="mt-2 text-xs leading-5 text-muted-foreground">
2066
- Assistant found evidence, but none of its linked log IDs are in the current
2067
- loaded window. Open an evidence card in Assistant or restore the full log view.
2068
- </p>
2069
- <button
2070
- type="button"
2071
- onClick={() => setAssistantSearchResult(null)}
2072
- className="mt-4 inline-flex h-8 items-center rounded-md bg-cyan-400/10 px-3 text-xs text-cyan-100 transition-colors hover:bg-cyan-400/16"
2073
- >
2074
- Show all logs
2075
- </button>
2076
- </div>
2077
- </div>
2078
- ) : logs.length === 0 ? (
2079
- selectedSession !== "__all__" ? (
2080
- <div className="py-16 text-center text-muted-foreground">
2081
- <div className="bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)] mx-auto max-w-xl space-y-4 rounded-[8px] px-4 py-8">
2082
- <p className="text-sm font-medium">
2083
- {isLoading
2084
- ? "Loading session data..."
2085
- : sessionLoadError === undefined
2086
- ? "No logs loaded for this session"
2087
- : "Session logs could not be loaded"}
2088
- </p>
2089
- <p className="inline-block max-w-full rounded bg-muted px-3 py-1 font-mono text-xs break-all">
2090
- {truncateSessionId(selectedSession)}
2091
- </p>
2092
- {sessionLoadProgress !== undefined && (
2093
- <div className="mx-auto max-w-md rounded-md bg-black/20 px-2 py-2 text-left shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
2094
- <SessionLoadProgressBar progress={sessionLoadProgress} />
2095
- </div>
2096
- )}
2097
- {sessionMemberships.length > 0 && (
2098
- <div className="text-left">
2099
- <SessionMembershipPanel memberships={sessionMemberships} />
2100
- </div>
2101
- )}
2102
- {!isLoading && (
2103
- <div className="flex flex-col items-center gap-3 text-xs">
2104
- <p>Stored logs may still be available.</p>
2105
- <div className="flex flex-wrap items-center justify-center gap-2">
2106
- {onReloadSession !== undefined && (
2107
- <button
2108
- type="button"
2109
- onClick={handleReloadCurrentSession}
2110
- className="bg-white/[0.045] hover:bg-white/[0.075] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
2111
- >
2112
- <RefreshCw className="size-3.5" />
2113
- <span>Load session</span>
2114
- </button>
2115
- )}
2116
- {hideSessionFilter ? (
2117
- <a
2118
- href={withBasePath("/")}
2119
- className="inline-flex h-8 items-center rounded-md px-3 text-xs text-muted-foreground underline transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
2120
- >
2121
- Back to all sessions
2122
- </a>
2123
- ) : (
2124
- <button
2125
- type="button"
2126
- onClick={() => onSessionChange("__all__")}
2127
- className="inline-flex h-8 items-center rounded-md px-3 text-xs text-muted-foreground underline transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
2128
- >
2129
- Show all sessions
2130
- </button>
2131
- )}
2132
- </div>
2133
- </div>
2134
- )}
2135
- </div>
2136
- </div>
2137
- ) : (
2138
- <div className="py-16 text-center text-muted-foreground">
2139
- <div className="bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)] mx-auto max-w-2xl space-y-4 rounded-[8px] px-4 py-8">
2140
- <p className="text-sm font-medium">{liveEmptyStateCopy.title}</p>
2141
- <p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
2142
- {liveEmptyStateCopy.description}
2143
- </p>
2144
- {liveEmptyState === "empty" && sessions.length > 0 && (
2145
- <p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
2146
- Pick a session from the filter panel to load stored logs on demand.
2147
- </p>
2148
- )}
2149
- {liveEmptyStateCopy.showConnectionCommands && connectionEndpoints !== null && (
2150
- <>
2151
- <div className="flex flex-col items-center gap-2">
2152
- {buildHomeConnectionCommands(connectionEndpoints).map((command) => (
2153
- <CopyableCommand key={command} command={command} />
2154
- ))}
2155
- </div>
2156
- <p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
2157
- Container note: if your AI tool runs in a different container or host than
2158
- Agent Inspector, replace localhost with an address reachable from that
2159
- tool and verify the network/firewall path.
2160
- </p>
2161
- </>
2162
- )}
2163
- </div>
2164
- </div>
2165
- )
2166
- ) : (
2167
- <div
2168
- ref={logListWrapperRef}
2169
- tabIndex={0}
2170
- aria-label="Captured logs"
2171
- className="flex flex-col gap-2 rounded-md focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none"
2172
- >
2173
- <div ref={logListRef}>
2174
- <ConversationGroupList
2175
- groups={groups}
2176
- viewMode={viewMode}
2177
- strip={strip}
2178
- slowResponseThresholdSeconds={slowResponseThresholdSeconds}
2179
- cacheTrends={cacheTrends}
2180
- onCompareWithPrevious={handleCompareWithPrevious}
2181
- comparisonPredecessors={comparisonPredecessors}
2182
- onClearGroup={onClearGroup}
2183
- defaultExpanded={groups.length === 1}
2184
- timeDisplayFormat={timeDisplayFormat}
2185
- />
2186
- </div>
2187
- {assistantSearchResult === null && (
2188
- <div className="mt-3">
2189
- <LogPaginationBar logs={logs} pagination={pagination} />
2190
- </div>
2191
- )}
2192
- </div>
2193
- )}
2194
- </div>
2195
- </main>
2196
- </div>
2197
-
2198
- <Dialog open={rawExportDialogOpen} onOpenChange={setRawExportDialogOpen}>
2199
- <DialogContent className={RAW_EXPORT_DIALOG_CLASS_NAME}>
2200
- <RawExportWarning
2201
- onCancel={() => setRawExportDialogOpen(false)}
2202
- onExportRedacted={handleExportRedactedInstead}
2203
- onConfirmRaw={handleConfirmRawExport}
2204
- />
2205
- </DialogContent>
2206
- </Dialog>
2207
-
2208
- <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
2209
- <DialogContent className="border-red-500/20 bg-zinc-950 text-zinc-100 sm:max-w-md">
2210
- <DialogHeader>
2211
- <DialogTitle>Delete {deleteTargetLabel} history?</DialogTitle>
2212
- <DialogDescription>
2213
- Clear only hides logs from the UI. Delete removes stored history from disk.
2214
- </DialogDescription>
2215
- </DialogHeader>
2216
- <div className="rounded-md bg-red-500/[0.08] px-3 py-3 text-sm text-red-100 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.16)]">
2217
- <span className="block font-medium">This is a full history delete.</span>
2218
- <span className="mt-1 block text-xs text-red-200/80">{deleteTargetDescription}</span>
2219
- </div>
2220
- <div className="flex justify-end gap-2">
2221
- <Button
2222
- type="button"
2223
- variant="outline"
2224
- size="sm"
2225
- disabled={deletingCurrentView}
2226
- onClick={() => setDeleteDialogOpen(false)}
2227
- >
2228
- Cancel
2229
- </Button>
2230
- <Button
2231
- type="button"
2232
- variant="destructive"
2233
- size="sm"
2234
- disabled={deletingCurrentView}
2235
- onClick={handleConfirmDeleteCurrentView}
2236
- >
2237
- {deletingCurrentView ? "Deleting..." : "Delete history"}
2238
- </Button>
2239
- </div>
2240
- </DialogContent>
2241
- </Dialog>
2242
-
2243
- {/* Compare drawer: sibling of the log list, not a route change. */}
2244
- {comparePair !== null && (
2245
- <LazyFeatureBoundary feature="Compare">
2246
- <Suspense fallback={null}>
2247
- <LazyCompareDrawer
2248
- left={comparePair[0]}
2249
- right={comparePair[1]}
2250
- leftDisplayNumber={displayNumberForLog(comparePair[0], displayNumberByLogId)}
2251
- rightDisplayNumber={displayNumberForLog(comparePair[1], displayNumberByLogId)}
2252
- onClose={closeCompare}
2253
- />
2254
- </Suspense>
2255
- </LazyFeatureBoundary>
2256
- )}
2257
- </div>
2258
- );
2259
- }