@drewsepsi/nextpi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-path-routes-manifest.json +13 -0
  3. package/.next/build/chunks/33f32_323ea524._.js +6766 -0
  4. package/.next/build/chunks/33f32_323ea524._.js.map +47 -0
  5. package/.next/build/chunks/[root-of-the-server]__6ead02f9._.js +500 -0
  6. package/.next/build/chunks/[root-of-the-server]__6ead02f9._.js.map +11 -0
  7. package/.next/build/chunks/[root-of-the-server]__777e9116._.js +206 -0
  8. package/.next/build/chunks/[root-of-the-server]__777e9116._.js.map +8 -0
  9. package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_597188b4._.js +13 -0
  10. package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_597188b4._.js.map +5 -0
  11. package/.next/build/chunks/[turbopack]_runtime.js +795 -0
  12. package/.next/build/chunks/[turbopack]_runtime.js.map +10 -0
  13. package/.next/build/package.json +1 -0
  14. package/.next/build/postcss.js +6 -0
  15. package/.next/build/postcss.js.map +5 -0
  16. package/.next/build-manifest.json +19 -0
  17. package/.next/cache/.previewinfo +1 -0
  18. package/.next/cache/.rscinfo +1 -0
  19. package/.next/cache/.tsbuildinfo +1 -0
  20. package/.next/diagnostics/build-diagnostics.json +6 -0
  21. package/.next/diagnostics/framework.json +1 -0
  22. package/.next/export-marker.json +6 -0
  23. package/.next/fallback-build-manifest.json +12 -0
  24. package/.next/images-manifest.json +67 -0
  25. package/.next/next-minimal-server.js.nft.json +1 -0
  26. package/.next/next-server.js.nft.json +1 -0
  27. package/.next/package.json +1 -0
  28. package/.next/prerender-manifest.json +114 -0
  29. package/.next/required-server-files.js +324 -0
  30. package/.next/required-server-files.json +324 -0
  31. package/.next/routes-manifest.json +113 -0
  32. package/.next/server/app/_global-error/page/app-paths-manifest.json +3 -0
  33. package/.next/server/app/_global-error/page/build-manifest.json +16 -0
  34. package/.next/server/app/_global-error/page/next-font-manifest.json +6 -0
  35. package/.next/server/app/_global-error/page/react-loadable-manifest.json +1 -0
  36. package/.next/server/app/_global-error/page/server-reference-manifest.json +4 -0
  37. package/.next/server/app/_global-error/page.js +11 -0
  38. package/.next/server/app/_global-error/page.js.map +5 -0
  39. package/.next/server/app/_global-error/page.js.nft.json +1 -0
  40. package/.next/server/app/_global-error/page_client-reference-manifest.js +2 -0
  41. package/.next/server/app/_global-error.html +2 -0
  42. package/.next/server/app/_global-error.meta +15 -0
  43. package/.next/server/app/_global-error.rsc +13 -0
  44. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +5 -0
  45. package/.next/server/app/_global-error.segments/_full.segment.rsc +13 -0
  46. package/.next/server/app/_global-error.segments/_head.segment.rsc +6 -0
  47. package/.next/server/app/_global-error.segments/_index.segment.rsc +4 -0
  48. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
  49. package/.next/server/app/_not-found/page/app-paths-manifest.json +3 -0
  50. package/.next/server/app/_not-found/page/build-manifest.json +16 -0
  51. package/.next/server/app/_not-found/page/next-font-manifest.json +11 -0
  52. package/.next/server/app/_not-found/page/react-loadable-manifest.json +1 -0
  53. package/.next/server/app/_not-found/page/server-reference-manifest.json +4 -0
  54. package/.next/server/app/_not-found/page.js +14 -0
  55. package/.next/server/app/_not-found/page.js.map +5 -0
  56. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  57. package/.next/server/app/_not-found/page_client-reference-manifest.js +2 -0
  58. package/.next/server/app/_not-found.html +1 -0
  59. package/.next/server/app/_not-found.meta +16 -0
  60. package/.next/server/app/_not-found.rsc +15 -0
  61. package/.next/server/app/_not-found.segments/_full.segment.rsc +15 -0
  62. package/.next/server/app/_not-found.segments/_head.segment.rsc +6 -0
  63. package/.next/server/app/_not-found.segments/_index.segment.rsc +6 -0
  64. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -0
  65. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +4 -0
  66. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -0
  67. package/.next/server/app/api/chat/route/app-paths-manifest.json +3 -0
  68. package/.next/server/app/api/chat/route/build-manifest.json +11 -0
  69. package/.next/server/app/api/chat/route/server-reference-manifest.json +4 -0
  70. package/.next/server/app/api/chat/route.js +7 -0
  71. package/.next/server/app/api/chat/route.js.map +5 -0
  72. package/.next/server/app/api/chat/route.js.nft.json +1 -0
  73. package/.next/server/app/api/chat/route_client-reference-manifest.js +2 -0
  74. package/.next/server/app/api/config/route/app-paths-manifest.json +3 -0
  75. package/.next/server/app/api/config/route/build-manifest.json +11 -0
  76. package/.next/server/app/api/config/route/server-reference-manifest.json +4 -0
  77. package/.next/server/app/api/config/route.js +7 -0
  78. package/.next/server/app/api/config/route.js.map +5 -0
  79. package/.next/server/app/api/config/route.js.nft.json +1 -0
  80. package/.next/server/app/api/config/route_client-reference-manifest.js +2 -0
  81. package/.next/server/app/api/files/[...path]/route/app-paths-manifest.json +3 -0
  82. package/.next/server/app/api/files/[...path]/route/build-manifest.json +11 -0
  83. package/.next/server/app/api/files/[...path]/route/server-reference-manifest.json +4 -0
  84. package/.next/server/app/api/files/[...path]/route.js +7 -0
  85. package/.next/server/app/api/files/[...path]/route.js.map +5 -0
  86. package/.next/server/app/api/files/[...path]/route.js.nft.json +1 -0
  87. package/.next/server/app/api/files/[...path]/route_client-reference-manifest.js +2 -0
  88. package/.next/server/app/api/files/route/app-paths-manifest.json +3 -0
  89. package/.next/server/app/api/files/route/build-manifest.json +11 -0
  90. package/.next/server/app/api/files/route/server-reference-manifest.json +4 -0
  91. package/.next/server/app/api/files/route.js +7 -0
  92. package/.next/server/app/api/files/route.js.map +5 -0
  93. package/.next/server/app/api/files/route.js.nft.json +1 -0
  94. package/.next/server/app/api/files/route_client-reference-manifest.js +2 -0
  95. package/.next/server/app/api/messages/route/app-paths-manifest.json +3 -0
  96. package/.next/server/app/api/messages/route/build-manifest.json +11 -0
  97. package/.next/server/app/api/messages/route/server-reference-manifest.json +4 -0
  98. package/.next/server/app/api/messages/route.js +7 -0
  99. package/.next/server/app/api/messages/route.js.map +5 -0
  100. package/.next/server/app/api/messages/route.js.nft.json +1 -0
  101. package/.next/server/app/api/messages/route_client-reference-manifest.js +2 -0
  102. package/.next/server/app/api/session/route/app-paths-manifest.json +3 -0
  103. package/.next/server/app/api/session/route/build-manifest.json +11 -0
  104. package/.next/server/app/api/session/route/server-reference-manifest.json +4 -0
  105. package/.next/server/app/api/session/route.js +7 -0
  106. package/.next/server/app/api/session/route.js.map +5 -0
  107. package/.next/server/app/api/session/route.js.nft.json +1 -0
  108. package/.next/server/app/api/session/route_client-reference-manifest.js +2 -0
  109. package/.next/server/app/api/stream/route/app-paths-manifest.json +3 -0
  110. package/.next/server/app/api/stream/route/build-manifest.json +11 -0
  111. package/.next/server/app/api/stream/route/server-reference-manifest.json +4 -0
  112. package/.next/server/app/api/stream/route.js +6 -0
  113. package/.next/server/app/api/stream/route.js.map +5 -0
  114. package/.next/server/app/api/stream/route.js.nft.json +1 -0
  115. package/.next/server/app/api/stream/route_client-reference-manifest.js +2 -0
  116. package/.next/server/app/favicon.ico/route/app-paths-manifest.json +3 -0
  117. package/.next/server/app/favicon.ico/route/build-manifest.json +11 -0
  118. package/.next/server/app/favicon.ico/route.js +8 -0
  119. package/.next/server/app/favicon.ico/route.js.map +5 -0
  120. package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
  121. package/.next/server/app/favicon.ico.body +0 -0
  122. package/.next/server/app/favicon.ico.meta +1 -0
  123. package/.next/server/app/index.html +1 -0
  124. package/.next/server/app/index.meta +14 -0
  125. package/.next/server/app/index.rsc +21 -0
  126. package/.next/server/app/index.segments/__PAGE__.segment.rsc +9 -0
  127. package/.next/server/app/index.segments/_full.segment.rsc +21 -0
  128. package/.next/server/app/index.segments/_head.segment.rsc +6 -0
  129. package/.next/server/app/index.segments/_index.segment.rsc +6 -0
  130. package/.next/server/app/index.segments/_tree.segment.rsc +4 -0
  131. package/.next/server/app/page/app-paths-manifest.json +3 -0
  132. package/.next/server/app/page/build-manifest.json +16 -0
  133. package/.next/server/app/page/next-font-manifest.json +11 -0
  134. package/.next/server/app/page/react-loadable-manifest.json +1 -0
  135. package/.next/server/app/page/server-reference-manifest.json +4 -0
  136. package/.next/server/app/page.js +16 -0
  137. package/.next/server/app/page.js.map +5 -0
  138. package/.next/server/app/page.js.nft.json +1 -0
  139. package/.next/server/app/page_client-reference-manifest.js +2 -0
  140. package/.next/server/app-paths-manifest.json +13 -0
  141. package/.next/server/chunks/33f32_next_b78429d5._.js +17 -0
  142. package/.next/server/chunks/33f32_next_b78429d5._.js.map +1 -0
  143. package/.next/server/chunks/33f32_next_dist_esm_build_templates_app-route_5a539e27.js +3 -0
  144. package/.next/server/chunks/33f32_next_dist_esm_build_templates_app-route_5a539e27.js.map +1 -0
  145. package/.next/server/chunks/[externals]_next_dist_b89b5a39._.js +3 -0
  146. package/.next/server/chunks/[externals]_next_dist_b89b5a39._.js.map +1 -0
  147. package/.next/server/chunks/[root-of-the-server]__2554f0b0._.js +17 -0
  148. package/.next/server/chunks/[root-of-the-server]__2554f0b0._.js.map +1 -0
  149. package/.next/server/chunks/[root-of-the-server]__5e671cad._.js +7 -0
  150. package/.next/server/chunks/[root-of-the-server]__5e671cad._.js.map +1 -0
  151. package/.next/server/chunks/[root-of-the-server]__629f5815._.js +3 -0
  152. package/.next/server/chunks/[root-of-the-server]__629f5815._.js.map +1 -0
  153. package/.next/server/chunks/[root-of-the-server]__633f9ccd._.js +3 -0
  154. package/.next/server/chunks/[root-of-the-server]__633f9ccd._.js.map +1 -0
  155. package/.next/server/chunks/[root-of-the-server]__65cdeb73._.js +17 -0
  156. package/.next/server/chunks/[root-of-the-server]__65cdeb73._.js.map +1 -0
  157. package/.next/server/chunks/[root-of-the-server]__bc57ef5b._.js +21 -0
  158. package/.next/server/chunks/[root-of-the-server]__bc57ef5b._.js.map +1 -0
  159. package/.next/server/chunks/[root-of-the-server]__be62c218._.js +17 -0
  160. package/.next/server/chunks/[root-of-the-server]__be62c218._.js.map +1 -0
  161. package/.next/server/chunks/[root-of-the-server]__e87dbf93._.js +3 -0
  162. package/.next/server/chunks/[root-of-the-server]__e87dbf93._.js.map +1 -0
  163. package/.next/server/chunks/[turbopack]_runtime.js +795 -0
  164. package/.next/server/chunks/[turbopack]_runtime.js.map +10 -0
  165. package/.next/server/chunks/c45a0_nextpi__next-internal_server_app_api_files_[___path]_route_actions_16a66e62.js +3 -0
  166. package/.next/server/chunks/c45a0_nextpi__next-internal_server_app_api_files_[___path]_route_actions_16a66e62.js.map +1 -0
  167. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_chat_route_actions_e342cdbf.js +3 -0
  168. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_chat_route_actions_e342cdbf.js.map +1 -0
  169. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_config_route_actions_04d1272c.js +3 -0
  170. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_config_route_actions_04d1272c.js.map +1 -0
  171. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_files_route_actions_a8035013.js +3 -0
  172. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_files_route_actions_a8035013.js.map +1 -0
  173. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_messages_route_actions_4f52ac4b.js +3 -0
  174. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_messages_route_actions_4f52ac4b.js.map +1 -0
  175. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_session_route_actions_ff684c69.js +3 -0
  176. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_session_route_actions_ff684c69.js.map +1 -0
  177. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_stream_route_actions_7a0f6dba.js +3 -0
  178. package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_stream_route_actions_7a0f6dba.js.map +1 -0
  179. package/.next/server/chunks/picode_nextpi__next-internal_server_app_favicon_ico_route_actions_2526216f.js +3 -0
  180. package/.next/server/chunks/picode_nextpi__next-internal_server_app_favicon_ico_route_actions_2526216f.js.map +1 -0
  181. package/.next/server/chunks/ssr/33f32_next_dist_580d3573._.js +6 -0
  182. package/.next/server/chunks/ssr/33f32_next_dist_580d3573._.js.map +1 -0
  183. package/.next/server/chunks/ssr/33f32_next_dist_85b0ccc0._.js +4 -0
  184. package/.next/server/chunks/ssr/33f32_next_dist_85b0ccc0._.js.map +1 -0
  185. package/.next/server/chunks/ssr/33f32_next_dist_client_components_a8ffe8d4._.js +3 -0
  186. package/.next/server/chunks/ssr/33f32_next_dist_client_components_a8ffe8d4._.js.map +1 -0
  187. package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_forbidden_ad97660b.js +3 -0
  188. package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_forbidden_ad97660b.js.map +1 -0
  189. package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_global-error_ab28468b.js +3 -0
  190. package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_global-error_ab28468b.js.map +1 -0
  191. package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_unauthorized_edd1fe08.js +3 -0
  192. package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_unauthorized_edd1fe08.js.map +1 -0
  193. package/.next/server/chunks/ssr/33f32_next_dist_esm_build_templates_app-page_5ef96b51.js +4 -0
  194. package/.next/server/chunks/ssr/33f32_next_dist_esm_build_templates_app-page_5ef96b51.js.map +1 -0
  195. package/.next/server/chunks/ssr/33f32_next_dist_server_route-modules_app-page_vendored_ssr_react-dom_33aeae20.js +3 -0
  196. package/.next/server/chunks/ssr/33f32_next_dist_server_route-modules_app-page_vendored_ssr_react-dom_33aeae20.js.map +1 -0
  197. package/.next/server/chunks/ssr/[root-of-the-server]__028106ef._.js +3 -0
  198. package/.next/server/chunks/ssr/[root-of-the-server]__028106ef._.js.map +1 -0
  199. package/.next/server/chunks/ssr/[root-of-the-server]__0dc9a9e1._.js +4 -0
  200. package/.next/server/chunks/ssr/[root-of-the-server]__0dc9a9e1._.js.map +1 -0
  201. package/.next/server/chunks/ssr/[root-of-the-server]__15f57d5d._.js +3 -0
  202. package/.next/server/chunks/ssr/[root-of-the-server]__15f57d5d._.js.map +1 -0
  203. package/.next/server/chunks/ssr/[root-of-the-server]__26f2451b._.js +3 -0
  204. package/.next/server/chunks/ssr/[root-of-the-server]__26f2451b._.js.map +1 -0
  205. package/.next/server/chunks/ssr/[root-of-the-server]__42ddc417._.js +3 -0
  206. package/.next/server/chunks/ssr/[root-of-the-server]__42ddc417._.js.map +1 -0
  207. package/.next/server/chunks/ssr/[root-of-the-server]__6823e2d4._.js +3 -0
  208. package/.next/server/chunks/ssr/[root-of-the-server]__6823e2d4._.js.map +1 -0
  209. package/.next/server/chunks/ssr/[root-of-the-server]__6bf5dd5d._.js +3 -0
  210. package/.next/server/chunks/ssr/[root-of-the-server]__6bf5dd5d._.js.map +1 -0
  211. package/.next/server/chunks/ssr/[root-of-the-server]__7b7d477c._.js +10 -0
  212. package/.next/server/chunks/ssr/[root-of-the-server]__7b7d477c._.js.map +1 -0
  213. package/.next/server/chunks/ssr/[root-of-the-server]__85c46341._.js +3 -0
  214. package/.next/server/chunks/ssr/[root-of-the-server]__85c46341._.js.map +1 -0
  215. package/.next/server/chunks/ssr/[root-of-the-server]__99c61367._.js +3 -0
  216. package/.next/server/chunks/ssr/[root-of-the-server]__99c61367._.js.map +1 -0
  217. package/.next/server/chunks/ssr/[root-of-the-server]__a0bdeac8._.js +3 -0
  218. package/.next/server/chunks/ssr/[root-of-the-server]__a0bdeac8._.js.map +1 -0
  219. package/.next/server/chunks/ssr/[turbopack]_runtime.js +795 -0
  220. package/.next/server/chunks/ssr/[turbopack]_runtime.js.map +10 -0
  221. package/.next/server/chunks/ssr/_091ac666._.js +3 -0
  222. package/.next/server/chunks/ssr/_091ac666._.js.map +1 -0
  223. package/.next/server/chunks/ssr/picode_nextpi_1e69e558._.js +4 -0
  224. package/.next/server/chunks/ssr/picode_nextpi_1e69e558._.js.map +1 -0
  225. package/.next/server/chunks/ssr/picode_nextpi_4fadcd01._.js +3 -0
  226. package/.next/server/chunks/ssr/picode_nextpi_4fadcd01._.js.map +1 -0
  227. package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app__global-error_page_actions_91eb1cf1.js +3 -0
  228. package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app__global-error_page_actions_91eb1cf1.js.map +1 -0
  229. package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app__not-found_page_actions_0a31df41.js +3 -0
  230. package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app__not-found_page_actions_0a31df41.js.map +1 -0
  231. package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app_page_actions_b14d985f.js +3 -0
  232. package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app_page_actions_b14d985f.js.map +1 -0
  233. package/.next/server/chunks/ssr/picode_nextpi_app_77d1bd95._.js +3 -0
  234. package/.next/server/chunks/ssr/picode_nextpi_app_77d1bd95._.js.map +1 -0
  235. package/.next/server/chunks/ssr/picode_nextpi_app_page_tsx_cebd7814._.js +3 -0
  236. package/.next/server/chunks/ssr/picode_nextpi_app_page_tsx_cebd7814._.js.map +1 -0
  237. package/.next/server/functions-config-manifest.json +4 -0
  238. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  239. package/.next/server/middleware-build-manifest.js +20 -0
  240. package/.next/server/middleware-manifest.json +6 -0
  241. package/.next/server/next-font-manifest.js +1 -0
  242. package/.next/server/next-font-manifest.json +15 -0
  243. package/.next/server/pages/404.html +1 -0
  244. package/.next/server/pages/500.html +2 -0
  245. package/.next/server/pages-manifest.json +4 -0
  246. package/.next/server/server-reference-manifest.js +1 -0
  247. package/.next/server/server-reference-manifest.json +5 -0
  248. package/.next/static/PdRtiPZ_ExBLCzmKGXOBT/_buildManifest.js +11 -0
  249. package/.next/static/PdRtiPZ_ExBLCzmKGXOBT/_clientMiddlewareManifest.json +1 -0
  250. package/.next/static/PdRtiPZ_ExBLCzmKGXOBT/_ssgManifest.js +1 -0
  251. package/.next/static/chunks/0ab43a45e07af0f9.js +1 -0
  252. package/.next/static/chunks/265e06106152dcbb.js +1 -0
  253. package/.next/static/chunks/65d5124f3edd1b84.js +1 -0
  254. package/.next/static/chunks/76111d9b2044b643.js +1 -0
  255. package/.next/static/chunks/81dfdd74986c8afe.css +3 -0
  256. package/.next/static/chunks/93b85e5de0842835.js +5 -0
  257. package/.next/static/chunks/a6dad97d9634a72d.js +1 -0
  258. package/.next/static/chunks/a6dad97d9634a72d.js.map +1 -0
  259. package/.next/static/chunks/cace1b477872431f.js +1 -0
  260. package/.next/static/chunks/turbopack-775e95d747b6e667.js +4 -0
  261. package/.next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
  262. package/.next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
  263. package/.next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
  264. package/.next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
  265. package/.next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
  266. package/.next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
  267. package/.next/static/media/favicon.0b3bf435.ico +0 -0
  268. package/.next/trace +1 -0
  269. package/.next/trace-build +1 -0
  270. package/.next/turbopack +0 -0
  271. package/.next/types/routes.d.ts +79 -0
  272. package/.next/types/validator.ts +133 -0
  273. package/README.md +50 -0
  274. package/app/api/chat/route.ts +28 -0
  275. package/app/api/config/route.ts +25 -0
  276. package/app/api/files/[...path]/route.ts +55 -0
  277. package/app/api/files/route.ts +62 -0
  278. package/app/api/messages/route.ts +26 -0
  279. package/app/api/session/route.ts +55 -0
  280. package/app/api/stream/route.ts +76 -0
  281. package/app/favicon.ico +0 -0
  282. package/app/globals.css +1143 -0
  283. package/app/layout.tsx +46 -0
  284. package/app/page.tsx +1643 -0
  285. package/bin/cli.js +85 -0
  286. package/components/.gitkeep +0 -0
  287. package/components/theme-provider.tsx +71 -0
  288. package/components/ui/button.tsx +67 -0
  289. package/components/ui/file-tree.tsx +560 -0
  290. package/lib/.gitkeep +0 -0
  291. package/lib/agent.ts +105 -0
  292. package/lib/config.ts +50 -0
  293. package/lib/root-path.ts +15 -0
  294. package/lib/session-singleton.ts +48 -0
  295. package/lib/utils.ts +6 -0
  296. package/package.json +72 -0
  297. package/public/.gitkeep +0 -0
  298. package/src/agent.ts +82 -0
  299. package/src/cli.ts +107 -0
  300. package/src/tools.ts +71 -0
  301. package/src/tui.ts +238 -0
  302. package/src/web-server.ts +280 -0
  303. package/tsconfig.json +34 -0
