@tonyclaw/agent-inspector 2.0.0

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 (400) hide show
  1. package/.output/cli.js +1611 -0
  2. package/.output/nitro.json +17 -0
  3. package/.output/public/assets/CompareDrawer-CU5ZrWcL.js +1 -0
  4. package/.output/public/assets/ProxyViewerContainer-pEBqVp1d.js +101 -0
  5. package/.output/public/assets/ReplayDialog-F58yNg5j.js +1 -0
  6. package/.output/public/assets/RequestAnatomy-C9lT0qE_.js +1 -0
  7. package/.output/public/assets/ResponseView-DHJq6bnz.js +1 -0
  8. package/.output/public/assets/StreamingChunkSequence-BTgfpFUT.js +1 -0
  9. package/.output/public/assets/_sessionId-DsNRbnNm.js +1 -0
  10. package/.output/public/assets/alibaba-TTwafVwX.svg +1 -0
  11. package/.output/public/assets/index-CpWG2hFn.css +1 -0
  12. package/.output/public/assets/index-DmBV8Gve.js +1 -0
  13. package/.output/public/assets/json-viewer-CZVYLR8j.js +14 -0
  14. package/.output/public/assets/main-DHs7FBK3.js +18 -0
  15. package/.output/public/assets/minimax-BPMzvuL-.jpeg +0 -0
  16. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  17. package/.output/public/assets/zhipuai-BPNAnxo-.svg +219 -0
  18. package/.output/server/_chunks/ssr-renderer.mjs +22 -0
  19. package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +1 -0
  20. package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +210 -0
  21. package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +2 -0
  22. package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +2 -0
  23. package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +2 -0
  24. package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +11 -0
  25. package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +69 -0
  26. package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +1 -0
  27. package/.output/server/_libs/@radix-ui/react-use-escape-keydown+[...].mjs +17 -0
  28. package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +1 -0
  29. package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +6 -0
  30. package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +34 -0
  31. package/.output/server/_libs/ajv-formats.mjs +330 -0
  32. package/.output/server/_libs/ajv.mjs +11444 -0
  33. package/.output/server/_libs/aria-hidden.mjs +122 -0
  34. package/.output/server/_libs/atomically.mjs +152 -0
  35. package/.output/server/_libs/bail.mjs +8 -0
  36. package/.output/server/_libs/cfworker__json-schema.mjs +1 -0
  37. package/.output/server/_libs/character-entities.mjs +2130 -0
  38. package/.output/server/_libs/class-variance-authority.mjs +44 -0
  39. package/.output/server/_libs/clsx.mjs +16 -0
  40. package/.output/server/_libs/comma-separated-tokens.mjs +10 -0
  41. package/.output/server/_libs/conf.mjs +635 -0
  42. package/.output/server/_libs/cookie-es.mjs +58 -0
  43. package/.output/server/_libs/core-util-is.mjs +75 -0
  44. package/.output/server/_libs/croner.mjs +1 -0
  45. package/.output/server/_libs/crossws.mjs +1 -0
  46. package/.output/server/_libs/debounce-fn.mjs +69 -0
  47. package/.output/server/_libs/decode-named-character-reference+[...].mjs +8 -0
  48. package/.output/server/_libs/dequal.mjs +27 -0
  49. package/.output/server/_libs/detect-node-es.mjs +1 -0
  50. package/.output/server/_libs/devlop.mjs +8 -0
  51. package/.output/server/_libs/diff.mjs +320 -0
  52. package/.output/server/_libs/dot-prop.mjs +265 -0
  53. package/.output/server/_libs/env-paths.mjs +57 -0
  54. package/.output/server/_libs/estree-util-is-identifier-name.mjs +11 -0
  55. package/.output/server/_libs/extend.mjs +97 -0
  56. package/.output/server/_libs/fast-deep-equal.mjs +38 -0
  57. package/.output/server/_libs/fast-uri.mjs +812 -0
  58. package/.output/server/_libs/floating-ui__core.mjs +725 -0
  59. package/.output/server/_libs/floating-ui__dom.mjs +622 -0
  60. package/.output/server/_libs/floating-ui__react-dom.mjs +292 -0
  61. package/.output/server/_libs/floating-ui__utils.mjs +320 -0
  62. package/.output/server/_libs/get-nonce.mjs +9 -0
  63. package/.output/server/_libs/h3-v2.mjs +276 -0
  64. package/.output/server/_libs/h3.mjs +408 -0
  65. package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +388 -0
  66. package/.output/server/_libs/hast-util-whitespace.mjs +10 -0
  67. package/.output/server/_libs/hookable.mjs +1 -0
  68. package/.output/server/_libs/html-url-attributes.mjs +26 -0
  69. package/.output/server/_libs/immediate.mjs +74 -0
  70. package/.output/server/_libs/inherits.mjs +50 -0
  71. package/.output/server/_libs/inline-style-parser.mjs +142 -0
  72. package/.output/server/_libs/is-plain-obj.mjs +10 -0
  73. package/.output/server/_libs/isarray.mjs +14 -0
  74. package/.output/server/_libs/isbot.mjs +20 -0
  75. package/.output/server/_libs/json-schema-traverse.mjs +180 -0
  76. package/.output/server/_libs/jszip.mjs +3051 -0
  77. package/.output/server/_libs/lie.mjs +273 -0
  78. package/.output/server/_libs/lucide-react.mjs +492 -0
  79. package/.output/server/_libs/mdast-util-from-markdown.mjs +717 -0
  80. package/.output/server/_libs/mdast-util-to-hast.mjs +710 -0
  81. package/.output/server/_libs/mdast-util-to-string.mjs +38 -0
  82. package/.output/server/_libs/micromark-core-commonmark.mjs +2259 -0
  83. package/.output/server/_libs/micromark-factory-destination.mjs +94 -0
  84. package/.output/server/_libs/micromark-factory-label.mjs +63 -0
  85. package/.output/server/_libs/micromark-factory-space.mjs +24 -0
  86. package/.output/server/_libs/micromark-factory-title.mjs +65 -0
  87. package/.output/server/_libs/micromark-factory-whitespace.mjs +22 -0
  88. package/.output/server/_libs/micromark-util-character.mjs +44 -0
  89. package/.output/server/_libs/micromark-util-chunked.mjs +36 -0
  90. package/.output/server/_libs/micromark-util-classify-character+[...].mjs +12 -0
  91. package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +41 -0
  92. package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +19 -0
  93. package/.output/server/_libs/micromark-util-decode-string.mjs +21 -0
  94. package/.output/server/_libs/micromark-util-encode.mjs +1 -0
  95. package/.output/server/_libs/micromark-util-html-tag-name.mjs +69 -0
  96. package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +6 -0
  97. package/.output/server/_libs/micromark-util-resolve-all.mjs +15 -0
  98. package/.output/server/_libs/micromark-util-sanitize-uri.mjs +41 -0
  99. package/.output/server/_libs/micromark-util-subtokenize.mjs +346 -0
  100. package/.output/server/_libs/micromark.mjs +906 -0
  101. package/.output/server/_libs/mimic-function.mjs +47 -0
  102. package/.output/server/_libs/modelcontextprotocol__server.mjs +9738 -0
  103. package/.output/server/_libs/ocache.mjs +1 -0
  104. package/.output/server/_libs/ohash.mjs +1 -0
  105. package/.output/server/_libs/pako.mjs +4223 -0
  106. package/.output/server/_libs/process-nextick-args.mjs +48 -0
  107. package/.output/server/_libs/property-information.mjs +1209 -0
  108. package/.output/server/_libs/radix-ui.mjs +1 -0
  109. package/.output/server/_libs/radix-ui__number.mjs +6 -0
  110. package/.output/server/_libs/radix-ui__primitive.mjs +11 -0
  111. package/.output/server/_libs/radix-ui__react-accordion.mjs +1 -0
  112. package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +1 -0
  113. package/.output/server/_libs/radix-ui__react-arrow.mjs +23 -0
  114. package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +1 -0
  115. package/.output/server/_libs/radix-ui__react-avatar.mjs +1 -0
  116. package/.output/server/_libs/radix-ui__react-checkbox.mjs +1 -0
  117. package/.output/server/_libs/radix-ui__react-collapsible.mjs +144 -0
  118. package/.output/server/_libs/radix-ui__react-collection.mjs +69 -0
  119. package/.output/server/_libs/radix-ui__react-compose-refs.mjs +39 -0
  120. package/.output/server/_libs/radix-ui__react-context-menu.mjs +1 -0
  121. package/.output/server/_libs/radix-ui__react-context.mjs +78 -0
  122. package/.output/server/_libs/radix-ui__react-dialog.mjs +325 -0
  123. package/.output/server/_libs/radix-ui__react-direction.mjs +9 -0
  124. package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +1 -0
  125. package/.output/server/_libs/radix-ui__react-focus-guards.mjs +29 -0
  126. package/.output/server/_libs/radix-ui__react-focus-scope.mjs +206 -0
  127. package/.output/server/_libs/radix-ui__react-form.mjs +1 -0
  128. package/.output/server/_libs/radix-ui__react-hover-card.mjs +1 -0
  129. package/.output/server/_libs/radix-ui__react-id.mjs +14 -0
  130. package/.output/server/_libs/radix-ui__react-label.mjs +1 -0
  131. package/.output/server/_libs/radix-ui__react-menu.mjs +1 -0
  132. package/.output/server/_libs/radix-ui__react-menubar.mjs +1 -0
  133. package/.output/server/_libs/radix-ui__react-popover.mjs +1 -0
  134. package/.output/server/_libs/radix-ui__react-popper.mjs +286 -0
  135. package/.output/server/_libs/radix-ui__react-portal.mjs +16 -0
  136. package/.output/server/_libs/radix-ui__react-presence.mjs +128 -0
  137. package/.output/server/_libs/radix-ui__react-primitive.mjs +42 -0
  138. package/.output/server/_libs/radix-ui__react-progress.mjs +1 -0
  139. package/.output/server/_libs/radix-ui__react-radio-group.mjs +1 -0
  140. package/.output/server/_libs/radix-ui__react-roving-focus.mjs +224 -0
  141. package/.output/server/_libs/radix-ui__react-scroll-area.mjs +721 -0
  142. package/.output/server/_libs/radix-ui__react-select.mjs +1163 -0
  143. package/.output/server/_libs/radix-ui__react-separator.mjs +28 -0
  144. package/.output/server/_libs/radix-ui__react-slider.mjs +1 -0
  145. package/.output/server/_libs/radix-ui__react-slot.mjs +99 -0
  146. package/.output/server/_libs/radix-ui__react-switch.mjs +1 -0
  147. package/.output/server/_libs/radix-ui__react-tabs.mjs +189 -0
  148. package/.output/server/_libs/radix-ui__react-toast.mjs +2 -0
  149. package/.output/server/_libs/radix-ui__react-toggle-group.mjs +1 -0
  150. package/.output/server/_libs/radix-ui__react-toggle.mjs +1 -0
  151. package/.output/server/_libs/radix-ui__react-toolbar.mjs +1 -0
  152. package/.output/server/_libs/radix-ui__react-tooltip.mjs +495 -0
  153. package/.output/server/_libs/radix-ui__react-use-previous.mjs +14 -0
  154. package/.output/server/_libs/radix-ui__react-use-size.mjs +39 -0
  155. package/.output/server/_libs/react-dom.mjs +10781 -0
  156. package/.output/server/_libs/react-markdown.mjs +147 -0
  157. package/.output/server/_libs/react-remove-scroll-bar.mjs +82 -0
  158. package/.output/server/_libs/react-remove-scroll.mjs +328 -0
  159. package/.output/server/_libs/react-style-singleton.mjs +69 -0
  160. package/.output/server/_libs/react.mjs +515 -0
  161. package/.output/server/_libs/readable-stream.mjs +1518 -0
  162. package/.output/server/_libs/remark-parse.mjs +19 -0
  163. package/.output/server/_libs/remark-rehype.mjs +21 -0
  164. package/.output/server/_libs/rou3.mjs +14 -0
  165. package/.output/server/_libs/safe-buffer.mjs +64 -0
  166. package/.output/server/_libs/semver.mjs +1938 -0
  167. package/.output/server/_libs/seroval-plugins.mjs +58 -0
  168. package/.output/server/_libs/seroval.mjs +1765 -0
  169. package/.output/server/_libs/setimmediate.mjs +152 -0
  170. package/.output/server/_libs/space-separated-tokens.mjs +6 -0
  171. package/.output/server/_libs/srvx.mjs +1029 -0
  172. package/.output/server/_libs/stubborn-fs.mjs +91 -0
  173. package/.output/server/_libs/stubborn-utils.mjs +66 -0
  174. package/.output/server/_libs/style-to-js.mjs +72 -0
  175. package/.output/server/_libs/style-to-object.mjs +38 -0
  176. package/.output/server/_libs/swr.mjs +939 -0
  177. package/.output/server/_libs/tailwind-merge.mjs +3010 -0
  178. package/.output/server/_libs/tanstack__history.mjs +217 -0
  179. package/.output/server/_libs/tanstack__react-router.mjs +1480 -0
  180. package/.output/server/_libs/tanstack__react-store.mjs +1 -0
  181. package/.output/server/_libs/tanstack__react-virtual.mjs +44 -0
  182. package/.output/server/_libs/tanstack__router-core.mjs +4827 -0
  183. package/.output/server/_libs/tanstack__store.mjs +1 -0
  184. package/.output/server/_libs/tanstack__virtual-core.mjs +1225 -0
  185. package/.output/server/_libs/tiny-invariant.mjs +12 -0
  186. package/.output/server/_libs/tiny-warning.mjs +5 -0
  187. package/.output/server/_libs/trim-lines.mjs +41 -0
  188. package/.output/server/_libs/trough.mjs +85 -0
  189. package/.output/server/_libs/tslib.mjs +1 -0
  190. package/.output/server/_libs/ufo.mjs +54 -0
  191. package/.output/server/_libs/uint8array-extras.mjs +69 -0
  192. package/.output/server/_libs/ungap__structured-clone.mjs +212 -0
  193. package/.output/server/_libs/unified.mjs +661 -0
  194. package/.output/server/_libs/unist-util-is.mjs +100 -0
  195. package/.output/server/_libs/unist-util-position.mjs +27 -0
  196. package/.output/server/_libs/unist-util-stringify-position.mjs +27 -0
  197. package/.output/server/_libs/unist-util-visit-parents.mjs +82 -0
  198. package/.output/server/_libs/unist-util-visit.mjs +24 -0
  199. package/.output/server/_libs/unstorage.mjs +1 -0
  200. package/.output/server/_libs/use-callback-ref.mjs +66 -0
  201. package/.output/server/_libs/use-sidecar.mjs +106 -0
  202. package/.output/server/_libs/use-sync-external-store.mjs +64 -0
  203. package/.output/server/_libs/util-deprecate.mjs +12 -0
  204. package/.output/server/_libs/vfile-message.mjs +138 -0
  205. package/.output/server/_libs/vfile.mjs +467 -0
  206. package/.output/server/_libs/when-exit.mjs +53 -0
  207. package/.output/server/_libs/zod.mjs +4524 -0
  208. package/.output/server/_sessionId-wMLPvC5g.mjs +123 -0
  209. package/.output/server/_ssr/CompareDrawer-BU4V0uVf.mjs +1041 -0
  210. package/.output/server/_ssr/ProxyViewerContainer-BnRwFEnn.mjs +5972 -0
  211. package/.output/server/_ssr/ReplayDialog-C7dn9pd_.mjs +322 -0
  212. package/.output/server/_ssr/RequestAnatomy-C1rWpe9-.mjs +353 -0
  213. package/.output/server/_ssr/ResponseView-hGpPaYsf.mjs +602 -0
  214. package/.output/server/_ssr/StreamingChunkSequence-BRWI1r_G.mjs +302 -0
  215. package/.output/server/_ssr/index-BKURLVPz.mjs +118 -0
  216. package/.output/server/_ssr/index.mjs +1184 -0
  217. package/.output/server/_ssr/json-viewer-BBd2DtQP.mjs +515 -0
  218. package/.output/server/_ssr/router-BcZ0D6AB.mjs +6317 -0
  219. package/.output/server/_ssr/start-HYkvq4Ni.mjs +4 -0
  220. package/.output/server/_tanstack-start-manifest_v-1y8ZVxRI.mjs +4 -0
  221. package/.output/server/index.mjs +436 -0
  222. package/.output/server/node_modules/tslib/modules/index.js +70 -0
  223. package/.output/server/node_modules/tslib/modules/package.json +3 -0
  224. package/.output/server/node_modules/tslib/package.json +47 -0
  225. package/.output/server/node_modules/tslib/tslib.js +484 -0
  226. package/.output/server/package.json +9 -0
  227. package/LICENSE +21 -0
  228. package/README.md +52 -0
  229. package/package.json +110 -0
  230. package/src/assets/favicon.svg +31 -0
  231. package/src/assets/logos/alibaba.svg +1 -0
  232. package/src/assets/logos/anthropic.svg +1 -0
  233. package/src/assets/logos/claude-code.svg +4 -0
  234. package/src/assets/logos/deepseek.svg +1 -0
  235. package/src/assets/logos/mcp.png +0 -0
  236. package/src/assets/logos/minimax.jpeg +0 -0
  237. package/src/assets/logos/openai.svg +1 -0
  238. package/src/assets/logos/opencode.svg +4 -0
  239. package/src/assets/logos/qwen.png +0 -0
  240. package/src/assets/logos/zhipuai.svg +219 -0
  241. package/src/cli/detect-tools.ts +147 -0
  242. package/src/cli/doctor.ts +521 -0
  243. package/src/cli/onboard.ts +224 -0
  244. package/src/cli/templates/command-onboard.ts +17 -0
  245. package/src/cli/templates/skill-onboard.ts +547 -0
  246. package/src/cli.ts +345 -0
  247. package/src/components/OnboardingBanner.tsx +67 -0
  248. package/src/components/ProxyViewer.tsx +545 -0
  249. package/src/components/ProxyViewerContainer.tsx +363 -0
  250. package/src/components/providers/ImportWizardDialog.tsx +349 -0
  251. package/src/components/providers/ProviderCard.tsx +474 -0
  252. package/src/components/providers/ProviderForm.tsx +494 -0
  253. package/src/components/providers/ProviderLogo.tsx +117 -0
  254. package/src/components/providers/ProvidersPanel.tsx +619 -0
  255. package/src/components/providers/SettingsDialog.tsx +202 -0
  256. package/src/components/proxy-viewer/CompareDrawer.tsx +893 -0
  257. package/src/components/proxy-viewer/ConversationGroup.tsx +107 -0
  258. package/src/components/proxy-viewer/ConversationHeader.tsx +300 -0
  259. package/src/components/proxy-viewer/LogEntry.tsx +543 -0
  260. package/src/components/proxy-viewer/LogEntryHeader.tsx +501 -0
  261. package/src/components/proxy-viewer/ReplayDialog.tsx +218 -0
  262. package/src/components/proxy-viewer/ResponseView.tsx +171 -0
  263. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +188 -0
  264. package/src/components/proxy-viewer/ThreadConnector.tsx +136 -0
  265. package/src/components/proxy-viewer/TurnGroup.tsx +337 -0
  266. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +98 -0
  267. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +196 -0
  268. package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +53 -0
  269. package/src/components/proxy-viewer/anatomy/types.ts +39 -0
  270. package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +114 -0
  271. package/src/components/proxy-viewer/cacheTrend.ts +50 -0
  272. package/src/components/proxy-viewer/diff/DiffView.tsx +321 -0
  273. package/src/components/proxy-viewer/diff/computeDiff.ts +178 -0
  274. package/src/components/proxy-viewer/diff/index.ts +3 -0
  275. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +157 -0
  276. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +66 -0
  277. package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +21 -0
  278. package/src/components/proxy-viewer/formats/index.tsx +33 -0
  279. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +170 -0
  280. package/src/components/proxy-viewer/index.ts +9 -0
  281. package/src/components/proxy-viewer/lazy.ts +37 -0
  282. package/src/components/proxy-viewer/log-formats/anthropic.ts +194 -0
  283. package/src/components/proxy-viewer/log-formats/index.ts +23 -0
  284. package/src/components/proxy-viewer/log-formats/openai.ts +167 -0
  285. package/src/components/proxy-viewer/log-formats/types.ts +40 -0
  286. package/src/components/proxy-viewer/log-formats/unknown.ts +18 -0
  287. package/src/components/proxy-viewer/logEntryVisibility.ts +39 -0
  288. package/src/components/proxy-viewer/requestDiff.ts +277 -0
  289. package/src/components/proxy-viewer/useCopyFeedback.ts +36 -0
  290. package/src/components/proxy-viewer/useKeyboardNavigation.ts +190 -0
  291. package/src/components/proxy-viewer/viewerState.ts +66 -0
  292. package/src/components/ui/badge.tsx +47 -0
  293. package/src/components/ui/button.tsx +47 -0
  294. package/src/components/ui/collapsible.tsx +21 -0
  295. package/src/components/ui/confirm-dialog.tsx +51 -0
  296. package/src/components/ui/crab-logo.tsx +95 -0
  297. package/src/components/ui/crab-variants.tsx +467 -0
  298. package/src/components/ui/dialog.tsx +129 -0
  299. package/src/components/ui/json-expansion-button.tsx +56 -0
  300. package/src/components/ui/json-viewer-bulk.ts +97 -0
  301. package/src/components/ui/json-viewer.tsx +494 -0
  302. package/src/components/ui/mcp-logo.tsx +20 -0
  303. package/src/components/ui/scroll-area.tsx +54 -0
  304. package/src/components/ui/select.tsx +178 -0
  305. package/src/components/ui/separator.tsx +28 -0
  306. package/src/components/ui/tabs.tsx +88 -0
  307. package/src/components/ui/tooltip.tsx +51 -0
  308. package/src/index.css +11 -0
  309. package/src/knowledge/candidateStore.ts +63 -0
  310. package/src/knowledge/distiller.ts +98 -0
  311. package/src/knowledge/openclawClient.ts +118 -0
  312. package/src/knowledge/redactor.ts +80 -0
  313. package/src/knowledge/types.ts +84 -0
  314. package/src/lib/apiClient.ts +49 -0
  315. package/src/lib/export-logs.ts +51 -0
  316. package/src/lib/mask.ts +4 -0
  317. package/src/lib/objectUtils.ts +22 -0
  318. package/src/lib/providerContract.ts +26 -0
  319. package/src/lib/providerTestContract.ts +107 -0
  320. package/src/lib/runtimeConfig.ts +25 -0
  321. package/src/lib/serverPort.ts +41 -0
  322. package/src/lib/sessionUrl.ts +44 -0
  323. package/src/lib/stopReason.ts +58 -0
  324. package/src/lib/useOnboarding.ts +80 -0
  325. package/src/lib/useProviders.ts +30 -0
  326. package/src/lib/useStripConfig.ts +108 -0
  327. package/src/lib/utils.ts +21 -0
  328. package/src/mcp/loopback.ts +76 -0
  329. package/src/mcp/previewExtractor.ts +166 -0
  330. package/src/mcp/server.ts +396 -0
  331. package/src/mcp/toolHandlers.ts +341 -0
  332. package/src/proxy/chunkStorage.ts +112 -0
  333. package/src/proxy/claudeCodeStrip.ts +99 -0
  334. package/src/proxy/config.ts +172 -0
  335. package/src/proxy/constants.ts +47 -0
  336. package/src/proxy/dataDir.ts +86 -0
  337. package/src/proxy/formats/anthropic/anthropicProvider.ts +75 -0
  338. package/src/proxy/formats/anthropic/handler.ts +71 -0
  339. package/src/proxy/formats/anthropic/index.ts +14 -0
  340. package/src/proxy/formats/anthropic/register.ts +4 -0
  341. package/src/proxy/formats/anthropic/schemas.ts +237 -0
  342. package/src/proxy/formats/anthropic/stream.ts +205 -0
  343. package/src/proxy/formats/handler.ts +46 -0
  344. package/src/proxy/formats/index.ts +12 -0
  345. package/src/proxy/formats/jsonSchema.ts +36 -0
  346. package/src/proxy/formats/openai/alibabaProvider.ts +38 -0
  347. package/src/proxy/formats/openai/handler.ts +96 -0
  348. package/src/proxy/formats/openai/index.ts +25 -0
  349. package/src/proxy/formats/openai/provider.ts +50 -0
  350. package/src/proxy/formats/openai/register.ts +4 -0
  351. package/src/proxy/formats/openai/schemas.ts +187 -0
  352. package/src/proxy/formats/openai/stream.ts +206 -0
  353. package/src/proxy/formats/protocol.ts +50 -0
  354. package/src/proxy/formats/providerRegistry.ts +51 -0
  355. package/src/proxy/formats/providers/index.ts +3 -0
  356. package/src/proxy/formats/registry.ts +66 -0
  357. package/src/proxy/handler.ts +334 -0
  358. package/src/proxy/logFinalizer.ts +305 -0
  359. package/src/proxy/logFinalizer.worker.ts +24 -0
  360. package/src/proxy/logIndex.ts +268 -0
  361. package/src/proxy/logger.ts +179 -0
  362. package/src/proxy/openaiOrphanToolStrip.ts +142 -0
  363. package/src/proxy/providerImporters.ts +491 -0
  364. package/src/proxy/providers.ts +613 -0
  365. package/src/proxy/schemas.ts +209 -0
  366. package/src/proxy/sessionProcess.ts +140 -0
  367. package/src/proxy/sessionRuntime.ts +85 -0
  368. package/src/proxy/sessionSupervisor.ts +283 -0
  369. package/src/proxy/sessionWorkerEntry.ts +26 -0
  370. package/src/proxy/socketTracker.ts +255 -0
  371. package/src/proxy/store.ts +412 -0
  372. package/src/proxy/upstream.ts +90 -0
  373. package/src/router.tsx +16 -0
  374. package/src/routes/__root.tsx +45 -0
  375. package/src/routes/api/config.paths.ts +14 -0
  376. package/src/routes/api/config.ts +53 -0
  377. package/src/routes/api/health.ts +15 -0
  378. package/src/routes/api/knowledge.candidates.$candidateId.promote.ts +32 -0
  379. package/src/routes/api/knowledge.candidates.ts +10 -0
  380. package/src/routes/api/knowledge.project-context.ts +18 -0
  381. package/src/routes/api/knowledge.search.ts +31 -0
  382. package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +16 -0
  383. package/src/routes/api/logs.$id.chunks.ts +36 -0
  384. package/src/routes/api/logs.$id.replay.ts +191 -0
  385. package/src/routes/api/logs.$id.ts +22 -0
  386. package/src/routes/api/logs.stream.ts +74 -0
  387. package/src/routes/api/logs.ts +59 -0
  388. package/src/routes/api/mcp.ts +25 -0
  389. package/src/routes/api/models.ts +10 -0
  390. package/src/routes/api/providers.$providerId.test.log.ts +293 -0
  391. package/src/routes/api/providers.$providerId.ts +50 -0
  392. package/src/routes/api/providers.export.ts +26 -0
  393. package/src/routes/api/providers.import.ts +47 -0
  394. package/src/routes/api/providers.scan.ts +23 -0
  395. package/src/routes/api/providers.ts +45 -0
  396. package/src/routes/api/sessions.ts +17 -0
  397. package/src/routes/index.tsx +6 -0
  398. package/src/routes/proxy/$.ts +15 -0
  399. package/src/routes/session/$sessionId.tsx +23 -0
  400. package/styles/globals.css +188 -0
