@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,363 @@
1
+ import { useState, useEffect, useCallback, useRef, useMemo, type JSX } from "react";
2
+ import { z } from "zod";
3
+ import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";
4
+ import { useStripConfig } from "../lib/useStripConfig";
5
+ import { OnboardingBanner } from "./OnboardingBanner";
6
+ import { ProxyViewer } from "./ProxyViewer";
7
+
8
+ type SSEUpdate =
9
+ | {
10
+ type: "init";
11
+ logs: CapturedLog[];
12
+ }
13
+ | {
14
+ type: "update";
15
+ log: CapturedLog;
16
+ };
17
+
18
+ const SSEUpdateSchema = z.union([
19
+ z.object({
20
+ type: z.literal("init"),
21
+ logs: z.array(CapturedLogSchema),
22
+ }),
23
+ z.object({
24
+ type: z.literal("update"),
25
+ log: CapturedLogSchema,
26
+ }),
27
+ ]);
28
+
29
+ function extractSessions(logs: CapturedLog[]): string[] {
30
+ const set = new Set<string>();
31
+ for (const l of logs) {
32
+ if (l.sessionId !== null && l.sessionId !== "") set.add(l.sessionId);
33
+ }
34
+ return [...set];
35
+ }
36
+
37
+ function extractModels(logs: CapturedLog[]): string[] {
38
+ const set = new Set<string>();
39
+ for (const l of logs) {
40
+ if (l.model !== null && l.model !== "") set.add(l.model);
41
+ }
42
+ return [...set];
43
+ }
44
+
45
+ /**
46
+ * Filter logs by selected session/model. Both `__all__` and missing values
47
+ * pass through. Returns the input array reference when no log matches the
48
+ * filter, so referential equality is preserved for empty results.
49
+ */
50
+ function filterLogs(
51
+ logs: CapturedLog[],
52
+ selectedSession: string,
53
+ selectedModel: string,
54
+ ): CapturedLog[] {
55
+ if (selectedSession === "__all__" && selectedModel === "__all__") return logs;
56
+ return logs.filter((l) => {
57
+ if (selectedSession !== "__all__" && l.sessionId !== selectedSession) return false;
58
+ if (selectedModel !== "__all__" && l.model !== selectedModel) return false;
59
+ return true;
60
+ });
61
+ }
62
+
63
+ const DEBOUNCE_MS = 50;
64
+ const HASH_SCROLL_ATTEMPTS = 12;
65
+ const HASH_HIGHLIGHT_MS = 1800;
66
+
67
+ function buildLogsStreamUrl(sessionId: string | undefined): string {
68
+ if (sessionId === undefined) return "/api/logs/stream";
69
+ const params = new URLSearchParams({ sessionId });
70
+ return `/api/logs/stream?${params.toString()}`;
71
+ }
72
+
73
+ export function ProxyViewerContainer({
74
+ initialSessionId,
75
+ }: {
76
+ /**
77
+ * Initial session filter. When the route is `/session/$id`, pass the URL
78
+ * param here so the page opens already scoped to that session. Default
79
+ * `__all__` keeps the existing "all sessions" behavior on `/`.
80
+ *
81
+ * NOTE: the value is only used as the initial state on mount. To pick up
82
+ * a new value (e.g. user navigates to a different `/session/$id`), the
83
+ * caller should also pass a `key` prop matching the session id, which
84
+ * forces a remount.
85
+ */
86
+ initialSessionId?: string;
87
+ } = {}): JSX.Element {
88
+ // `allLogs` is the unfiltered set populated by the SSE. The single SSE
89
+ // connection never re-opens on filter change — we always carry the full
90
+ // set and derive the displayed view with `useMemo` below.
91
+ const [allLogs, setAllLogs] = useState<CapturedLog[]>([]);
92
+ const [selectedSession, setSelectedSession] = useState(initialSessionId ?? "__all__");
93
+ const [selectedModel, setSelectedModel] = useState("__all__");
94
+ const [viewMode, setViewMode] = useState<"simple" | "full">("simple");
95
+ const [error, setError] = useState<string | null>(null);
96
+ const eventSourceRef = useRef<EventSource | null>(null);
97
+ const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
98
+ const handledHashRef = useRef<string | null>(null);
99
+
100
+ // O(1) log lookup by id
101
+ const logIndexRef = useRef<Map<number, number>>(new Map());
102
+
103
+ // Debounce buffer for SSE updates
104
+ const pendingUpdatesRef = useRef<CapturedLog[]>([]);
105
+ const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
106
+
107
+ // Derived view: the logs the user actually sees. Recomputed only when the
108
+ // underlying set or filter changes — never on every SSE message.
109
+ const logs = useMemo(
110
+ () => filterLogs(allLogs, selectedSession, selectedModel),
111
+ [allLogs, selectedSession, selectedModel],
112
+ );
113
+ const sessions = useMemo(() => extractSessions(allLogs), [allLogs]);
114
+ const models = useMemo(() => extractModels(allLogs), [allLogs]);
115
+
116
+ const flushUpdates = useCallback(() => {
117
+ flushTimerRef.current = null;
118
+ const updates = pendingUpdatesRef.current;
119
+ pendingUpdatesRef.current = [];
120
+ if (updates.length === 0) return;
121
+
122
+ setAllLogs((prev) => {
123
+ let next = prev;
124
+ for (const log of updates) {
125
+ const idx = logIndexRef.current.get(log.id);
126
+ if (idx !== undefined) {
127
+ // Replace existing entry
128
+ next = [...next.slice(0, idx), log, ...next.slice(idx + 1)];
129
+ } else {
130
+ logIndexRef.current.set(log.id, next.length);
131
+ next = [...next, log];
132
+ }
133
+ }
134
+ return next;
135
+ });
136
+ }, []);
137
+
138
+ const scheduleUpdate = useCallback(
139
+ (log: CapturedLog) => {
140
+ pendingUpdatesRef.current.push(log);
141
+ if (flushTimerRef.current === null) {
142
+ flushTimerRef.current = setTimeout(flushUpdates, DEBOUNCE_MS);
143
+ }
144
+ },
145
+ [flushUpdates],
146
+ );
147
+
148
+ const connectSSE = useCallback(() => {
149
+ // Clean up existing connection
150
+ if (eventSourceRef.current) {
151
+ eventSourceRef.current.close();
152
+ }
153
+
154
+ const es = new EventSource(buildLogsStreamUrl(initialSessionId));
155
+ eventSourceRef.current = es;
156
+
157
+ es.onmessage = (event: MessageEvent) => {
158
+ try {
159
+ const rawData = String(event.data);
160
+ const parsed: unknown = JSON.parse(rawData);
161
+ const updateResult = SSEUpdateSchema.safeParse(parsed);
162
+ if (!updateResult.success) {
163
+ setError("Failed to parse SSE data");
164
+ return;
165
+ }
166
+ const update = updateResult.data;
167
+ if (update.type === "init") {
168
+ // Flush any pending debounce
169
+ if (flushTimerRef.current !== null) {
170
+ clearTimeout(flushTimerRef.current);
171
+ flushTimerRef.current = null;
172
+ }
173
+ pendingUpdatesRef.current = [];
174
+
175
+ // Build fresh index
176
+ const idx = new Map<number, number>();
177
+ for (let i = 0; i < update.logs.length; i++) {
178
+ const log = update.logs[i];
179
+ if (log !== undefined) idx.set(log.id, i);
180
+ }
181
+ logIndexRef.current = idx;
182
+ setAllLogs(update.logs);
183
+ setError(null);
184
+ } else if (update.type === "update") {
185
+ scheduleUpdate(update.log);
186
+ }
187
+ } catch {
188
+ setError("Failed to parse SSE data");
189
+ }
190
+ };
191
+
192
+ es.onerror = () => {
193
+ setError("SSE connection lost, reconnecting...");
194
+ es.close();
195
+ if (reconnectTimeoutRef.current !== null) {
196
+ clearTimeout(reconnectTimeoutRef.current);
197
+ }
198
+ reconnectTimeoutRef.current = setTimeout(connectSSE, 3000);
199
+ };
200
+ }, [initialSessionId, scheduleUpdate]);
201
+
202
+ useEffect(() => {
203
+ connectSSE();
204
+ return () => {
205
+ if (eventSourceRef.current) {
206
+ eventSourceRef.current.close();
207
+ eventSourceRef.current = null;
208
+ }
209
+ if (reconnectTimeoutRef.current !== null) {
210
+ clearTimeout(reconnectTimeoutRef.current);
211
+ reconnectTimeoutRef.current = null;
212
+ }
213
+ if (flushTimerRef.current !== null) {
214
+ clearTimeout(flushTimerRef.current);
215
+ flushTimerRef.current = null;
216
+ }
217
+ };
218
+ }, [connectSSE]);
219
+
220
+ useEffect(() => {
221
+ const hash = window.location.hash;
222
+ if (!hash.startsWith("#log-")) return;
223
+ if (handledHashRef.current === hash) return;
224
+ const targetId = hash.slice(1);
225
+ let cancelled = false;
226
+ let attempts = 0;
227
+ let highlightedTarget: HTMLElement | null = null;
228
+ let highlightTimer: number | null = null;
229
+
230
+ const tryScrollToLog = (): void => {
231
+ if (cancelled) return;
232
+ const target = document.getElementById(targetId);
233
+ if (target !== null) {
234
+ handledHashRef.current = hash;
235
+ target.scrollIntoView({ block: "center", behavior: "smooth" });
236
+ if (target instanceof HTMLElement) {
237
+ highlightedTarget = target;
238
+ target.setAttribute("data-deep-link-highlight", "true");
239
+ highlightTimer = window.setTimeout(() => {
240
+ target.removeAttribute("data-deep-link-highlight");
241
+ }, HASH_HIGHLIGHT_MS);
242
+ target.focus({ preventScroll: true });
243
+ if (target.getAttribute("data-nav-action") === "expand") {
244
+ target.click();
245
+ }
246
+ }
247
+ return;
248
+ }
249
+
250
+ attempts += 1;
251
+ if (attempts < HASH_SCROLL_ATTEMPTS) {
252
+ window.setTimeout(tryScrollToLog, 100);
253
+ }
254
+ };
255
+
256
+ tryScrollToLog();
257
+
258
+ return () => {
259
+ cancelled = true;
260
+ if (highlightTimer !== null) {
261
+ window.clearTimeout(highlightTimer);
262
+ }
263
+ if (highlightedTarget !== null) {
264
+ highlightedTarget.removeAttribute("data-deep-link-highlight");
265
+ }
266
+ };
267
+ }, [logs.length]);
268
+
269
+ const handleClearAll = useCallback(() => {
270
+ if (initialSessionId !== undefined && allLogs.length === 0) return;
271
+ void (async () => {
272
+ try {
273
+ const body =
274
+ initialSessionId === undefined
275
+ ? undefined
276
+ : JSON.stringify({ ids: allLogs.map((log) => log.id) });
277
+ const res = await fetch("/api/logs", {
278
+ method: "DELETE",
279
+ ...(body === undefined
280
+ ? {}
281
+ : {
282
+ headers: { "Content-Type": "application/json" },
283
+ body,
284
+ }),
285
+ });
286
+ if (!res.ok) {
287
+ setError("Failed to clear logs");
288
+ return;
289
+ }
290
+ logIndexRef.current.clear();
291
+ setAllLogs([]);
292
+ setError(null);
293
+ } catch (err) {
294
+ setError(err instanceof Error ? err.message : "Unknown error clearing logs");
295
+ }
296
+ })();
297
+ }, [allLogs, initialSessionId]);
298
+
299
+ const handleClearGroup = useCallback((ids: number[]) => {
300
+ if (ids.length === 0) return;
301
+ void (async () => {
302
+ try {
303
+ const res = await fetch("/api/logs", {
304
+ method: "DELETE",
305
+ headers: { "Content-Type": "application/json" },
306
+ body: JSON.stringify({ ids }),
307
+ });
308
+ if (!res.ok) {
309
+ setError("Failed to clear group");
310
+ return;
311
+ }
312
+ const idSet = new Set(ids);
313
+ setAllLogs((prev) => {
314
+ const remaining = prev.filter((l) => !idSet.has(l.id));
315
+ // Rebuild index
316
+ const idx = new Map<number, number>();
317
+ for (let i = 0; i < remaining.length; i++) {
318
+ const log = remaining[i];
319
+ if (log !== undefined) idx.set(log.id, i);
320
+ }
321
+ logIndexRef.current = idx;
322
+ return remaining;
323
+ });
324
+ setError(null);
325
+ } catch (err) {
326
+ setError(err instanceof Error ? err.message : "Unknown error clearing group");
327
+ }
328
+ })();
329
+ }, []);
330
+
331
+ // Read the strip config once at the container so the virtualized list does
332
+ // not need N independent SWR subscriptions per row.
333
+ const { strip, slowResponseThresholdSeconds } = useStripConfig();
334
+
335
+ return (
336
+ <>
337
+ {error !== null && (
338
+ <div className="fixed top-4 right-4 bg-destructive text-destructive-foreground px-4 py-2 rounded-md text-sm z-50">
339
+ {error}
340
+ </div>
341
+ )}
342
+ <OnboardingBanner />
343
+ <ProxyViewer
344
+ logs={logs}
345
+ sessions={sessions}
346
+ models={models}
347
+ selectedSession={selectedSession}
348
+ selectedModel={selectedModel}
349
+ onSessionChange={setSelectedSession}
350
+ onModelChange={setSelectedModel}
351
+ onClearAll={handleClearAll}
352
+ onClearGroup={handleClearGroup}
353
+ viewMode={viewMode}
354
+ onViewModeChange={setViewMode}
355
+ strip={strip}
356
+ slowResponseThresholdSeconds={slowResponseThresholdSeconds}
357
+ // Session filter is the URL's job when `initialSessionId` was given.
358
+ hideSessionFilter={initialSessionId !== undefined}
359
+ pinnedSessionId={initialSessionId}
360
+ />
361
+ </>
362
+ );
363
+ }
@@ -0,0 +1,349 @@
1
+ import { type JSX, useState, useCallback, useEffect } from "react";
2
+ import { z } from "zod";
3
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
4
+ import { Button } from "../ui/button";
5
+
6
+ import { Loader2, Download, AlertCircle } from "lucide-react";
7
+
8
+ function ClaudeCodeIcon(): JSX.Element {
9
+ return (
10
+ <svg
11
+ fill="currentColor"
12
+ fillRule="evenodd"
13
+ viewBox="0 0 24 24"
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ className="size-6 shrink-0"
16
+ >
17
+ <title>Claude Code</title>
18
+ <path
19
+ clipRule="evenodd"
20
+ d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z"
21
+ />
22
+ </svg>
23
+ );
24
+ }
25
+
26
+ function OpenCodeIcon(): JSX.Element {
27
+ return (
28
+ <svg
29
+ fill="currentColor"
30
+ fillRule="evenodd"
31
+ viewBox="0 0 24 24"
32
+ xmlns="http://www.w3.org/2000/svg"
33
+ className="size-6 shrink-0"
34
+ >
35
+ <title>OpenCode</title>
36
+ <path d="M16 6H8v12h8V6zm4 16H4V2h16v20z" />
37
+ </svg>
38
+ );
39
+ }
40
+
41
+ function MiMoCodeIcon(): JSX.Element {
42
+ return (
43
+ <svg
44
+ fill="currentColor"
45
+ viewBox="0 0 24 24"
46
+ xmlns="http://www.w3.org/2000/svg"
47
+ className="size-6 shrink-0"
48
+ >
49
+ <title>MiMo Code</title>
50
+ <text
51
+ x="12"
52
+ y="18"
53
+ textAnchor="middle"
54
+ fontSize="16"
55
+ fontWeight="800"
56
+ fontFamily="system-ui, sans-serif"
57
+ >
58
+ M
59
+ </text>
60
+ </svg>
61
+ );
62
+ }
63
+
64
+ const ExternalProviderSchema = z.object({
65
+ name: z.string(),
66
+ apiKey: z.string(),
67
+ format: z.enum(["anthropic", "openai"]),
68
+ anthropicBaseUrl: z.string(),
69
+ openaiBaseUrl: z.string(),
70
+ models: z.array(z.string()),
71
+ sourceTool: z.enum(["claude-code", "opencode", "mimo-code"]),
72
+ alreadyExists: z.boolean(),
73
+ });
74
+
75
+ const ScanResponseSchema = z.object({
76
+ providers: z.array(ExternalProviderSchema),
77
+ warnings: z.array(z.string()).optional(),
78
+ });
79
+
80
+ const ImportResponseSchema = z.object({
81
+ success: z.boolean().optional(),
82
+ imported: z.number().optional(),
83
+ message: z.string().optional(),
84
+ errors: z.array(z.string()).optional(),
85
+ });
86
+
87
+ const sourceLogoMap: Record<string, () => JSX.Element> = {
88
+ "claude-code": ClaudeCodeIcon,
89
+ opencode: OpenCodeIcon,
90
+ "mimo-code": MiMoCodeIcon,
91
+ };
92
+
93
+ type ImportWizardDialogProps = {
94
+ open: boolean;
95
+ onOpenChange: (open: boolean) => void;
96
+ onImportComplete: () => void;
97
+ };
98
+
99
+ export function ImportWizardDialog({
100
+ open,
101
+ onOpenChange,
102
+ onImportComplete,
103
+ }: ImportWizardDialogProps): JSX.Element {
104
+ const [scanning, setScanning] = useState(false);
105
+ const [scanError, setScanError] = useState<string | null>(null);
106
+ const [providers, setProviders] = useState<z.infer<typeof ExternalProviderSchema>[]>([]);
107
+ const [warnings, setWarnings] = useState<string[]>([]);
108
+ const [selected, setSelected] = useState<Set<number>>(new Set());
109
+ const [importing, setImporting] = useState(false);
110
+ const [importResult, setImportResult] = useState<string | null>(null);
111
+ const [importError, setImportError] = useState<string | null>(null);
112
+
113
+ const scan = useCallback(() => {
114
+ setScanning(true);
115
+ setScanError(null);
116
+ setWarnings([]);
117
+ setProviders([]);
118
+ setSelected(new Set());
119
+ setImportResult(null);
120
+ setImportError(null);
121
+
122
+ fetch("/api/providers/scan")
123
+ .then((res) => {
124
+ if (!res.ok) {
125
+ return res.text().then((text) => {
126
+ setScanError(`Scan failed (${res.status}): ${text}`);
127
+ });
128
+ }
129
+ return res.json().then((data: unknown) => {
130
+ const parsed = ScanResponseSchema.safeParse(data);
131
+ if (!parsed.success) {
132
+ setScanError(`Invalid response: ${parsed.error.message}`);
133
+ return;
134
+ }
135
+ setProviders(parsed.data.providers);
136
+ setWarnings(parsed.data.warnings ?? []);
137
+ // Pre-select non-existing providers
138
+ const indices = new Set<number>();
139
+ parsed.data.providers.forEach((p, i) => {
140
+ if (!p.alreadyExists) indices.add(i);
141
+ });
142
+ setSelected(indices);
143
+ });
144
+ })
145
+ .catch((err: unknown) => {
146
+ setScanError(err instanceof Error ? err.message : String(err));
147
+ })
148
+ .finally(() => {
149
+ setScanning(false);
150
+ });
151
+ }, []);
152
+
153
+ // Auto-scan when dialog opens
154
+ useEffect(() => {
155
+ if (open) {
156
+ scan();
157
+ }
158
+ }, [open, scan]);
159
+
160
+ const toggleProvider = useCallback((index: number) => {
161
+ setSelected((prev) => {
162
+ const next = new Set(prev);
163
+ if (next.has(index)) {
164
+ next.delete(index);
165
+ } else {
166
+ next.add(index);
167
+ }
168
+ return next;
169
+ });
170
+ }, []);
171
+
172
+ const importSelected = useCallback(() => {
173
+ const toImport = providers.filter((_, i) => selected.has(i));
174
+ if (toImport.length === 0) return;
175
+
176
+ setImporting(true);
177
+ setImportError(null);
178
+ setImportResult(null);
179
+
180
+ // Convert ExternalProvider[] to ProviderConfig shape for import
181
+ const now = new Date().toISOString();
182
+ const providersPayload = toImport.map((p) => ({
183
+ id: window.crypto.randomUUID(),
184
+ name: p.name,
185
+ apiKey: p.apiKey,
186
+ format: p.format,
187
+ anthropicBaseUrl: p.anthropicBaseUrl,
188
+ openaiBaseUrl: p.openaiBaseUrl,
189
+ models: p.models,
190
+ createdAt: now,
191
+ updatedAt: now,
192
+ }));
193
+
194
+ fetch("/api/providers/import", {
195
+ method: "POST",
196
+ headers: { "Content-Type": "application/json" },
197
+ body: JSON.stringify({ providers: providersPayload }),
198
+ })
199
+ .then((res) => res.json())
200
+ .then((data: unknown) => {
201
+ const parsed = ImportResponseSchema.safeParse(data);
202
+ if (!parsed.success) {
203
+ setImportError(`Invalid response: ${parsed.error.message}`);
204
+ return;
205
+ }
206
+ const result = parsed.data;
207
+ // Mark imported providers as already existing so they can't be re-selected
208
+ setProviders((prev) =>
209
+ prev.map((p, i) => (selected.has(i) ? { ...p, alreadyExists: true } : p)),
210
+ );
211
+ setSelected(new Set());
212
+
213
+ if (
214
+ result.errors !== undefined &&
215
+ result.errors.length > 0 &&
216
+ result.message !== undefined
217
+ ) {
218
+ setImportResult(result.message);
219
+ } else {
220
+ setImportResult(`Imported ${result.imported ?? toImport.length} provider(s)`);
221
+ }
222
+ onImportComplete();
223
+ })
224
+ .catch((err: unknown) => {
225
+ setImportError(err instanceof Error ? err.message : String(err));
226
+ })
227
+ .finally(() => {
228
+ setImporting(false);
229
+ });
230
+ }, [providers, selected, onImportComplete]);
231
+
232
+ const hasSelectable = providers.some((p) => !p.alreadyExists);
233
+
234
+ return (
235
+ <Dialog open={open} onOpenChange={onOpenChange}>
236
+ <DialogContent className="max-w-xl max-h-[80vh] overflow-hidden flex flex-col">
237
+ <DialogHeader>
238
+ <DialogTitle>Import from External Tools</DialogTitle>
239
+ <p className="text-xs text-muted-foreground">
240
+ Detect provider configurations from Claude Code, OpenCode, and MiMo Code.
241
+ </p>
242
+ </DialogHeader>
243
+
244
+ <div className="flex-1 overflow-y-auto space-y-3">
245
+ {scanning && (
246
+ <div className="flex items-center justify-center py-8 gap-2 text-muted-foreground">
247
+ <Loader2 className="size-4 animate-spin" />
248
+ <span className="text-sm">Scanning for external providers...</span>
249
+ </div>
250
+ )}
251
+
252
+ {scanError !== null && (
253
+ <div className="flex items-center gap-2 text-destructive text-sm py-4">
254
+ <AlertCircle className="size-4" />
255
+ {scanError}
256
+ </div>
257
+ )}
258
+
259
+ {!scanning && scanError === null && providers.length === 0 && (
260
+ <p className="text-sm text-muted-foreground py-8 text-center">
261
+ No external provider configurations found.
262
+ <br />
263
+ <span className="text-xs">
264
+ Supported tools: Claude Code (~/.claude/settings.json), OpenCode
265
+ (~/.config/opencode/opencode.json), MiMo Code (~/.config/mimocode/mimocode.jsonc)
266
+ </span>
267
+ </p>
268
+ )}
269
+
270
+ {!scanning &&
271
+ providers.map((p, i) => (
272
+ <label
273
+ key={`${p.sourceTool}-${p.name}`}
274
+ className={`flex items-start gap-3 p-3 border rounded-md ${p.alreadyExists ? "" : "cursor-pointer hover:bg-muted/50"}`}
275
+ >
276
+ {!p.alreadyExists && (
277
+ <input
278
+ type="checkbox"
279
+ checked={selected.has(i)}
280
+ disabled={importing}
281
+ onChange={() => toggleProvider(i)}
282
+ className="mt-0.5 size-4"
283
+ />
284
+ )}
285
+ <div className="flex-1 min-w-0">
286
+ <div className="flex items-center justify-between">
287
+ {(() => {
288
+ const LogoComponent = sourceLogoMap[p.sourceTool];
289
+ if (LogoComponent === undefined) return null;
290
+ return <LogoComponent />;
291
+ })()}
292
+ {p.alreadyExists && (
293
+ <span className="text-[10px] text-muted-foreground">Already added</span>
294
+ )}
295
+ </div>
296
+ <div className="text-xs text-muted-foreground mt-1 truncate">
297
+ {p.models.slice(0, 4).join(", ")}
298
+ {p.models.length > 4 ? ` +${p.models.length - 4} more` : ""}
299
+ </div>
300
+ <div className="text-xs text-muted-foreground mt-0.5 truncate">
301
+ {p.format === "anthropic" ? p.anthropicBaseUrl : p.openaiBaseUrl}
302
+ </div>
303
+ <div className="text-xs text-muted-foreground mt-0.5 font-mono">
304
+ {p.apiKey.length > 8
305
+ ? `${p.apiKey.slice(0, 4)}••••${p.apiKey.slice(-4)}`
306
+ : "••••"}
307
+ </div>
308
+ </div>
309
+ </label>
310
+ ))}
311
+
312
+ {warnings.length > 0 && (
313
+ <div className="text-xs text-muted-foreground space-y-1 border-t pt-2">
314
+ {warnings.map((w, i) => (
315
+ <div key={i} className="flex items-center gap-1">
316
+ <AlertCircle className="size-3" />
317
+ {w}
318
+ </div>
319
+ ))}
320
+ </div>
321
+ )}
322
+
323
+ {importResult !== null && (
324
+ <div className="text-sm text-green-500 border-t pt-2">{importResult}</div>
325
+ )}
326
+
327
+ {importError !== null && (
328
+ <div className="text-sm text-destructive border-t pt-2">{importError}</div>
329
+ )}
330
+ </div>
331
+
332
+ <div className="flex items-center justify-between pt-3 border-t">
333
+ <Button variant="outline" size="sm" onClick={scan} disabled={scanning}>
334
+ <Loader2 className={`size-3 mr-1 ${scanning ? "animate-spin" : ""}`} />
335
+ Rescan
336
+ </Button>
337
+ <Button
338
+ size="sm"
339
+ onClick={importSelected}
340
+ disabled={!hasSelectable || selected.size === 0 || importing}
341
+ >
342
+ <Download className="size-3 mr-1" />
343
+ {importing ? "Importing..." : `Import Selected (${selected.size})`}
344
+ </Button>
345
+ </div>
346
+ </DialogContent>
347
+ </Dialog>
348
+ );
349
+ }