@aprovan/patchwork-web 0.1.0-dev.4d82df8

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 (330) hide show
  1. package/.turbo/turbo-build.log +327 -0
  2. package/.utcp_config.json +65 -0
  3. package/LICENSE +373 -0
  4. package/dist/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  5. package/dist/assets/abap-BdImnpbu.js +1 -0
  6. package/dist/assets/actionscript-3-CoDkCxhg.js +1 -0
  7. package/dist/assets/ada-bCR0ucgS.js +1 -0
  8. package/dist/assets/andromeeda-C4gqWexZ.js +1 -0
  9. package/dist/assets/angular-html-CU67Zn6k.js +1 -0
  10. package/dist/assets/angular-ts-BwZT4LLn.js +1 -0
  11. package/dist/assets/apache-Pmp26Uib.js +1 -0
  12. package/dist/assets/apex-D8_7TLub.js +1 -0
  13. package/dist/assets/apl-dKokRX4l.js +1 -0
  14. package/dist/assets/applescript-Co6uUVPk.js +1 -0
  15. package/dist/assets/ara-BRHolxvo.js +1 -0
  16. package/dist/assets/asciidoc-Ve4PFQV2.js +1 -0
  17. package/dist/assets/asm-D_Q5rh1f.js +1 -0
  18. package/dist/assets/astro-CbQHKStN.js +1 -0
  19. package/dist/assets/aurora-x-D-2ljcwZ.js +1 -0
  20. package/dist/assets/awk-DMzUqQB5.js +1 -0
  21. package/dist/assets/ayu-dark-DYE7WIF3.js +1 -0
  22. package/dist/assets/ayu-light-BA47KaF1.js +1 -0
  23. package/dist/assets/ayu-mirage-32ctXXKs.js +1 -0
  24. package/dist/assets/ballerina-BFfxhgS-.js +1 -0
  25. package/dist/assets/bat-BkioyH1T.js +1 -0
  26. package/dist/assets/beancount-k_qm7-4y.js +1 -0
  27. package/dist/assets/berry-uYugtg8r.js +1 -0
  28. package/dist/assets/bibtex-CHM0blh-.js +1 -0
  29. package/dist/assets/bicep-Bmn6On1c.js +1 -0
  30. package/dist/assets/bird2-DPOp833l.js +1 -0
  31. package/dist/assets/blade-D4QpJJKB.js +1 -0
  32. package/dist/assets/bsl-BO_Y6i37.js +1 -0
  33. package/dist/assets/c-BIGW1oBm.js +1 -0
  34. package/dist/assets/c3-eo99z4R2.js +1 -0
  35. package/dist/assets/cadence-Bv_4Rxtq.js +1 -0
  36. package/dist/assets/cairo-KRGpt6FW.js +1 -0
  37. package/dist/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  38. package/dist/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  39. package/dist/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  40. package/dist/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  41. package/dist/assets/clarity-D53aC0YG.js +1 -0
  42. package/dist/assets/clojure-P80f7IUj.js +1 -0
  43. package/dist/assets/cmake-D1j8_8rp.js +1 -0
  44. package/dist/assets/cobol-nwyudZeR.js +1 -0
  45. package/dist/assets/codeowners-Bp6g37R7.js +1 -0
  46. package/dist/assets/codeql-DsOJ9woJ.js +1 -0
  47. package/dist/assets/coffee-Ch7k5sss.js +1 -0
  48. package/dist/assets/common-lisp-Cg-RD9OK.js +1 -0
  49. package/dist/assets/coq-DkFqJrB1.js +1 -0
  50. package/dist/assets/cpp-CofmeUqb.js +1 -0
  51. package/dist/assets/crystal-tKQVLTB8.js +1 -0
  52. package/dist/assets/csharp-COcwbKMJ.js +1 -0
  53. package/dist/assets/css-DPfMkruS.js +1 -0
  54. package/dist/assets/csv-fuZLfV_i.js +1 -0
  55. package/dist/assets/cue-D82EKSYY.js +1 -0
  56. package/dist/assets/cypher-COkxafJQ.js +1 -0
  57. package/dist/assets/d-85-TOEBH.js +1 -0
  58. package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
  59. package/dist/assets/dart-CF10PKvl.js +1 -0
  60. package/dist/assets/dax-CEL-wOlO.js +1 -0
  61. package/dist/assets/desktop-BmXAJ9_W.js +1 -0
  62. package/dist/assets/diff-D97Zzqfu.js +1 -0
  63. package/dist/assets/docker-BcOcwvcX.js +1 -0
  64. package/dist/assets/dotenv-Da5cRb03.js +1 -0
  65. package/dist/assets/dracula-BzJJZx-M.js +1 -0
  66. package/dist/assets/dracula-soft-BXkSAIEj.js +1 -0
  67. package/dist/assets/dream-maker-BtqSS_iP.js +1 -0
  68. package/dist/assets/edge-BkV0erSs.js +1 -0
  69. package/dist/assets/elixir-CDX3lj18.js +1 -0
  70. package/dist/assets/elm-DbKCFpqz.js +1 -0
  71. package/dist/assets/emacs-lisp-C9XAeP06.js +1 -0
  72. package/dist/assets/erb-B12qg9BL.js +1 -0
  73. package/dist/assets/erlang-DsQrWhSR.js +1 -0
  74. package/dist/assets/everforest-dark-BgDCqdQA.js +1 -0
  75. package/dist/assets/everforest-light-C8M2exoo.js +1 -0
  76. package/dist/assets/fennel-BYunw83y.js +1 -0
  77. package/dist/assets/fish-BvzEVeQv.js +1 -0
  78. package/dist/assets/fluent-C4IJs8-o.js +1 -0
  79. package/dist/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  80. package/dist/assets/fortran-free-form-BxgE0vQu.js +1 -0
  81. package/dist/assets/fsharp-CXgrBDvD.js +1 -0
  82. package/dist/assets/gdresource-BOOCDP_w.js +1 -0
  83. package/dist/assets/gdscript-C5YyOfLZ.js +1 -0
  84. package/dist/assets/gdshader-DkwncUOv.js +1 -0
  85. package/dist/assets/genie-D0YGMca9.js +1 -0
  86. package/dist/assets/gherkin-DyxjwDmM.js +1 -0
  87. package/dist/assets/git-commit-F4YmCXRG.js +1 -0
  88. package/dist/assets/git-rebase-r7XF79zn.js +1 -0
  89. package/dist/assets/github-dark-DHJKELXO.js +1 -0
  90. package/dist/assets/github-dark-default-Cuk6v7N8.js +1 -0
  91. package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  92. package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  93. package/dist/assets/github-light-DAi9KRSo.js +1 -0
  94. package/dist/assets/github-light-default-D7oLnXFd.js +1 -0
  95. package/dist/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  96. package/dist/assets/gleam-BspZqrRM.js +1 -0
  97. package/dist/assets/glimmer-js-Rg0-pVw9.js +1 -0
  98. package/dist/assets/glimmer-ts-U6CK756n.js +1 -0
  99. package/dist/assets/glsl-DplSGwfg.js +1 -0
  100. package/dist/assets/gn-n2N0HUVH.js +1 -0
  101. package/dist/assets/gnuplot-DdkO51Og.js +1 -0
  102. package/dist/assets/go-CxLEBnE3.js +1 -0
  103. package/dist/assets/graphql-ChdNCCLP.js +1 -0
  104. package/dist/assets/groovy-gcz8RCvz.js +1 -0
  105. package/dist/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  106. package/dist/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  107. package/dist/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  108. package/dist/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  109. package/dist/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  110. package/dist/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  111. package/dist/assets/hack-CaT9iCJl.js +1 -0
  112. package/dist/assets/haml-B8DHNrY2.js +1 -0
  113. package/dist/assets/handlebars-BL8al0AC.js +1 -0
  114. package/dist/assets/haskell-Df6bDoY_.js +1 -0
  115. package/dist/assets/haxe-CzTSHFRz.js +1 -0
  116. package/dist/assets/hcl-BWvSN4gD.js +1 -0
  117. package/dist/assets/hjson-D5-asLiD.js +1 -0
  118. package/dist/assets/hlsl-D3lLCCz7.js +1 -0
  119. package/dist/assets/horizon-BUw7H-hv.js +1 -0
  120. package/dist/assets/horizon-bright-Cn-bp-IR.js +1 -0
  121. package/dist/assets/houston-DnULxvSX.js +1 -0
  122. package/dist/assets/html-GMplVEZG.js +1 -0
  123. package/dist/assets/html-derivative-BFtXZ54Q.js +1 -0
  124. package/dist/assets/http-jrhK8wxY.js +1 -0
  125. package/dist/assets/hurl-irOxFIW8.js +1 -0
  126. package/dist/assets/hxml-Bvhsp5Yf.js +1 -0
  127. package/dist/assets/hy-DFXneXwc.js +1 -0
  128. package/dist/assets/imba-DGztddWO.js +1 -0
  129. package/dist/assets/index-B_lEiI-9.js +1544 -0
  130. package/dist/assets/index-CQT9NtoK.css +1 -0
  131. package/dist/assets/ini-BEwlwnbL.js +1 -0
  132. package/dist/assets/java-CylS5w8V.js +1 -0
  133. package/dist/assets/javascript-wDzz0qaB.js +1 -0
  134. package/dist/assets/jinja-4LBKfQ-Z.js +1 -0
  135. package/dist/assets/jison-wvAkD_A8.js +1 -0
  136. package/dist/assets/json-Cp-IABpG.js +1 -0
  137. package/dist/assets/json5-C9tS-k6U.js +1 -0
  138. package/dist/assets/jsonc-Des-eS-w.js +1 -0
  139. package/dist/assets/jsonl-DcaNXYhu.js +1 -0
  140. package/dist/assets/jsonnet-DFQXde-d.js +1 -0
  141. package/dist/assets/jssm-C2t-YnRu.js +1 -0
  142. package/dist/assets/jsx-g9-lgVsj.js +1 -0
  143. package/dist/assets/julia-CxzCAyBv.js +1 -0
  144. package/dist/assets/just-Cw27pwNe.js +1 -0
  145. package/dist/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  146. package/dist/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  147. package/dist/assets/kanagawa-wave-DWedfzmr.js +1 -0
  148. package/dist/assets/kdl-DV7GczEv.js +1 -0
  149. package/dist/assets/kotlin-BdnUsdx6.js +1 -0
  150. package/dist/assets/kusto-DZf3V79B.js +1 -0
  151. package/dist/assets/laserwave-DUszq2jm.js +1 -0
  152. package/dist/assets/latex-CWtU0Tv5.js +1 -0
  153. package/dist/assets/lean-BZvkOJ9d.js +1 -0
  154. package/dist/assets/less-B1dDrJ26.js +1 -0
  155. package/dist/assets/light-plus-B7mTdjB0.js +1 -0
  156. package/dist/assets/liquid-DYVedYrR.js +1 -0
  157. package/dist/assets/llvm-DjAJT7YJ.js +1 -0
  158. package/dist/assets/log-2UxHyX5q.js +1 -0
  159. package/dist/assets/logo-BtOb2qkB.js +1 -0
  160. package/dist/assets/lua-BaeVxFsk.js +1 -0
  161. package/dist/assets/luau-C-HG3fhB.js +1 -0
  162. package/dist/assets/make-CHLpvVh8.js +1 -0
  163. package/dist/assets/markdown-Cvjx9yec.js +1 -0
  164. package/dist/assets/marko-CnJfTvn9.js +1 -0
  165. package/dist/assets/material-theme-D5KoaKCx.js +1 -0
  166. package/dist/assets/material-theme-darker-BfHTSMKl.js +1 -0
  167. package/dist/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  168. package/dist/assets/material-theme-ocean-CyktbL80.js +1 -0
  169. package/dist/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  170. package/dist/assets/matlab-D7o27uSR.js +1 -0
  171. package/dist/assets/mdc-BMNejdWA.js +1 -0
  172. package/dist/assets/mdx-Cmh6b_Ma.js +1 -0
  173. package/dist/assets/mermaid-mWjccvbQ.js +1 -0
  174. package/dist/assets/min-dark-CafNBF8u.js +1 -0
  175. package/dist/assets/min-light-CTRr51gU.js +1 -0
  176. package/dist/assets/mipsasm-CKIfxQSi.js +1 -0
  177. package/dist/assets/mojo-rZm6bMo-.js +1 -0
  178. package/dist/assets/monokai-D4h5O-jR.js +1 -0
  179. package/dist/assets/moonbit-_H4v1dQx.js +1 -0
  180. package/dist/assets/move-IF9eRakj.js +1 -0
  181. package/dist/assets/narrat-DRg8JJMk.js +1 -0
  182. package/dist/assets/nextflow-Zz6hmt5N.js +1 -0
  183. package/dist/assets/nextflow-groovy-BeH2EWoN.js +1 -0
  184. package/dist/assets/nginx-BpAMiNFr.js +1 -0
  185. package/dist/assets/night-owl-C39BiMTA.js +1 -0
  186. package/dist/assets/night-owl-light-CMTm3GFP.js +1 -0
  187. package/dist/assets/nim-CVrawwO9.js +1 -0
  188. package/dist/assets/nix-CwoSXNpI.js +1 -0
  189. package/dist/assets/nord-Ddv68eIx.js +1 -0
  190. package/dist/assets/nushell-Cz2AlsmD.js +1 -0
  191. package/dist/assets/objective-c-DXmwc3jG.js +1 -0
  192. package/dist/assets/objective-cpp-CLxacb5B.js +1 -0
  193. package/dist/assets/ocaml-C0hk2d4L.js +1 -0
  194. package/dist/assets/odin-BBf5iR-q.js +1 -0
  195. package/dist/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  196. package/dist/assets/one-light-C3Wv6jpd.js +1 -0
  197. package/dist/assets/openscad-C4EeE6gA.js +1 -0
  198. package/dist/assets/pascal-D93ZcfNL.js +1 -0
  199. package/dist/assets/perl-C0TMdlhV.js +1 -0
  200. package/dist/assets/php-Dhbhpdrm.js +1 -0
  201. package/dist/assets/pkl-u5AG7uiY.js +1 -0
  202. package/dist/assets/plastic-3e1v2bzS.js +1 -0
  203. package/dist/assets/plsql-ChMvpjG-.js +1 -0
  204. package/dist/assets/po-BTJTHyun.js +1 -0
  205. package/dist/assets/poimandres-CS3Unz2-.js +1 -0
  206. package/dist/assets/polar-C0HS_06l.js +1 -0
  207. package/dist/assets/postcss-CXtECtnM.js +1 -0
  208. package/dist/assets/powerquery-CEu0bR-o.js +1 -0
  209. package/dist/assets/powershell-Dpen1YoG.js +1 -0
  210. package/dist/assets/prisma-Dd19v3D-.js +1 -0
  211. package/dist/assets/prolog-CbFg5uaA.js +1 -0
  212. package/dist/assets/proto-C7zT0LnQ.js +1 -0
  213. package/dist/assets/pug-CGlum2m_.js +1 -0
  214. package/dist/assets/puppet-BMWR74SV.js +1 -0
  215. package/dist/assets/purescript-CklMAg4u.js +1 -0
  216. package/dist/assets/python-B6aJPvgy.js +1 -0
  217. package/dist/assets/qml-3beO22l8.js +1 -0
  218. package/dist/assets/qmldir-C8lEn-DE.js +1 -0
  219. package/dist/assets/qss-IeuSbFQv.js +1 -0
  220. package/dist/assets/r-Dspwwk_N.js +1 -0
  221. package/dist/assets/racket-BqYA7rlc.js +1 -0
  222. package/dist/assets/raku-DXvB9xmW.js +1 -0
  223. package/dist/assets/razor-Uh8Bk_45.js +1 -0
  224. package/dist/assets/red-bN70gL4F.js +1 -0
  225. package/dist/assets/reg-C-SQnVFl.js +1 -0
  226. package/dist/assets/regexp-CDVJQ6XC.js +1 -0
  227. package/dist/assets/rel-C3B-1QV4.js +1 -0
  228. package/dist/assets/riscv-BM1_JUlF.js +1 -0
  229. package/dist/assets/ron-D8l8udqQ.js +1 -0
  230. package/dist/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  231. package/dist/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  232. package/dist/assets/rose-pine-qdsjHGoJ.js +1 -0
  233. package/dist/assets/rosmsg-BJDFO7_C.js +1 -0
  234. package/dist/assets/rst-BrH8l1NY.js +1 -0
  235. package/dist/assets/ruby-Dw2BHqvy.js +1 -0
  236. package/dist/assets/rust-B1yitclQ.js +1 -0
  237. package/dist/assets/sas-cz2c8ADy.js +1 -0
  238. package/dist/assets/sass-Cj5Yp3dK.js +1 -0
  239. package/dist/assets/scala-C151Ov-r.js +1 -0
  240. package/dist/assets/scheme-C98Dy4si.js +1 -0
  241. package/dist/assets/scss-OYdSNvt2.js +1 -0
  242. package/dist/assets/sdbl-DVxCFoDh.js +1 -0
  243. package/dist/assets/shaderlab-Dg9Lc6iA.js +1 -0
  244. package/dist/assets/shellscript-Yzrsuije.js +1 -0
  245. package/dist/assets/shellsession-BADoaaVG.js +1 -0
  246. package/dist/assets/slack-dark-BthQWCQV.js +1 -0
  247. package/dist/assets/slack-ochin-DqwNpetd.js +1 -0
  248. package/dist/assets/smalltalk-BERRCDM3.js +1 -0
  249. package/dist/assets/snazzy-light-Bw305WKR.js +1 -0
  250. package/dist/assets/solarized-dark-DXbdFlpD.js +1 -0
  251. package/dist/assets/solarized-light-L9t79GZl.js +1 -0
  252. package/dist/assets/solidity-rGO070M0.js +1 -0
  253. package/dist/assets/soy-Brmx7dQM.js +1 -0
  254. package/dist/assets/sparql-rVzFXLq3.js +1 -0
  255. package/dist/assets/splunk-BtCnVYZw.js +1 -0
  256. package/dist/assets/sql-BLtJtn59.js +1 -0
  257. package/dist/assets/ssh-config-_ykCGR6B.js +1 -0
  258. package/dist/assets/stata-BH5u7GGu.js +1 -0
  259. package/dist/assets/stylus-BEDo0Tqx.js +1 -0
  260. package/dist/assets/surrealql-Bq5Q-fJD.js +1 -0
  261. package/dist/assets/svelte-C_ipcX3V.js +1 -0
  262. package/dist/assets/swift-D82vCrfD.js +1 -0
  263. package/dist/assets/synthwave-84-CbfX1IO0.js +1 -0
  264. package/dist/assets/system-verilog-CnnmHF94.js +1 -0
  265. package/dist/assets/systemd-4A_iFExJ.js +1 -0
  266. package/dist/assets/talonscript-CkByrt1z.js +1 -0
  267. package/dist/assets/tasl-QIJgUcNo.js +1 -0
  268. package/dist/assets/tcl-dwOrl1Do.js +1 -0
  269. package/dist/assets/templ-P3uqSqPl.js +1 -0
  270. package/dist/assets/terraform-BETggiCN.js +1 -0
  271. package/dist/assets/tex-idrVyKtj.js +1 -0
  272. package/dist/assets/tokyo-night-hegEt444.js +1 -0
  273. package/dist/assets/toml-vGWfd6FD.js +1 -0
  274. package/dist/assets/ts-tags-zn1MmPIZ.js +1 -0
  275. package/dist/assets/tsv-B_m7g4N7.js +1 -0
  276. package/dist/assets/tsx-COt5Ahok.js +1 -0
  277. package/dist/assets/turtle-BsS91CYL.js +1 -0
  278. package/dist/assets/twig-DNn4PbVi.js +1 -0
  279. package/dist/assets/typescript-BPQ3VLAy.js +1 -0
  280. package/dist/assets/typespec-BGHnOYBU.js +1 -0
  281. package/dist/assets/typst-DHCkPAjA.js +1 -0
  282. package/dist/assets/v-BcVCzyr7.js +1 -0
  283. package/dist/assets/vala-CsfeWuGM.js +1 -0
  284. package/dist/assets/vb-D17OF-Vu.js +1 -0
  285. package/dist/assets/verilog-BQ8w6xss.js +1 -0
  286. package/dist/assets/vesper-DU1UobuO.js +1 -0
  287. package/dist/assets/vhdl-CeAyd5Ju.js +1 -0
  288. package/dist/assets/viml-CJc9bBzg.js +1 -0
  289. package/dist/assets/vitesse-black-Bkuqu6BP.js +1 -0
  290. package/dist/assets/vitesse-dark-D0r3Knsf.js +1 -0
  291. package/dist/assets/vitesse-light-CVO1_9PV.js +1 -0
  292. package/dist/assets/vue-DN_0RTcg.js +1 -0
  293. package/dist/assets/vue-html-AaS7Mt5G.js +1 -0
  294. package/dist/assets/vue-vine-CQOfvN7w.js +1 -0
  295. package/dist/assets/vyper-CDx5xZoG.js +1 -0
  296. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  297. package/dist/assets/wasm-MzD3tlZU.js +1 -0
  298. package/dist/assets/wenyan-BV7otONQ.js +1 -0
  299. package/dist/assets/wgsl-Dx-B1_4e.js +1 -0
  300. package/dist/assets/wikitext-BhOHFoWU.js +1 -0
  301. package/dist/assets/wit-5i3qLPDT.js +1 -0
  302. package/dist/assets/wolfram-lXgVvXCa.js +1 -0
  303. package/dist/assets/xml-sdJ4AIDG.js +1 -0
  304. package/dist/assets/xsl-CtQFsRM5.js +1 -0
  305. package/dist/assets/yaml-Buea-lGh.js +1 -0
  306. package/dist/assets/zenscript-DVFEvuxE.js +1 -0
  307. package/dist/assets/zig-VOosw3JB.js +1 -0
  308. package/dist/index.html +18 -0
  309. package/index.html +17 -0
  310. package/package.json +54 -0
  311. package/postcss.config.js +6 -0
  312. package/scripts/start-dev.ts +68 -0
  313. package/src/App.tsx +7 -0
  314. package/src/components/WorkspaceSwitcher.tsx +125 -0
  315. package/src/components/ui/avatar.tsx +47 -0
  316. package/src/components/ui/badge.tsx +35 -0
  317. package/src/components/ui/button.tsx +55 -0
  318. package/src/components/ui/card.tsx +85 -0
  319. package/src/components/ui/collapsible.tsx +9 -0
  320. package/src/components/ui/dialog.tsx +60 -0
  321. package/src/components/ui/input.tsx +24 -0
  322. package/src/components/ui/scroll-area.tsx +45 -0
  323. package/src/index.css +190 -0
  324. package/src/lib/utils.ts +6 -0
  325. package/src/lib/workspace-vfs.ts +190 -0
  326. package/src/main.tsx +10 -0
  327. package/src/pages/ChatPage.tsx +1098 -0
  328. package/tailwind.config.js +73 -0
  329. package/tsconfig.json +22 -0
  330. package/vite.config.ts +18 -0