@@ -0,0 +1,893 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import type { JSX } from "react";
3
+ import {
4
+ Check,
5
+ ChevronRight,
6
+ Columns2,
7
+ Copy,
8
+ Equal,
9
+ Minus,
10
+ Pencil,
11
+ Plus,
12
+ Rows3,
13
+ X,
14
+ } from "lucide-react";
15
+ import { cn, formatTokens } from "../../lib/utils";
16
+ import type { CapturedLog } from "../../proxy/schemas";
17
+ import {
18
+ type DiffOp,
19
+ type JsonNode,
20
+ diffTrees,
21
+ normalizeRequest,
22
+ previewNode,
23
+ } from "./requestDiff";
24
+ import { getConversationId } from "./ConversationHeader";
25
+ import { JsonViewerFromString } from "../ui/json-viewer";
26
+ import { Badge } from "../ui/badge";
27
+ import { getLogFormatAdapter, resolveLogFormat } from "./log-formats";
28
+
29
+ export type CompareDrawerProps = {
30
+ /** Log selected first (shown on the left). */
31
+ left: CapturedLog;
32
+ /** Log selected second (shown on the right). */
33
+ right: CapturedLog;
34
+ onClose: () => void;
35
+ };
36
+
37
+ type EqualOp = Extract<DiffOp, { kind: "equal" }>;
38
+ type AddedOp = Extract<DiffOp, { kind: "added" }>;
39
+ type RemovedOp = Extract<DiffOp, { kind: "removed" }>;
40
+ type ChangedOp = Extract<DiffOp, { kind: "changed" }>;
41
+
42
+ type DiffMode = "unified" | "split";
43
+
44
+ /** Walk the JsonNode tree and pretty-print it back to a JSON string for the
45
+ * expanded-equal-subtree view. The node is a plain object structure so
46
+ * `JSON.stringify` produces correct output. */
47
+ function nodeToJsonString(node: JsonNode, indent = 2): string {
48
+ return JSON.stringify(nodeToJsonValue(node), null, indent);
49
+ }
50
+
51
+ function nodeToJsonValue(node: JsonNode): unknown {
52
+ switch (node.kind) {
53
+ case "primitive":
54
+ return node.value;
55
+ case "array":
56
+ return node.value.map(nodeToJsonValue);
57
+ case "object": {
58
+ const out: Record<string, unknown> = {};
59
+ for (const [k, v] of Object.entries(node.value)) {
60
+ out[k] = nodeToJsonValue(v);
61
+ }
62
+ return out;
63
+ }
64
+ }
65
+ }
66
+
67
+ /** The parent path of a JSON path string. E.g. `messages[3].content` →
68
+ * `messages[3]`, `messages[3]` → `messages`, `messages` → `""`. */
69
+ function parentPath(path: string): string {
70
+ if (path === "") return "";
71
+ for (let i = path.length - 1; i >= 0; i--) {
72
+ const ch = path[i];
73
+ if (ch === "." || ch === "[") {
74
+ return path.substring(0, i);
75
+ }
76
+ }
77
+ return "";
78
+ }
79
+
80
+ /** Group contiguous deep-equal ops (object/array, not primitive) that share
81
+ * a common parent into a single collapsed row, so an unchanged block of N
82
+ * sibling subtrees renders as one row instead of N. Primitive equals
83
+ * always render as their own row (they're 1 line each). */
84
+ type GroupedOp = { kind: "single"; op: DiffOp } | { kind: "equal-run"; ops: EqualOp[] };
85
+
86
+ function isDeepEqual(op: DiffOp): op is EqualOp {
87
+ return op.kind === "equal" && (op.value.kind === "object" || op.value.kind === "array");
88
+ }
89
+
90
+ function groupContiguousEquals(ops: DiffOp[]): GroupedOp[] {
91
+ const out: GroupedOp[] = [];
92
+ let i = 0;
93
+ while (i < ops.length) {
94
+ const op = ops[i];
95
+ if (op !== undefined && isDeepEqual(op)) {
96
+ const startParent = parentPath(op.path);
97
+ let j = i + 1;
98
+ while (j < ops.length) {
99
+ const next = ops[j];
100
+ if (next === undefined) break;
101
+ if (!isDeepEqual(next)) break;
102
+ if (parentPath(next.path) !== startParent) break;
103
+ j++;
104
+ }
105
+ if (j - i > 1) {
106
+ const equalOps: EqualOp[] = [];
107
+ for (let k = i; k < j; k++) {
108
+ const eop = ops[k];
109
+ if (eop !== undefined && eop.kind === "equal") {
110
+ equalOps.push(eop);
111
+ }
112
+ }
113
+ out.push({ kind: "equal-run", ops: equalOps });
114
+ i = j;
115
+ continue;
116
+ }
117
+ }
118
+ if (op !== undefined) {
119
+ out.push({ kind: "single", op });
120
+ }
121
+ i++;
122
+ }
123
+ return out;
124
+ }
125
+
126
+ /** Visual configuration for each diff kind. Centralized so the unified row,
127
+ * summary chips, and split columns all use the same colors / icons. */
128
+ type DiffKind = "added" | "removed" | "changed" | "equal";
129
+
130
+ const KIND_VISUAL: Record<
131
+ DiffKind,
132
+ {
133
+ icon: typeof Plus;
134
+ /** Accent color for the icon + label. */
135
+ accent: string;
136
+ /** Background tint applied to the row. */
137
+ bg: string;
138
+ /** Thick left-border color. */
139
+ border: string;
140
+ /** Short text label (uppercase, used in the row badge). */
141
+ label: string;
142
+ }
143
+ > = {
144
+ added: {
145
+ icon: Plus,
146
+ accent: "text-emerald-600 dark:text-emerald-400",
147
+ bg: "bg-emerald-500/5 hover:bg-emerald-500/10",
148
+ border: "border-l-emerald-500",
149
+ label: "ADDED",
150
+ },
151
+ removed: {
152
+ icon: Minus,
153
+ accent: "text-rose-600 dark:text-rose-400",
154
+ bg: "bg-rose-500/5 hover:bg-rose-500/10",
155
+ border: "border-l-rose-500",
156
+ label: "REMOVED",
157
+ },
158
+ changed: {
159
+ icon: Pencil,
160
+ accent: "text-amber-600 dark:text-amber-400",
161
+ bg: "bg-amber-500/5 hover:bg-amber-500/10",
162
+ border: "border-l-amber-500",
163
+ label: "CHANGED",
164
+ },
165
+ equal: {
166
+ icon: Equal,
167
+ accent: "text-muted-foreground/70",
168
+ bg: "bg-muted/20 hover:bg-muted/30",
169
+ border: "border-l-muted-foreground/20",
170
+ label: "EQUAL",
171
+ },
172
+ };
173
+
174
+ function EqualRunRow({
175
+ ops,
176
+ expanded,
177
+ onToggle,
178
+ }: {
179
+ ops: EqualOp[];
180
+ expanded: boolean;
181
+ onToggle: () => void;
182
+ }): JSX.Element {
183
+ const first = ops[0];
184
+ const last = ops[ops.length - 1];
185
+ if (first === undefined || last === undefined) {
186
+ return <div className="text-muted-foreground/40 text-xs">—</div>;
187
+ }
188
+ const firstPath = first.path;
189
+ const lastPath = last.path;
190
+ const label = ops.length === 1 ? firstPath : `${firstPath} … ${lastPath}`;
191
+ const summary =
192
+ first.value.kind === "array"
193
+ ? `${ops.length} equal arrays`
194
+ : first.value.kind === "object"
195
+ ? `${ops.length} equal objects`
196
+ : "equal";
197
+
198
+ const v = KIND_VISUAL.equal;
199
+
200
+ return (
201
+ <div className={cn("border-l-4 rounded-sm", v.border, v.bg)}>
202
+ <button
203
+ type="button"
204
+ onClick={onToggle}
205
+ className="w-full text-left flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground cursor-pointer"
206
+ >
207
+ <ChevronRight
208
+ className={cn("size-3 transition-transform shrink-0", expanded && "rotate-90")}
209
+ />
210
+ <v.icon className={cn("size-3 shrink-0", v.accent)} />
211
+ <span className="font-mono truncate flex-1" title={`${firstPath} … ${lastPath}`}>
212
+ {label}
213
+ </span>
214
+ <span className={cn("text-[10px] uppercase tracking-wider shrink-0", v.accent)}>
215
+ {v.label}
216
+ </span>
217
+ <span className="text-muted-foreground/60 shrink-0">({summary})</span>
218
+ </button>
219
+ {expanded && (
220
+ <div className="ml-5 mt-1 mb-2 space-y-2 pr-2">
221
+ {ops.map((op) => (
222
+ <div key={op.path} className="border border-border/50 rounded p-2 bg-muted/20">
223
+ <div className="font-mono text-xs text-muted-foreground mb-1">{op.path}</div>
224
+ <JsonViewerFromString text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
225
+ </div>
226
+ ))}
227
+ </div>
228
+ )}
229
+ </div>
230
+ );
231
+ }
232
+
233
+ /** A single diff row in the unified view. Renders the icon, path, kind label,
234
+ * value preview, and a copy-path button. `data-diff-idx` lets the parent
235
+ * scroll the row into view when a summary chip is clicked.
236
+ *
237
+ * When `expanded` is true (controlled by the parent), renders the full
238
+ * added / removed / changed subtree below the row. Primitive changes and
239
+ * primitive equal values are not expandable (the value is already fully
240
+ * shown on the row). */
241
+ function UnifiedOpRow({
242
+ op,
243
+ idx,
244
+ copiedPath,
245
+ onCopyPath,
246
+ expanded,
247
+ onToggle,
248
+ }: {
249
+ op: AddedOp | RemovedOp | ChangedOp | EqualOp;
250
+ idx: number;
251
+ copiedPath: string | null;
252
+ onCopyPath: (path: string) => void;
253
+ expanded: boolean;
254
+ onToggle: () => void;
255
+ }): JSX.Element {
256
+ const v = KIND_VISUAL[op.kind];
257
+ const Icon = v.icon;
258
+
259
+ // True iff this row has a useful subtree to drill into. Primitives (and
260
+ // the root "equal" pseudo-op) have no extra detail beyond the preview.
261
+ const isExpandable =
262
+ op.kind === "added" || op.kind === "removed"
263
+ ? op.value.kind === "object" || op.value.kind === "array"
264
+ : op.kind === "changed"
265
+ ? op.left.kind === "object" ||
266
+ op.left.kind === "array" ||
267
+ op.right.kind === "object" ||
268
+ op.right.kind === "array"
269
+ : false; // equal (single primitive) — preview is the whole value
270
+
271
+ // For changed rows, show the left and right previews stacked. For added /
272
+ // removed / equal, one preview is enough.
273
+ const preview =
274
+ op.kind === "changed"
275
+ ? [
276
+ {
277
+ text: previewNode(op.left, 400),
278
+ tone: "text-rose-700 dark:text-rose-300 line-through",
279
+ },
280
+ { text: previewNode(op.right, 400), tone: "text-emerald-700 dark:text-emerald-300" },
281
+ ]
282
+ : op.kind === "removed"
283
+ ? [
284
+ {
285
+ text: previewNode(op.value, 400),
286
+ tone: "text-rose-700 dark:text-rose-300 line-through",
287
+ },
288
+ ]
289
+ : op.kind === "added"
290
+ ? [{ text: previewNode(op.value, 400), tone: "text-emerald-700 dark:text-emerald-300" }]
291
+ : [{ text: previewNode(op.value, 400), tone: "text-muted-foreground" }];
292
+
293
+ const justCopied = copiedPath === op.path && op.path !== "";
294
+
295
+ return (
296
+ <div
297
+ data-diff-idx={idx}
298
+ data-diff-kind={op.kind}
299
+ className={cn("border-l-4 rounded-sm px-3 py-2 my-0.5 transition-colors", v.border, v.bg)}
300
+ >
301
+ <button
302
+ type="button"
303
+ onClick={onToggle}
304
+ disabled={!isExpandable}
305
+ className={cn(
306
+ "w-full flex items-center gap-2 text-xs text-left rounded-sm",
307
+ isExpandable ? "cursor-pointer" : "cursor-default",
308
+ )}
309
+ aria-expanded={isExpandable ? expanded : undefined}
310
+ aria-label={
311
+ isExpandable
312
+ ? expanded
313
+ ? `Collapse ${op.path || "root"}`
314
+ : `Expand ${op.path || "root"}`
315
+ : undefined
316
+ }
317
+ >
318
+ {isExpandable ? (
319
+ <ChevronRight
320
+ className={cn(
321
+ "size-3 shrink-0 transition-transform",
322
+ v.accent,
323
+ expanded && "rotate-90",
324
+ )}
325
+ />
326
+ ) : (
327
+ <span className="size-3 shrink-0" aria-hidden="true" />
328
+ )}
329
+ <Icon className={cn("size-3.5 shrink-0", v.accent)} strokeWidth={2.5} />
330
+ <span className="font-mono truncate flex-1 min-w-0" title={op.path || "(root)"}>
331
+ {op.path === "" ? "(root)" : op.path}
332
+ </span>
333
+ <span
334
+ className={cn(
335
+ "text-[9px] font-bold uppercase tracking-wider shrink-0 px-1.5 py-0.5 rounded",
336
+ v.accent,
337
+ op.kind === "equal" ? "bg-muted/40" : "bg-background/60",
338
+ )}
339
+ >
340
+ {v.label}
341
+ </span>
342
+ {op.path !== "" && (
343
+ <span
344
+ role="button"
345
+ tabIndex={0}
346
+ onClick={(e) => {
347
+ e.stopPropagation();
348
+ onCopyPath(op.path);
349
+ }}
350
+ onKeyDown={(e) => {
351
+ if (e.key === "Enter" || e.key === " ") {
352
+ e.stopPropagation();
353
+ e.preventDefault();
354
+ onCopyPath(op.path);
355
+ }
356
+ }}
357
+ className={cn(
358
+ "shrink-0 p-1 rounded transition-colors cursor-pointer inline-flex items-center justify-center",
359
+ justCopied
360
+ ? "text-emerald-500"
361
+ : "text-muted-foreground/50 hover:text-foreground hover:bg-muted",
362
+ )}
363
+ aria-label={justCopied ? "Copied" : "Copy"}
364
+ title={justCopied ? "Copied!" : "Copy"}
365
+ >
366
+ {justCopied ? <Check className="size-3" /> : <Copy className="size-3" />}
367
+ </span>
368
+ )}
369
+ </button>
370
+ {preview.map((p, i) => (
371
+ // biome-ignore lint/suspicious/noArrayIndexKey: preview list is rebuilt on every render and is positional
372
+ <div key={i} className={cn("font-mono text-xs mt-1 break-all pl-5", p.tone)}>
373
+ {p.text}
374
+ </div>
375
+ ))}
376
+ <div
377
+ className="overflow-hidden transition-all duration-200"
378
+ style={{ maxHeight: expanded && isExpandable ? "2000px" : "0" }}
379
+ aria-hidden={!expanded}
380
+ >
381
+ {expanded && isExpandable && op.kind !== "equal" ? <ExpandedSubtree op={op} /> : null}
382
+ </div>
383
+ </div>
384
+ );
385
+ }
386
+
387
+ /** Render the full subtree for an expanded diff row.
388
+ * - added / removed: a single JsonViewerFromString for the value.
389
+ * - changed (object/array on either side): two viewers side by side.
390
+ * - changed (both primitives): a small "values are shown above" note. */
391
+ function ExpandedSubtree({ op }: { op: AddedOp | RemovedOp | ChangedOp }): JSX.Element {
392
+ if (op.kind === "added" || op.kind === "removed") {
393
+ return (
394
+ <div className="pl-5 mt-2 border border-border/50 rounded p-2 bg-muted/20">
395
+ <JsonViewerFromString text={nodeToJsonString(op.value)} defaultExpandDepth={0} />
396
+ </div>
397
+ );
398
+ }
399
+ // changed
400
+ const leftIsStructured = op.left.kind === "object" || op.left.kind === "array";
401
+ const rightIsStructured = op.right.kind === "object" || op.right.kind === "array";
402
+ if (!leftIsStructured && !rightIsStructured) {
403
+ return (
404
+ <div className="pl-5 mt-2 text-xs text-muted-foreground/70 italic">
405
+ Primitive values are shown inline above.
406
+ </div>
407
+ );
408
+ }
409
+ return (
410
+ <div className="pl-5 mt-2 grid grid-cols-1 md:grid-cols-2 gap-2">
411
+ <div className="border border-rose-500/30 rounded p-2 bg-rose-500/5">
412
+ <div className="text-[10px] uppercase tracking-wider text-rose-500 mb-1">Old</div>
413
+ <JsonViewerFromString text={nodeToJsonString(op.left)} defaultExpandDepth={0} />
414
+ </div>
415
+ <div className="border border-emerald-500/30 rounded p-2 bg-emerald-500/5">
416
+ <div className="text-[10px] uppercase tracking-wider text-emerald-500 mb-1">New</div>
417
+ <JsonViewerFromString text={nodeToJsonString(op.right)} defaultExpandDepth={0} />
418
+ </div>
419
+ </div>
420
+ );
421
+ }
422
+
423
+ /** Summary chips at the top of the body. Each chip is a button that scrolls
424
+ * the body to the first op of that kind. Disabled when the count is 0. */
425
+ function SummaryChips({
426
+ counts,
427
+ onJumpTo,
428
+ }: {
429
+ counts: { added: number; removed: number; changed: number };
430
+ onJumpTo: (kind: "added" | "removed" | "changed") => void;
431
+ }): JSX.Element {
432
+ const total = counts.added + counts.removed + counts.changed;
433
+ return (
434
+ <div className="px-4 py-2 border-b border-border bg-muted/20 flex items-center gap-2 text-xs flex-wrap">
435
+ <span className="text-muted-foreground font-medium">
436
+ {total} {total === 1 ? "change" : "changes"}
437
+ </span>
438
+ <button
439
+ type="button"
440
+ onClick={() => onJumpTo("removed")}
441
+ disabled={counts.removed === 0}
442
+ className={cn(
443
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
444
+ counts.removed > 0
445
+ ? "border-rose-500/40 text-rose-600 dark:text-rose-400 bg-rose-500/10 hover:bg-rose-500/20"
446
+ : "border-border text-muted-foreground/40 cursor-not-allowed",
447
+ )}
448
+ title={counts.removed > 0 ? "Jump to first removed" : "No removals"}
449
+ >
450
+ <Minus className="size-3" />
451
+ {counts.removed} removed
452
+ </button>
453
+ <button
454
+ type="button"
455
+ onClick={() => onJumpTo("added")}
456
+ disabled={counts.added === 0}
457
+ className={cn(
458
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
459
+ counts.added > 0
460
+ ? "border-emerald-500/40 text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 hover:bg-emerald-500/20"
461
+ : "border-border text-muted-foreground/40 cursor-not-allowed",
462
+ )}
463
+ title={counts.added > 0 ? "Jump to first added" : "No additions"}
464
+ >
465
+ <Plus className="size-3" />
466
+ {counts.added} added
467
+ </button>
468
+ <button
469
+ type="button"
470
+ onClick={() => onJumpTo("changed")}
471
+ disabled={counts.changed === 0}
472
+ className={cn(
473
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
474
+ counts.changed > 0
475
+ ? "border-amber-500/40 text-amber-600 dark:text-amber-400 bg-amber-500/10 hover:bg-amber-500/20"
476
+ : "border-border text-muted-foreground/40 cursor-not-allowed",
477
+ )}
478
+ title={counts.changed > 0 ? "Jump to first changed" : "No changes"}
479
+ >
480
+ <Pencil className="size-3" />
481
+ {counts.changed} changed
482
+ </button>
483
+ </div>
484
+ );
485
+ }
486
+
487
+ /** Mode toggle in the header: unified (single column) or split (path | left | right). */
488
+ function ModeToggle({
489
+ mode,
490
+ onChange,
491
+ }: {
492
+ mode: DiffMode;
493
+ onChange: (mode: DiffMode) => void;
494
+ }): JSX.Element {
495
+ return (
496
+ <div className="inline-flex rounded-md border border-border overflow-hidden">
497
+ <button
498
+ type="button"
499
+ onClick={() => onChange("unified")}
500
+ aria-pressed={mode === "unified"}
501
+ className={cn(
502
+ "flex items-center gap-1 px-2 py-1 text-xs transition-colors cursor-pointer",
503
+ mode === "unified"
504
+ ? "bg-muted text-foreground"
505
+ : "hover:bg-muted/50 text-muted-foreground",
506
+ )}
507
+ title="Unified view (single column, emphasized diffs)"
508
+ >
509
+ <Rows3 className="size-3" />
510
+ Unified
511
+ </button>
512
+ <button
513
+ type="button"
514
+ onClick={() => onChange("split")}
515
+ aria-pressed={mode === "split"}
516
+ className={cn(
517
+ "flex items-center gap-1 px-2 py-1 text-xs transition-colors border-l border-border cursor-pointer",
518
+ mode === "split" ? "bg-muted text-foreground" : "hover:bg-muted/50 text-muted-foreground",
519
+ )}
520
+ title="Split view (path | left | right)"
521
+ >
522
+ <Columns2 className="size-3" />
523
+ Split
524
+ </button>
525
+ </div>
526
+ );
527
+ }
528
+
529
+ function SideSummary({ log, side }: { log: CapturedLog; side: "left" | "right" }): JSX.Element {
530
+ const conversationId = getConversationId(log);
531
+ return (
532
+ <div className="flex-1 min-w-0 space-y-1 text-xs">
533
+ <div className="flex items-center gap-2">
534
+ <Badge
535
+ variant="outline"
536
+ className={cn(
537
+ "text-[10px] px-1.5 py-0 h-5 font-mono shrink-0",
538
+ side === "left"
539
+ ? "border-rose-500/40 text-rose-400"
540
+ : "border-emerald-500/40 text-emerald-400",
541
+ )}
542
+ >
543
+ {side === "left" ? "← Left" : "Right →"}
544
+ </Badge>
545
+ <span className="font-mono text-blue-400/80">#{log.id}</span>
546
+ {log.model !== null && (
547
+ <span className="font-mono text-muted-foreground truncate">{log.model}</span>
548
+ )}
549
+ </div>
550
+ <div className="flex items-center gap-3 text-muted-foreground font-mono">
551
+ {log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && (
552
+ <span className="text-emerald-400">
553
+ Cache +{formatTokens(log.cacheCreationInputTokens)}
554
+ </span>
555
+ )}
556
+ {log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && (
557
+ <span className="text-purple-400">Cache ~{formatTokens(log.cacheReadInputTokens)}</span>
558
+ )}
559
+ <span className="truncate" title={log.timestamp}>
560
+ {log.timestamp}
561
+ </span>
562
+ </div>
563
+ <div className="text-muted-foreground/70 font-mono truncate" title={conversationId}>
564
+ session: {conversationId}
565
+ </div>
566
+ </div>
567
+ );
568
+ }
569
+
570
+ export function CompareDrawer({ left, right, onClose }: CompareDrawerProps): JSX.Element {
571
+ // Memoize the diff so re-renders (e.g. parent re-renders) don't recompute.
572
+ const ops = useMemo<DiffOp[]>(() => {
573
+ const leftRequest = getLogFormatAdapter(resolveLogFormat(left)).analyzeRequest(
574
+ left.rawRequestBody,
575
+ );
576
+ const rightRequest = getLogFormatAdapter(resolveLogFormat(right)).analyzeRequest(
577
+ right.rawRequestBody,
578
+ );
579
+ const l = normalizeRequest(leftRequest.comparisonValue);
580
+ const r = normalizeRequest(rightRequest.comparisonValue);
581
+ return diffTrees(l, r);
582
+ }, [
583
+ left.apiFormat,
584
+ left.path,
585
+ left.rawRequestBody,
586
+ right.apiFormat,
587
+ right.path,
588
+ right.rawRequestBody,
589
+ ]);
590
+
591
+ const grouped = useMemo(() => groupContiguousEquals(ops), [ops]);
592
+
593
+ // Count diff ops by kind (ignoring equal-run groups, which are always
594
+ // collapsed and not actionable). Drives the summary chips.
595
+ const counts = useMemo(() => {
596
+ let added = 0;
597
+ let removed = 0;
598
+ let changed = 0;
599
+ for (const g of grouped) {
600
+ if (g.kind !== "single") continue;
601
+ switch (g.op.kind) {
602
+ case "added":
603
+ added++;
604
+ break;
605
+ case "removed":
606
+ removed++;
607
+ break;
608
+ case "changed":
609
+ changed++;
610
+ break;
611
+ case "equal":
612
+ break;
613
+ }
614
+ }
615
+ return { added, removed, changed };
616
+ }, [grouped]);
617
+
618
+ // Track which collapsed equal runs are expanded.
619
+ const [expandedRuns, setExpandedRuns] = useState<Set<number>>(new Set());
620
+ const toggleRun = (idx: number) => {
621
+ setExpandedRuns((prev) => {
622
+ const next = new Set(prev);
623
+ if (next.has(idx)) next.delete(idx);
624
+ else next.add(idx);
625
+ return next;
626
+ });
627
+ };
628
+
629
+ // Track which individual diff rows (added / removed / changed) are
630
+ // expanded. Independent of `expandedRuns` (which is for equal-run groups)
631
+ // so the two expand affordances don't interfere.
632
+ const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
633
+ const toggleRow = (idx: number) => {
634
+ setExpandedRows((prev) => {
635
+ const next = new Set(prev);
636
+ if (next.has(idx)) next.delete(idx);
637
+ else next.add(idx);
638
+ return next;
639
+ });
640
+ };
641
+
642
+ // Reset per-row expand state when the comparison pair changes.
643
+ useEffect(() => {
644
+ setExpandedRows(new Set());
645
+ }, [left.id, right.id]);
646
+
647
+ const [mode, setMode] = useState<DiffMode>("unified");
648
+
649
+ // Body scroll container — used to scroll a target row into view when a
650
+ // summary chip is clicked.
651
+ const bodyRef = useRef<HTMLDivElement>(null);
652
+
653
+ // Most-recently-copied path. Used to flash the icon from Copy → Check.
654
+ const [copiedPath, setCopiedPath] = useState<string | null>(null);
655
+ const copyResetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
656
+ const onCopyPath = (path: string) => {
657
+ void window.navigator.clipboard.writeText(path).then(() => {
658
+ setCopiedPath(path);
659
+ if (copyResetTimer.current !== null) clearTimeout(copyResetTimer.current);
660
+ copyResetTimer.current = setTimeout(() => setCopiedPath(null), 1500);
661
+ });
662
+ };
663
+ useEffect(() => {
664
+ return () => {
665
+ if (copyResetTimer.current !== null) clearTimeout(copyResetTimer.current);
666
+ };
667
+ }, []);
668
+
669
+ // Find the grouped-index of the first op matching `kind`, and scroll to it.
670
+ const jumpToKind = (kind: "added" | "removed" | "changed") => {
671
+ const idx = grouped.findIndex((g) => g.kind === "single" && g.op.kind === kind);
672
+ if (idx === -1) return;
673
+ const root = bodyRef.current;
674
+ if (root === null) return;
675
+ const el = root.querySelector(`[data-diff-idx="${idx}"]`);
676
+ if (el !== null) {
677
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
678
+ }
679
+ };
680
+
681
+ // Esc keybinding + body scroll lock while the drawer is open.
682
+ useEffect(() => {
683
+ const onKey = (e: KeyboardEvent) => {
684
+ if (e.key === "Escape") onClose();
685
+ };
686
+ document.addEventListener("keydown", onKey);
687
+ const prevOverflow = document.body.style.overflow;
688
+ document.body.style.overflow = "hidden";
689
+ return () => {
690
+ document.removeEventListener("keydown", onKey);
691
+ document.body.style.overflow = prevOverflow;
692
+ };
693
+ }, [onClose]);
694
+
695
+ const sameSession = getConversationId(left) === getConversationId(right);
696
+ const allEqual = ops.length === 1 && ops[0]?.kind === "equal";
697
+
698
+ return (
699
+ <div
700
+ className="fixed inset-0 z-50 flex justify-end"
701
+ role="dialog"
702
+ aria-modal="true"
703
+ aria-label="Compare two log requests"
704
+ >
705
+ {/* Backdrop */}
706
+ <button
707
+ type="button"
708
+ onClick={onClose}
709
+ aria-label="Close compare drawer"
710
+ className="absolute inset-0 bg-black/40 cursor-default"
711
+ tabIndex={-1}
712
+ />
713
+
714
+ {/* Drawer panel */}
715
+ <div
716
+ className={cn(
717
+ "relative bg-background border-l border-border shadow-xl",
718
+ "w-full md:w-[70vw] max-w-[1100px] flex flex-col h-full",
719
+ )}
720
+ onClick={(e) => e.stopPropagation()}
721
+ onKeyDown={(e) => e.stopPropagation()}
722
+ >
723
+ {/* Header */}
724
+ <div className="flex items-start gap-4 px-4 py-3 border-b border-border">
725
+ <div className="flex-1 flex gap-4 min-w-0">
726
+ <SideSummary log={left} side="left" />
727
+ <SideSummary log={right} side="right" />
728
+ </div>
729
+ <div className="flex items-center gap-2 shrink-0">
730
+ <ModeToggle mode={mode} onChange={setMode} />
731
+ <button
732
+ type="button"
733
+ onClick={onClose}
734
+ aria-label="Close"
735
+ className="p-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer"
736
+ >
737
+ <X className="size-4" />
738
+ </button>
739
+ </div>
740
+ </div>
741
+
742
+ {!sameSession && (
743
+ <div className="px-4 py-1.5 text-xs text-amber-400 bg-amber-500/10 border-b border-border">
744
+ Heads up: the two selected logs are from different sessions.
745
+ </div>
746
+ )}
747
+
748
+ {allEqual ? (
749
+ <div className="flex-1 min-h-0 overflow-y-auto flex items-center justify-center text-muted-foreground text-sm">
750
+ The two Request payloads are identical.
751
+ </div>
752
+ ) : (
753
+ <>
754
+ <SummaryChips counts={counts} onJumpTo={jumpToKind} />
755
+ <div ref={bodyRef} className="flex-1 min-h-0 overflow-y-auto">
756
+ {mode === "unified" ? (
757
+ <div className="px-3 py-2 space-y-0.5">
758
+ {grouped.map((g, i) => {
759
+ if (g.kind === "equal-run") {
760
+ return (
761
+ <EqualRunRow
762
+ key={`r${i}`}
763
+ ops={g.ops}
764
+ expanded={expandedRuns.has(i)}
765
+ onToggle={() => toggleRun(i)}
766
+ />
767
+ );
768
+ }
769
+ const op = g.op;
770
+ return (
771
+ <UnifiedOpRow
772
+ key={`o${i}`}
773
+ op={op}
774
+ idx={i}
775
+ copiedPath={copiedPath}
776
+ onCopyPath={onCopyPath}
777
+ expanded={expandedRows.has(i)}
778
+ onToggle={() => toggleRow(i)}
779
+ />
780
+ );
781
+ })}
782
+ </div>
783
+ ) : (
784
+ <SplitBody grouped={grouped} left={left} right={right} />
785
+ )}
786
+ </div>
787
+ </>
788
+ )}
789
+ </div>
790
+ </div>
791
+ );
792
+ }
793
+
794
+ /** Legacy 3-column (path | left | right) view, retained as a toggle option. */
795
+ function SplitBody({
796
+ grouped,
797
+ left,
798
+ right,
799
+ }: {
800
+ grouped: GroupedOp[];
801
+ left: CapturedLog;
802
+ right: CapturedLog;
803
+ }): JSX.Element {
804
+ return (
805
+ <div className="grid grid-cols-[200px_1fr_1fr] gap-x-2 gap-y-0.5 px-3 py-2 text-xs">
806
+ {/* Column headers */}
807
+ <div className="grid grid-cols-[200px_1fr_1fr] gap-x-2 col-span-3 pb-2 mb-2 border-b border-border text-[10px] uppercase tracking-wider text-muted-foreground">
808
+ <span>Path</span>
809
+ <span>Left (Log #{left.id})</span>
810
+ <span>Right (Log #{right.id})</span>
811
+ </div>
812
+
813
+ {grouped.map((g, i) => {
814
+ if (g.kind === "equal-run") {
815
+ return (
816
+ <div
817
+ // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
818
+ key={i}
819
+ className="col-span-3 px-2 py-1 text-xs text-muted-foreground/60"
820
+ >
821
+ {g.ops.length} equal siblings collapsed — switch to Unified to expand
822
+ </div>
823
+ );
824
+ }
825
+ const op = g.op;
826
+ if (op.kind === "equal") {
827
+ return (
828
+ <div
829
+ // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
830
+ key={i}
831
+ className="col-span-3 grid grid-cols-[200px_1fr_1fr] gap-x-2 px-2 py-0.5 text-muted-foreground"
832
+ >
833
+ <span className="font-mono text-xs truncate" title={op.path}>
834
+ {op.path}
835
+ </span>
836
+ <span className="font-mono text-xs break-all opacity-60">
837
+ {previewNode(op.value, 200)}
838
+ </span>
839
+ <span className="font-mono text-xs break-all opacity-60">
840
+ {previewNode(op.value, 200)}
841
+ </span>
842
+ </div>
843
+ );
844
+ }
845
+ if (op.kind === "added") {
846
+ return (
847
+ <div
848
+ // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
849
+ key={i}
850
+ className="col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-emerald-400/70 bg-emerald-500/5"
851
+ >
852
+ <div className="font-mono text-xs text-muted-foreground mb-0.5">{op.path}</div>
853
+ <div className="font-mono break-all text-emerald-300/90">
854
+ + {previewNode(op.value, 400)}
855
+ </div>
856
+ </div>
857
+ );
858
+ }
859
+ if (op.kind === "removed") {
860
+ return (
861
+ <div
862
+ // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
863
+ key={i}
864
+ className="col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-rose-400/70 bg-rose-500/5"
865
+ >
866
+ <div className="font-mono text-xs text-muted-foreground mb-0.5">{op.path}</div>
867
+ <div className="font-mono break-all text-rose-300/90 line-through">
868
+ − {previewNode(op.value, 400)}
869
+ </div>
870
+ </div>
871
+ );
872
+ }
873
+ return (
874
+ <div
875
+ // biome-ignore lint/suspicious/noArrayIndexKey: positional in the grouped list
876
+ key={i}
877
+ className="col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-amber-400/70 bg-amber-500/5"
878
+ >
879
+ <div className="font-mono text-xs text-muted-foreground mb-1">{op.path}</div>
880
+ <div className="grid grid-cols-2 gap-2">
881
+ <div className="font-mono text-rose-300/90 break-all line-through">
882
+ {previewNode(op.left, 400)}
883
+ </div>
884
+ <div className="font-mono text-emerald-300/90 break-all">
885
+ {previewNode(op.right, 400)}
886
+ </div>
887
+ </div>
888
+ </div>
889
+ );
890
+ })}
891
+ </div>
892
+ );
893
+ }