package/app/page.tsx ADDED
@@ -0,0 +1,1643 @@
1
+ // Styling updated to use globals.css — visual changes only
2
+ 'use client';
3
+
4
+ import { useState, useEffect, useRef, useCallback } from 'react';
5
+ import {
6
+ Search,
7
+ RefreshCw,
8
+ Send,
9
+ Terminal,
10
+ Trash2,
11
+ ChevronUp,
12
+ ChevronDown,
13
+ Activity,
14
+ Folder,
15
+ FolderOpen,
16
+ Brain,
17
+ User,
18
+ Bot,
19
+ Wrench,
20
+ FileText,
21
+ Save,
22
+ X,
23
+ FilePlus,
24
+ RotateCcw,
25
+ AlertTriangle,
26
+ Undo,
27
+ ChevronsDownUp,
28
+ ChevronsUpDown,
29
+ Play,
30
+ Square,
31
+ Globe,
32
+ ExternalLink,
33
+ HelpCircle,
34
+ Command,
35
+ Settings,
36
+ } from 'lucide-react';
37
+ import { Tree, type TreeViewElement } from '@/components/ui/file-tree';
38
+
39
+ // Types
40
+ interface Message {
41
+ id: string;
42
+ content: string;
43
+ thinking?: string;
44
+ isUser: boolean;
45
+ isStreaming?: boolean;
46
+ }
47
+
48
+ interface Activity {
49
+ id: string;
50
+ name: string;
51
+ type: string;
52
+ status: 'running' | 'success' | 'error';
53
+ details?: string;
54
+ time: string;
55
+ elapsed?: number;
56
+ }
57
+
58
+ interface TerminalLine {
59
+ id: string;
60
+ content: string;
61
+ type: 'info' | 'command' | 'output' | 'error' | 'success';
62
+ timestamp: string;
63
+ }
64
+
65
+ interface SessionInfo {
66
+ model: string;
67
+ messages: number;
68
+ tokens: number;
69
+ tools: string[];
70
+ }
71
+
72
+ // Markdown renderer
73
+ function renderMarkdown(text: string): string {
74
+ let html = text
75
+ .replace(/&/g, '&')
76
+ .replace(/</g, '&lt;')
77
+ .replace(/>/g, '&gt;');
78
+
79
+ // Code blocks
80
+ html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
81
+ return `<pre><code class="language-${lang || 'text'}">${code.trim()}</code></pre>`;
82
+ });
83
+
84
+ // Inline code
85
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
86
+
87
+ // Headers
88
+ html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
89
+ html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
90
+ html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
91
+
92
+ // Bold and italic
93
+ html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
94
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
95
+ html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
96
+
97
+ // Links
98
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
99
+
100
+ // Line breaks
101
+ html = html.replace(/\n\n/g, '</p><p>');
102
+ html = html.replace(/\n/g, '<br>');
103
+
104
+ if (!html.startsWith('<')) {
105
+ html = `<p>${html}</p>`;
106
+ }
107
+
108
+ return html;
109
+ }
110
+
111
+ const MODELS = [
112
+ { id: 'openrouter/free', name: 'OpenRouter Free (Auto)', description: 'Fast and reliable, uses free models.' },
113
+ { id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', description: 'Very capable, balanced speed & intelligence.' },
114
+ { id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', description: 'Most powerful Claude model.' },
115
+ { id: 'openai/gpt-4o', name: 'GPT-4o', description: 'Flagship OpenAI model.' },
116
+ { id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini', description: 'Fastest OpenAI model.' },
117
+ { id: 'google/gemini-2.0-flash-exp:free', name: 'Gemini 2.0 Flash', description: "Google's fastest large model (Free Preview)." },
118
+ { id: 'google/gemini-pro-1.5', name: 'Gemini 1.5 Pro', description: "Google's most capable model." },
119
+ { id: 'meta-llama/llama-3.2-90b-vision-instruct', name: 'Llama 3.2 90B', description: "Meta's latest high-performing open model." },
120
+ { id: 'qwen/qwen-2.5-72b-instruct', name: 'Qwen 2.5 72B', description: 'Powerful instruction-tuned model from Alibaba.' },
121
+ ];
122
+
123
+ export default function Home() {
124
+ // State
125
+ const [messages, setMessages] = useState<Message[]>([]);
126
+ const [input, setInput] = useState('');
127
+ const [isStreaming, setIsStreaming] = useState(false);
128
+ const [currentMessage, setCurrentMessage] = useState('');
129
+ const [currentThinking, setCurrentThinking] = useState('');
130
+ const [activities, setActivities] = useState<Activity[]>([]);
131
+ const [terminalLines, setTerminalLines] = useState<TerminalLine[]>([]);
132
+ const [isTerminalExpanded, setIsTerminalExpanded] = useState(false);
133
+ const [activeTab, setActiveTab] = useState<'activity' | 'files'>('activity');
134
+ const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'error'>('connecting');
135
+ const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null);
136
+ const [showThinking, setShowThinking] = useState(true);
137
+ const [fileElements, setFileElements] = useState<TreeViewElement[]>([]);
138
+ const [expandedFolders, setExpandedFolders] = useState<string[]>([]);
139
+ const [selectedFileId, setSelectedFileId] = useState<string | undefined>(undefined);
140
+ const isInitialLoad = useRef(true);
141
+
142
+ // File Editor State
143
+ const [editingFile, setEditingFile] = useState<{ id: string, name: string } | null>(null);
144
+ const [editorContent, setEditorContent] = useState('');
145
+ const [originalContent, setOriginalContent] = useState('');
146
+ const [isEditorOpen, setIsEditorOpen] = useState(false);
147
+ const [isSaving, setIsSaving] = useState(false);
148
+ const [isDeleting, setIsDeleting] = useState(false);
149
+ const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
150
+ const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false);
151
+ const [newFileName, setNewFileName] = useState('');
152
+ const [targetDirectory, setTargetDirectory] = useState<string>('');
153
+ const [contextMenu, setContextMenu] = useState<{ x: number, y: number, folderId: string } | null>(null);
154
+ const [isModelSelectorOpen, setIsModelSelectorOpen] = useState(false);
155
+
156
+ // Preview State
157
+ const [isPreviewOpen, setIsPreviewOpen] = useState(false);
158
+ const [previewUrl, setPreviewUrl] = useState('http://localhost:3000');
159
+
160
+ // Settings & Config State
161
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
162
+ const [configInfo, setConfigInfo] = useState<{ hasKey: boolean, maskedKey: string | null }>({ hasKey: false, maskedKey: null });
163
+ const [tempApiKey, setTempApiKey] = useState('');
164
+ const [isSavingConfig, setIsSavingConfig] = useState(false);
165
+ const [isHelpOpen, setIsHelpOpen] = useState(false);
166
+
167
+ // Refs
168
+ const chatRef = useRef<HTMLDivElement>(null);
169
+ const inputRef = useRef<HTMLTextAreaElement>(null);
170
+ const eventSourceRef = useRef<EventSource | null>(null);
171
+ const activityTimers = useRef<Map<string, number>>(new Map());
172
+ const accumulatedMessageRef = useRef<string>('');
173
+ const accumulatedThinkingRef = useRef<string>('');
174
+
175
+ // Scroll to bottom of chat
176
+ const scrollToBottom = useCallback(() => {
177
+ if (chatRef.current) {
178
+ chatRef.current.scrollTop = chatRef.current.scrollHeight;
179
+ }
180
+ }, []);
181
+
182
+ // Add terminal line
183
+ const addTerminalLine = useCallback((content: string, type: TerminalLine['type'] = 'info') => {
184
+ const timestamp = new Date().toLocaleTimeString('en-US', {
185
+ hour12: false,
186
+ hour: '2-digit',
187
+ minute: '2-digit',
188
+ second: '2-digit'
189
+ });
190
+
191
+ setTerminalLines(prev => [...prev, {
192
+ id: `term-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
193
+ content,
194
+ type,
195
+ timestamp
196
+ }]);
197
+ }, []);
198
+
199
+ // Add activity
200
+ const addActivity = useCallback((name: string, type: string, details?: string, explicitId?: string) => {
201
+ // Use explicitId (e.g. tool name) when provided so tool_end can find it by the same key
202
+ const id = explicitId ?? `activity-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
203
+ const time = new Date().toLocaleTimeString('en-US', {
204
+ hour12: false,
205
+ hour: '2-digit',
206
+ minute: '2-digit'
207
+ });
208
+
209
+ const activity: Activity = {
210
+ id,
211
+ name,
212
+ type,
213
+ status: 'running',
214
+ details,
215
+ time,
216
+ elapsed: 0
217
+ };
218
+
219
+ setActivities(prev => [activity, ...prev]);
220
+
221
+ // Start elapsed timer
222
+ const startTime = Date.now();
223
+ const timer = window.setInterval(() => {
224
+ setActivities(prev => prev.map(a =>
225
+ a.id === id ? { ...a, elapsed: Math.round((Date.now() - startTime) / 100) / 10 } : a
226
+ ));
227
+ }, 100);
228
+
229
+ activityTimers.current.set(id, timer);
230
+ return id;
231
+ }, []);
232
+
233
+ // Get all folders for selection
234
+ const getAllFolders = useCallback((elements: TreeViewElement[]): { id: string, name: string }[] => {
235
+ const folders: { id: string, name: string }[] = [{ id: '', name: 'Project Root' }];
236
+
237
+ const scan = (items: TreeViewElement[]) => {
238
+ for (const item of items) {
239
+ if (item.type === 'folder') {
240
+ folders.push({ id: item.id, name: item.id });
241
+ if (item.children) scan(item.children);
242
+ }
243
+ }
244
+ };
245
+
246
+ scan(elements);
247
+ return folders;
248
+ }, []);
249
+
250
+ const handleAddNewFile = useCallback((dirId: string = '') => {
251
+ setTargetDirectory(dirId);
252
+ setNewFileName('');
253
+ setIsNewFileModalOpen(true);
254
+ setContextMenu(null);
255
+ }, []);
256
+ const handleContextMenu = useCallback((e: React.MouseEvent, folderId: string) => {
257
+ e.preventDefault();
258
+ setContextMenu({ x: e.clientX, y: e.clientY, folderId });
259
+ }, []);
260
+
261
+ // Fetch config on mount
262
+ useEffect(() => {
263
+ fetch('/api/config')
264
+ .then(res => res.json())
265
+ .then(data => {
266
+ setConfigInfo(data);
267
+ if (!data.hasKey) {
268
+ setIsSettingsOpen(true);
269
+ }
270
+ });
271
+ }, []);
272
+
273
+ const handleSaveConfig = async () => {
274
+ if (!tempApiKey.trim()) return;
275
+ setIsSavingConfig(true);
276
+ try {
277
+ const res = await fetch('/api/config', {
278
+ method: 'POST',
279
+ headers: { 'Content-Type': 'application/json' },
280
+ body: JSON.stringify({ apiKey: tempApiKey })
281
+ });
282
+ if (res.ok) {
283
+ setTempApiKey('');
284
+ const data = await (await fetch('/api/config')).json();
285
+ setConfigInfo(data);
286
+ setIsSettingsOpen(false);
287
+ addTerminalLine('API configuration updated successfully', 'success');
288
+ }
289
+ } catch (err) {
290
+ addTerminalLine('Failed to save API configuration', 'error');
291
+ } finally {
292
+ setIsSavingConfig(false);
293
+ }
294
+ };
295
+
296
+ useEffect(() => {
297
+ const handleClickOutside = () => setContextMenu(null);
298
+ window.addEventListener('click', handleClickOutside);
299
+ return () => window.removeEventListener('click', handleClickOutside);
300
+ }, []);
301
+
302
+ const expandAll = useCallback(() => {
303
+ const getAllFolderIds = (elements: TreeViewElement[]): string[] => {
304
+ const ids: string[] = [];
305
+ for (const el of elements) {
306
+ if (el.type === 'folder') {
307
+ ids.push(el.id);
308
+ if (el.children) {
309
+ ids.push(...getAllFolderIds(el.children));
310
+ }
311
+ }
312
+ }
313
+ return ids;
314
+ };
315
+ setExpandedFolders(getAllFolderIds(fileElements));
316
+ }, [fileElements]);
317
+
318
+ const collapseAll = useCallback(() => {
319
+ setExpandedFolders([]);
320
+ }, []);
321
+
322
+
323
+ const toggleAllFolders = useCallback(() => {
324
+ if (expandedFolders.length > 0) {
325
+ collapseAll();
326
+ } else {
327
+ expandAll();
328
+ }
329
+ }, [expandedFolders.length, expandAll, collapseAll]);
330
+
331
+ // Complete activity
332
+ const completeActivity = useCallback((id: string, success: boolean) => {
333
+ // Clear timer
334
+ const timer = activityTimers.current.get(id);
335
+ if (timer) {
336
+ clearInterval(timer);
337
+ activityTimers.current.delete(id);
338
+ }
339
+
340
+ setActivities(prev => prev.map(a =>
341
+ a.id === id ? { ...a, status: success ? 'success' : 'error' } : a
342
+ ));
343
+ }, []);
344
+
345
+ // Load chat history
346
+ const loadMessages = useCallback(async () => {
347
+ try {
348
+ const response = await fetch('/api/messages');
349
+ if (response.ok) {
350
+ const data = await response.json();
351
+ if (data.messages && data.messages.length > 0) {
352
+ setMessages(data.messages);
353
+ }
354
+ }
355
+ } catch (err) {
356
+ console.error('Failed to load messages:', err);
357
+ }
358
+ }, []);
359
+
360
+ // Load session info
361
+ const loadSessionInfo = useCallback(async () => {
362
+ try {
363
+ const response = await fetch('/api/session');
364
+ if (response.ok) {
365
+ const data = await response.json();
366
+ setSessionInfo(data);
367
+ }
368
+ } catch (err) {
369
+ console.error('Failed to load session info:', err);
370
+ }
371
+ }, []);
372
+
373
+ // Load file tree
374
+ const loadFileTree = useCallback(async () => {
375
+ try {
376
+ const response = await fetch('/api/files');
377
+ if (response.ok) {
378
+ const data = await response.json();
379
+ setFileElements(data.elements);
380
+ // Only auto-expand if no folders are currently expanded and it's the initial load
381
+ if (isInitialLoad.current && expandedFolders.length === 0) {
382
+ const getIdsToExpand = (elements: TreeViewElement[], depth = 0): string[] => {
383
+ if (depth >= 2) return [];
384
+ const ids: string[] = [];
385
+ for (const el of elements) {
386
+ if (el.type === 'folder') {
387
+ ids.push(el.id);
388
+ if (el.children) {
389
+ ids.push(...getIdsToExpand(el.children, depth + 1));
390
+ }
391
+ }
392
+ }
393
+ return ids;
394
+ };
395
+ setExpandedFolders(getIdsToExpand(data.elements));
396
+ isInitialLoad.current = false;
397
+ }
398
+ }
399
+ } catch (err) {
400
+ console.error('Failed to load file tree:', err);
401
+ }
402
+ }, []);
403
+
404
+ // Handle file operations
405
+ const handleFileSelect = useCallback(async (id: string | undefined) => {
406
+ if (!id) return;
407
+
408
+ // Find the element to see if it's a file
409
+ const findElement = (elements: TreeViewElement[], targetId: string): TreeViewElement | null => {
410
+ for (const el of elements) {
411
+ if (el.id === targetId) return el;
412
+ if (el.children) {
413
+ const found = findElement(el.children, targetId);
414
+ if (found) return found;
415
+ }
416
+ }
417
+ return null;
418
+ };
419
+
420
+ const element = findElement(fileElements, id);
421
+ if (!element || element.type !== 'file') return;
422
+
423
+ try {
424
+ // The id is the relative path
425
+ const pathSegments = id.replace(/\\/g, '/').split('/');
426
+ const response = await fetch(`/api/files/${pathSegments.join('/')}`);
427
+ if (response.ok) {
428
+ const data = await response.json();
429
+ setEditingFile({ id, name: element.name });
430
+ setEditorContent(data.content);
431
+ setOriginalContent(data.content);
432
+ setIsEditorOpen(true);
433
+ } else {
434
+ addTerminalLine(`Failed to load file: ${id}`, 'error');
435
+ }
436
+ } catch (err) {
437
+ console.error('Error loading file:', err);
438
+ addTerminalLine(`Error loading file: ${id}`, 'error');
439
+ }
440
+ }, [fileElements, addTerminalLine]);
441
+
442
+ const saveFile = async () => {
443
+ if (!editingFile) return;
444
+ setIsSaving(true);
445
+ try {
446
+ const pathSegments = editingFile.id.replace(/\\/g, '/').split('/');
447
+ const response = await fetch(`/api/files/${pathSegments.join('/')}`, {
448
+ method: 'PUT',
449
+ headers: { 'Content-Type': 'application/json' },
450
+ body: JSON.stringify({ content: editorContent })
451
+ });
452
+
453
+ if (response.ok) {
454
+ addTerminalLine(`File saved: ${editingFile.name}`, 'info');
455
+ setOriginalContent(editorContent);
456
+ setIsEditorOpen(false);
457
+ loadFileTree();
458
+ } else {
459
+ const data = await response.json();
460
+ addTerminalLine(`Failed to save file: ${data.error}`, 'error');
461
+ }
462
+ } catch (err) {
463
+ addTerminalLine(`Error saving file: ${(err as Error).message}`, 'error');
464
+ } finally {
465
+ setIsSaving(false);
466
+ }
467
+ };
468
+
469
+ const deleteFile = async () => {
470
+ if (!editingFile) return;
471
+
472
+ setIsDeleting(true);
473
+ try {
474
+ const pathSegments = editingFile.id.replace(/\\/g, '/').split('/');
475
+ const response = await fetch(`/api/files/${pathSegments.join('/')}`, {
476
+ method: 'DELETE'
477
+ });
478
+
479
+ if (response.ok) {
480
+ addTerminalLine(`File deleted: ${editingFile.name}`, 'info');
481
+ setIsEditorOpen(false);
482
+ setIsDeleteConfirmOpen(false);
483
+ loadFileTree();
484
+ } else {
485
+ const data = await response.json();
486
+ addTerminalLine(`Failed to delete file: ${data.error}`, 'error');
487
+ }
488
+ } catch (err) {
489
+ addTerminalLine(`Error deleting file: ${(err as Error).message}`, 'error');
490
+ } finally {
491
+ setIsDeleting(false);
492
+ }
493
+ };
494
+
495
+ const handleExpandedChange = useCallback((items: string[] | undefined) => {
496
+ setExpandedFolders(items || []);
497
+ }, []);
498
+
499
+ const handleSelectedIdChange = useCallback((id: string | undefined) => {
500
+ setSelectedFileId(id);
501
+ if (id) {
502
+ handleFileSelect(id);
503
+ }
504
+ }, [handleFileSelect]);
505
+
506
+ // Connect to SSE
507
+ useEffect(() => {
508
+ const connectEventSource = () => {
509
+ if (eventSourceRef.current) {
510
+ eventSourceRef.current.close();
511
+ }
512
+
513
+ const eventSource = new EventSource('/api/stream');
514
+ eventSourceRef.current = eventSource;
515
+
516
+ eventSource.onopen = () => {
517
+ setConnectionStatus('connected');
518
+ };
519
+
520
+ eventSource.onmessage = (e) => {
521
+ const data = JSON.parse(e.data);
522
+
523
+ switch (data.type) {
524
+ case 'start':
525
+ setIsStreaming(true);
526
+ accumulatedMessageRef.current = '';
527
+ accumulatedThinkingRef.current = '';
528
+ setCurrentMessage('');
529
+ setCurrentThinking('');
530
+ addTerminalLine('Agent started responding', 'info');
531
+ break;
532
+
533
+ case 'thinking':
534
+ accumulatedThinkingRef.current += data.content;
535
+ setCurrentThinking(accumulatedThinkingRef.current);
536
+ scrollToBottom();
537
+ break;
538
+
539
+ case 'content':
540
+ accumulatedMessageRef.current += data.content;
541
+ setCurrentMessage(accumulatedMessageRef.current);
542
+ scrollToBottom();
543
+ break;
544
+
545
+ case 'tool_start':
546
+ // Use tool name as the activity ID so tool_end can look it up
547
+ addActivity(data.tool, 'tool', JSON.stringify(data.args).slice(0, 80), data.tool);
548
+ addTerminalLine(`$ ${data.tool}: ${JSON.stringify(data.args)}`, 'command');
549
+ break;
550
+
551
+ case 'tool_end':
552
+ completeActivity(data.tool, !data.isError);
553
+ if (data.output) {
554
+ addTerminalLine(data.output.slice(0, 500), 'output');
555
+ }
556
+ if (data.error) {
557
+ addTerminalLine(`Error: ${data.error}`, 'error');
558
+ }
559
+ break;
560
+
561
+ case 'end':
562
+ setIsStreaming(false);
563
+ // Move accumulated content to a local variable before clearing
564
+ const finalContent = accumulatedMessageRef.current;
565
+ const finalThinking = accumulatedThinkingRef.current;
566
+
567
+ if (finalContent || finalThinking) {
568
+ setMessages(prev => [...prev, {
569
+ id: `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
570
+ content: finalContent,
571
+ thinking: finalThinking,
572
+ isUser: false
573
+ }]);
574
+ }
575
+
576
+ // Clear refs and state
577
+ accumulatedMessageRef.current = '';
578
+ accumulatedThinkingRef.current = '';
579
+ setCurrentMessage('');
580
+ setCurrentThinking('');
581
+
582
+ addTerminalLine('Agent finished', 'info');
583
+ loadSessionInfo();
584
+ // Also sync from backend to be absolutely sure we have the full history
585
+ setTimeout(loadMessages, 500);
586
+ break;
587
+
588
+ case 'compacting':
589
+ addActivity('context-compact', 'tool', 'Compacting conversation');
590
+ addTerminalLine('Compacting context...', 'info');
591
+ break;
592
+
593
+ case 'retry':
594
+ addActivity(`Retry ${data.attempt}/${data.maxAttempts}`, 'retry', `Delay: ${data.delayMs}ms`);
595
+ addTerminalLine(`Retrying attempt ${data.attempt}/${data.maxAttempts}`, 'info');
596
+ break;
597
+
598
+ case 'connected':
599
+ setConnectionStatus('connected');
600
+ break;
601
+ }
602
+ };
603
+
604
+ eventSource.onerror = () => {
605
+ setConnectionStatus('error');
606
+ eventSource.close();
607
+ setTimeout(connectEventSource, 3000);
608
+ };
609
+ };
610
+
611
+ connectEventSource();
612
+ loadSessionInfo();
613
+ loadMessages(); // Load existing history
614
+ loadFileTree();
615
+
616
+ return () => {
617
+ eventSourceRef.current?.close();
618
+ // Clear all timers
619
+ activityTimers.current.forEach(timer => clearInterval(timer));
620
+ };
621
+ }, [addActivity, addTerminalLine, completeActivity, loadFileTree, loadSessionInfo, loadMessages]);
622
+
623
+ // Send message
624
+ const sendMessage = async () => {
625
+ if (!input.trim() || isStreaming) return;
626
+
627
+ const userMessage = input.trim();
628
+ setInput('');
629
+
630
+ // Slash command handling
631
+ if (userMessage.startsWith('/')) {
632
+ const command = userMessage.split(' ')[0].toLowerCase();
633
+
634
+ if (command === '/clear' || command === '/new') {
635
+ addTerminalLine(`Executing command: ${command}`, 'info');
636
+ await resetSession();
637
+ return;
638
+ }
639
+
640
+ if (command === '/model') {
641
+ setIsModelSelectorOpen(true);
642
+ addTerminalLine('Opening model selector...', 'info');
643
+ return;
644
+ }
645
+
646
+ // If unrecognized slash command, maybe just inform the user or ignore
647
+ addTerminalLine(`Unknown command: ${command}`, 'error');
648
+ return;
649
+ }
650
+
651
+ // Add user message
652
+ setMessages(prev => [...prev, {
653
+ id: `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
654
+ content: userMessage,
655
+ isUser: true
656
+ }]);
657
+
658
+ setIsStreaming(true);
659
+
660
+ try {
661
+ const response = await fetch('/api/chat', {
662
+ method: 'POST',
663
+ headers: { 'Content-Type': 'application/json' },
664
+ body: JSON.stringify({ message: userMessage })
665
+ });
666
+
667
+ if (!response.ok) {
668
+ throw new Error('Failed to send message');
669
+ }
670
+ } catch (err: unknown) {
671
+ setIsStreaming(false);
672
+ addTerminalLine(`Error: ${(err as Error).message}`, 'error');
673
+ }
674
+ };
675
+
676
+ // Select model
677
+ const selectModel = async (modelId: string) => {
678
+ setIsModelSelectorOpen(false);
679
+ addTerminalLine(`Switching to model: ${modelId}`, 'info');
680
+
681
+ try {
682
+ const response = await fetch('/api/session', {
683
+ method: 'POST',
684
+ headers: { 'Content-Type': 'application/json' },
685
+ body: JSON.stringify({ modelId })
686
+ });
687
+
688
+ if (response.ok) {
689
+ setMessages([]);
690
+ setCurrentMessage('');
691
+ setActivities([]);
692
+ setTerminalLines([]);
693
+ addTerminalLine(`Switched to ${modelId}`, 'info');
694
+ loadSessionInfo();
695
+ } else {
696
+ addTerminalLine(`Failed to switch model: ${response.statusText}`, 'error');
697
+ }
698
+ } catch (err) {
699
+ console.error('Failed to switch model:', err);
700
+ addTerminalLine('Error switching model', 'error');
701
+ }
702
+ };
703
+
704
+ // Reset session
705
+ const resetSession = async () => {
706
+ try {
707
+ const response = await fetch('/api/session', { method: 'POST' });
708
+ if (response.ok) {
709
+ setMessages([]);
710
+ setCurrentMessage('');
711
+ setActivities([]);
712
+ setTerminalLines([]);
713
+ addTerminalLine('Session reset', 'info');
714
+ loadSessionInfo();
715
+ }
716
+ } catch (err) {
717
+ console.error('Failed to reset session:', err);
718
+ }
719
+ };
720
+
721
+ // Handle input keydown
722
+ const handleKeyDown = (e: React.KeyboardEvent) => {
723
+ if (e.key === 'Enter' && !e.shiftKey) {
724
+ e.preventDefault();
725
+ sendMessage();
726
+ }
727
+ };
728
+
729
+ // Get activity icon
730
+ const getActivityIcon = (type: string) => {
731
+ switch (type) {
732
+ case 'tool': return Wrench;
733
+ case 'retry': return RefreshCw;
734
+ default: return Activity;
735
+ }
736
+ };
737
+
738
+ return (
739
+ <div className="app-wrapper">
740
+ {/* Sidebar */}
741
+ <aside className="sidebar" role="complementary" aria-label="Activity and file browser">
742
+ {/* Connection Status */}
743
+ <div className="connection-status">
744
+ <span
745
+ className={`status-indicator ${connectionStatus}`}
746
+ role="status"
747
+ aria-label={`Connection: ${connectionStatus}`}
748
+ ></span>
749
+ <span className="status-text">
750
+ {connectionStatus === 'connected' ? 'Connected' :
751
+ connectionStatus === 'connecting' ? 'Connecting…' : 'Disconnected'}
752
+ </span>
753
+ </div>
754
+
755
+ {/* Tabs */}
756
+ <div className="sidebar-tabs" role="tablist" aria-label="Sidebar panels">
757
+ <button
758
+ className={`sidebar-tab ${activeTab === 'activity' ? 'active' : ''}`}
759
+ onClick={() => setActiveTab('activity')}
760
+ role="tab"
761
+ aria-selected={activeTab === 'activity'}
762
+ aria-controls="panel-activity"
763
+ >
764
+ <Activity size={14} />
765
+ <span>Activity</span>
766
+ </button>
767
+ <button
768
+ className={`sidebar-tab ${activeTab === 'files' ? 'active' : ''}`}
769
+ onClick={() => setActiveTab('files')}
770
+ role="tab"
771
+ aria-selected={activeTab === 'files'}
772
+ aria-controls="panel-files"
773
+ >
774
+ <Folder size={14} />
775
+ <span>Files</span>
776
+ </button>
777
+ </div>
778
+
779
+ {/* Activity Panel */}
780
+ {activeTab === 'activity' && (
781
+ <div className="sidebar-panel active" id="panel-activity" role="tabpanel">
782
+ <div className="panel-header">
783
+ <h3>Activity Log</h3>
784
+ <button
785
+ className="btn-icon"
786
+ onClick={() => setActivities([])}
787
+ aria-label="Clear activity log"
788
+ >
789
+ <Trash2 size={14} />
790
+ </button>
791
+ </div>
792
+ <div className="activity-log">
793
+ {activities.length === 0 ? (
794
+ <div className="activity-empty">
795
+ {/* Accessible empty state */}
796
+ <span>Waiting for activity…</span>
797
+ </div>
798
+ ) : (
799
+ activities.map(activity => {
800
+ const Icon = getActivityIcon(activity.type);
801
+ return (
802
+ <div key={activity.id} className="activity-item">
803
+ <div className="activity-item-header">
804
+ <Icon
805
+ size={14}
806
+ className={`activity-item-icon ${activity.status === 'running' ? 'running' : ''}`}
807
+ />
808
+ <span className="activity-item-name">{activity.name}</span>
809
+ <span className={`activity-item-status ${activity.status}`}>
810
+ {activity.status === 'running'
811
+ ? `${activity.elapsed?.toFixed(1)}s`
812
+ : activity.status}
813
+ </span>
814
+ </div>
815
+ {activity.details && (
816
+ <div className="activity-item-details">{activity.details}</div>
817
+ )}
818
+ <div className="activity-item-time">{activity.time}</div>
819
+ </div>
820
+ );
821
+ })
822
+ )}
823
+ </div>
824
+ </div>
825
+ )}
826
+
827
+ {/* Files Panel */}
828
+ {activeTab === 'files' && (
829
+ <div className="sidebar-panel active flex flex-col" id="panel-files" role="tabpanel">
830
+ <div className="panel-header">
831
+ <div className="flex items-center gap-2">
832
+ <FolderOpen size={14} className="text-muted-foreground" />
833
+ <h3>Files</h3>
834
+ </div>
835
+ <div className="flex items-center gap-1">
836
+ <button
837
+ className="btn-icon h-8 w-8 bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground transition-all duration-200"
838
+ onClick={toggleAllFolders}
839
+ title={expandedFolders.length > 0 ? "Collapse All" : "Expand All"}
840
+ >
841
+ {expandedFolders.length > 0 ? <ChevronsDownUp size={16} /> : <ChevronsUpDown size={16} />}
842
+ </button>
843
+ <button
844
+ className="btn-icon h-8 w-8 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground transition-all duration-200 shadow-sm"
845
+ onClick={() => {
846
+ setNewFileName('');
847
+ setTargetDirectory('');
848
+ setIsNewFileModalOpen(true);
849
+ }}
850
+ title="New File"
851
+ >
852
+ <FilePlus size={18} />
853
+ </button>
854
+ <span className="text-xs text-muted-foreground ml-1">
855
+ {fileElements.length} {fileElements.length === 1 ? 'item' : 'items'}
856
+ </span>
857
+ </div>
858
+ </div>
859
+ <div className="file-browser flex-1 overflow-hidden px-2 py-2">
860
+ {fileElements.length === 0 ? (
861
+ <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
862
+ Loading…
863
+ </div>
864
+ ) : (
865
+ <Tree
866
+ elements={fileElements}
867
+ initialExpandedItems={expandedFolders}
868
+ initialSelectedId={selectedFileId}
869
+ onExpandedItemsChange={handleExpandedChange}
870
+ onSelectedIdChange={handleSelectedIdChange}
871
+ onFolderContextMenu={handleContextMenu}
872
+ className="h-full"
873
+ />
874
+ )}
875
+ </div>
876
+ </div>
877
+ )}
878
+ </aside>
879
+
880
+ {/* Main Content */}
881
+ <div className="app">
882
+ {/* Header */}
883
+ <header className="header">
884
+ <div className="header-brand">
885
+ <Search className="header-icon" size={18} />
886
+ <h1 className="header-title">NextPi</h1>
887
+ </div>
888
+
889
+ <div className="header-meta">
890
+ <div className="header-info">
891
+ <span><strong>Model</strong> {sessionInfo?.model || '—'}</span>
892
+ <span><strong>Messages</strong> {sessionInfo?.messages || 0}</span>
893
+ <span><strong>Tokens</strong> ~{sessionInfo?.tokens || 0}</span>
894
+ </div>
895
+
896
+ <label className="thinking-toggle">
897
+ <input
898
+ type="checkbox"
899
+ checked={showThinking}
900
+ onChange={(e) => setShowThinking(e.target.checked)}
901
+ />
902
+ <Brain size={14} />
903
+ <span>Thinking</span>
904
+ </label>
905
+
906
+ <button
907
+ className={`thinking-toggle ${isSettingsOpen ? 'bg-accent text-foreground' : ''}`}
908
+ onClick={() => setIsSettingsOpen(true)}
909
+ >
910
+ <Settings size={14} />
911
+ <span>Settings</span>
912
+ </button>
913
+
914
+ <button
915
+ className={`thinking-toggle ${isHelpOpen ? 'bg-accent text-foreground' : ''}`}
916
+ onClick={() => setIsHelpOpen(true)}
917
+ >
918
+ <HelpCircle size={14} />
919
+ <span>Help</span>
920
+ </button>
921
+
922
+ <button className="btn btn-secondary" onClick={resetSession} aria-label="Start new session">
923
+ <RefreshCw size={14} />
924
+ <span>New Session</span>
925
+ </button>
926
+
927
+ <div className="w-px h-6 bg-border mx-1" />
928
+
929
+ <button
930
+ className={`btn ${isPreviewOpen ? 'bg-destructive/10 text-destructive hover:bg-destructive/20 border-destructive/20' : 'btn-primary'} h-9 px-4 transition-all duration-300 shadow-sm`}
931
+ onClick={() => setIsPreviewOpen(!isPreviewOpen)}
932
+ aria-label={isPreviewOpen ? "Stop App" : "Run App"}
933
+ >
934
+ {isPreviewOpen ? (
935
+ <>
936
+ <div className="size-2 rounded-full bg-destructive animate-pulse mr-2 shadow-[0_0_10px_rgba(239,68,68,0.6)]" />
937
+ <Square size={14} className="mr-2 fill-current opacity-70" />
938
+ <span>Stop App</span>
939
+ </>
940
+ ) : (
941
+ <>
942
+ <Play size={14} className="mr-2 fill-current" />
943
+ <span>Run App</span>
944
+ </>
945
+ )}
946
+ </button>
947
+ </div>
948
+ </header>
949
+
950
+ <div className="app-content-area">
951
+ <div className="chat-section">
952
+ <main className="chat-container" ref={chatRef} aria-label="Chat messages" aria-live="polite">
953
+ {messages.length === 0 && !isStreaming && (
954
+ <div className="empty-state">
955
+ <Bot className="empty-state-icon" size={48} />
956
+ <p className="empty-state-title">How can I help you today?</p>
957
+ <p className="empty-state-hint">
958
+ Ask me to research topics, search the web, fetch documentation,
959
+ or help with coding tasks. Use <kbd>Shift+Enter</kbd> for new lines.
960
+ </p>
961
+ </div>
962
+ )}
963
+
964
+ {messages.map(msg => (
965
+ <div key={msg.id} className={`message message-${msg.isUser ? 'user' : 'assistant'}`}>
966
+ <div className="message-header">
967
+ {msg.isUser ? <User size={14} /> : <Bot size={14} />}
968
+ <span>{msg.isUser ? 'You' : 'Assistant'}</span>
969
+ </div>
970
+ <div className="message-bubble">
971
+ {showThinking && msg.thinking && (
972
+ <div className="thinking-block">
973
+ <div className="thinking-header">
974
+ <Brain size={12} className="text-muted-foreground" />
975
+ <span>Reasoning</span>
976
+ </div>
977
+ <div className="thinking-content italic text-muted-foreground/80 leading-relaxed text-sm">
978
+ {msg.thinking}
979
+ </div>
980
+ </div>
981
+ )}
982
+ <div
983
+ className="message-content"
984
+ dangerouslySetInnerHTML={{
985
+ __html: msg.isUser
986
+ ? msg.content.replace(/</g, '&lt;').replace(/>/g, '&gt;')
987
+ : renderMarkdown(msg.content)
988
+ }}
989
+ />
990
+ </div>
991
+ </div>
992
+ ))}
993
+
994
+ {(isStreaming && (currentMessage || currentThinking)) && (
995
+ <div className="message message-assistant">
996
+ <div className="message-header">
997
+ <Bot size={14} />
998
+ <span>NextPi</span>
999
+ </div>
1000
+ <div className="message-bubble">
1001
+ {showThinking && currentThinking && (
1002
+ <div className="thinking-block">
1003
+ <div className="thinking-header">
1004
+ <Brain size={12} className="text-muted-foreground" />
1005
+ <span>Reasoning</span>
1006
+ </div>
1007
+ <div className="thinking-content italic text-muted-foreground/80 leading-relaxed text-sm">
1008
+ {currentThinking}
1009
+ </div>
1010
+ </div>
1011
+ )}
1012
+ {currentMessage ? (
1013
+ <div
1014
+ className="message-content"
1015
+ dangerouslySetInnerHTML={{ __html: renderMarkdown(currentMessage) }}
1016
+ />
1017
+ ) : (
1018
+ <div className="flex gap-2 text-sm text-muted-foreground animate-pulse mt-2 ml-1 items-center">
1019
+ <div className="size-1.5 rounded-full bg-current" />
1020
+ Generating...
1021
+ </div>
1022
+ )}
1023
+ </div>
1024
+ </div>
1025
+ )}
1026
+
1027
+ {isStreaming && !currentMessage && !currentThinking && (
1028
+ <div className="flex gap-2 text-sm text-muted-foreground animate-pulse mt-2 ml-4 mb-4 items-center">
1029
+ <div className="size-1.5 rounded-full bg-current" />
1030
+ Thinking...
1031
+ </div>
1032
+ )}
1033
+ </main>
1034
+
1035
+ {/* Terminal Panel */}
1036
+ <div className={`terminal-panel ${isTerminalExpanded ? 'expanded' : 'collapsed'}`}>
1037
+ <div
1038
+ className="terminal-header"
1039
+ onClick={() => setIsTerminalExpanded(!isTerminalExpanded)}
1040
+ role="button"
1041
+ tabIndex={0}
1042
+ aria-expanded={isTerminalExpanded}
1043
+ aria-controls="terminal-content"
1044
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setIsTerminalExpanded(!isTerminalExpanded); } }}
1045
+ >
1046
+ <div className="terminal-title">
1047
+ <Terminal size={14} />
1048
+ <span>Terminal Output</span>
1049
+ {terminalLines.length > 0 && (
1050
+ <span className="terminal-badge">{terminalLines.length}</span>
1051
+ )}
1052
+ </div>
1053
+ <div className="terminal-controls">
1054
+ <button
1055
+ className="btn-icon"
1056
+ onClick={(e) => { e.stopPropagation(); setTerminalLines([]); }}
1057
+ aria-label="Clear terminal"
1058
+ >
1059
+ <Trash2 size={14} />
1060
+ </button>
1061
+ <button className="btn-icon" aria-label={isTerminalExpanded ? 'Collapse terminal' : 'Expand terminal'}>
1062
+ {isTerminalExpanded ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
1063
+ </button>
1064
+ </div>
1065
+ </div>
1066
+ <div className="terminal-content" id="terminal-content">
1067
+ <div className="terminal-lines">
1068
+ {terminalLines.length === 0 ? (
1069
+ <div className="terminal-line terminal-line-info">
1070
+ Terminal ready. Agent commands will appear here.
1071
+ </div>
1072
+ ) : (
1073
+ terminalLines.map(line => (
1074
+ <div key={line.id} className={`terminal-line terminal-line-${line.type}`}>
1075
+ <span className="terminal-line-timestamp">[{line.timestamp}]</span>
1076
+ {line.content}
1077
+ </div>
1078
+ ))
1079
+ )}
1080
+ </div>
1081
+ </div>
1082
+ </div>
1083
+
1084
+ {/* Input */}
1085
+ <footer className="input-area relative">
1086
+ {input.trim().startsWith('/') && (
1087
+
1088
+ <div className="absolute bottom-full left-4 mb-2 w-48 bg-card border rounded-lg shadow-xl overflow-hidden animate-in slide-in-from-bottom-2 duration-200 z-50">
1089
+ <div className="p-2 border-b bg-muted/50 text-[10px] font-bold text-muted-foreground uppercase tracking-wider">
1090
+ Available Commands
1091
+ </div>
1092
+ <div className="p-1">
1093
+ {[
1094
+ { cmd: '/clear', desc: 'Clear chat & reset' },
1095
+ { cmd: '/new', desc: 'Start new session' },
1096
+ { cmd: '/model', desc: 'Select AI model' },
1097
+ ].map((s) => (
1098
+ <button
1099
+ key={s.cmd}
1100
+ className="w-full flex flex-col items-start px-3 py-1.5 hover:bg-muted rounded-md transition-colors text-left"
1101
+ onClick={async () => {
1102
+ if (s.cmd === '/model') {
1103
+ setIsModelSelectorOpen(true);
1104
+ setInput('');
1105
+ } else if (s.cmd === '/clear' || s.cmd === '/new') {
1106
+ setInput('');
1107
+ await resetSession();
1108
+ } else {
1109
+ setInput(s.cmd);
1110
+ inputRef.current?.focus();
1111
+ }
1112
+ }}
1113
+
1114
+ >
1115
+ <span className="text-sm font-mono font-semibold text-primary">{s.cmd}</span>
1116
+ <span className="text-[10px] text-muted-foreground">{s.desc}</span>
1117
+ </button>
1118
+ ))}
1119
+ </div>
1120
+ </div>
1121
+ )}
1122
+ <div className="input-wrapper">
1123
+ <div className="flex items-end gap-3.5">
1124
+ <textarea
1125
+ ref={inputRef}
1126
+ className="message-input"
1127
+ placeholder="Ask anything or type / for commands…"
1128
+ rows={1}
1129
+ value={input}
1130
+ onChange={(e) => setInput(e.target.value)}
1131
+ onKeyDown={handleKeyDown}
1132
+ disabled={isStreaming}
1133
+ aria-label="Message input"
1134
+ />
1135
+ <button
1136
+ className="btn-send mb-0.5"
1137
+ onClick={sendMessage}
1138
+ disabled={!input.trim() || isStreaming}
1139
+ aria-label="Send message"
1140
+ >
1141
+ <Send size={18} />
1142
+ </button>
1143
+ </div>
1144
+ <span className="input-hint">Enter to send · Shift+Enter for new line</span>
1145
+ </div>
1146
+ </footer>
1147
+ </div>
1148
+
1149
+ {isPreviewOpen && (
1150
+ <aside className="preview-section">
1151
+ <div className="preview-header">
1152
+ <div className="preview-address-bar">
1153
+ <Globe size={14} />
1154
+ <input
1155
+ type="text"
1156
+ value={previewUrl}
1157
+ onChange={(e) => setPreviewUrl(e.target.value)}
1158
+ onKeyDown={(e) => {
1159
+ if (e.key === 'Enter') {
1160
+ const url = previewUrl;
1161
+ setPreviewUrl('');
1162
+ setTimeout(() => setPreviewUrl(url), 10);
1163
+ }
1164
+ }}
1165
+ />
1166
+ <button
1167
+ className="btn-icon h-7 w-7"
1168
+ onClick={() => {
1169
+ const url = previewUrl;
1170
+ setPreviewUrl('');
1171
+ setTimeout(() => setPreviewUrl(url), 10);
1172
+ }}
1173
+ title="Reload Preview"
1174
+ >
1175
+ <RefreshCw size={12} />
1176
+ </button>
1177
+ </div>
1178
+ <a
1179
+ href={previewUrl}
1180
+ target="_blank"
1181
+ rel="noopener noreferrer"
1182
+ className="btn-icon h-8 w-8 text-muted-foreground hover:text-foreground"
1183
+ title="Open in new tab"
1184
+ >
1185
+ <ExternalLink size={16} />
1186
+ </a>
1187
+ <button
1188
+ className="btn-icon h-8 w-8 hover:bg-destructive/10 hover:text-destructive"
1189
+ onClick={() => setIsPreviewOpen(false)}
1190
+ title="Close Preview"
1191
+ >
1192
+ <X size={18} />
1193
+ </button>
1194
+ </div>
1195
+ <div className="preview-content">
1196
+ <iframe
1197
+ src={previewUrl}
1198
+ title="App Preview"
1199
+ sandbox="allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts"
1200
+ />
1201
+ </div>
1202
+ </aside>
1203
+ )}
1204
+ </div>
1205
+ </div>
1206
+
1207
+ {/* File Editor Modal */}
1208
+ {isEditorOpen && (
1209
+ <div
1210
+ className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm animate-in fade-in duration-200"
1211
+ onClick={(e) => { if (e.target === e.currentTarget) setIsEditorOpen(false); }}
1212
+ >
1213
+ <div className="bg-card w-full max-w-4xl h-[80vh] border rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
1214
+ {/* Modal Header */}
1215
+ <div className="px-6 py-4 border-b flex items-center justify-between bg-muted/30">
1216
+ <div className="flex items-center gap-3">
1217
+ <FileText className="text-sky-400" size={18} />
1218
+ <div>
1219
+ <h2 className="text-lg font-semibold leading-none">{editingFile?.name}</h2>
1220
+ <p className="text-xs text-muted-foreground mt-1">{editingFile?.id}</p>
1221
+ </div>
1222
+ </div>
1223
+ <div className="flex items-center gap-2">
1224
+ <button
1225
+ className="btn-icon text-muted-foreground hover:bg-destructive/10 hover:text-destructive transition-colors mr-2"
1226
+ onClick={() => setIsDeleteConfirmOpen(true)}
1227
+ title="Delete File"
1228
+ >
1229
+ <Trash2 size={18} />
1230
+ </button>
1231
+ <div className="w-px h-6 bg-border mr-1" />
1232
+ {editorContent !== originalContent && (
1233
+ <button
1234
+ className="btn btn-secondary h-9 px-3 animate-in fade-in slide-in-from-right-2 duration-200"
1235
+ onClick={() => setEditorContent(originalContent)}
1236
+ disabled={isSaving || isDeleting}
1237
+ >
1238
+ <RotateCcw size={14} className="mr-2" />
1239
+ <span>Revert</span>
1240
+ </button>
1241
+ )}
1242
+ <button
1243
+ className="btn btn-primary h-9 px-4"
1244
+ onClick={saveFile}
1245
+ disabled={isSaving || isDeleting}
1246
+ >
1247
+ {isSaving ? <RefreshCw size={14} className="mr-2 animate-spin" /> : <Save size={14} className="mr-2" />}
1248
+ <span>Save Changes</span>
1249
+ </button>
1250
+ <div className="w-px h-6 bg-border mx-1" />
1251
+ <button
1252
+ className="btn-icon hover:bg-muted"
1253
+ onClick={() => setIsEditorOpen(false)}
1254
+ >
1255
+ <X size={20} />
1256
+ </button>
1257
+ </div>
1258
+ </div>
1259
+
1260
+ {/* Modal Content */}
1261
+ <div className="flex-1 overflow-hidden p-4 bg-zinc-950">
1262
+ <textarea
1263
+ className="w-full h-full bg-transparent text-zinc-300 font-mono text-sm resize-none focus:outline-none scrollbar-thin p-2"
1264
+ spellCheck={false}
1265
+ value={editorContent}
1266
+ onChange={(e) => setEditorContent(e.target.value)}
1267
+ placeholder="File is empty..."
1268
+ />
1269
+ </div>
1270
+
1271
+ {/* Modal Footer */}
1272
+ <div className="px-6 py-3 border-t bg-muted/30 flex justify-between items-center text-xs text-muted-foreground font-mono">
1273
+ <div>
1274
+ {editorContent.length} characters · {editorContent.split('\n').length} lines
1275
+ </div>
1276
+ <div>
1277
+ UTF-8
1278
+ </div>
1279
+ </div>
1280
+ </div>
1281
+ </div>
1282
+ )}
1283
+
1284
+ {/* Delete Confirmation Modal */}
1285
+ {isDeleteConfirmOpen && (
1286
+ <div
1287
+ className="fixed inset-0 z-[60] flex items-center justify-center bg-background/60 backdrop-blur-md animate-in fade-in duration-200"
1288
+ onClick={(e) => { if (e.target === e.currentTarget) setIsDeleteConfirmOpen(false); }}
1289
+ >
1290
+ <div className="bg-card w-full max-w-sm border rounded-xl shadow-2xl p-6 animate-in zoom-in-95 duration-200">
1291
+ <div className="flex items-center gap-3 mb-4 text-destructive">
1292
+ <div className="p-2 bg-destructive/10 rounded-lg">
1293
+ <AlertTriangle size={20} />
1294
+ </div>
1295
+ <h3 className="text-lg font-semibold">Delete File?</h3>
1296
+ </div>
1297
+ <p className="text-sm text-muted-foreground mb-6 leading-relaxed">
1298
+ Are you sure you want to delete <span className="font-mono text-foreground font-medium">{editingFile?.name}</span>? This action is permanent and cannot be undone.
1299
+ </p>
1300
+ <div className="flex justify-end gap-3">
1301
+ <button
1302
+ className="btn btn-secondary h-9 px-4"
1303
+ onClick={() => setIsDeleteConfirmOpen(false)}
1304
+ disabled={isDeleting}
1305
+ >
1306
+ Cancel
1307
+ </button>
1308
+ <button
1309
+ className="btn bg-destructive text-destructive-foreground hover:bg-destructive/90 h-9 px-4 shadow-sm"
1310
+ onClick={deleteFile}
1311
+ disabled={isDeleting}
1312
+ >
1313
+ {isDeleting ? <RefreshCw size={14} className="mr-2 animate-spin" /> : <Trash2 size={14} className="mr-2" />}
1314
+ Delete Forever
1315
+ </button>
1316
+ </div>
1317
+ </div>
1318
+ </div>
1319
+ )}
1320
+
1321
+ {/* New File Modal */}
1322
+ {isNewFileModalOpen && (
1323
+ <div
1324
+ className="fixed inset-0 z-[70] flex items-center justify-center bg-background/60 backdrop-blur-md animate-in fade-in duration-200"
1325
+ onClick={(e) => { if (e.target === e.currentTarget) setIsNewFileModalOpen(false); }}
1326
+ >
1327
+ <div className="bg-card w-full max-w-sm border rounded-xl shadow-2xl p-6 animate-in zoom-in-95 duration-200">
1328
+ <div className="flex items-center gap-3 mb-6">
1329
+ <div className="p-2 bg-primary/10 rounded-lg text-primary">
1330
+ <FilePlus size={20} />
1331
+ </div>
1332
+ <h3 className="text-lg font-semibold text-foreground">Create New File</h3>
1333
+ </div>
1334
+
1335
+ <div className="space-y-6">
1336
+ <div className="space-y-2">
1337
+ <label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1338
+ Target Directory
1339
+ </label>
1340
+ <div className="grid grid-cols-1 gap-2 max-h-40 overflow-y-auto pr-2 scrollbar-thin">
1341
+ {getAllFolders(fileElements).map((folder) => (
1342
+ <button
1343
+ key={folder.id}
1344
+ onClick={() => setTargetDirectory(folder.id)}
1345
+ className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all ${targetDirectory === folder.id
1346
+ ? 'bg-primary text-primary-foreground shadow-sm'
1347
+ : 'bg-muted/30 text-muted-foreground hover:bg-muted hover:text-foreground'
1348
+ }`}
1349
+ >
1350
+ <Folder size={14} className={targetDirectory === folder.id ? 'text-primary-foreground' : 'text-amber-500/70'} />
1351
+ <span className="truncate">{folder.name}</span>
1352
+ </button>
1353
+ ))}
1354
+ </div>
1355
+ </div>
1356
+
1357
+ <div className="space-y-2">
1358
+ <label htmlFor="filename" className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1359
+ File Name
1360
+ </label>
1361
+ <input
1362
+ id="filename"
1363
+ autoFocus
1364
+ type="text"
1365
+ className="w-full bg-muted/50 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all font-mono"
1366
+ placeholder="e.g. index.ts"
1367
+ value={newFileName}
1368
+ onChange={(e) => setNewFileName(e.target.value)}
1369
+ onKeyDown={(e) => {
1370
+ if (e.key === 'Enter' && newFileName.trim()) {
1371
+ const fullPath = targetDirectory ? `${targetDirectory}/${newFileName.trim()}` : newFileName.trim();
1372
+ setEditingFile({ id: fullPath, name: newFileName.trim() });
1373
+ setEditorContent('');
1374
+ setIsEditorOpen(true);
1375
+ setIsNewFileModalOpen(false);
1376
+ } else if (e.key === 'Escape') {
1377
+ setIsNewFileModalOpen(false);
1378
+ }
1379
+ }}
1380
+ />
1381
+ </div>
1382
+ </div>
1383
+
1384
+ <div className="flex justify-end gap-3 mt-8">
1385
+ <button
1386
+ className="btn btn-secondary h-10 px-4"
1387
+ onClick={() => setIsNewFileModalOpen(false)}
1388
+ >
1389
+ Cancel
1390
+ </button>
1391
+ <button
1392
+ className="btn btn-primary h-10 px-6"
1393
+ disabled={!newFileName.trim()}
1394
+ onClick={() => {
1395
+ const fullPath = targetDirectory ? `${targetDirectory}/${newFileName.trim()}` : newFileName.trim();
1396
+ setEditingFile({ id: fullPath, name: newFileName.trim() });
1397
+ setEditorContent('');
1398
+ setIsEditorOpen(true);
1399
+ setIsNewFileModalOpen(false);
1400
+ }}
1401
+ >
1402
+ Create File
1403
+ </button>
1404
+ </div>
1405
+ </div>
1406
+ </div>
1407
+ )}
1408
+
1409
+ {/* Model Selector Modal */}
1410
+ {isModelSelectorOpen && (
1411
+ <div
1412
+ className="fixed inset-0 z-[80] flex items-center justify-center bg-background/60 backdrop-blur-md animate-in fade-in duration-200"
1413
+ onClick={(e) => { if (e.target === e.currentTarget) setIsModelSelectorOpen(false); }}
1414
+ >
1415
+ <div className="bg-card w-full max-w-md border rounded-xl shadow-2xl p-6 animate-in zoom-in-95 duration-200">
1416
+ <div className="flex items-center justify-between mb-6">
1417
+ <div className="flex items-center gap-3">
1418
+ <div className="p-2 bg-primary/10 rounded-lg text-primary">
1419
+ <Bot size={20} />
1420
+ </div>
1421
+ <h3 className="text-lg font-semibold text-foreground">Select AI Model</h3>
1422
+ </div>
1423
+ <button
1424
+ className="btn-icon hover:bg-muted"
1425
+ onClick={() => setIsModelSelectorOpen(false)}
1426
+ >
1427
+ <X size={20} />
1428
+ </button>
1429
+ </div>
1430
+
1431
+ <div className="grid grid-cols-1 gap-2 max-h-[60vh] overflow-y-auto pr-2 scrollbar-thin">
1432
+ {MODELS.map((model) => (
1433
+ <button
1434
+ key={model.id}
1435
+ onClick={() => selectModel(model.id)}
1436
+ className={`flex flex-col gap-1 px-4 py-3 rounded-lg text-left transition-all border ${sessionInfo?.model === model.id
1437
+ ? 'bg-primary/5 border-primary shadow-sm'
1438
+ : 'bg-muted/30 border-transparent hover:bg-muted hover:border-border'
1439
+ }`}
1440
+ >
1441
+ <div className="flex items-center justify-between">
1442
+ <span className={`font-medium ${sessionInfo?.model === model.id ? 'text-primary' : 'text-foreground'}`}>
1443
+ {model.name}
1444
+ </span>
1445
+ {sessionInfo?.model === model.id && (
1446
+ <span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-full font-bold uppercase tracking-wider">
1447
+ Active
1448
+ </span>
1449
+ )}
1450
+ </div>
1451
+ <span className="text-xs text-muted-foreground leading-relaxed">
1452
+ {model.description}
1453
+ </span>
1454
+ </button>
1455
+ ))}
1456
+ </div>
1457
+
1458
+ <div className="mt-6 pt-6 border-t flex justify-center">
1459
+ <p className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold flex items-center gap-2">
1460
+ <RefreshCw size={10} />
1461
+ Session will be reset on model change
1462
+ </p>
1463
+ </div>
1464
+ </div>
1465
+ </div>
1466
+ )}
1467
+
1468
+ {/* Context Menu */}
1469
+
1470
+ {contextMenu && (
1471
+ <div
1472
+ className="fixed z-[100] bg-card border rounded-xl shadow-2xl py-1.5 min-w-44 animate-in fade-in zoom-in-95 duration-100"
1473
+ style={{ top: contextMenu.y, left: contextMenu.x }}
1474
+ onClick={(e) => e.stopPropagation()}
1475
+ >
1476
+ <button
1477
+ className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-left"
1478
+ onClick={() => handleAddNewFile(contextMenu.folderId)}
1479
+ >
1480
+ <FilePlus size={15} />
1481
+ <span>Add File here</span>
1482
+ </button>
1483
+ <div className="h-px bg-border my-1 mx-2" />
1484
+ <button
1485
+ className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-muted-foreground/60 transition-colors text-left cursor-not-allowed"
1486
+ disabled
1487
+ >
1488
+ <Folder size={15} />
1489
+ <span>New Folder (soon)</span>
1490
+ </button>
1491
+ </div>
1492
+ )}
1493
+
1494
+ {/* Settings Modal */}
1495
+ {isSettingsOpen && (
1496
+ <div className="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm animate-in fade-in duration-200">
1497
+ <div className="bg-card w-full max-w-md border rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
1498
+ <div className="p-6">
1499
+ <div className="flex items-center gap-3 mb-6">
1500
+ <div className="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center">
1501
+ <Settings size={22} />
1502
+ </div>
1503
+ <div>
1504
+ <h2 className="text-xl font-semibold tracking-tight">Settings</h2>
1505
+ <p className="text-sm text-muted-foreground line-clamp-1">Configure your NextPi workspace</p>
1506
+ </div>
1507
+ </div>
1508
+
1509
+ <div className="space-y-6">
1510
+ <div className="space-y-2.5">
1511
+ <div className="flex items-center justify-between">
1512
+ <label className="text-sm font-medium">OpenRouter API Key</label>
1513
+ {configInfo.hasKey && (
1514
+ <span className="text-[10px] uppercase tracking-wider font-bold text-emerald-500 bg-emerald-500/10 px-1.5 py-0.5 rounded">Active</span>
1515
+ )}
1516
+ </div>
1517
+ <input
1518
+ type="password"
1519
+ className="w-full px-3.5 py-2.5 bg-background border rounded-xl outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all text-sm"
1520
+ placeholder={configInfo.hasKey ? `Keys ends in ${configInfo.maskedKey?.slice(-4)}` : "sk-or-v1-..."}
1521
+ value={tempApiKey}
1522
+ onChange={(e) => setTempApiKey(e.target.value)}
1523
+ />
1524
+ <p className="text-[11px] text-muted-foreground leading-relaxed">
1525
+ Your key is stored locally in <code className="bg-muted px-1 rounded">~/.nextpi/config.json</code>.
1526
+ It is never shared with anyone except OpenRouter.
1527
+ </p>
1528
+ </div>
1529
+
1530
+ <div className="flex items-center gap-3 pt-2">
1531
+ <button
1532
+ className="flex-1 btn btn-primary py-2.5 justify-center"
1533
+ onClick={handleSaveConfig}
1534
+ disabled={!tempApiKey.trim() || isSavingConfig}
1535
+ >
1536
+ {isSavingConfig ? 'Saving...' : 'Save Configuration'}
1537
+ </button>
1538
+ {configInfo.hasKey && (
1539
+ <button
1540
+ className="btn btn-secondary py-2.5 px-4"
1541
+ onClick={() => {
1542
+ setIsSettingsOpen(false);
1543
+ setTempApiKey('');
1544
+ }}
1545
+ >
1546
+ Close
1547
+ </button>
1548
+ )}
1549
+ </div>
1550
+ </div>
1551
+ </div>
1552
+
1553
+ <div className="px-6 py-4 bg-muted/50 border-t flex items-center justify-between">
1554
+ <span className="text-[11px] text-muted-foreground">NextPi v0.1.0</span>
1555
+ <a
1556
+ href="https://openrouter.ai/keys"
1557
+ target="_blank"
1558
+ rel="noreferrer"
1559
+ className="text-[11px] text-primary hover:underline flex items-center gap-1"
1560
+ >
1561
+ Get a key <ExternalLink size={10} />
1562
+ </a>
1563
+ </div>
1564
+ </div>
1565
+ </div>
1566
+ )}
1567
+
1568
+ {/* Help Modal */}
1569
+ {isHelpOpen && (
1570
+ <div className="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm animate-in fade-in duration-200">
1571
+ <div className="bg-card w-full max-w-lg border rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
1572
+ <div className="p-6">
1573
+ <div className="flex items-center justify-between mb-6">
1574
+ <div className="flex items-center gap-3">
1575
+ <div className="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center">
1576
+ <HelpCircle size={22} />
1577
+ </div>
1578
+ <div>
1579
+ <h2 className="text-xl font-semibold tracking-tight">Quick Guide</h2>
1580
+ <p className="text-sm text-muted-foreground">Master the NextPi workspace</p>
1581
+ </div>
1582
+ </div>
1583
+ <button onClick={() => setIsHelpOpen(false)} className="btn-icon">
1584
+ <X size={20} />
1585
+ </button>
1586
+ </div>
1587
+
1588
+ <div className="space-y-6">
1589
+ {/* Keyboard Shortcuts */}
1590
+ <div className="space-y-3">
1591
+ <h3 className="text-sm font-bold uppercase tracking-widest text-muted-foreground/50 flex items-center gap-2">
1592
+ <Command size={14} /> Keyboard Shortcuts
1593
+ </h3>
1594
+ <div className="grid grid-cols-2 gap-2">
1595
+ {[
1596
+ { key: 'Enter', desc: 'Send Message' },
1597
+ { key: 'Shift + Enter', desc: 'New Line' },
1598
+ { key: '/', desc: 'Quick Commands' },
1599
+ { key: 'Esc', desc: 'Close Modals' },
1600
+ ].map(s => (
1601
+ <div key={s.key} className="flex items-center justify-between p-2.5 bg-muted/30 rounded-xl border border-border/50">
1602
+ <span className="text-xs text-muted-foreground">{s.desc}</span>
1603
+ <kbd className="px-1.5 py-0.5 bg-background border rounded text-[10px] font-bold shadow-sm">{s.key}</kbd>
1604
+ </div>
1605
+ ))}
1606
+ </div>
1607
+ </div>
1608
+
1609
+ {/* Slash Commands */}
1610
+ <div className="space-y-3">
1611
+ <h3 className="text-sm font-bold uppercase tracking-widest text-muted-foreground/50 flex items-center gap-2">
1612
+ <Play size={14} /> Core Concepts
1613
+ </h3>
1614
+ <div className="space-y-2">
1615
+ <div className="p-3 bg-primary/[0.03] border border-primary/10 rounded-xl">
1616
+ <p className="text-xs leading-relaxed text-foreground/80">
1617
+ <strong>The Agent </strong> is aware of your entire project directory. It can create files, run terminal commands, and research your codebase automatically.
1618
+ </p>
1619
+ </div>
1620
+ <div className="p-3 bg-primary/[0.03] border border-primary/10 rounded-xl">
1621
+ <p className="text-xs leading-relaxed text-foreground/80">
1622
+ <strong>Thinking Phase </strong> shows the agent's internal reasoning. Toggle it in the header if you want a cleaner view or need to understand its "logic."
1623
+ </p>
1624
+ </div>
1625
+ </div>
1626
+ </div>
1627
+ </div>
1628
+
1629
+ <div className="mt-8 flex justify-center">
1630
+ <button
1631
+ className="btn btn-primary px-8 py-2.5 rounded-full"
1632
+ onClick={() => setIsHelpOpen(false)}
1633
+ >
1634
+ Got it!
1635
+ </button>
1636
+ </div>
1637
+ </div>
1638
+ </div>
1639
+ </div>
1640
+ )}
1641
+ </div>
1642
+ );
1643
+ }