@@ -0,0 +1,1098 @@
1
+ import { useChat } from "@ai-sdk/react";
2
+ import { Bobbin } from '@aprovan/bobbin';
3
+ import { createCompiler, type Compiler , type VirtualProject } from "@aprovan/patchwork-compiler";
4
+ import {
5
+ extractCodeBlocks,
6
+ CodePreview,
7
+ WidgetPreview,
8
+ MarkdownEditor,
9
+ ServicesInspector,
10
+ EditModal,
11
+ FileTree,
12
+ type ServiceInfo,
13
+ } from "@aprovan/patchwork-editor";
14
+ import { DefaultChatTransport } from "ai";
15
+ import {
16
+ Send,
17
+ Loader2,
18
+ Wrench,
19
+ AlertCircle,
20
+ Brain,
21
+ ChevronDown,
22
+ Minus,
23
+ RefreshCw,
24
+ X,
25
+ } from "lucide-react";
26
+ import {
27
+ useState,
28
+ useRef,
29
+ useEffect,
30
+ useMemo,
31
+ useCallback,
32
+ createContext,
33
+ useContext,
34
+ } from "react";
35
+ import Markdown from "react-markdown";
36
+ import remarkGfm from "remark-gfm";
37
+ import type { UIMessage } from "ai";
38
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
39
+ import { Badge } from "@/components/ui/badge";
40
+ import { Button } from "@/components/ui/button";
41
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
42
+ import {
43
+ Collapsible,
44
+ CollapsibleContent,
45
+ CollapsibleTrigger,
46
+ } from "@/components/ui/collapsible";
47
+ import {
48
+ Dialog,
49
+ DialogHeader,
50
+ DialogContent,
51
+ DialogClose,
52
+ } from "@/components/ui/dialog";
53
+ import { Input } from "@/components/ui/input";
54
+ import { ScrollArea } from "@/components/ui/scroll-area";
55
+ import WorkspaceSwitcher from "@/components/WorkspaceSwitcher";
56
+ import {
57
+ listWorkspaceEntries,
58
+ listWorkspacePaths,
59
+ toWorkspaceTreeFiles,
60
+ loadWorkspaceDirectoryProject,
61
+ loadWorkspaceFileProject,
62
+ createSingleWorkspaceFileProject,
63
+ saveWorkspaceProject,
64
+ subscribeToWorkspaceChanges,
65
+ resetStore,
66
+ } from "@/lib/workspace-vfs";
67
+
68
+ const APROVAN_LOGO =
69
+ "https://raw.githubusercontent.com/AprovanLabs/aprovan.com/main/docs/assets/social-labs.png";
70
+
71
+ interface PatchworkContext {
72
+ compiler: Compiler | null;
73
+ namespaces: string[];
74
+ }
75
+
76
+ const PatchworkCtx = createContext<PatchworkContext>({
77
+ compiler: null,
78
+ namespaces: [],
79
+ });
80
+ const useCompiler = () => useContext(PatchworkCtx).compiler;
81
+ const useServices = () => useContext(PatchworkCtx).namespaces;
82
+
83
+ function createPreviewManifest(services?: string[]) {
84
+ return {
85
+ name: "preview",
86
+ version: "1.0.0",
87
+ platform: "browser" as const,
88
+ image: "@aprovan/patchwork-image-shadcn",
89
+ services,
90
+ };
91
+ }
92
+
93
+ const SharedEditSessionCtx = createContext<
94
+ | ((session: {
95
+ projectId: string;
96
+ entryFile: string;
97
+ filePath?: string;
98
+ initialCode: string;
99
+ initialProject: VirtualProject;
100
+ }) => void)
101
+ | null
102
+ >(null);
103
+
104
+ const useSharedEditSession = () => useContext(SharedEditSessionCtx);
105
+
106
+ function ReasoningPart({
107
+ text,
108
+ isStreaming,
109
+ }: {
110
+ text: string;
111
+ isStreaming?: boolean;
112
+ }) {
113
+ return (
114
+ <Collapsible defaultOpen={isStreaming}>
115
+ <CollapsibleTrigger className="flex items-center gap-2 text-yellow-700 dark:text-yellow-400 hover:opacity-80 w-full">
116
+ <Brain className="h-4 w-4" />
117
+ <span className="text-xs font-medium">Thinking</span>
118
+ {isStreaming && <Loader2 className="h-3 w-3 animate-spin" />}
119
+ <ChevronDown className="h-3 w-3 ml-auto transition-transform [[data-state=open]>&]:rotate-180" />
120
+ </CollapsibleTrigger>
121
+ <CollapsibleContent>
122
+ <div className="mt-2 p-3 rounded border-l-4 border-yellow-500 bg-yellow-50 dark:bg-yellow-950/50">
123
+ <p className="text-sm text-muted-foreground italic whitespace-pre-wrap">
124
+ {text}
125
+ </p>
126
+ </div>
127
+ </CollapsibleContent>
128
+ </Collapsible>
129
+ );
130
+ }
131
+
132
+ function ToolPart({
133
+ toolName,
134
+ state,
135
+ input,
136
+ output,
137
+ errorText,
138
+ }: {
139
+ toolName: string;
140
+ state: string;
141
+ input: unknown;
142
+ output?: unknown;
143
+ errorText?: string;
144
+ }) {
145
+ const isRunning = state === "input-streaming" || state === "input-available";
146
+ const hasError = state === "output-error";
147
+
148
+ return (
149
+ <Collapsible className="my-1 w-full">
150
+ <CollapsibleTrigger className="inline-flex items-center gap-2 px-3 py-1 rounded-full border bg-muted/50 hover:bg-muted text-xs transition-colors">
151
+ <Wrench className="h-3 w-3 text-muted-foreground" />
152
+ <span className="font-mono">{toolName}</span>
153
+ <span className="w-3 h-3 flex items-center justify-center">
154
+ {isRunning && (
155
+ <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
156
+ )}
157
+ {hasError && <AlertCircle className="h-3 w-3 text-destructive" />}
158
+ </span>
159
+ <ChevronDown className="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-180" />
160
+ </CollapsibleTrigger>
161
+
162
+ <CollapsibleContent className="mt-2 p-3 rounded-lg border bg-white space-y-2">
163
+ {input !== undefined && (
164
+ <div>
165
+ <span className="text-xs font-medium text-muted-foreground">
166
+ Input
167
+ </span>
168
+ <div className="mt-1 p-2 bg-muted/30 rounded text-xs overflow-auto max-h-48">
169
+ <pre className="whitespace-pre-wrap break-words m-0">
170
+ {typeof input === "string"
171
+ ? input
172
+ : JSON.stringify(input, null, 2)}
173
+ </pre>
174
+ </div>
175
+ </div>
176
+ )}
177
+
178
+ {output !== undefined && (
179
+ <div>
180
+ <span className="text-xs font-medium text-muted-foreground">
181
+ Output
182
+ </span>
183
+ <div className="mt-1 p-2 bg-muted/30 rounded text-xs overflow-auto max-h-48">
184
+ <pre className="whitespace-pre-wrap break-words m-0">
185
+ {typeof output === "string"
186
+ ? output
187
+ : JSON.stringify(output, null, 2)}
188
+ </pre>
189
+ </div>
190
+ </div>
191
+ )}
192
+
193
+ {errorText && (
194
+ <div className="text-sm text-destructive flex items-center gap-2">
195
+ <AlertCircle className="h-4 w-4 shrink-0" />
196
+ <span className="break-words">{errorText}</span>
197
+ </div>
198
+ )}
199
+ </CollapsibleContent>
200
+ </Collapsible>
201
+ );
202
+ }
203
+
204
+ function MessageBubble({ message }: { message: UIMessage }) {
205
+ const isUser = message.role === "user";
206
+ const isStreaming = message.parts?.some(
207
+ (p) =>
208
+ "state" in p &&
209
+ (p.state === "input-streaming" || p.state === "input-available"),
210
+ );
211
+
212
+ return (
213
+ <div className={`flex gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
214
+ {!isUser && (
215
+ <Avatar className="h-8 w-8 shrink-0">
216
+ <img src={APROVAN_LOGO} alt="Assistant" className="rounded-full" />
217
+ <AvatarFallback className="bg-primary text-primary-foreground">
218
+ A
219
+ </AvatarFallback>
220
+ </Avatar>
221
+ )}
222
+
223
+ <div
224
+ className={`flex flex-col gap-1 max-w-[80%] min-w-0 ${
225
+ isUser ? "items-end" : "items-start"
226
+ }`}
227
+ >
228
+ <div className="flex items-center gap-2 h-5">
229
+ <span className="text-xs text-muted-foreground capitalize">
230
+ {message.role}
231
+ </span>
232
+ {isStreaming && (
233
+ <Badge variant="outline" className="text-xs">
234
+ <Loader2 className="h-3 w-3 mr-1 animate-spin" />
235
+ streaming
236
+ </Badge>
237
+ )}
238
+ </div>
239
+
240
+ <div
241
+ className={`rounded-lg px-4 py-2 overflow-hidden w-full ${
242
+ isUser ? "bg-primary text-primary-foreground" : "bg-muted"
243
+ }`}
244
+ >
245
+ {message.parts?.map((part, i) => {
246
+ if (part.type === "text") {
247
+ return (
248
+ <TextPartWithSession key={i} text={part.text} isUser={isUser} />
249
+ );
250
+ }
251
+
252
+ if (part.type === "reasoning") {
253
+ return (
254
+ <ReasoningPart
255
+ key={i}
256
+ text={part.text}
257
+ isStreaming={part.state === "streaming"}
258
+ />
259
+ );
260
+ }
261
+
262
+ if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
263
+ const toolPart = part as {
264
+ type: string;
265
+ toolName?: string;
266
+ toolCallId: string;
267
+ state: string;
268
+ input?: unknown;
269
+ output?: unknown;
270
+ errorText?: string;
271
+ };
272
+ const toolName =
273
+ toolPart.toolName ?? part.type.replace("tool-", "");
274
+ return (
275
+ <ToolPart
276
+ key={i}
277
+ toolName={toolName}
278
+ state={toolPart.state}
279
+ input={toolPart.input}
280
+ output={toolPart.output}
281
+ errorText={toolPart.errorText}
282
+ />
283
+ );
284
+ }
285
+
286
+ return null;
287
+ })}
288
+ </div>
289
+ </div>
290
+
291
+ {isUser && (
292
+ <Avatar className="h-8 w-8 shrink-0">
293
+ <AvatarFallback className="bg-secondary">U</AvatarFallback>
294
+ </Avatar>
295
+ )}
296
+ </div>
297
+ );
298
+ }
299
+
300
+ function TextPartWithSession({
301
+ text,
302
+ isUser,
303
+ }: {
304
+ text: string;
305
+ isUser: boolean;
306
+ }) {
307
+ const open = useSharedEditSession();
308
+ const compiler = useCompiler();
309
+ const services = useServices();
310
+
311
+ if (isUser) {
312
+ return (
313
+ <div className="prose prose-sm prose-invert prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 prose-pre:my-2 prose-code:before:content-none prose-code:after:content-none">
314
+ <Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
315
+ </div>
316
+ );
317
+ }
318
+
319
+ const parts = extractCodeBlocks(text);
320
+
321
+ return (
322
+ <div className="prose prose-sm dark:prose-invert prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 prose-pre:my-2 prose-code:before:content-none prose-code:after:content-none">
323
+ {parts.map((part, index) => {
324
+ if (part.type === "code") {
325
+ return (
326
+ <CodePreview
327
+ key={index}
328
+ code={part.content}
329
+ compiler={compiler}
330
+ services={services}
331
+ filePath={part.attributes?.path}
332
+ entrypoint="main.tsx"
333
+ onOpenEditSession={open ?? undefined}
334
+ />
335
+ );
336
+ }
337
+ return (
338
+ <Markdown key={index} remarkPlugins={[remarkGfm]}>
339
+ {part.content}
340
+ </Markdown>
341
+ );
342
+ })}
343
+ </div>
344
+ );
345
+ }
346
+
347
+ const PROXY_URL = "/api/proxy";
348
+ const IMAGE_SPEC = "@aprovan/patchwork-image-shadcn";
349
+ // Local proxy for loading image packages, esm.sh for widget imports
350
+ const IMAGE_CDN_URL = import.meta.env.DEV
351
+ ? "/_local-packages"
352
+ : "https://esm.sh";
353
+ const WIDGET_CDN_URL = "https://esm.sh"; // Widget imports need esm.sh bundles like @packagedcn
354
+
355
+ function toProjectRelativePath(projectId: string, path: string): string {
356
+ const normalizedProjectId = projectId.replace(/^\/+|\/+$/g, "");
357
+ const normalizedPath = path.replace(/^\/+|\/+$/g, "");
358
+ if (!normalizedProjectId) return normalizedPath;
359
+ const prefix = `${normalizedProjectId}/`;
360
+ if (normalizedPath.startsWith(prefix)) {
361
+ return normalizedPath.slice(prefix.length);
362
+ }
363
+ return normalizedPath;
364
+ }
365
+
366
+ const TABS_KEY_PREFIX = 'patchwork:open-tabs';
367
+ const ACTIVE_WORKSPACE_KEY = 'patchwork:active-workspace';
368
+
369
+ function getTabsStorageKey(workspaceId: string | null): string {
370
+ return workspaceId ? `${TABS_KEY_PREFIX}:${workspaceId}` : TABS_KEY_PREFIX;
371
+ }
372
+
373
+ function loadPersistedTabState(workspaceId: string | null): { paths: string[]; activePath: string | null } {
374
+ try {
375
+ const raw = localStorage.getItem(getTabsStorageKey(workspaceId));
376
+ if (!raw) return { paths: [], activePath: null };
377
+ const parsed = JSON.parse(raw);
378
+ return {
379
+ paths: Array.isArray(parsed.paths) ? parsed.paths : [],
380
+ activePath: typeof parsed.activePath === 'string' ? parsed.activePath : null,
381
+ };
382
+ } catch {
383
+ return { paths: [], activePath: null };
384
+ }
385
+ }
386
+
387
+ function persistTabState(paths: string[], activePath: string | null, workspaceId: string | null) {
388
+ localStorage.setItem(getTabsStorageKey(workspaceId), JSON.stringify({ paths, activePath }));
389
+ }
390
+
391
+ export default function ChatPage() {
392
+ const [input, setInput] = useState(
393
+ "What's the weather in Houston, Texas like?",
394
+ );
395
+ const [compiler, setCompiler] = useState<Compiler | null>(null);
396
+ const [namespaces, setNamespaces] = useState<string[]>([]);
397
+ const [services, setServices] = useState<ServiceInfo[]>([]);
398
+ const [workspaceFiles, setWorkspaceFiles] = useState<string[]>([]);
399
+ const [workspaceActivePath, setWorkspaceActivePath] = useState("");
400
+ const [workspaceFilter, setWorkspaceFilter] = useState("");
401
+ const [workspaceTreeVersion, setWorkspaceTreeVersion] = useState(0);
402
+ const [workspaceLoading, setWorkspaceLoading] = useState(false);
403
+ const [workspaceError, setWorkspaceError] = useState<string | null>(null);
404
+ const [activeWorkspaceId, setActiveWorkspaceId] = useState<string | null>(
405
+ () => localStorage.getItem(ACTIVE_WORKSPACE_KEY),
406
+ );
407
+ const [chatContainer, setChatContainer] = useState<HTMLDivElement | null>(
408
+ null,
409
+ );
410
+ const [editSession, setEditSession] = useState<{
411
+ project: VirtualProject;
412
+ initialTreePath?: string;
413
+ initialActiveFile?: string;
414
+ } | null>(null);
415
+ const [openTabs, setOpenTabs] = useState<
416
+ Map<string, { code: string; loading: boolean; error: string | null }>
417
+ >(() => {
418
+ const wsId = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
419
+ const { paths } = loadPersistedTabState(wsId);
420
+ return new Map(paths.map((p) => [p, { code: '', loading: true, error: null }]));
421
+ });
422
+ const [activeTabPath, setActiveTabPath] = useState<string | null>(() => {
423
+ const wsId = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
424
+ const { paths, activePath } = loadPersistedTabState(wsId);
425
+ if (activePath && paths.includes(activePath)) return activePath;
426
+ return paths[0] ?? null;
427
+ });
428
+ const [previewCollapsed, setPreviewCollapsed] = useState(false);
429
+ const scrollRef = useRef<HTMLDivElement>(null);
430
+ const tabRequestRefs = useRef<Map<string, number>>(new Map());
431
+
432
+ const [pinnedPaths, setPinnedPaths] = useState<Map<string, boolean>>(() => {
433
+ try {
434
+ const stored = localStorage.getItem('patchwork:pinned-paths');
435
+ if (!stored) return new Map();
436
+ const parsed = JSON.parse(stored) as Array<[string, boolean]> | string[];
437
+ if (parsed.length > 0 && Array.isArray(parsed[0])) {
438
+ return new Map(parsed as Array<[string, boolean]>);
439
+ }
440
+ return new Map((parsed as string[]).map((p) => [p, false]));
441
+ } catch {
442
+ return new Map();
443
+ }
444
+ });
445
+
446
+ const togglePin = useCallback((path: string, isDir: boolean) => {
447
+ setPinnedPaths((prev) => {
448
+ const next = new Map(prev);
449
+ if (next.has(path)) next.delete(path);
450
+ else next.set(path, isDir);
451
+ localStorage.setItem('patchwork:pinned-paths', JSON.stringify(Array.from(next)));
452
+ return next;
453
+ });
454
+ }, []);
455
+
456
+ const refreshWorkspace = useCallback(async () => {
457
+ setWorkspaceLoading(true);
458
+ setWorkspaceError(null);
459
+ try {
460
+ if (workspaceFilter.trim()) {
461
+ const paths = await listWorkspacePaths();
462
+ setWorkspaceFiles(paths);
463
+ }
464
+ setWorkspaceTreeVersion((prev) => prev + 1);
465
+ } catch (err) {
466
+ setWorkspaceError(
467
+ err instanceof Error ? err.message : "Failed to load workspace",
468
+ );
469
+ } finally {
470
+ setWorkspaceLoading(false);
471
+ }
472
+ }, [workspaceFilter]);
473
+
474
+ useEffect(() => {
475
+ return subscribeToWorkspaceChanges(() => {
476
+ setWorkspaceTreeVersion((prev) => prev + 1);
477
+ listWorkspacePaths()
478
+ .then((allPaths) => {
479
+ if (workspaceFilter.trim()) setWorkspaceFiles(allPaths);
480
+ const existing = new Set(allPaths);
481
+ setOpenTabs((prev) => {
482
+ let changed = false;
483
+ const next = new Map(prev);
484
+ for (const path of next.keys()) {
485
+ if (!existing.has(path)) { next.delete(path); changed = true; }
486
+ }
487
+ return changed ? next : prev;
488
+ });
489
+ })
490
+ .catch(() => {});
491
+ });
492
+ }, [workspaceFilter]);
493
+
494
+ useEffect(() => {
495
+ if (!workspaceFilter.trim()) return;
496
+
497
+ setWorkspaceLoading(true);
498
+ setWorkspaceError(null);
499
+
500
+ listWorkspacePaths()
501
+ .then(setWorkspaceFiles)
502
+ .catch((err) => {
503
+ setWorkspaceError(
504
+ err instanceof Error ? err.message : "Failed to load workspace",
505
+ );
506
+ })
507
+ .finally(() => setWorkspaceLoading(false));
508
+ }, [workspaceFilter]);
509
+
510
+ useEffect(() => {
511
+ // Fetch available services
512
+ fetch("/api/services")
513
+ .then((res) => res.json())
514
+ .then((data) => {
515
+ setNamespaces(data.namespaces ?? []);
516
+ // In dev mode, also store full service details for inspection
517
+ if (import.meta.env.DEV && data.services) {
518
+ setServices(data.services);
519
+ }
520
+ })
521
+ .catch(() => {
522
+ setNamespaces([]);
523
+ setServices([]);
524
+ });
525
+
526
+ // Initialize compiler
527
+ createCompiler({
528
+ image: IMAGE_SPEC,
529
+ proxyUrl: PROXY_URL,
530
+ cdnBaseUrl: IMAGE_CDN_URL,
531
+ widgetCdnBaseUrl: WIDGET_CDN_URL,
532
+ })
533
+ .then(setCompiler)
534
+ .catch(console.error);
535
+
536
+ void refreshWorkspace();
537
+ }, []);
538
+
539
+ // Load content for tabs restored from localStorage
540
+ useEffect(() => {
541
+ openTabs.forEach((tab, path) => {
542
+ if (!tab.loading) return;
543
+ const requestId = (tabRequestRefs.current.get(path) ?? 0) + 1;
544
+ tabRequestRefs.current.set(path, requestId);
545
+ loadWorkspaceFileProject(path)
546
+ .then((project) => {
547
+ if (tabRequestRefs.current.get(path) !== requestId) return;
548
+ if (!project) {
549
+ setOpenTabs((prev) => { const next = new Map(prev); next.delete(path); return next; });
550
+ return;
551
+ }
552
+ const file = project.files.get(project.entry);
553
+ setOpenTabs((prev) => {
554
+ const next = new Map(prev);
555
+ next.set(path, { code: file?.content ?? '', loading: false, error: null });
556
+ return next;
557
+ });
558
+ })
559
+ .catch(() => {
560
+ if (tabRequestRefs.current.get(path) !== requestId) return;
561
+ setOpenTabs((prev) => { const next = new Map(prev); next.delete(path); return next; });
562
+ });
563
+ });
564
+ }, []);
565
+
566
+ // Persist open tabs to localStorage (scoped by active workspace)
567
+ useEffect(() => {
568
+ persistTabState([...openTabs.keys()], activeTabPath, activeWorkspaceId);
569
+ }, [openTabs, activeTabPath, activeWorkspaceId]);
570
+
571
+ // Fix activeTabPath when its tab is removed
572
+ useEffect(() => {
573
+ if (activeTabPath !== null && !openTabs.has(activeTabPath)) {
574
+ setActiveTabPath([...openTabs.keys()][0] ?? null);
575
+ }
576
+ }, [openTabs, activeTabPath]);
577
+
578
+ const openSharedEditSession = useCallback(
579
+ async (session: {
580
+ projectId: string;
581
+ entryFile: string;
582
+ filePath?: string;
583
+ initialCode: string;
584
+ initialProject: VirtualProject;
585
+ }) => {
586
+ const { projectId, filePath, entryFile, initialCode, initialProject } =
587
+ session;
588
+ const directoryProject = await loadWorkspaceDirectoryProject(projectId);
589
+ const filePathKey = filePath ?? `${projectId}/${entryFile}`;
590
+
591
+ if (directoryProject) {
592
+ const relativePath = toProjectRelativePath(projectId, filePathKey);
593
+ setWorkspaceActivePath(filePathKey);
594
+ setEditSession({
595
+ project: directoryProject,
596
+ initialTreePath: relativePath,
597
+ initialActiveFile: relativePath,
598
+ });
599
+ return;
600
+ }
601
+
602
+ const fallbackFilePath = filePathKey;
603
+ const fallbackProject = filePath
604
+ ? createSingleWorkspaceFileProject(filePath, initialCode)
605
+ : initialProject;
606
+ setWorkspaceActivePath(fallbackFilePath);
607
+ setEditSession({
608
+ project: fallbackProject,
609
+ initialTreePath: fallbackProject.entry,
610
+ initialActiveFile: fallbackProject.entry,
611
+ });
612
+ },
613
+ [],
614
+ );
615
+
616
+ const openWorkspaceSession = useCallback(
617
+ async (path: string, isDir: boolean) => {
618
+ const project = isDir
619
+ ? await loadWorkspaceDirectoryProject(path)
620
+ : await loadWorkspaceFileProject(path);
621
+ if (!project) return;
622
+
623
+ setWorkspaceActivePath(path);
624
+ setEditSession({
625
+ project,
626
+ initialTreePath: project.entry,
627
+ initialActiveFile: project.entry,
628
+ });
629
+ },
630
+ [],
631
+ );
632
+
633
+ const openWorkspacePreview = useCallback((path: string) => {
634
+ setWorkspaceActivePath(path);
635
+ setActiveTabPath(path);
636
+ setPreviewCollapsed(false);
637
+
638
+ // If tab already open, just activate it
639
+ setOpenTabs((prev) => {
640
+ if (prev.has(path)) return prev;
641
+ const next = new Map(prev);
642
+ next.set(path, { code: "", loading: true, error: null });
643
+ return next;
644
+ });
645
+
646
+ const requestId = (tabRequestRefs.current.get(path) ?? 0) + 1;
647
+ tabRequestRefs.current.set(path, requestId);
648
+
649
+ void loadWorkspaceFileProject(path)
650
+ .then((project) => {
651
+ if (tabRequestRefs.current.get(path) !== requestId) return;
652
+ if (!project) {
653
+ setOpenTabs((prev) => {
654
+ const next = new Map(prev);
655
+ next.set(path, { code: "", loading: false, error: "Failed to load file preview" });
656
+ return next;
657
+ });
658
+ return;
659
+ }
660
+ const file = project.files.get(project.entry);
661
+ setOpenTabs((prev) => {
662
+ const next = new Map(prev);
663
+ next.set(path, { code: file?.content ?? "", loading: false, error: null });
664
+ return next;
665
+ });
666
+ })
667
+ .catch((err) => {
668
+ if (tabRequestRefs.current.get(path) !== requestId) return;
669
+ setOpenTabs((prev) => {
670
+ const next = new Map(prev);
671
+ next.set(path, {
672
+ code: "",
673
+ loading: false,
674
+ error: err instanceof Error ? err.message : "Failed to load file preview",
675
+ });
676
+ return next;
677
+ });
678
+ });
679
+ }, []);
680
+
681
+ const closeTab = useCallback((path: string) => {
682
+ setOpenTabs((prev) => {
683
+ const next = new Map(prev);
684
+ next.delete(path);
685
+ return next;
686
+ });
687
+ setActiveTabPath((prev) => {
688
+ if (prev !== path) return prev;
689
+ // Activate an adjacent tab
690
+ const paths = [...openTabs.keys()];
691
+ const idx = paths.indexOf(path);
692
+ if (paths.length <= 1) return null;
693
+ return paths[idx > 0 ? idx - 1 : idx + 1] ?? null;
694
+ });
695
+ }, [openTabs]);
696
+
697
+ const closeAllTabs = useCallback(() => {
698
+ setOpenTabs(new Map());
699
+ setActiveTabPath(null);
700
+ }, []);
701
+
702
+ const handleWorkspaceSwitch = useCallback(
703
+ (newWorkspaceId: string) => {
704
+ localStorage.setItem(ACTIVE_WORKSPACE_KEY, newWorkspaceId);
705
+ setActiveWorkspaceId(newWorkspaceId);
706
+ setOpenTabs(new Map());
707
+ setActiveTabPath(null);
708
+ setPinnedPaths(new Map());
709
+ setEditSession(null);
710
+ resetStore();
711
+ void refreshWorkspace();
712
+ },
713
+ [refreshWorkspace],
714
+ );
715
+
716
+ const handleWorkspaceLoad = useCallback(
717
+ (serverActiveId: string | null) => {
718
+ if (!serverActiveId) return;
719
+ const storedId = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
720
+ if (serverActiveId === storedId) return;
721
+ // Server and localStorage disagree — trust the server
722
+ localStorage.setItem(ACTIVE_WORKSPACE_KEY, serverActiveId);
723
+ setActiveWorkspaceId(serverActiveId);
724
+ setOpenTabs(new Map());
725
+ setActiveTabPath(null);
726
+ setPinnedPaths(new Map());
727
+ resetStore();
728
+ void refreshWorkspace();
729
+ },
730
+ [refreshWorkspace],
731
+ );
732
+
733
+ const filteredWorkspaceFiles = useMemo(() => {
734
+ const query = workspaceFilter.trim().toLowerCase();
735
+ if (!query) return workspaceFiles;
736
+ return workspaceFiles.filter((path) => path.toLowerCase().includes(query));
737
+ }, [workspaceFiles, workspaceFilter]);
738
+
739
+ const patchworkCtx = useMemo(
740
+ () => ({ compiler, namespaces }),
741
+ [compiler, namespaces],
742
+ );
743
+
744
+ const transport = useMemo(
745
+ () =>
746
+ new DefaultChatTransport({
747
+ body: () => ({
748
+ prompt: {
749
+ id: "chat-patchwork-widget",
750
+ vars: { compilers: [IMAGE_SPEC] },
751
+ },
752
+ }),
753
+ }),
754
+ [],
755
+ );
756
+
757
+ const { messages, sendMessage, status, error } = useChat({ transport });
758
+
759
+ const isLoading = status === "submitted" || status === "streaming";
760
+
761
+ const handleSubmit = useCallback(
762
+ (e?: React.FormEvent) => {
763
+ e?.preventDefault();
764
+ if (!input.trim()) return;
765
+ sendMessage({ text: input });
766
+ setInput("");
767
+ },
768
+ [input, sendMessage],
769
+ );
770
+
771
+ useEffect(() => {
772
+ scrollRef.current?.scrollTo({
773
+ top: scrollRef.current.scrollHeight,
774
+ behavior: "smooth",
775
+ });
776
+ }, [messages]);
777
+
778
+ return (
779
+ <PatchworkCtx.Provider value={patchworkCtx}>
780
+ <SharedEditSessionCtx.Provider value={openSharedEditSession}>
781
+ <div
782
+ className="flex flex-col h-screen max-w-6xl mx-auto p-4"
783
+ ref={setChatContainer}
784
+ >
785
+ <Card className="flex-1 flex flex-col min-h-0 overflow-hidden border">
786
+ <CardHeader className="border-b py-3">
787
+ <CardTitle className="flex items-center gap-3">
788
+ <img
789
+ src={APROVAN_LOGO}
790
+ alt="Aprovan"
791
+ className="h-8 w-8 rounded-full"
792
+ />
793
+ <span className="text-lg">patchwork</span>
794
+ <WorkspaceSwitcher
795
+ onLoad={handleWorkspaceLoad}
796
+ onSwitch={handleWorkspaceSwitch}
797
+ />
798
+ <ServicesInspector
799
+ namespaces={namespaces}
800
+ services={services}
801
+ DialogComponent={({ open, onOpenChange, children }) => (
802
+ <Dialog
803
+ open={open ?? false}
804
+ onOpenChange={onOpenChange ?? (() => undefined)}
805
+ >
806
+ {children}
807
+ </Dialog>
808
+ )}
809
+ DialogHeaderComponent={DialogHeader}
810
+ DialogContentComponent={DialogContent}
811
+ DialogCloseComponent={({ onClose }) => (
812
+ <DialogClose onClose={onClose ?? (() => undefined)} />
813
+ )}
814
+ />
815
+ </CardTitle>
816
+ </CardHeader>
817
+
818
+ <CardContent className="flex-1 p-0 min-h-0 flex">
819
+ <div className="w-64 border-r bg-muted/20 min-h-0 flex flex-col min-w-sm">
820
+ <div className="px-3 py-2 border-b flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
821
+ <span>Workspace</span>
822
+ <button
823
+ onClick={() => void refreshWorkspace()}
824
+ className="ml-auto p-1 rounded hover:bg-muted"
825
+ title="Refresh workspace"
826
+ >
827
+ <RefreshCw
828
+ className={`h-3 w-3 ${
829
+ workspaceLoading ? "animate-spin" : ""
830
+ }`}
831
+ />
832
+ </button>
833
+ </div>
834
+ <div className="p-2 border-b">
835
+ <Input
836
+ value={workspaceFilter}
837
+ onChange={(e) => setWorkspaceFilter(e.target.value)}
838
+ placeholder="Filter files..."
839
+ className="h-8"
840
+ />
841
+ </div>
842
+ {workspaceError ? (
843
+ <div className="p-3 text-xs text-destructive">
844
+ {workspaceError}
845
+ </div>
846
+ ) : workspaceFilter.trim() ? (
847
+ <FileTree
848
+ files={toWorkspaceTreeFiles(filteredWorkspaceFiles)}
849
+ activePath={workspaceActivePath}
850
+ onSelectFile={openWorkspacePreview}
851
+ onSelectDirectory={setWorkspaceActivePath}
852
+ onOpenInEditor={openWorkspaceSession}
853
+ openInEditorMode="all"
854
+ openInEditorTitle="Edit"
855
+ pinnedPaths={pinnedPaths}
856
+ onTogglePin={togglePin}
857
+ title="Files"
858
+ />
859
+ ) : (
860
+ <FileTree
861
+ files={[]}
862
+ activePath={workspaceActivePath}
863
+ onSelectFile={openWorkspacePreview}
864
+ onSelectDirectory={setWorkspaceActivePath}
865
+ onOpenInEditor={openWorkspaceSession}
866
+ openInEditorMode="all"
867
+ openInEditorTitle="Edit"
868
+ directoryLoader={listWorkspaceEntries}
869
+ pageSize={10}
870
+ reloadToken={workspaceTreeVersion}
871
+ pinnedPaths={pinnedPaths}
872
+ onTogglePin={togglePin}
873
+ title="Files"
874
+ />
875
+ )}
876
+ </div>
877
+ <div className="flex-1 min-h-0 flex flex-col">
878
+ {openTabs.size > 0 && (
879
+ <div className="border-b bg-muted/10">
880
+ {/* Tab bar */}
881
+ <div className="flex items-center border-b bg-muted/30">
882
+ <div className="flex-1 flex items-center overflow-x-auto min-w-0">
883
+ {[...openTabs.entries()].map(([path]) => {
884
+ const fileName = path.split("/").pop() ?? path;
885
+ const isActive = path === activeTabPath;
886
+ return (
887
+ <button
888
+ key={path}
889
+ onClick={() => {
890
+ setActiveTabPath(path);
891
+ setWorkspaceActivePath(path);
892
+ setPreviewCollapsed(false);
893
+ }}
894
+ className={`group relative flex items-center gap-1.5 px-3 py-1.5 text-xs border-r shrink-0 max-w-[180px] ${
895
+ isActive
896
+ ? "bg-background text-foreground border-b-2 border-b-primary"
897
+ : "text-muted-foreground hover:bg-muted/50"
898
+ }`}
899
+ title={path}
900
+ >
901
+ <span className="truncate">{fileName}</span>
902
+ <span
903
+ role="button"
904
+ onClick={(e) => {
905
+ e.stopPropagation();
906
+ closeTab(path);
907
+ }}
908
+ className="shrink-0 p-0.5 rounded hover:bg-muted-foreground/20 opacity-0 group-hover:opacity-100 transition-opacity"
909
+ title="Close tab"
910
+ >
911
+ <X className="h-3 w-3" />
912
+ </span>
913
+ </button>
914
+ );
915
+ })}
916
+ </div>
917
+ <div className="flex items-center gap-0.5 px-1 shrink-0">
918
+ <button
919
+ onClick={() => setPreviewCollapsed((p) => !p)}
920
+ className="p-1 rounded hover:bg-muted"
921
+ title={previewCollapsed ? "Expand preview" : "Collapse preview"}
922
+ >
923
+ {previewCollapsed ? (
924
+ <ChevronDown className="h-3.5 w-3.5" />
925
+ ) : (
926
+ <Minus className="h-3.5 w-3.5" />
927
+ )}
928
+ </button>
929
+ <button
930
+ onClick={closeAllTabs}
931
+ className="p-1 rounded hover:bg-muted"
932
+ title="Close all tabs"
933
+ >
934
+ <X className="h-3.5 w-3.5" />
935
+ </button>
936
+ </div>
937
+ </div>
938
+
939
+ {/* Active tab content */}
940
+ {!previewCollapsed && activeTabPath && openTabs.has(activeTabPath) && (() => {
941
+ const tab = openTabs.get(activeTabPath)!;
942
+ return (
943
+ <div key={activeTabPath} className="bg-white min-h-24">
944
+ {tab.loading ? (
945
+ <div className="p-3 flex items-center gap-2 text-muted-foreground">
946
+ <Loader2 className="h-4 w-4 animate-spin" />
947
+ <span className="text-sm">Loading file preview...</span>
948
+ </div>
949
+ ) : tab.error ? (
950
+ <div className="p-3 text-sm text-destructive flex items-center gap-2">
951
+ <AlertCircle className="h-4 w-4 shrink-0" />
952
+ <span>{tab.error}</span>
953
+ </div>
954
+ ) : (
955
+ <CodePreview
956
+ code={tab.code}
957
+ compiler={compiler}
958
+ services={namespaces}
959
+ filePath={activeTabPath}
960
+ onOpenEditSession={openSharedEditSession}
961
+ />
962
+ )}
963
+ </div>
964
+ );
965
+ })()}
966
+ </div>
967
+ )}
968
+ <ScrollArea className="h-full flex-1" ref={scrollRef}>
969
+ <div className="p-4 space-y-4">
970
+ {messages.length === 0 ? (
971
+ <div className="text-center text-muted-foreground py-12">
972
+ <img
973
+ src={APROVAN_LOGO}
974
+ alt=""
975
+ className="h-12 w-12 mx-auto mb-4 opacity-50 rounded-full"
976
+ />
977
+ <p>Start a conversation</p>
978
+ </div>
979
+ ) : (
980
+ messages.map((msg) => (
981
+ <MessageBubble key={msg.id} message={msg} />
982
+ ))
983
+ )}
984
+
985
+ {isLoading &&
986
+ messages[messages.length - 1]?.role !== "assistant" && (
987
+ <div className="flex gap-3 justify-start">
988
+ <Avatar className="h-8 w-8 shrink-0">
989
+ <img
990
+ src={APROVAN_LOGO}
991
+ alt=""
992
+ className="rounded-full"
993
+ />
994
+ <AvatarFallback>A</AvatarFallback>
995
+ </Avatar>
996
+ <div className="flex flex-col gap-1">
997
+ <div className="h-5" />
998
+ <div className="bg-muted rounded-lg px-4 py-2">
999
+ <Loader2 className="h-4 w-4 animate-spin" />
1000
+ </div>
1001
+ </div>
1002
+ </div>
1003
+ )}
1004
+ </div>
1005
+ </ScrollArea>
1006
+ </div>
1007
+ </CardContent>
1008
+
1009
+ {error && (
1010
+ <div className="px-4 py-2 bg-destructive/10 text-destructive text-sm flex items-center gap-2">
1011
+ <AlertCircle className="h-4 w-4" />
1012
+ {error.message}
1013
+ </div>
1014
+ )}
1015
+
1016
+ <div className="p-4 border-t">
1017
+ <form onSubmit={handleSubmit} className="flex gap-2 items-end">
1018
+ <MarkdownEditor
1019
+ value={input}
1020
+ onChange={setInput}
1021
+ onSubmit={() => {
1022
+ if (!isLoading && input.trim()) {
1023
+ handleSubmit();
1024
+ }
1025
+ }}
1026
+ placeholder="Type a message... (Shift+Enter for new line)"
1027
+ disabled={isLoading}
1028
+ />
1029
+ <Button
1030
+ type="submit"
1031
+ disabled={isLoading || !input.trim()}
1032
+ className="shrink-0"
1033
+ >
1034
+ {isLoading ? (
1035
+ <Loader2 className="h-4 w-4 animate-spin" />
1036
+ ) : (
1037
+ <Send className="h-4 w-4" />
1038
+ )}
1039
+ </Button>
1040
+ </form>
1041
+ </div>
1042
+ </Card>
1043
+
1044
+ {!editSession && (
1045
+ <Bobbin
1046
+ container={chatContainer}
1047
+ pillContainer={chatContainer}
1048
+ defaultActive={false}
1049
+ showInspector
1050
+ onChanges={() => undefined}
1051
+ exclude={[".bobbin-pill", "[data-bobbin]"]}
1052
+ />
1053
+ )}
1054
+ </div>
1055
+ {editSession && (
1056
+ <EditModal
1057
+ isOpen
1058
+ onClose={() => setEditSession(null)}
1059
+ onSaveProject={async (project) => {
1060
+ await saveWorkspaceProject(project);
1061
+ await refreshWorkspace();
1062
+ }}
1063
+ originalProject={editSession.project}
1064
+ initialActiveFile={editSession.initialActiveFile}
1065
+ initialTreePath={editSession.initialTreePath}
1066
+ apiEndpoint="/api/edit"
1067
+ initialState={{ showPreview: true, showTree: true }}
1068
+ compile={async (code) => {
1069
+ if (!compiler) return { success: true };
1070
+ try {
1071
+ await compiler.compile(
1072
+ code,
1073
+ createPreviewManifest(namespaces),
1074
+ { typescript: true },
1075
+ );
1076
+ return { success: true };
1077
+ } catch (err) {
1078
+ return {
1079
+ success: false,
1080
+ error:
1081
+ err instanceof Error ? err.message : "Compilation failed",
1082
+ };
1083
+ }
1084
+ }}
1085
+ renderPreview={(code) => (
1086
+ <WidgetPreview
1087
+ code={code}
1088
+ compiler={compiler}
1089
+ services={namespaces}
1090
+ />
1091
+ )}
1092
+ previewLoading={!compiler}
1093
+ />
1094
+ )}
1095
+ </SharedEditSessionCtx.Provider>
1096
+ </PatchworkCtx.Provider>
1097
+ );
1098
+ }