@aprovan/patchwork-chat 0.1.0-dev.6bd527d → 0.1.0-dev.f456953

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