wiki-viewer 1.14.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (559) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  4. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/standalone/.next/server/app/api/agent/activity/route.js +6 -6
  20. package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js +8 -8
  22. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +9 -9
  24. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js +8 -8
  26. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js +8 -8
  28. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js +8 -8
  30. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js +9 -9
  32. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js +8 -8
  34. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/agent/settings/route.js +7 -7
  36. package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js +6 -6
  38. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/assets/[...path]/route.js +5 -5
  41. package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/share/[token]/route.js +6 -5
  43. package/.next/standalone/.next/server/app/api/share/[token]/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/share/route.js +4 -4
  45. package/.next/standalone/.next/server/app/api/share/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/system/reveal/route.js +6 -6
  49. package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/system/set-root/route.js +6 -5
  52. package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route.js +5 -4
  54. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/refresh/route.js +6 -5
  56. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/refresh/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route.js +5 -4
  58. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/system/workspaces/route.js +7 -6
  60. package/.next/standalone/.next/server/app/api/system/workspaces/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/upload/[...path]/route.js +5 -5
  62. package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/wiki/app/route.js +7 -7
  64. package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/api/wiki/backlinks/route.js +7 -7
  66. package/.next/standalone/.next/server/app/api/wiki/backlinks/route.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/api/wiki/content/route.js +7 -7
  68. package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/api/wiki/download/route.js +6 -6
  70. package/.next/standalone/.next/server/app/api/wiki/download/route.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/api/wiki/file/route.js +5 -5
  72. package/.next/standalone/.next/server/app/api/wiki/file/route.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/api/wiki/folder/route.js +5 -5
  74. package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/api/wiki/git-branches/route.js +4 -4
  76. package/.next/standalone/.next/server/app/api/wiki/git-branches/route.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/api/wiki/git-checkout/route.js +6 -6
  78. package/.next/standalone/.next/server/app/api/wiki/git-checkout/route.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/api/wiki/git-diff/route.js +4 -4
  80. package/.next/standalone/.next/server/app/api/wiki/git-diff/route.js.nft.json +1 -1
  81. package/.next/standalone/.next/server/app/api/wiki/git-file-info/route.js +4 -4
  82. package/.next/standalone/.next/server/app/api/wiki/git-file-info/route.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/api/wiki/git-history/route.js +4 -4
  84. package/.next/standalone/.next/server/app/api/wiki/git-history/route.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/api/wiki/git-pull/route.js +8 -8
  86. package/.next/standalone/.next/server/app/api/wiki/git-pull/route.js.nft.json +1 -1
  87. package/.next/standalone/.next/server/app/api/wiki/move/route.js +6 -6
  88. package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
  89. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js +5 -5
  90. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/api/wiki/outlinks/route.js +7 -7
  92. package/.next/standalone/.next/server/app/api/wiki/outlinks/route.js.nft.json +1 -1
  93. package/.next/standalone/.next/server/app/api/wiki/page/route.js +5 -5
  94. package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
  95. package/.next/standalone/.next/server/app/api/wiki/presence/route.js +6 -6
  96. package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -1
  97. package/.next/standalone/.next/server/app/api/wiki/route.js +5 -5
  98. package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
  99. package/.next/standalone/.next/server/app/api/wiki/search/route.js +5 -5
  100. package/.next/standalone/.next/server/app/api/wiki/search/route.js.nft.json +1 -1
  101. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js +5 -5
  102. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
  103. package/.next/standalone/.next/server/app/api/wiki/upload/route.js +5 -5
  104. package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/api/wiki/watch/route.js +5 -5
  106. package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/index.html +1 -1
  108. package/.next/standalone/.next/server/app/index.rsc +3 -3
  109. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  110. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  111. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  112. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  113. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  114. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  115. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  116. package/.next/standalone/.next/server/app/s/[token]/page_client-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
  118. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-jzw2_._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.-oq-7._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/[root-of-the-server]__000spch._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01oin-a._.js +3 -0
  122. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01qcera._.js +16 -0
  123. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02.uul~._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/[root-of-the-server]__024967n._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06booxp._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/[root-of-the-server]__071_pmz._.js +3 -0
  127. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0bqpg._.js +3 -0
  128. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_1f2cw._.js +3 -0
  129. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0a6j6r4._.js +3 -0
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0aoqat-._.js +3 -0
  131. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0as-u6z._.js +3 -0
  132. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bvtbax._.js +3 -0
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0c-ee48._.js +5 -0
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e_9pc5._.js +3 -0
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0esmskc._.js +3 -0
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0fshk._._.js +20 -0
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0fu72o7._.js +3 -0
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0k9mili._.js +3 -0
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l-wu5u._.js +3 -0
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0lvz22t._.js +20 -0
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0n-chxk._.js +16 -0
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0n9zdf0._.js +3 -0
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nmx3k2._.js +12 -0
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nzlmkb._.js +3 -0
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o0v6hf._.js +3 -0
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s38pid._.js +3 -0
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s_8poz._.js +3 -0
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sfrx~.._.js +3 -0
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sggstu._.js +3 -0
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0u81k~2._.js +3 -0
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uibxf_._.js +3 -0
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wfv_by._.js +3 -0
  153. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wlj-yw._.js +3 -0
  154. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w~__xz._.js +3 -0
  155. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yat~ul._.js +3 -0
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yxauu6._.js +3 -0
  157. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zbkgnr._.js +3 -0
  158. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zv_j05._.js +3 -0
  159. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0~rntxj._.js +3 -0
  160. package/.next/standalone/.next/server/chunks/src_lib_search_0d-_j3u._.js +4 -4
  161. package/.next/standalone/.next/server/chunks/src_lib_search_0ymmp0c._.js +3 -3
  162. package/.next/standalone/.next/server/chunks/src_lib_workspaces_ts_0qon40u._.js +4 -0
  163. package/.next/standalone/.next/server/chunks/ssr/{0~w0_katex_dist_katex_mjs_029cdbg._.js → 0~w0_katex_dist_katex_mjs_0s4yb1t._.js} +1 -1
  164. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0z41nrr._.js → [root-of-the-server]__10te1ye._.js} +2 -2
  165. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_13p348f._.js → node_modules__pnpm_0-a_zjq._.js} +1 -1
  166. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0gos._c._.js → node_modules__pnpm_02l_1_8._.js} +1 -1
  167. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0ru1i2-._.js → node_modules__pnpm_03rzttw._.js} +2 -2
  168. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_08xpp3w._.js → node_modules__pnpm_04frxp9._.js} +1 -1
  169. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0b8odbc._.js → node_modules__pnpm_057-t6m._.js} +1 -1
  170. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0f_n_zp._.js → node_modules__pnpm_05_oahi._.js} +1 -1
  171. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0~i3a.i._.js → node_modules__pnpm_05ans~m._.js} +1 -1
  172. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0i2m0b.._.js → node_modules__pnpm_06-2ia.._.js} +2 -2
  173. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0aha1~u._.js → node_modules__pnpm_086.ow4._.js} +1 -1
  174. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0w.kwue._.js → node_modules__pnpm_0cahog3._.js} +1 -1
  175. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0e0r_qk._.js → node_modules__pnpm_0cxanql._.js} +2 -2
  176. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0ajqqlq._.js → node_modules__pnpm_0e09gmf._.js} +1 -1
  177. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hzzx9d._.js → node_modules__pnpm_0eqjo2f._.js} +1 -1
  178. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_02bje_g._.js → node_modules__pnpm_0f-yazg._.js} +1 -1
  179. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0xk042~._.js → node_modules__pnpm_0iu44jv._.js} +1 -1
  180. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_11ede0p._.js → node_modules__pnpm_0jh8q66._.js} +2 -2
  181. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0k-7uzm._.js → node_modules__pnpm_0m39f~t._.js} +1 -1
  182. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0xr.ztu._.js → node_modules__pnpm_0ma-8l.._.js} +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0i6_kfe._.js → node_modules__pnpm_0met6sx._.js} +2 -2
  184. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_04v1_ty._.js → node_modules__pnpm_0mtrsi0._.js} +2 -2
  185. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0607~kg._.js → node_modules__pnpm_0n_swwn._.js} +2 -2
  186. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0t4l89_._.js → node_modules__pnpm_0ndyy93._.js} +1 -1
  187. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0-_58b8._.js → node_modules__pnpm_0s-cj7c._.js} +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0y2h_cz._.js → node_modules__pnpm_0tr41-t._.js} +1 -1
  189. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_08edl1n._.js → node_modules__pnpm_0u.uija._.js} +1 -1
  190. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0026mgr._.js → node_modules__pnpm_0wiyn6d._.js} +1 -1
  191. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_09o_spj._.js → node_modules__pnpm_0ygzt82._.js} +1 -1
  192. package/.next/standalone/.next/server/chunks/ssr/src_app_page_tsx_0ss2.w7._.js +2 -2
  193. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  194. package/.next/standalone/.next/server/middleware-manifest.json +1 -1
  195. package/.next/standalone/.next/server/pages/404.html +1 -1
  196. package/.next/standalone/.next/server/pages/500.html +1 -1
  197. package/.next/standalone/.next/static/chunks/0ci7khnv7i~7_.js +17 -0
  198. package/.next/standalone/.next/static/chunks/0sx~65~lg4mgl.css +5 -0
  199. package/.next/standalone/package.json +1 -1
  200. package/README.md +88 -17
  201. package/bin/postbuild.js +21 -0
  202. package/bin/wiki-viewer.js +198 -6
  203. package/package.json +1 -1
  204. package/.next/standalone/.github/workflows/publish.yml +0 -37
  205. package/.next/standalone/.handoffs/archive/handoff-20260612-113332.md +0 -98
  206. package/.next/standalone/.handoffs/archive/handoff-20260612-141032.md +0 -73
  207. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-.y6im._.js +0 -3
  208. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-3_t2y._.js +0 -3
  209. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-kjkzt._.js +0 -3
  210. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-puax4._.js +0 -4
  211. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01h996g._.js +0 -3
  212. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0329ktu._.js +0 -3
  213. package/.next/standalone/.next/server/chunks/[root-of-the-server]__03_xj4r._.js +0 -3
  214. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05398dt._.js +0 -4
  215. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06lfvxh._.js +0 -3
  216. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06w0aet._.js +0 -3
  217. package/.next/standalone/.next/server/chunks/[root-of-the-server]__070_p2a._.js +0 -4
  218. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0905hi1._.js +0 -21
  219. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0aa63~j._.js +0 -5
  220. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0avcplk._.js +0 -4
  221. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b8_kyo._.js +0 -16
  222. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bticq1._.js +0 -3
  223. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bwevvu._.js +0 -3
  224. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0c__k3q._.js +0 -4
  225. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cp~z4x._.js +0 -3
  226. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0fmdv9h._.js +0 -20
  227. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0h0qbwr._.js +0 -3
  228. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jk~oyi._.js +0 -3
  229. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ndq77b._.js +0 -3
  230. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nvgpuk._.js +0 -3
  231. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ocvng2._.js +0 -3
  232. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ps4h~.._.js +0 -3
  233. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q7jqwp._.js +0 -12
  234. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qew~6p._.js +0 -3
  235. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s_9f-a._.js +0 -3
  236. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uv7b8u._.js +0 -3
  237. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uv8.9i._.js +0 -3
  238. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vfe2io._.js +0 -4
  239. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w11kjy._.js +0 -3
  240. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wc-.e4._.js +0 -3
  241. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ymovmp._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0~c6ouv._.js +0 -3
  243. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0~rhs0u._.js +0 -16
  244. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10h9tid._.js +0 -3
  245. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10yk-u1._.js +0 -3
  246. package/.next/standalone/.next/server/chunks/[root-of-the-server]__11105ew._.js +0 -3
  247. package/.next/standalone/.next/server/chunks/[root-of-the-server]__11a3dhw._.js +0 -3
  248. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13420tt._.js +0 -3
  249. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13h~pdf._.js +0 -3
  250. package/.next/standalone/.next/static/chunks/02kg.lbkfi1yc.css +0 -5
  251. package/.next/standalone/.next/static/chunks/0rcwd6-.iu~nd.js +0 -17
  252. package/.next/standalone/.pi/ownership.json +0 -3
  253. package/.next/standalone/AGENTS.md +0 -114
  254. package/.next/standalone/DESIGN.md +0 -521
  255. package/.next/standalone/README.md +0 -821
  256. package/.next/standalone/TODO.md +0 -20
  257. package/.next/standalone/agents/bootstrap-prompt.md +0 -14
  258. package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +0 -456
  259. package/.next/standalone/bin/postbuild.js +0 -78
  260. package/.next/standalone/bin/wiki-viewer.js +0 -922
  261. package/.next/standalone/certificates/localhost-key.pem +0 -28
  262. package/.next/standalone/certificates/localhost.pem +0 -24
  263. package/.next/standalone/docs/agent-collab-plan.md +0 -1615
  264. package/.next/standalone/docs/agent-collab-v2-plan.md +0 -771
  265. package/.next/standalone/docs/agent-fs-plan.md +0 -232
  266. package/.next/standalone/docs/editorial-skin-spec.md +0 -242
  267. package/.next/standalone/docs/file-vs-collab-authority.md +0 -124
  268. package/.next/standalone/docs/mobile-readiness-spec.md +0 -229
  269. package/.next/standalone/docs/workspacing-plan.md +0 -364
  270. package/.next/standalone/packages/wiki-viewer-mcp/README.md +0 -195
  271. package/.next/standalone/packages/wiki-viewer-mcp/bench-e2e.mjs +0 -166
  272. package/.next/standalone/packages/wiki-viewer-mcp/bench-latency.mjs +0 -87
  273. package/.next/standalone/packages/wiki-viewer-mcp/bench-payload.mjs +0 -45
  274. package/.next/standalone/packages/wiki-viewer-mcp/dist/http-client.js +0 -331
  275. package/.next/standalone/packages/wiki-viewer-mcp/dist/index.js +0 -531
  276. package/.next/standalone/packages/wiki-viewer-mcp/dist/register.js +0 -94
  277. package/.next/standalone/packages/wiki-viewer-mcp/dist/state-cache.js +0 -28
  278. package/.next/standalone/packages/wiki-viewer-mcp/package-lock.json +0 -1716
  279. package/.next/standalone/packages/wiki-viewer-mcp/package.json +0 -41
  280. package/.next/standalone/packages/wiki-viewer-mcp/src/http-client.ts +0 -459
  281. package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +0 -638
  282. package/.next/standalone/packages/wiki-viewer-mcp/src/register.ts +0 -144
  283. package/.next/standalone/packages/wiki-viewer-mcp/src/state-cache.ts +0 -55
  284. package/.next/standalone/packages/wiki-viewer-mcp/tsconfig.json +0 -19
  285. package/.next/standalone/pnpm-lock.yaml +0 -6697
  286. package/.next/standalone/pnpm-workspace.yaml +0 -8
  287. package/.next/standalone/postcss.config.mjs +0 -2
  288. package/.next/standalone/src/app/api/agent/activity/route.ts +0 -59
  289. package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +0 -40
  290. package/.next/standalone/src/app/api/agent/admin/agents/route.ts +0 -33
  291. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +0 -83
  292. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +0 -36
  293. package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +0 -27
  294. package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +0 -148
  295. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +0 -219
  296. package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +0 -446
  297. package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +0 -152
  298. package/.next/standalone/src/app/api/agent/fs/move/route.ts +0 -130
  299. package/.next/standalone/src/app/api/agent/fs/search/route.ts +0 -272
  300. package/.next/standalone/src/app/api/agent/internal/span/route.ts +0 -121
  301. package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +0 -50
  302. package/.next/standalone/src/app/api/agent/register/route.ts +0 -118
  303. package/.next/standalone/src/app/api/agent/settings/route.ts +0 -34
  304. package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +0 -20
  305. package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +0 -53
  306. package/.next/standalone/src/app/api/agents/install/route.ts +0 -143
  307. package/.next/standalone/src/app/api/agents/skill/route.ts +0 -20
  308. package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +0 -87
  309. package/.next/standalone/src/app/api/app-proxy/[...path]/route.ts +0 -247
  310. package/.next/standalone/src/app/api/assets/[...path]/route.ts +0 -75
  311. package/.next/standalone/src/app/api/auth/[...all]/route.ts +0 -7
  312. package/.next/standalone/src/app/api/owner/init/route.ts +0 -14
  313. package/.next/standalone/src/app/api/share/[token]/route.ts +0 -269
  314. package/.next/standalone/src/app/api/share/route.ts +0 -127
  315. package/.next/standalone/src/app/api/system/admins/route.ts +0 -103
  316. package/.next/standalone/src/app/api/system/auth-config/route.ts +0 -22
  317. package/.next/standalone/src/app/api/system/auth-settings/route.ts +0 -86
  318. package/.next/standalone/src/app/api/system/browse/route.ts +0 -82
  319. package/.next/standalone/src/app/api/system/clear-root/route.ts +0 -16
  320. package/.next/standalone/src/app/api/system/config/route.ts +0 -14
  321. package/.next/standalone/src/app/api/system/pins/route.ts +0 -38
  322. package/.next/standalone/src/app/api/system/reveal/route.ts +0 -36
  323. package/.next/standalone/src/app/api/system/root-status/route.ts +0 -13
  324. package/.next/standalone/src/app/api/system/set-root/route.ts +0 -53
  325. package/.next/standalone/src/app/api/system/users/route.ts +0 -120
  326. package/.next/standalone/src/app/api/system/workspaces/[id]/open/route.ts +0 -34
  327. package/.next/standalone/src/app/api/system/workspaces/[id]/refresh/route.ts +0 -32
  328. package/.next/standalone/src/app/api/system/workspaces/[id]/route.ts +0 -78
  329. package/.next/standalone/src/app/api/system/workspaces/route.ts +0 -128
  330. package/.next/standalone/src/app/api/upload/[...path]/route.ts +0 -85
  331. package/.next/standalone/src/app/api/wiki/app/route.ts +0 -60
  332. package/.next/standalone/src/app/api/wiki/backlinks/route.ts +0 -29
  333. package/.next/standalone/src/app/api/wiki/content/route.ts +0 -159
  334. package/.next/standalone/src/app/api/wiki/download/route.ts +0 -100
  335. package/.next/standalone/src/app/api/wiki/file/route.ts +0 -57
  336. package/.next/standalone/src/app/api/wiki/folder/route.ts +0 -34
  337. package/.next/standalone/src/app/api/wiki/git-branches/route.ts +0 -32
  338. package/.next/standalone/src/app/api/wiki/git-checkout/route.ts +0 -45
  339. package/.next/standalone/src/app/api/wiki/git-diff/route.ts +0 -33
  340. package/.next/standalone/src/app/api/wiki/git-file-info/route.ts +0 -27
  341. package/.next/standalone/src/app/api/wiki/git-history/route.ts +0 -27
  342. package/.next/standalone/src/app/api/wiki/git-pull/route.ts +0 -48
  343. package/.next/standalone/src/app/api/wiki/move/route.ts +0 -61
  344. package/.next/standalone/src/app/api/wiki/new-file/route.ts +0 -56
  345. package/.next/standalone/src/app/api/wiki/outlinks/route.ts +0 -76
  346. package/.next/standalone/src/app/api/wiki/page/route.ts +0 -92
  347. package/.next/standalone/src/app/api/wiki/presence/route.ts +0 -56
  348. package/.next/standalone/src/app/api/wiki/route.ts +0 -116
  349. package/.next/standalone/src/app/api/wiki/search/route.ts +0 -44
  350. package/.next/standalone/src/app/api/wiki/slugs/route.ts +0 -54
  351. package/.next/standalone/src/app/api/wiki/upload/route.ts +0 -95
  352. package/.next/standalone/src/app/api/wiki/watch/route.ts +0 -67
  353. package/.next/standalone/src/app/globals.css +0 -1249
  354. package/.next/standalone/src/app/icon.svg +0 -11
  355. package/.next/standalone/src/app/layout.tsx +0 -78
  356. package/.next/standalone/src/app/manifest.ts +0 -30
  357. package/.next/standalone/src/app/page.tsx +0 -3260
  358. package/.next/standalone/src/app/s/[token]/page.tsx +0 -357
  359. package/.next/standalone/src/app/signin/page.tsx +0 -16
  360. package/.next/standalone/src/app/signin/signin-form.tsx +0 -238
  361. package/.next/standalone/src/components/ai-panel/activity-row.tsx +0 -284
  362. package/.next/standalone/src/components/ai-panel/ai-panel.tsx +0 -311
  363. package/.next/standalone/src/components/ai-panel/token-section.tsx +0 -313
  364. package/.next/standalone/src/components/auth-settings-sheet.tsx +0 -474
  365. package/.next/standalone/src/components/confirm-dialog.tsx +0 -56
  366. package/.next/standalone/src/components/dir-picker.tsx +0 -606
  367. package/.next/standalone/src/components/editor/backlinks-panel.tsx +0 -139
  368. package/.next/standalone/src/components/editor/bubble-menu.tsx +0 -346
  369. package/.next/standalone/src/components/editor/callout-extension.ts +0 -36
  370. package/.next/standalone/src/components/editor/comment-pip.tsx +0 -56
  371. package/.next/standalone/src/components/editor/comment-thread.tsx +0 -254
  372. package/.next/standalone/src/components/editor/csv-viewer.tsx +0 -379
  373. package/.next/standalone/src/components/editor/document-outline.tsx +0 -230
  374. package/.next/standalone/src/components/editor/editor-toolbar.tsx +0 -564
  375. package/.next/standalone/src/components/editor/editor.tsx +0 -1327
  376. package/.next/standalone/src/components/editor/extensions/drag-handle.ts +0 -325
  377. package/.next/standalone/src/components/editor/extensions/heading-anchors.ts +0 -55
  378. package/.next/standalone/src/components/editor/extensions/proof-span.ts +0 -60
  379. package/.next/standalone/src/components/editor/extensions/resizable-image.tsx +0 -159
  380. package/.next/standalone/src/components/editor/extensions.ts +0 -150
  381. package/.next/standalone/src/components/editor/file-fallback-viewer.tsx +0 -75
  382. package/.next/standalone/src/components/editor/folder-index.tsx +0 -236
  383. package/.next/standalone/src/components/editor/google-doc-viewer.tsx +0 -73
  384. package/.next/standalone/src/components/editor/image-viewer.tsx +0 -57
  385. package/.next/standalone/src/components/editor/large-file-gate.tsx +0 -108
  386. package/.next/standalone/src/components/editor/link-popover.tsx +0 -111
  387. package/.next/standalone/src/components/editor/media-popover.tsx +0 -212
  388. package/.next/standalone/src/components/editor/media-viewer.tsx +0 -70
  389. package/.next/standalone/src/components/editor/mermaid-viewer.tsx +0 -286
  390. package/.next/standalone/src/components/editor/node-app-viewer.tsx +0 -328
  391. package/.next/standalone/src/components/editor/notebook-viewer.tsx +0 -371
  392. package/.next/standalone/src/components/editor/office/docx-viewer.tsx +0 -93
  393. package/.next/standalone/src/components/editor/office/office-chrome.tsx +0 -83
  394. package/.next/standalone/src/components/editor/office/pptx-viewer.tsx +0 -104
  395. package/.next/standalone/src/components/editor/office/xlsx-viewer.tsx +0 -192
  396. package/.next/standalone/src/components/editor/pdf-viewer.tsx +0 -32
  397. package/.next/standalone/src/components/editor/proof-span-popover.tsx +0 -162
  398. package/.next/standalone/src/components/editor/slash-commands.tsx +0 -629
  399. package/.next/standalone/src/components/editor/source-viewer.tsx +0 -289
  400. package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +0 -229
  401. package/.next/standalone/src/components/editor/suggestion-card.tsx +0 -206
  402. package/.next/standalone/src/components/editor/table-menu.tsx +0 -232
  403. package/.next/standalone/src/components/editor/view-mode-comment-button.tsx +0 -77
  404. package/.next/standalone/src/components/editor/website-viewer.tsx +0 -75
  405. package/.next/standalone/src/components/editor/wiki-link-create-dialog.tsx +0 -189
  406. package/.next/standalone/src/components/editor/wiki-link-decorator.ts +0 -79
  407. package/.next/standalone/src/components/editor/wiki-link-extension.ts +0 -120
  408. package/.next/standalone/src/components/editor/wiki-link-picker.tsx +0 -250
  409. package/.next/standalone/src/components/layout/viewer-toolbar.tsx +0 -54
  410. package/.next/standalone/src/components/search/search-command-dialog.tsx +0 -246
  411. package/.next/standalone/src/components/search/sidebar-search-box.tsx +0 -121
  412. package/.next/standalone/src/components/search/snippet-text.tsx +0 -48
  413. package/.next/standalone/src/components/share-dialog.tsx +0 -307
  414. package/.next/standalone/src/components/theme-provider.tsx +0 -16
  415. package/.next/standalone/src/components/theme-toggle.tsx +0 -24
  416. package/.next/standalone/src/components/ui/badge.tsx +0 -35
  417. package/.next/standalone/src/components/ui/button.tsx +0 -56
  418. package/.next/standalone/src/components/ui/card.tsx +0 -63
  419. package/.next/standalone/src/components/ui/collapsible.tsx +0 -9
  420. package/.next/standalone/src/components/ui/command.tsx +0 -151
  421. package/.next/standalone/src/components/ui/context-menu.tsx +0 -191
  422. package/.next/standalone/src/components/ui/dialog.tsx +0 -119
  423. package/.next/standalone/src/components/ui/dropdown-menu.tsx +0 -194
  424. package/.next/standalone/src/components/ui/input.tsx +0 -22
  425. package/.next/standalone/src/components/ui/label.tsx +0 -25
  426. package/.next/standalone/src/components/ui/popover.tsx +0 -33
  427. package/.next/standalone/src/components/ui/scroll-area.tsx +0 -47
  428. package/.next/standalone/src/components/ui/select.tsx +0 -156
  429. package/.next/standalone/src/components/ui/separator.tsx +0 -30
  430. package/.next/standalone/src/components/ui/sheet.tsx +0 -140
  431. package/.next/standalone/src/components/ui/skeleton.tsx +0 -15
  432. package/.next/standalone/src/components/ui/switch.tsx +0 -28
  433. package/.next/standalone/src/components/ui/tabs.tsx +0 -54
  434. package/.next/standalone/src/components/ui/textarea.tsx +0 -21
  435. package/.next/standalone/src/components/ui/tip.tsx +0 -23
  436. package/.next/standalone/src/components/ui/toggle-group.tsx +0 -41
  437. package/.next/standalone/src/components/ui/tooltip.tsx +0 -27
  438. package/.next/standalone/src/components/view-width-toggle.tsx +0 -70
  439. package/.next/standalone/src/components/wiki/frontmatter-header.tsx +0 -164
  440. package/.next/standalone/src/hooks/use-is-mobile.ts +0 -14
  441. package/.next/standalone/src/lib/app-runner.ts +0 -272
  442. package/.next/standalone/src/lib/auth/admin.ts +0 -120
  443. package/.next/standalone/src/lib/auth/allowlist.ts +0 -50
  444. package/.next/standalone/src/lib/auth/client.ts +0 -4
  445. package/.next/standalone/src/lib/auth/csrf.ts +0 -68
  446. package/.next/standalone/src/lib/auth/server.ts +0 -215
  447. package/.next/standalone/src/lib/cabinets/tree.ts +0 -75
  448. package/.next/standalone/src/lib/config.ts +0 -114
  449. package/.next/standalone/src/lib/embeds/detect.ts +0 -142
  450. package/.next/standalone/src/lib/git-secrets.ts +0 -65
  451. package/.next/standalone/src/lib/git.ts +0 -503
  452. package/.next/standalone/src/lib/google/detect.ts +0 -107
  453. package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +0 -104
  454. package/.next/standalone/src/lib/markdown/remark-wikilinks.ts +0 -100
  455. package/.next/standalone/src/lib/markdown/sanitize-schema.ts +0 -106
  456. package/.next/standalone/src/lib/markdown/to-html.ts +0 -282
  457. package/.next/standalone/src/lib/markdown/to-markdown.ts +0 -252
  458. package/.next/standalone/src/lib/proof/activity-shared.ts +0 -31
  459. package/.next/standalone/src/lib/proof/activity.ts +0 -74
  460. package/.next/standalone/src/lib/proof/audit.ts +0 -88
  461. package/.next/standalone/src/lib/proof/auth.ts +0 -143
  462. package/.next/standalone/src/lib/proof/block-refs.ts +0 -133
  463. package/.next/standalone/src/lib/proof/blocks.ts +0 -73
  464. package/.next/standalone/src/lib/proof/client-auth.ts +0 -23
  465. package/.next/standalone/src/lib/proof/collab-state.ts +0 -81
  466. package/.next/standalone/src/lib/proof/event-bus.ts +0 -47
  467. package/.next/standalone/src/lib/proof/file-lock.ts +0 -43
  468. package/.next/standalone/src/lib/proof/glob.ts +0 -51
  469. package/.next/standalone/src/lib/proof/idempotency.ts +0 -37
  470. package/.next/standalone/src/lib/proof/lease.ts +0 -117
  471. package/.next/standalone/src/lib/proof/mutex.ts +0 -36
  472. package/.next/standalone/src/lib/proof/ops-applier.ts +0 -727
  473. package/.next/standalone/src/lib/proof/owner-auth.ts +0 -2
  474. package/.next/standalone/src/lib/proof/pending.ts +0 -97
  475. package/.next/standalone/src/lib/proof/proof-span.ts +0 -141
  476. package/.next/standalone/src/lib/proof/rate-limit.ts +0 -53
  477. package/.next/standalone/src/lib/proof/raw-fs.ts +0 -197
  478. package/.next/standalone/src/lib/proof/register-rate-limit.ts +0 -53
  479. package/.next/standalone/src/lib/proof/registry.ts +0 -195
  480. package/.next/standalone/src/lib/proof/sidecar.ts +0 -103
  481. package/.next/standalone/src/lib/proof/suggest-capture.ts +0 -69
  482. package/.next/standalone/src/lib/proof/types.ts +0 -182
  483. package/.next/standalone/src/lib/proof-config.ts +0 -14
  484. package/.next/standalone/src/lib/root-dir.ts +0 -35
  485. package/.next/standalone/src/lib/search/indexable-exts.ts +0 -25
  486. package/.next/standalone/src/lib/search/indexer.ts +0 -634
  487. package/.next/standalone/src/lib/search/sanitize.ts +0 -35
  488. package/.next/standalone/src/lib/search/search-db.ts +0 -57
  489. package/.next/standalone/src/lib/search/watcher-pool.ts +0 -79
  490. package/.next/standalone/src/lib/shared-docs/db.ts +0 -177
  491. package/.next/standalone/src/lib/toast.ts +0 -9
  492. package/.next/standalone/src/lib/utils.ts +0 -6
  493. package/.next/standalone/src/lib/wiki-helpers.ts +0 -36
  494. package/.next/standalone/src/lib/workspace-client.ts +0 -82
  495. package/.next/standalone/src/lib/workspace-context.ts +0 -173
  496. package/.next/standalone/src/lib/workspaces.ts +0 -400
  497. package/.next/standalone/src/middleware.ts +0 -44
  498. package/.next/standalone/src/stores/ai-panel-store.ts +0 -79
  499. package/.next/standalone/src/stores/editor-store.ts +0 -439
  500. package/.next/standalone/src/stores/pin-store.ts +0 -56
  501. package/.next/standalone/src/stores/proof-store.ts +0 -124
  502. package/.next/standalone/src/stores/recent-store.ts +0 -51
  503. package/.next/standalone/src/stores/search-store.ts +0 -104
  504. package/.next/standalone/src/stores/show-hidden-store.ts +0 -26
  505. package/.next/standalone/src/stores/sidebar-width-store.ts +0 -39
  506. package/.next/standalone/src/stores/skin-store.ts +0 -42
  507. package/.next/standalone/src/stores/tree-store.ts +0 -52
  508. package/.next/standalone/src/stores/view-width-store.ts +0 -101
  509. package/.next/standalone/src/stores/wiki-slugs-store.ts +0 -75
  510. package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +0 -146
  511. package/.next/standalone/src/tests/proof/admin-create-user.test.ts +0 -43
  512. package/.next/standalone/src/tests/proof/agent-fs.test.ts +0 -973
  513. package/.next/standalone/src/tests/proof/agent-ownership.test.ts +0 -140
  514. package/.next/standalone/src/tests/proof/agent-workspace-scope.test.ts +0 -134
  515. package/.next/standalone/src/tests/proof/agents-install.test.ts +0 -110
  516. package/.next/standalone/src/tests/proof/bench-write.ts +0 -179
  517. package/.next/standalone/src/tests/proof/better-auth.test.ts +0 -68
  518. package/.next/standalone/src/tests/proof/block-refs.test.ts +0 -108
  519. package/.next/standalone/src/tests/proof/blocks.test.ts +0 -92
  520. package/.next/standalone/src/tests/proof/collab-state.test.ts +0 -501
  521. package/.next/standalone/src/tests/proof/comments-ops.test.ts +0 -177
  522. package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +0 -85
  523. package/.next/standalone/src/tests/proof/external-edit.test.ts +0 -58
  524. package/.next/standalone/src/tests/proof/file-lock.test.ts +0 -80
  525. package/.next/standalone/src/tests/proof/git-branches-checkout.test.ts +0 -136
  526. package/.next/standalone/src/tests/proof/git-read-features.test.ts +0 -473
  527. package/.next/standalone/src/tests/proof/git-secrets.test.ts +0 -138
  528. package/.next/standalone/src/tests/proof/git-sparse.test.ts +0 -257
  529. package/.next/standalone/src/tests/proof/git-workspace.test.ts +0 -135
  530. package/.next/standalone/src/tests/proof/git.test.ts +0 -243
  531. package/.next/standalone/src/tests/proof/glob.test.ts +0 -82
  532. package/.next/standalone/src/tests/proof/helpers/auth-session.ts +0 -27
  533. package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +0 -46
  534. package/.next/standalone/src/tests/proof/ops-applier.test.ts +0 -368
  535. package/.next/standalone/src/tests/proof/owner-init.test.ts +0 -2
  536. package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +0 -60
  537. package/.next/standalone/src/tests/proof/preload.ts +0 -21
  538. package/.next/standalone/src/tests/proof/proof-span.test.ts +0 -98
  539. package/.next/standalone/src/tests/proof/rate-limit.test.ts +0 -54
  540. package/.next/standalone/src/tests/proof/readonly-enforcement.test.ts +0 -161
  541. package/.next/standalone/src/tests/proof/reconcile-sidecar.test.ts +0 -195
  542. package/.next/standalone/src/tests/proof/registration-flow.test.ts +0 -330
  543. package/.next/standalone/src/tests/proof/registry.test.ts +0 -169
  544. package/.next/standalone/src/tests/proof/routes.test.ts +0 -516
  545. package/.next/standalone/src/tests/proof/search-index.test.ts +0 -376
  546. package/.next/standalone/src/tests/proof/sidecar-lifecycle.test.ts +0 -177
  547. package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +0 -58
  548. package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +0 -280
  549. package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +0 -140
  550. package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +0 -208
  551. package/.next/standalone/src/tests/proof/workspace-isolation.test.ts +0 -87
  552. package/.next/standalone/src/tests/proof/workspaces-api.test.ts +0 -227
  553. package/.next/standalone/src/types/index.ts +0 -47
  554. package/.next/standalone/tailwind.config.ts +0 -191
  555. package/.next/standalone/tsconfig.json +0 -41
  556. package/.next/standalone/tsconfig.tsbuildinfo +0 -1
  557. /package/.next/standalone/.next/static/{WMOTgfV82apmdovxzgbMx → ecpoWt_SP3WkUTh0NsnuT}/_buildManifest.js +0 -0
  558. /package/.next/standalone/.next/static/{WMOTgfV82apmdovxzgbMx → ecpoWt_SP3WkUTh0NsnuT}/_clientMiddlewareManifest.js +0 -0
  559. /package/.next/standalone/.next/static/{WMOTgfV82apmdovxzgbMx → ecpoWt_SP3WkUTh0NsnuT}/_ssgManifest.js +0 -0
@@ -1,3260 +0,0 @@
1
- "use client";
2
-
3
- import {
4
- AlertCircle,
5
- Bot,
6
- Check,
7
- ChevronDown,
8
- ChevronRight,
9
- Copy,
10
- Download,
11
- File,
12
- FilePlus,
13
- FileText,
14
- Folder,
15
- FolderOpen,
16
- FolderPlus,
17
- GitBranch,
18
- GitMerge,
19
- Globe,
20
- History,
21
- User,
22
- Image as ImageIcon,
23
- Link,
24
- Loader2,
25
- Maximize2,
26
- MoreHorizontal,
27
- PanelLeftClose,
28
- PanelLeftOpen,
29
- Pencil,
30
- Eye,
31
- EyeOff,
32
- Pin,
33
- Plus,
34
- RefreshCw,
35
- Search,
36
- Settings,
37
- Share,
38
- Slash,
39
- SortAsc,
40
- Sparkles,
41
- Star,
42
- Terminal,
43
- Trash2,
44
- Upload,
45
- X,
46
- } from "lucide-react";
47
- import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
48
- import { createPortal } from "react-dom";
49
-
50
- import { ConfirmDialog } from "@/components/confirm-dialog";
51
- import { CsvViewer } from "@/components/editor/csv-viewer";
52
- import { KBEditor } from "@/components/editor/editor";
53
- import { FileFallbackViewer } from "@/components/editor/file-fallback-viewer";
54
- import { LargeFileGate } from "@/components/editor/large-file-gate";
55
- import { ImageViewer } from "@/components/editor/image-viewer";
56
- import { MediaViewer } from "@/components/editor/media-viewer";
57
- import { MermaidViewer } from "@/components/editor/mermaid-viewer";
58
- import { NotebookViewer } from "@/components/editor/notebook-viewer";
59
- import { DocxViewer } from "@/components/editor/office/docx-viewer";
60
- import { PptxViewer } from "@/components/editor/office/pptx-viewer";
61
- import { XlsxViewer } from "@/components/editor/office/xlsx-viewer";
62
- import { PdfViewer } from "@/components/editor/pdf-viewer";
63
- import { SourceViewer } from "@/components/editor/source-viewer";
64
- import { WebsiteViewer } from "@/components/editor/website-viewer";
65
- import { NodeAppViewer } from "@/components/editor/node-app-viewer";
66
- import { DirPicker } from "@/components/dir-picker";
67
- import {
68
- DropdownMenu,
69
- DropdownMenuContent,
70
- DropdownMenuItem,
71
- DropdownMenuSeparator,
72
- DropdownMenuTrigger,
73
- } from "@/components/ui/dropdown-menu";
74
- import { getActiveWorkspaceId, withWs, wsFetch } from "@/lib/workspace-client";
75
- import { markdownToHtml } from "@/lib/markdown/to-html";
76
- import { ThemeToggle } from "@/components/theme-toggle";
77
- import { AuthSettingsSheet } from "@/components/auth-settings-sheet";
78
- import { ShareDialog } from "@/components/share-dialog";
79
- import { Button } from "@/components/ui/button";
80
- import { Card } from "@/components/ui/card";
81
-
82
- import { showError, showSuccess } from "@/lib/toast";
83
- import {
84
- ContextMenu,
85
- ContextMenuContent,
86
- ContextMenuItem,
87
- ContextMenuSeparator,
88
- ContextMenuTrigger,
89
- } from "@/components/ui/context-menu";
90
- import { useRecentStore } from "@/stores/recent-store";
91
- import { usePinStore } from "@/stores/pin-store";
92
- import { cn } from "@/lib/utils";
93
- import { AIPanel } from "@/components/ai-panel/ai-panel";
94
- import { SearchCommandDialog } from "@/components/search/search-command-dialog";
95
- import { SidebarSearchBox } from "@/components/search/sidebar-search-box";
96
- import { useAIPanelStore } from "@/stores/ai-panel-store";
97
- import { useEditorStore, prefetchPage } from "@/stores/editor-store";
98
- import {
99
- useViewWidthStore,
100
- VIEW_WIDTH_CLASS,
101
- VIEW_ALIGN_CLASS,
102
- } from "@/stores/view-width-store";
103
- import {
104
- useSidebarWidthStore,
105
- SIDEBAR_MIN_WIDTH,
106
- SIDEBAR_MAX_WIDTH,
107
- } from "@/stores/sidebar-width-store";
108
- import { ViewWidthToggle } from "@/components/view-width-toggle";
109
- import { useWikiSlugsStore } from "@/stores/wiki-slugs-store";
110
- import { useShowHiddenStore } from "@/stores/show-hidden-store";
111
- import { useIsMobile } from "@/hooks/use-is-mobile";
112
-
113
- function timeAgo(iso: string): string {
114
- const t = new Date(iso).getTime();
115
- if (!Number.isFinite(t)) return "";
116
- const diff = Math.max(0, Date.now() - t);
117
- const s = Math.floor(diff / 1000);
118
- if (s < 60) return "just now";
119
- const m = Math.floor(s / 60);
120
- if (m < 60) return `${m}m ago`;
121
- const h = Math.floor(m / 60);
122
- if (h < 24) return `${h}h ago`;
123
- return `${Math.floor(h / 24)}d ago`;
124
- }
125
-
126
- interface TreeNode {
127
- name: string;
128
- path: string;
129
- type: "dir" | "file" | "app" | "node-app";
130
- size?: number;
131
- modifiedAt: string;
132
- children?: TreeNode[];
133
- expanded?: boolean;
134
- loading?: boolean;
135
- git?: { branch: string; dirty: boolean };
136
- }
137
-
138
- type ViewerKind =
139
- | "editor"
140
- | "csv"
141
- | "pdf"
142
- | "mermaid"
143
- | "notebook"
144
- | "image"
145
- | "media"
146
- | "docx"
147
- | "xlsx"
148
- | "pptx"
149
- | "source"
150
- | "fallback"
151
- | "app"
152
- | "html"
153
- | "node-app"
154
- | "text";
155
-
156
- // Viewer kinds safe to open at any size: they stream, paginate, or proxy and
157
- // never load the whole file into JS. Everything else goes behind LargeFileGate,
158
- // so a new viewer is fail-safe by default until proven safe here.
159
- const SAFE_VIEWER_KINDS = new Set<ViewerKind>([
160
- "image",
161
- "media",
162
- "pdf",
163
- "fallback",
164
- "app",
165
- "node-app",
166
- "html",
167
- ]);
168
-
169
- // Files above this size open behind a confirmation gate for unsafe viewers.
170
- const LARGE_FILE_GATE_BYTES = 5 * 1024 * 1024; // 5 MB
171
-
172
- function ext(name: string) {
173
- return name.split(".").pop()?.toLowerCase() ?? "";
174
- }
175
-
176
- function viewerKindFor(
177
- filename: string,
178
- nodeType: "file" | "app" | "dir" | "node-app",
179
- ): ViewerKind {
180
- if (nodeType === "node-app") return "node-app";
181
- if (nodeType === "app") return "app";
182
- if (nodeType === "dir") return "fallback";
183
- const base = filename.split("/").pop() ?? filename;
184
- // Dotfile with no real extension (".env", ".gitignore", ".bashrc"):
185
- // `".env".split(".").pop()` -> "env", which would match nothing below,
186
- // so treat any leading-dot name as text and let the viewer sniff bytes.
187
- if (base.startsWith(".") && base.indexOf(".", 1) === -1) return "source";
188
- const fileExt = ext(filename);
189
- // No extension at all ("Makefile", "LICENSE", "Dockerfile"): assume text.
190
- if (!fileExt) return "source";
191
- if (["md", "markdown"].includes(fileExt)) return "editor";
192
- if (fileExt === "txt") return "text";
193
- if (["csv", "tsv"].includes(fileExt)) return "csv";
194
- if (fileExt === "pdf") return "pdf";
195
- if (["mmd", "mermaid"].includes(fileExt)) return "mermaid";
196
- if (fileExt === "ipynb") return "notebook";
197
- if (
198
- ["png", "jpg", "jpeg", "gif", "webp", "svg", "avif", "ico", "bmp"].includes(
199
- fileExt,
200
- )
201
- )
202
- return "image";
203
- if (
204
- ["mp4", "webm", "mov", "m4v", "mp3", "wav", "ogg", "m4a", "aac"].includes(
205
- fileExt,
206
- )
207
- )
208
- return "media";
209
- if (fileExt === "docx") return "docx";
210
- if (["xlsx", "xlsm"].includes(fileExt)) return "xlsx";
211
- if (fileExt === "pptx") return "pptx";
212
- if (fileExt === "html") return "html";
213
- if (
214
- [
215
- "py", "js", "ts", "tsx", "jsx", "go", "rs", "java", "c", "cpp", "h",
216
- "sh", "bash", "zsh", "rb", "php", "swift", "kt", "lua", "sql", "yaml",
217
- "yml", "toml", "json", "xml", "css", "scss",
218
- ].includes(fileExt)
219
- )
220
- return "source";
221
- // Default: assume text and let SourceViewer sniff the bytes. If the file is
222
- // actually binary, SourceViewer degrades to a download/reveal fallback.
223
- // This avoids a brittle text-extension whitelist that always misses
224
- // something (.env, .ini, .lock, .gradle, .properties, ...).
225
- return "source";
226
- }
227
-
228
- const TEXT_EDITABLE_EXTS = new Set([
229
- "txt", "md", "markdown", "json", "yaml", "yml", "toml", "csv", "tsv",
230
- "xml", "html", "css", "js", "ts", "tsx", "jsx", "sh", "bash", "zsh",
231
- "rb", "py", "go", "rs", "java", "c", "cpp", "h", "php", "swift", "kt",
232
- "lua", "sql", "scss", "mmd", "mermaid", "ini", "env", "log", "conf",
233
- ]);
234
-
235
- function isText(name: string) {
236
- const kind = viewerKindFor(name, "file");
237
- if (kind === "editor" || kind === "text") return true;
238
- return TEXT_EDITABLE_EXTS.has(ext(name));
239
- }
240
-
241
- function isMarkdown(name: string) {
242
- return ["md", "markdown"].includes(ext(name));
243
- }
244
- function isImage(name: string) {
245
- return viewerKindFor(name, "file") === "image";
246
- }
247
- function isHtmlFile(name: string) {
248
- return viewerKindFor(name, "file") === "html";
249
- }
250
-
251
- async function fetchDir(dir: string): Promise<TreeNode[]> {
252
- const res = await wsFetch(`/api/wiki?dir=${encodeURIComponent(dir)}`);
253
- if (!res.ok) return [];
254
- const data: {
255
- entries: Array<{
256
- name: string;
257
- type: "dir" | "file" | "app" | "node-app";
258
- size?: number;
259
- modifiedAt: string;
260
- git?: { branch: string; dirty: boolean };
261
- }>;
262
- } = await res.json();
263
- return data.entries.map((e) => ({
264
- name: e.name,
265
- path: dir ? `${dir}/${e.name}` : e.name,
266
- type: e.type,
267
- size: e.size,
268
- modifiedAt: e.modifiedAt,
269
- expanded: false,
270
- git: e.git,
271
- }));
272
- }
273
-
274
- // One-shot hover-prefetch cache for directory listings: warmed on tree-row hover,
275
- // consumed (read + deleted) by the first expand so there's no long-lived staleness.
276
- // reloadDir/the file-watcher always fetch fresh and bypass this.
277
- const dirPrefetchCache = new Map<string, TreeNode[]>();
278
- const dirPrefetchInflight = new Map<string, Promise<TreeNode[]>>();
279
-
280
- function prefetchDir(dir: string): void {
281
- if (dirPrefetchCache.has(dir) || dirPrefetchInflight.has(dir)) return;
282
- const promise = fetchDir(dir)
283
- .then((children) => {
284
- dirPrefetchCache.set(dir, children);
285
- return children;
286
- })
287
- .finally(() => dirPrefetchInflight.delete(dir));
288
- dirPrefetchInflight.set(dir, promise);
289
- promise.catch(() => {});
290
- }
291
-
292
- /** Consume a prefetched (or in-flight) dir listing, if any. One-shot. */
293
- async function takePrefetchedDir(dir: string): Promise<TreeNode[] | null> {
294
- const ready = dirPrefetchCache.get(dir);
295
- if (ready) { dirPrefetchCache.delete(dir); return ready; }
296
- const inflight = dirPrefetchInflight.get(dir);
297
- if (inflight) { const r = await inflight; dirPrefetchCache.delete(dir); return r; }
298
- return null;
299
- }
300
-
301
- function updateNodes(
302
- nodes: TreeNode[],
303
- targetPath: string,
304
- updater: (n: TreeNode) => TreeNode,
305
- ): TreeNode[] {
306
- return nodes.map((n) => {
307
- if (n.path === targetPath) return updater(n);
308
- if (n.children)
309
- return { ...n, children: updateNodes(n.children, targetPath, updater) };
310
- return n;
311
- });
312
- }
313
-
314
- function removeNode(nodes: TreeNode[], targetPath: string): TreeNode[] {
315
- return nodes
316
- .filter((n) => n.path !== targetPath)
317
- .map((n) =>
318
- n.children ? { ...n, children: removeNode(n.children, targetPath) } : n,
319
- );
320
- }
321
-
322
- /**
323
- * Stable handler bundle passed to FileTree. Every method is referentially stable
324
- * across Page() renders (backed by a ref dispatcher), so FileTree's React.memo
325
- * holds and the whole tree skips re-rendering when unrelated Page state changes
326
- * (dialogs, search, editor typing, sidebar resize, dropdowns, etc.).
327
- */
328
- interface TreeCtx {
329
- toggleFolder: (node: TreeNode) => void;
330
- openViewer: (node: TreeNode) => void;
331
- copyPath: (path: string) => void;
332
- copyWikiLink: (name: string) => void;
333
- copyUrl: (path: string) => void;
334
- copyRawContent: (path: string) => void;
335
- copyFormattedContent: (path: string, name: string) => void;
336
- handleDownload: (node: TreeNode) => void;
337
- triggerUpload: (dir: string) => void;
338
- handleCreateFile: () => void;
339
- handleCreateFolder: () => void;
340
- handleDragStart: (e: React.DragEvent, node: TreeNode) => void;
341
- handleDragOver: (e: React.DragEvent, targetPath: string, targetType: "dir" | "root") => void;
342
- handleDropOnFolder: (e: React.DragEvent, targetDirPath: string) => void;
343
- handleGitPull: (nodePath: string, parentDir: string) => void;
344
- handleCheckout: (nodePath: string, branch: string, parentDir: string) => void;
345
- loadBranches: (nodePath: string) => void;
346
- prefetch: (node: TreeNode) => void;
347
- togglePin: (node: TreeNode, wsId: string | null) => void;
348
- setDragOverPath: (p: string | null) => void;
349
- setSidebarCollapsed: (b: boolean) => void;
350
- setBranchDropdownNode: (p: string | null) => void;
351
- setBranchDropdownPos: (p: { top: number; left: number } | null) => void;
352
- setNewFileParent: (p: string | null) => void;
353
- setNewFileName: (s: string) => void;
354
- setFileCreateError: (s: string | null) => void;
355
- setNewFolderParent: (p: string | null) => void;
356
- setNewFolderName: (s: string) => void;
357
- setFolderError: (s: string | null) => void;
358
- setDeletingPath: (p: string | null) => void;
359
- setDeletingIsDir: (b: boolean) => void;
360
- }
361
-
362
- interface FileTreeProps {
363
- ctx: TreeCtx;
364
- nodes: TreeNode[];
365
- openPath: string | null;
366
- dragOverPath: string | null;
367
- branchDropdownNode: string | null;
368
- branchDropdownPos: { top: number; left: number } | null;
369
- nodeBranches: Record<string, { name: string; current: boolean }[]>;
370
- branchesLoading: Record<string, boolean>;
371
- checkingOutBranch: string | null;
372
- pullingRepo: string | null;
373
- activePaths: Set<string>;
374
- pins: Array<{ path: string }>;
375
- isMobile: boolean;
376
- activeWorkspaceId: string | null;
377
- newFileParent: string | null;
378
- newFileName: string;
379
- fileCreateError: string | null;
380
- newFolderParent: string | null;
381
- newFolderName: string;
382
- folderError: string | null;
383
- sidebarScrollRef: React.RefObject<HTMLDivElement | null>;
384
- }
385
-
386
- // content-visibility:auto lets the browser skip layout/paint for off-screen
387
- // rows — virtualization without JS scroll math, and rows stay in the DOM so
388
- // keyboard nav (querySelectorAll) and Ctrl+F still work. Combined with per-row
389
- // memo (below), large trees stay cheap on both the React and browser sides.
390
- const ROW_CV: React.CSSProperties = { contentVisibility: "auto", containIntrinsicSize: "auto 32px" };
391
-
392
- interface TreeRowViewProps {
393
- node: TreeNode;
394
- depth: number;
395
- ctx: TreeCtx;
396
- isMobile: boolean;
397
- activeWorkspaceId: string | null;
398
- sidebarScrollRef: React.RefObject<HTMLDivElement | null>;
399
- isActive: boolean;
400
- isDragOver: boolean;
401
- isPinned: boolean;
402
- isAgentActive: boolean;
403
- isPulling: boolean;
404
- branchOpen: boolean;
405
- branchPos: { top: number; left: number } | null;
406
- branches: { name: string; current: boolean }[];
407
- branchLoading: boolean;
408
- checkingOut: string | null;
409
- onHoverEnter: (node: TreeNode) => void;
410
- onHoverLeave: () => void;
411
- }
412
-
413
- // Memoized row: re-renders only when its OWN derived props change. Navigation
414
- // (openPath change) re-renders just the two affected rows, not the whole tree.
415
- const TreeRowView = memo(function TreeRowView({
416
- node, depth, ctx, isMobile, activeWorkspaceId, sidebarScrollRef,
417
- isActive, isDragOver, isPinned, isAgentActive, isPulling,
418
- branchOpen, branchPos, branches, branchLoading, checkingOut,
419
- onHoverEnter, onHoverLeave,
420
- }: TreeRowViewProps) {
421
- return (
422
- <ContextMenu>
423
- <ContextMenuTrigger asChild>
424
- <div
425
- role="treeitem"
426
- tabIndex={0}
427
- draggable={!isMobile}
428
- onKeyDown={(e) => {
429
- if (e.key === "Enter" || e.key === " ") {
430
- e.preventDefault();
431
- if (node.type === "dir") ctx.toggleFolder(node);
432
- else if (node.type === "app" || node.type === "node-app") { ctx.openViewer(node); ctx.toggleFolder(node); }
433
- else ctx.openViewer(node);
434
- } else if (e.key === "ArrowDown") {
435
- e.preventDefault();
436
- const container = sidebarScrollRef.current;
437
- if (!container) return;
438
- const items = Array.from(container.querySelectorAll<HTMLElement>('[role="treeitem"]'));
439
- const idx = items.indexOf(e.currentTarget as HTMLElement);
440
- items[idx + 1]?.focus();
441
- } else if (e.key === "ArrowUp") {
442
- e.preventDefault();
443
- const container = sidebarScrollRef.current;
444
- if (!container) return;
445
- const items = Array.from(container.querySelectorAll<HTMLElement>('[role="treeitem"]'));
446
- const idx = items.indexOf(e.currentTarget as HTMLElement);
447
- items[idx - 1]?.focus();
448
- } else if (e.key === "ArrowRight") {
449
- e.preventDefault();
450
- if (node.type === "dir" || node.type === "app" || node.type === "node-app") {
451
- if (!node.expanded) {
452
- ctx.toggleFolder(node);
453
- } else {
454
- const container = sidebarScrollRef.current;
455
- if (!container) return;
456
- const items = Array.from(container.querySelectorAll<HTMLElement>('[role="treeitem"]'));
457
- const idx = items.indexOf(e.currentTarget as HTMLElement);
458
- items[idx + 1]?.focus();
459
- }
460
- }
461
- } else if (e.key === "ArrowLeft") {
462
- e.preventDefault();
463
- if ((node.type === "dir" || node.type === "app" || node.type === "node-app") && node.expanded) {
464
- ctx.toggleFolder(node);
465
- } else if (depth > 0) {
466
- const container = sidebarScrollRef.current;
467
- if (!container) return;
468
- const items = Array.from(container.querySelectorAll<HTMLElement>('[role="treeitem"]'));
469
- const current = e.currentTarget as HTMLElement;
470
- const idx = items.indexOf(current);
471
- const currentPL = Number.parseInt(current.style.paddingLeft ?? "0", 10);
472
- for (let i = idx - 1; i >= 0; i--) {
473
- const pl = Number.parseInt(items[i].style.paddingLeft ?? "0", 10);
474
- if (pl < currentPL) { items[i].focus(); break; }
475
- }
476
- }
477
- }
478
- }}
479
- onDragStart={(e) => ctx.handleDragStart(e, node)}
480
- onDragOver={(e) =>
481
- node.type === "dir"
482
- ? ctx.handleDragOver(e, node.path, "dir")
483
- : e.preventDefault()
484
- }
485
- onDragLeave={() => ctx.setDragOverPath(null)}
486
- onDrop={(e) =>
487
- node.type === "dir"
488
- ? ctx.handleDropOnFolder(e, node.path)
489
- : e.preventDefault()
490
- }
491
- onMouseEnter={() => onHoverEnter(node)}
492
- onMouseLeave={onHoverLeave}
493
- className={cn(
494
- "tree-row-reveal flex items-center gap-1.5 rounded-sm px-2 py-1 text-sm cursor-pointer group transition-colors select-none touch-target",
495
- isActive
496
- ? "bg-accent-soft text-foreground font-medium"
497
- : "hover:bg-muted",
498
- isDragOver && "ring-2 ring-primary bg-primary-soft",
499
- node.name.startsWith(".") && "opacity-40",
500
- )}
501
- style={{ paddingLeft: `${depth * 14 + 8}px`, ...ROW_CV }}
502
- onClick={() => {
503
- if (node.type === "dir") ctx.toggleFolder(node);
504
- else if (node.type === "app" || node.type === "node-app") { ctx.openViewer(node); ctx.toggleFolder(node); if (isMobile) ctx.setSidebarCollapsed(true); }
505
- else { ctx.openViewer(node); if (isMobile) ctx.setSidebarCollapsed(true); }
506
- }}
507
- >
508
- {(node.type === "dir" || node.type === "app" || node.type === "node-app") ? (
509
- node.loading ? (
510
- <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground" />
511
- ) : (
512
- <ChevronRight
513
- className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-200 ease-out"
514
- style={{ transform: node.expanded ? "rotate(90deg)" : "rotate(0deg)" }}
515
- />
516
- )
517
- ) : (
518
- <span className="w-3.5 shrink-0" />
519
- )}
520
-
521
- <span className="editorial-tree-typeicon">{node.type === "dir" ? (
522
- node.expanded ? (
523
- <FolderOpen className={cn("h-4 w-4 shrink-0", !isActive && "text-warning")} />
524
- ) : (
525
- <Folder className={cn("h-4 w-4 shrink-0", !isActive && "text-warning")} />
526
- )
527
- ) : node.type === "app" ? (
528
- <Globe className={cn("h-4 w-4 shrink-0", !isActive && "text-foreground/70")} />
529
- ) : node.type === "node-app" ? (
530
- <Terminal className={cn("h-4 w-4 shrink-0", !isActive && "text-emerald-500")} />
531
- ) : isHtmlFile(node.name) ? (
532
- <Globe className={cn("h-4 w-4 shrink-0", !isActive && "text-foreground/60")} />
533
- ) : isImage(node.name) ? (
534
- <ImageIcon className={cn("h-4 w-4 shrink-0", !isActive && "text-sunshine-700")} />
535
- ) : isText(node.name) ? (
536
- <FileText className={cn("h-4 w-4 shrink-0", !isActive && "text-foreground/70")} />
537
- ) : (
538
- <File className={cn("h-4 w-4 shrink-0", !isActive && "text-foreground/60")} />
539
- )}</span>
540
-
541
- <span className="min-w-0 flex-1 truncate">{node.name}</span>
542
-
543
- {/* Git repo badge */}
544
- {node.git && (
545
- <span className="relative flex shrink-0 items-center gap-0.5 rounded-sm bg-muted px-1 py-0.5 text-[10px] text-muted-foreground">
546
- <button
547
- type="button"
548
- className="flex items-center gap-0.5 hover:text-foreground"
549
- title="Switch branch"
550
- onClick={(e) => {
551
- e.stopPropagation();
552
- if (branchOpen) { ctx.setBranchDropdownNode(null); ctx.setBranchDropdownPos(null); return; }
553
- const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
554
- ctx.setBranchDropdownPos({ top: rect.bottom + 4, left: rect.left });
555
- ctx.setBranchDropdownNode(node.path);
556
- ctx.loadBranches(node.path);
557
- }}
558
- >
559
- <GitBranch className="h-2.5 w-2.5" />
560
- {node.git.branch}
561
- {node.git.dirty && <span className="ml-0.5 text-warning">*</span>}
562
- </button>
563
- {isPulling ? (
564
- <Loader2 className="ml-0.5 h-2.5 w-2.5 animate-spin" />
565
- ) : (
566
- <button
567
- type="button"
568
- onClick={(e) => {
569
- e.stopPropagation();
570
- const parentDir = node.path.includes("/")
571
- ? node.path.substring(0, node.path.lastIndexOf("/"))
572
- : "";
573
- ctx.handleGitPull(node.path, parentDir);
574
- }}
575
- className="ml-0.5 text-muted-foreground hover:text-foreground"
576
- title="Pull latest"
577
- >
578
- <RefreshCw className="h-2.5 w-2.5" />
579
- </button>
580
- )}
581
- {/* Branch dropdown rendered as portal to escape overflow-hidden */}
582
- {branchOpen && branchPos && typeof document !== "undefined" && createPortal(
583
- <div
584
- style={{ position: "fixed", top: branchPos.top, left: branchPos.left }}
585
- className="z-[9999] min-w-[120px] rounded-md border bg-popover p-1 shadow-md"
586
- onKeyDown={(e) => { if (e.key === "Escape") { ctx.setBranchDropdownNode(null); ctx.setBranchDropdownPos(null); } }}
587
- onClick={(e) => e.stopPropagation()}
588
- >
589
- {branchLoading ? (
590
- <div className="flex justify-center py-2">
591
- <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
592
- </div>
593
- ) : branches.length === 0 ? (
594
- <p className="px-2 py-1 text-[10px] text-muted-foreground">No branches</p>
595
- ) : (
596
- branches.map((b) => {
597
- const parentDir = node.path.includes("/")
598
- ? node.path.substring(0, node.path.lastIndexOf("/"))
599
- : "";
600
- return (
601
- <button
602
- key={b.name}
603
- type="button"
604
- disabled={checkingOut !== null || b.current}
605
- className={cn(
606
- "flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-[11px] hover:bg-muted",
607
- b.current && "font-semibold text-foreground",
608
- !b.current && "text-muted-foreground",
609
- )}
610
- onClick={(e) => {
611
- e.stopPropagation();
612
- if (!b.current) ctx.handleCheckout(node.path, b.name, parentDir);
613
- }}
614
- >
615
- {b.current ? <Check className="h-3 w-3 shrink-0" /> : <span className="w-3 shrink-0" />}
616
- {checkingOut === b.name ? <Loader2 className="h-3 w-3 animate-spin" /> : b.name}
617
- </button>
618
- );
619
- })
620
- )}
621
- </div>,
622
- document.body,
623
- )}
624
- </span>
625
- )}
626
-
627
- {/* Agent presence dot */}
628
- {isAgentActive && (
629
- <span
630
- className="ml-0.5 h-1.5 w-1.5 rounded-full bg-emerald-400 shrink-0 animate-pulse"
631
- title="Agent recently active"
632
- />
633
- )}
634
-
635
- <div
636
- className="hover-reveal flex max-w-0 shrink-0 items-center overflow-hidden opacity-0 transition-all duration-150 group-hover:max-w-7 group-hover:opacity-100 focus-within:max-w-7 focus-within:opacity-100"
637
- onClick={(e) => e.stopPropagation()}
638
- onKeyDown={(e) => e.stopPropagation()}
639
- >
640
- <DropdownMenu>
641
- <DropdownMenuTrigger asChild>
642
- <Button
643
- size="sm"
644
- variant="ghost"
645
- className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
646
- title="File actions"
647
- >
648
- <MoreHorizontal className="h-3.5 w-3.5" />
649
- </Button>
650
- </DropdownMenuTrigger>
651
- <DropdownMenuContent align="end" className="w-48">
652
- <DropdownMenuItem onClick={() => ctx.copyPath(node.path)}>
653
- <Copy className="mr-2 h-3.5 w-3.5" />
654
- Copy path
655
- </DropdownMenuItem>
656
- {isMarkdown(node.name) && (
657
- <DropdownMenuItem onClick={() => ctx.copyWikiLink(node.name)}>
658
- <FileText className="mr-2 h-3.5 w-3.5" />
659
- Copy wiki link
660
- </DropdownMenuItem>
661
- )}
662
- <DropdownMenuItem onClick={() => ctx.copyUrl(node.path)}>
663
- <Link className="mr-2 h-3.5 w-3.5" />
664
- Copy URL
665
- </DropdownMenuItem>
666
- {node.type === "file" && isText(node.name) && (
667
- <>
668
- <DropdownMenuItem onClick={() => ctx.copyRawContent(node.path)}>
669
- <FileText className="mr-2 h-3.5 w-3.5" />
670
- Copy raw content
671
- </DropdownMenuItem>
672
- <DropdownMenuItem onClick={() => ctx.copyFormattedContent(node.path, node.name)}>
673
- <FileText className="mr-2 h-3.5 w-3.5" />
674
- Copy formatted content
675
- </DropdownMenuItem>
676
- </>
677
- )}
678
- <DropdownMenuSeparator />
679
- {node.type === "dir" && (
680
- <>
681
- <DropdownMenuItem
682
- onClick={async () => {
683
- if (!node.expanded) ctx.toggleFolder(node);
684
- ctx.setNewFileParent(node.path);
685
- ctx.setNewFileName("");
686
- ctx.setFileCreateError(null);
687
- }}
688
- >
689
- <FilePlus className="mr-2 h-3.5 w-3.5" />
690
- New file here
691
- </DropdownMenuItem>
692
- <DropdownMenuItem onClick={() => ctx.triggerUpload(node.path)}>
693
- <Upload className="mr-2 h-3.5 w-3.5" />
694
- Upload here
695
- </DropdownMenuItem>
696
- <DropdownMenuItem
697
- onClick={() => {
698
- ctx.setNewFolderParent(node.path);
699
- ctx.setNewFolderName("");
700
- ctx.setFolderError(null);
701
- }}
702
- >
703
- <FolderPlus className="mr-2 h-3.5 w-3.5" />
704
- New subfolder
705
- </DropdownMenuItem>
706
- <DropdownMenuSeparator />
707
- </>
708
- )}
709
- <DropdownMenuItem onClick={() => ctx.handleDownload(node)}>
710
- <Download className="mr-2 h-3.5 w-3.5" />
711
- {node.type === "file" ? "Download" : "Download as zip"}
712
- </DropdownMenuItem>
713
- <DropdownMenuItem
714
- onClick={() => ctx.togglePin(node, activeWorkspaceId)}
715
- >
716
- <Star className={cn("mr-2 h-3.5 w-3.5", isPinned && "fill-current text-amber-400")} />
717
- {isPinned ? "Unpin" : "Pin to top"}
718
- </DropdownMenuItem>
719
- <DropdownMenuSeparator />
720
- <DropdownMenuItem
721
- className="text-destructive focus:text-destructive"
722
- onClick={() => {
723
- ctx.setDeletingPath(node.path);
724
- ctx.setDeletingIsDir(node.type !== "file");
725
- }}
726
- >
727
- <Trash2 className="mr-2 h-3.5 w-3.5" />
728
- Delete
729
- </DropdownMenuItem>
730
- </DropdownMenuContent>
731
- </DropdownMenu>
732
- </div>
733
- </div>
734
- </ContextMenuTrigger>
735
- <ContextMenuContent>
736
- <ContextMenuItem onSelect={() => ctx.copyPath(node.path)}>
737
- Copy path
738
- </ContextMenuItem>
739
- {isMarkdown(node.name) && (
740
- <ContextMenuItem onSelect={() => ctx.copyWikiLink(node.name)}>
741
- Copy wiki link
742
- </ContextMenuItem>
743
- )}
744
- <ContextMenuSeparator />
745
- <ContextMenuItem onSelect={() => ctx.copyUrl(node.path)}>
746
- Copy URL
747
- </ContextMenuItem>
748
- {node.type === "file" && isText(node.name) && (
749
- <>
750
- <ContextMenuSeparator />
751
- <ContextMenuItem onSelect={() => ctx.copyRawContent(node.path)}>
752
- Copy raw content
753
- </ContextMenuItem>
754
- <ContextMenuItem onSelect={() => ctx.copyFormattedContent(node.path, node.name)}>
755
- Copy formatted content
756
- </ContextMenuItem>
757
- </>
758
- )}
759
- </ContextMenuContent>
760
- </ContextMenu>
761
- );
762
- });
763
-
764
- const FileTree = memo(function FileTree(p: FileTreeProps) {
765
- const {
766
- ctx,
767
- openPath,
768
- dragOverPath,
769
- branchDropdownNode,
770
- branchDropdownPos,
771
- nodeBranches,
772
- branchesLoading,
773
- checkingOutBranch,
774
- pullingRepo,
775
- activePaths,
776
- pins,
777
- isMobile,
778
- activeWorkspaceId,
779
- newFileParent,
780
- newFileName,
781
- fileCreateError,
782
- newFolderParent,
783
- newFolderName,
784
- folderError,
785
- sidebarScrollRef,
786
- } = p;
787
-
788
- const showHidden = useShowHiddenStore((s) => s.showHidden);
789
-
790
- // Hover-intent prefetch: a single shared timer so only the row the pointer
791
- // settles on (>120ms) is prefetched — passing the cursor over rows doesn't
792
- // fire a request per row. Stable identities so TreeRowView's memo holds.
793
- const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
794
- const onHoverEnter = useCallback((node: TreeNode) => {
795
- const isCollapsedDir =
796
- (node.type === "dir" || node.type === "app" || node.type === "node-app") &&
797
- !node.expanded &&
798
- node.children === undefined;
799
- if (node.type !== "file" && !isCollapsedDir) return;
800
- if (hoverTimer.current) clearTimeout(hoverTimer.current);
801
- hoverTimer.current = setTimeout(() => ctx.prefetch(node), 120);
802
- }, [ctx]);
803
- const onHoverLeave = useCallback(() => {
804
- if (hoverTimer.current) { clearTimeout(hoverTimer.current); hoverTimer.current = null; }
805
- }, []);
806
-
807
- // Flatten the visible tree into a list. Removes the recursion that blocks
808
- // per-row memo and lets create-inputs / empty markers be plain siblings.
809
- // Recomputed only when the tree shape or which folder is mid-create changes.
810
- const flat = useMemo(() => {
811
- const out: Array<{ kind: "row" | "newfolder" | "newfile" | "empty"; node: TreeNode; depth: number }> = [];
812
- const walk = (nodes: TreeNode[], depth: number) => {
813
- for (const node of nodes) {
814
- if (!showHidden && node.name.startsWith(".")) continue;
815
- out.push({ kind: "row", node, depth });
816
- if (node.type === "dir") {
817
- if (newFolderParent === node.path) out.push({ kind: "newfolder", node, depth });
818
- if (newFileParent === node.path) out.push({ kind: "newfile", node, depth });
819
- }
820
- if ((node.type === "dir" || node.type === "app" || node.type === "node-app") && node.expanded) {
821
- if (node.children && node.children.length > 0) walk(node.children, depth + 1);
822
- else if (node.children?.length === 0) out.push({ kind: "empty", node, depth });
823
- }
824
- }
825
- };
826
- walk(p.nodes, 0);
827
- return out;
828
- }, [p.nodes, newFileParent, newFolderParent, showHidden]);
829
-
830
- return (
831
- <>
832
- {flat.map((item) => {
833
- const { node, depth } = item;
834
- if (item.kind === "row") {
835
- const branchOpen = branchDropdownNode === node.path;
836
- return (
837
- <TreeRowView
838
- key={node.path}
839
- node={node}
840
- depth={depth}
841
- ctx={ctx}
842
- isMobile={isMobile}
843
- activeWorkspaceId={activeWorkspaceId}
844
- sidebarScrollRef={sidebarScrollRef}
845
- isActive={openPath === node.path}
846
- isDragOver={dragOverPath === node.path}
847
- isPinned={pins.some((pin) => pin.path === node.path)}
848
- isAgentActive={activePaths.has(node.path)}
849
- isPulling={pullingRepo === node.path}
850
- branchOpen={branchOpen}
851
- branchPos={branchOpen ? branchDropdownPos : null}
852
- branches={node.git ? (nodeBranches[node.path] ?? []) : []}
853
- branchLoading={node.git ? !!branchesLoading[node.path] : false}
854
- checkingOut={branchOpen ? checkingOutBranch : null}
855
- onHoverEnter={onHoverEnter}
856
- onHoverLeave={onHoverLeave}
857
- />
858
- );
859
- }
860
- if (item.kind === "newfolder") {
861
- return (
862
- <div
863
- key={`nf-${node.path}`}
864
- className="flex items-center gap-1.5 px-2 py-1"
865
- style={{ paddingLeft: `${(depth + 1) * 14 + 8}px`, ...ROW_CV }}
866
- >
867
- <span className="w-3.5 shrink-0" />
868
- <Folder className="h-4 w-4 shrink-0 text-warning" />
869
- <input
870
- className="flex-1 bg-transparent text-sm outline-none border-b border-border min-w-0"
871
- placeholder="Folder name"
872
- value={newFolderName}
873
- onChange={(e) => ctx.setNewFolderName(e.target.value)}
874
- onKeyDown={(e) => {
875
- if (e.key === "Enter") ctx.handleCreateFolder();
876
- if (e.key === "Escape") { ctx.setNewFolderParent(null); ctx.setNewFolderName(""); }
877
- }}
878
- />
879
- {folderError && <span className="text-xs text-destructive">{folderError}</span>}
880
- <Button size="sm" variant="ghost" className="h-6 w-6 p-0" onClick={ctx.handleCreateFolder}>
881
- <Check className="h-3 w-3" />
882
- </Button>
883
- <Button size="sm" variant="ghost" className="h-6 w-6 p-0" onClick={() => { ctx.setNewFolderParent(null); ctx.setNewFolderName(""); }}>
884
- <X className="h-3 w-3" />
885
- </Button>
886
- </div>
887
- );
888
- }
889
- if (item.kind === "newfile") {
890
- return (
891
- <div
892
- key={`ff-${node.path}`}
893
- className="flex items-center gap-1.5 px-2 py-1"
894
- style={{ paddingLeft: `${(depth + 1) * 14 + 8}px`, ...ROW_CV }}
895
- >
896
- <span className="w-3.5 shrink-0" />
897
- <FileText className="h-4 w-4 shrink-0 text-accent" />
898
- <input
899
- autoFocus
900
- className="flex-1 bg-transparent text-sm outline-none border-b border-border min-w-0"
901
- placeholder="filename (default .md)"
902
- value={newFileName}
903
- onChange={(e) => ctx.setNewFileName(e.target.value)}
904
- onKeyDown={(e) => {
905
- if (e.key === "Enter") ctx.handleCreateFile();
906
- if (e.key === "Escape") { ctx.setNewFileParent(null); ctx.setNewFileName(""); }
907
- }}
908
- />
909
- {fileCreateError && <span className="text-xs text-destructive">{fileCreateError}</span>}
910
- <Button size="sm" variant="ghost" className="h-6 w-6 p-0" onClick={ctx.handleCreateFile}>
911
- <Check className="h-3 w-3" />
912
- </Button>
913
- <Button size="sm" variant="ghost" className="h-6 w-6 p-0" onClick={() => { ctx.setNewFileParent(null); ctx.setNewFileName(""); }}>
914
- <X className="h-3 w-3" />
915
- </Button>
916
- </div>
917
- );
918
- }
919
- // empty
920
- return (
921
- <div
922
- key={`empty-${node.path}`}
923
- className="text-xs text-muted-foreground/50 py-0.5"
924
- style={{ paddingLeft: `${(depth + 1) * 14 + 8 + 14 + 6 + 16 + 6}px`, ...ROW_CV }}
925
- >
926
- Empty
927
- </div>
928
- );
929
- })}
930
- </>
931
- );
932
- });
933
-
934
- export default function Page() {
935
- const slugsLoadedAt = useWikiSlugsStore((s) => s.loadedAt);
936
- useEffect(() => {
937
- void useWikiSlugsStore.getState().load();
938
- }, []);
939
- void slugsLoadedAt;
940
-
941
- // null = checking, false = not set, true = ready
942
- const [rootConfigured, setRootConfigured] = useState<boolean | null>(null);
943
- const [rootPath, setRootPath] = useState<string | null>(null);
944
- // Workspace state
945
- const [activeWorkspaceId, setActiveWorkspaceId] = useState<string | null>(() =>
946
- typeof window !== "undefined" ? getActiveWorkspaceId() : null
947
- );
948
- const [workspaces, setWorkspaces] = useState<Array<{id:string;name:string;rootDir:string;lastOpenedAt?:string;createdAt:string;readOnly?:boolean;git?:{remoteUrl:string;branch?:string;username?:string;lastPulledAt?:string;lastSha?:string;lastError?:string}}>>([]);
949
- const [isWsAdmin, setIsWsAdmin] = useState(false);
950
- const [addingWorkspace, setAddingWorkspace] = useState(false);
951
- const [deletingWorkspaceId, setDeletingWorkspaceId] = useState<string | null>(null);
952
- const [refreshingWsId, setRefreshingWsId] = useState<string | null>(null);
953
-
954
- const loadWorkspaces = useCallback(async () => {
955
- try {
956
- const res = await fetch("/api/system/workspaces");
957
- if (!res.ok) throw new Error("Failed");
958
- const d: { workspaces: Array<{id:string;name:string;rootDir:string;lastOpenedAt?:string;createdAt:string;readOnly?:boolean;git?:{remoteUrl:string;branch?:string;username?:string;lastPulledAt?:string;lastSha?:string;lastError?:string}}>; isAdmin: boolean } = await res.json();
959
- setWorkspaces(d.workspaces);
960
- setIsWsAdmin(d.isAdmin);
961
- if (d.workspaces.length > 0) {
962
- setRootConfigured(true);
963
- const urlWsId = new URLSearchParams(window.location.search).get("ws");
964
- const inList = urlWsId ? d.workspaces.find((w) => w.id === urlWsId) : null;
965
- const active = inList ?? [...d.workspaces].sort(
966
- (a, b) => (b.lastOpenedAt ?? b.createdAt).localeCompare(a.lastOpenedAt ?? a.createdAt)
967
- )[0];
968
- setActiveWorkspaceId(active.id);
969
- setRootPath(active.rootDir);
970
- if (!inList) {
971
- const u = new URL(location.href);
972
- u.searchParams.set("ws", active.id);
973
- history.replaceState(null, "", u.toString());
974
- }
975
- } else {
976
- setRootConfigured(false);
977
- }
978
- } catch {
979
- setRootConfigured(false);
980
- }
981
- }, []);
982
-
983
- useEffect(() => {
984
- void loadWorkspaces();
985
- }, [loadWorkspaces]);
986
-
987
- const editorCurrentPath = useEditorStore((s) => s.currentPath);
988
-
989
- // Recent files
990
- const recents = useRecentStore((s) => s.recents);
991
- const [pinnedCollapsed, setPinnedCollapsed] = useState(false);
992
- const [recentCollapsed, setRecentCollapsed] = useState(true);
993
-
994
- // Pins
995
- const pins = usePinStore((s) => s.pins);
996
-
997
- // Sidebar scroll container ref (for keyboard nav)
998
- const sidebarScrollRef = useRef<HTMLDivElement>(null);
999
-
1000
- // Agent presence: paths touched by agents in last 60s
1001
- const activity = useAIPanelStore((s) => s.activity);
1002
- const activePaths = useMemo(() => {
1003
- const cutoff = new Date(Date.now() - 60 * 1000).toISOString();
1004
- const paths = new Set<string>();
1005
- for (const ev of activity) {
1006
- // Only agent-authored events count as "agent presence".
1007
- if (ev.at >= cutoff && ev.path && ev.by?.startsWith("ai:"))
1008
- paths.add(ev.path);
1009
- }
1010
- return paths;
1011
- }, [activity]);
1012
-
1013
- // Poll activity for presence indicator
1014
- useEffect(() => {
1015
- const load = () => { void useAIPanelStore.getState().loadActivity(); };
1016
- load();
1017
- const id = setInterval(load, 10_000);
1018
- return () => clearInterval(id);
1019
- }, []);
1020
-
1021
- // Reload recents + pins when workspace changes
1022
- useEffect(() => {
1023
- useRecentStore.getState().loadForWorkspace(activeWorkspaceId);
1024
- usePinStore.getState().loadForWorkspace(activeWorkspaceId);
1025
- }, [activeWorkspaceId]);
1026
-
1027
- // Speculative warm-up: at idle, prefetch the markdown files the user is most
1028
- // likely to open next — pins and recents (the top of the sidebar). prefetchPage
1029
- // dedups, so this is a no-op for already-cached pages. Bounded to keep it cheap.
1030
- useEffect(() => {
1031
- const paths = [
1032
- ...pins.map((p) => p.path),
1033
- ...recents.slice(0, 8).map((r) => r.path),
1034
- ]
1035
- .filter((p, i, arr) => isMarkdown(p) && arr.indexOf(p) === i)
1036
- .slice(0, 12);
1037
- if (paths.length === 0) return;
1038
- const ric: (cb: () => void) => number =
1039
- typeof window !== "undefined" && "requestIdleCallback" in window
1040
- ? (cb) => (window as unknown as { requestIdleCallback: (c: () => void) => number }).requestIdleCallback(cb)
1041
- : (cb) => window.setTimeout(cb, 400);
1042
- const id = ric(() => { for (const p of paths) prefetchPage(p); });
1043
- return () => {
1044
- if (typeof window !== "undefined" && "cancelIdleCallback" in window) {
1045
- (window as unknown as { cancelIdleCallback: (h: number) => void }).cancelIdleCallback(id);
1046
- } else {
1047
- clearTimeout(id);
1048
- }
1049
- };
1050
- }, [pins, recents]);
1051
-
1052
- // Path captured from the URL at first render, before any effect can clear it.
1053
- // Restore reads from this ref (never the live URL) so URL sync can't break it.
1054
- const initialUrlPathRef = useRef<string | null>(
1055
- typeof window !== "undefined"
1056
- ? new URLSearchParams(window.location.search).get("path")
1057
- : null,
1058
- );
1059
- const didRestoreRef = useRef(false);
1060
-
1061
- const [roots, setRoots] = useState<TreeNode[]>([]);
1062
- const [rootLoaded, setRootLoaded] = useState(false);
1063
- const [rootLoading, setRootLoading] = useState(false);
1064
- const rootLoadingRef = useRef(false);
1065
- const [refreshingTree, setRefreshingTree] = useState(false);
1066
-
1067
- const [uploading, setUploading] = useState(false);
1068
- const [uploadError, setUploadError] = useState<string | null>(null);
1069
- const fileInputRef = useRef<HTMLInputElement>(null);
1070
- const uploadDirRef = useRef<string>("");
1071
-
1072
- const [newFolderParent, setNewFolderParent] = useState<string | null>(null);
1073
- const [newFolderName, setNewFolderName] = useState("");
1074
- const [folderError, setFolderError] = useState<string | null>(null);
1075
-
1076
- const [newFileParent, setNewFileParent] = useState<string | null>(null);
1077
- const [newFileName, setNewFileName] = useState("");
1078
- const [fileCreateError, setFileCreateError] = useState<string | null>(null);
1079
-
1080
- const [deletingPath, setDeletingPath] = useState<string | null>(null);
1081
- const [deletingIsDir, setDeletingIsDir] = useState(false);
1082
-
1083
- const [openFile, setOpenFile] = useState<{
1084
- path: string;
1085
- name: string;
1086
- nodeType: "file" | "app" | "node-app";
1087
- size?: number;
1088
- } | null>(null);
1089
- // Path the user clicked "open anyway" for. Keyed by path so confirming one
1090
- // large file does not open the next.
1091
- const [gateBypassPath, setGateBypassPath] = useState<string | null>(null);
1092
- const [appFullscreen, setAppFullscreen] = useState(false);
1093
- const [appKey, setAppKey] = useState(0);
1094
- const isMobile = useIsMobile();
1095
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
1096
- const sidebarWidth = useSidebarWidthStore((s) => s.width);
1097
- const showHidden = useShowHiddenStore((s) => s.showHidden);
1098
- const setSidebarWidth = useSidebarWidthStore((s) => s.setWidth);
1099
- // Mobile: when the viewport is mobile, the sidebar defaults to closed.
1100
- // Re-runs when isMobile flips (orientation change, devtools resize).
1101
- useEffect(() => {
1102
- if (isMobile) setSidebarCollapsed(true);
1103
- }, [isMobile]);
1104
- const [sidebarResizing, setSidebarResizing] = useState(false);
1105
- const [settingsOpen, setSettingsOpen] = useState(false);
1106
- const [shareDialogOpen, setShareDialogOpen] = useState(false);
1107
- const [fileContent, setFileContent] = useState<string | null>(null);
1108
- const [fileRevision, setFileRevision] = useState(0);
1109
- const [fileLoading, setFileLoading] = useState(false);
1110
- const [editing, setEditing] = useState(false);
1111
- const [editContent, setEditContent] = useState("");
1112
- const [saving, setSaving] = useState(false);
1113
- const [saveError, setSaveError] = useState<string | null>(null);
1114
-
1115
- // Drag state
1116
- const dragNodeRef = useRef<TreeNode | null>(null);
1117
- const [dragOverPath, setDragOverPath] = useState<string | null>(null);
1118
-
1119
- // Refs for watcher handler (avoids stale closures in the SSE effect)
1120
- const openFileRef = useRef<typeof openFile>(null);
1121
- const editingRef = useRef(false);
1122
- const refreshViewerRef = useRef<() => Promise<void>>(async () => {});
1123
-
1124
- useEffect(() => {
1125
- if (rootLoaded || rootLoadingRef.current) return;
1126
- rootLoadingRef.current = true;
1127
- setRootLoading(true);
1128
-
1129
- fetchDir("")
1130
- .then((nodes) => {
1131
- setRoots(nodes);
1132
- setRootLoaded(true);
1133
- setRootLoading(false);
1134
- })
1135
- .catch(() => {
1136
- rootLoadingRef.current = false;
1137
- setRootLoading(false);
1138
- });
1139
- }, [rootLoaded]);
1140
-
1141
- // Expand every ancestor folder of `p` so the file is visible in the tree.
1142
- // Best-effort: requires the root nodes to already be loaded.
1143
- const revealPath = useCallback(async (p: string) => {
1144
- const parts = p.split("/");
1145
- if (parts.length <= 1) return;
1146
-
1147
- // Fetch every ancestor's children first. We must NOT apply them with one
1148
- // setRoots per level: those updaters run against the same stale `prev`
1149
- // snapshot, so a nested prefix isn't in the tree yet and updateNodes
1150
- // silently no-ops (only the top level ever matched). Collect all levels,
1151
- // then splice them in a single pass so each parent's children exist before
1152
- // the next level is inserted.
1153
- const levels: { prefix: string; children: TreeNode[] }[] = [];
1154
- let prefix = "";
1155
- for (let i = 0; i < parts.length - 1; i++) {
1156
- prefix = prefix ? `${prefix}/${parts[i]}` : parts[i];
1157
- levels.push({ prefix, children: await fetchDir(prefix) });
1158
- }
1159
-
1160
- setRoots((prev) => {
1161
- let next = prev;
1162
- for (const { prefix: pfx, children } of levels) {
1163
- next = updateNodes(next, pfx, (n) => ({
1164
- ...n,
1165
- children,
1166
- expanded: true,
1167
- }));
1168
- }
1169
- return next;
1170
- });
1171
- }, []);
1172
-
1173
- const reloadDir = useCallback(async (dir: string) => {
1174
- const fresh = await fetchDir(dir);
1175
- if (dir === "") {
1176
- // Merge: keep expanded state + loaded children for nodes that still
1177
- // exist. A blind setRoots(fresh) would collapse the tree and wipe any
1178
- // expansion done by revealPath on reload (watcher fires this often).
1179
- setRoots((prev) => {
1180
- const prevByPath = new Map(prev.map((n) => [n.path, n]));
1181
- return fresh.map((n) => {
1182
- const old = prevByPath.get(n.path);
1183
- if (old && (old.type === "dir" || old.type === "app")) {
1184
- return {
1185
- ...n,
1186
- expanded: old.expanded,
1187
- children: old.children,
1188
- };
1189
- }
1190
- return n;
1191
- });
1192
- });
1193
- } else {
1194
- setRoots((prev) =>
1195
- updateNodes(prev, dir, (n) => ({
1196
- ...n,
1197
- children: fresh,
1198
- expanded: true,
1199
- })),
1200
- );
1201
- }
1202
- }, []);
1203
-
1204
- const [pullingRepo, setPullingRepo] = useState<string | null>(null);
1205
-
1206
- // Git file info (last author + date for open file)
1207
- const [gitFileInfo, setGitFileInfo] = useState<{ sha: string; author: string; date: string } | null>(null);
1208
-
1209
- // History panel
1210
- const [showHistory, setShowHistory] = useState(false);
1211
- const [historyCommits, setHistoryCommits] = useState<{ sha: string; shortSha: string; message: string; author: string; date: string }[]>([]);
1212
- const [historyLoading, setHistoryLoading] = useState(false);
1213
- const [selectedDiffSha, setSelectedDiffSha] = useState<string | null>(null);
1214
- const [diffContent, setDiffContent] = useState<string | null>(null);
1215
- const [diffLoading, setDiffLoading] = useState(false);
1216
-
1217
- // Branch switcher
1218
- const [nodeBranches, setNodeBranches] = useState<Record<string, { name: string; current: boolean }[]>>({});
1219
- const [branchesLoading, setBranchesLoading] = useState<Record<string, boolean>>({});
1220
- const [checkingOutBranch, setCheckingOutBranch] = useState<string | null>(null);
1221
- const [branchDropdownNode, setBranchDropdownNode] = useState<string | null>(null);
1222
- const [branchDropdownPos, setBranchDropdownPos] = useState<{ top: number; left: number } | null>(null);
1223
-
1224
- const handleGitPull = useCallback(async (nodePath: string, parentDir: string) => {
1225
- if (pullingRepo) return;
1226
- setPullingRepo(nodePath);
1227
- try {
1228
- const res = await wsFetch("/api/wiki/git-pull", {
1229
- method: "POST",
1230
- body: JSON.stringify({ path: nodePath }),
1231
- });
1232
- if (!res.ok) {
1233
- const e: { error?: string; message?: string } = await res.json();
1234
- showError(e.message ?? e.error ?? "Pull failed");
1235
- return;
1236
- }
1237
- const data: { branch: string; sha: string } = await res.json();
1238
- showSuccess(`Pulled ${nodePath} (${data.branch} @ ${data.sha.slice(0, 7)})`);
1239
- await reloadDir(parentDir);
1240
- } catch {
1241
- showError("Pull failed");
1242
- } finally {
1243
- setPullingRepo(null);
1244
- }
1245
- }, [pullingRepo, reloadDir]);
1246
-
1247
- // Reset history panel when file changes
1248
- useEffect(() => {
1249
- setShowHistory(false);
1250
- setHistoryCommits([]);
1251
- setSelectedDiffSha(null);
1252
- setDiffContent(null);
1253
- }, [openFile?.path]);
1254
-
1255
- // Fetch git metadata for open file. Keyed on path only: re-fetching on every
1256
- // openFile object re-ref (unrelated re-renders) wastes a git round-trip.
1257
- const openPath = openFile?.path;
1258
- useEffect(() => {
1259
- if (!openPath) { setGitFileInfo(null); return; }
1260
- let cancelled = false;
1261
- // Debounced: rapid navigation shouldn't fire a git round-trip per pass-through.
1262
- const timer = setTimeout(() => {
1263
- void (async () => {
1264
- try {
1265
- const res = await wsFetch(`/api/wiki/git-file-info?path=${encodeURIComponent(openPath)}`);
1266
- if (cancelled) return;
1267
- if (!res.ok) { setGitFileInfo(null); return; }
1268
- const d: { info: { sha: string; author: string; date: string } | null } = await res.json();
1269
- if (!cancelled) setGitFileInfo(d.info);
1270
- } catch { if (!cancelled) setGitFileInfo(null); }
1271
- })();
1272
- }, 200);
1273
- return () => { cancelled = true; clearTimeout(timer); };
1274
- }, [openPath]);
1275
-
1276
- const loadHistory = useCallback(async () => {
1277
- if (!openFile) return;
1278
- const path = openFile.path;
1279
- setShowHistory(true);
1280
- setHistoryLoading(true);
1281
- setHistoryCommits([]);
1282
- setSelectedDiffSha(null);
1283
- setDiffContent(null);
1284
- try {
1285
- const res = await wsFetch(`/api/wiki/git-history?path=${encodeURIComponent(path)}`);
1286
- if (!res.ok) { showError("Could not load history"); return; }
1287
- const d: { commits: { sha: string; shortSha: string; message: string; author: string; date: string }[] } = await res.json();
1288
- if (openFile.path === path) setHistoryCommits(d.commits);
1289
- } catch { showError("Could not load history"); }
1290
- finally { setHistoryLoading(false); }
1291
- }, [openFile]);
1292
-
1293
- const selectDiff = useCallback(async (sha: string) => {
1294
- if (!openFile) return;
1295
- if (selectedDiffSha === sha) { setSelectedDiffSha(null); setDiffContent(null); return; }
1296
- const path = openFile.path;
1297
- const targetSha = sha;
1298
- setSelectedDiffSha(sha);
1299
- setDiffLoading(true);
1300
- setDiffContent(null);
1301
- try {
1302
- const res = await wsFetch(`/api/wiki/git-diff?path=${encodeURIComponent(path)}&sha=${encodeURIComponent(sha)}`);
1303
- if (!res.ok) { showError("Could not load diff"); return; }
1304
- const d: { diff: string } = await res.json();
1305
- if (openFile.path === path && selectedDiffSha === targetSha) setDiffContent(d.diff);
1306
- } catch { showError("Could not load diff"); }
1307
- finally { setDiffLoading(false); }
1308
- }, [openFile, selectedDiffSha]);
1309
-
1310
- const loadBranches = useCallback(async (nodePath: string) => {
1311
- if (nodeBranches[nodePath] || branchesLoading[nodePath]) return;
1312
- let cancelled = false;
1313
- setBranchesLoading((prev) => ({ ...prev, [nodePath]: true }));
1314
- try {
1315
- const res = await wsFetch(`/api/wiki/git-branches?path=${encodeURIComponent(nodePath)}`);
1316
- if (cancelled) return;
1317
- if (!res.ok) { showError("Could not load branches"); return; }
1318
- const d: { branches: { name: string; current: boolean }[] } = await res.json();
1319
- if (!cancelled) setNodeBranches((prev) => ({ ...prev, [nodePath]: d.branches }));
1320
- } catch { if (!cancelled) showError("Could not load branches"); }
1321
- finally { if (!cancelled) setBranchesLoading((prev) => ({ ...prev, [nodePath]: false })); }
1322
- return () => { cancelled = true; };
1323
- }, [nodeBranches, branchesLoading]);
1324
-
1325
- const handleCheckout = useCallback(async (nodePath: string, branch: string, parentDir: string) => {
1326
- if (checkingOutBranch) return;
1327
- setCheckingOutBranch(branch);
1328
- try {
1329
- const res = await wsFetch("/api/wiki/git-checkout", {
1330
- method: "POST",
1331
- body: JSON.stringify({ path: nodePath, branch }),
1332
- });
1333
- if (res.status === 409) { showError("Repository has uncommitted changes"); return; }
1334
- if (!res.ok) { const e: { error?: string } = await res.json(); showError(e.error ?? "Checkout failed"); return; }
1335
- const d: { branch: string; sha: string } = await res.json();
1336
- showSuccess(`Switched to ${d.branch}`);
1337
- // Invalidate cached branches so next open re-fetches
1338
- setNodeBranches((prev) => { const n = { ...prev }; delete n[nodePath]; return n; });
1339
- setBranchDropdownNode(null);
1340
- setBranchDropdownPos(null);
1341
- await reloadDir(parentDir);
1342
- } catch { showError("Checkout failed"); }
1343
- finally { setCheckingOutBranch(null); }
1344
- }, [checkingOutBranch, reloadDir]);
1345
-
1346
- // Close branch dropdown on outside click or Escape
1347
- useEffect(() => {
1348
- if (!branchDropdownNode) return;
1349
- const handleClick = () => { setBranchDropdownNode(null); setBranchDropdownPos(null); };
1350
- const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") { setBranchDropdownNode(null); setBranchDropdownPos(null); } };
1351
- document.addEventListener("click", handleClick);
1352
- document.addEventListener("keydown", handleKey);
1353
- return () => {
1354
- document.removeEventListener("click", handleClick);
1355
- document.removeEventListener("keydown", handleKey);
1356
- };
1357
- }, [branchDropdownNode]);
1358
-
1359
- const collectExpandedPaths = useCallback((nodes: TreeNode[]): string[] => {
1360
- const paths: string[] = [];
1361
- for (const n of nodes) {
1362
- if ((n.type === "dir" || n.type === "app" || n.type === "node-app") && n.expanded && n.children) {
1363
- paths.push(n.path);
1364
- paths.push(...collectExpandedPaths(n.children));
1365
- }
1366
- }
1367
- return paths;
1368
- }, []);
1369
-
1370
- const refreshTree = useCallback(async () => {
1371
- setRefreshingTree(true);
1372
- try {
1373
- const expandedPaths = collectExpandedPaths(roots);
1374
- const fresh = await fetchDir("");
1375
- setRoots(fresh);
1376
- for (const p of expandedPaths) {
1377
- const dirFresh = await fetchDir(p);
1378
- setRoots((prev) =>
1379
- updateNodes(prev, p, (n) => ({
1380
- ...n,
1381
- children: dirFresh,
1382
- expanded: true,
1383
- })),
1384
- );
1385
- }
1386
- } finally {
1387
- setRefreshingTree(false);
1388
- }
1389
- }, [roots, collectExpandedPaths]);
1390
-
1391
- const refreshViewer = useCallback(async () => {
1392
- if (!openFile) return;
1393
- if (isMarkdown(openFile.name)) {
1394
- setFileLoading(true);
1395
- try {
1396
- await useEditorStore.getState().loadPage(openFile.path);
1397
- } finally {
1398
- setFileLoading(false);
1399
- }
1400
- return;
1401
- }
1402
- const kind = viewerKindFor(openFile.name, openFile.nodeType);
1403
- if (!["editor", "text"].includes(kind) && !isText(openFile.name)) return;
1404
- setFileLoading(true);
1405
- try {
1406
- const res = await wsFetch(
1407
- `/api/wiki/content?path=${encodeURIComponent(openFile.path)}`,
1408
- );
1409
- if (res.ok) {
1410
- const d: { content: string } = await res.json();
1411
- setFileContent(d.content);
1412
- setFileRevision(Number(res.headers.get("X-Wiki-Revision") ?? 0));
1413
- }
1414
- } catch {
1415
- /* ignore */
1416
- }
1417
- setFileLoading(false);
1418
- }, [openFile]);
1419
-
1420
- useEffect(() => {
1421
- if (!openFile || !isMarkdown(openFile.name)) return;
1422
- if (useEditorStore.getState().currentPath === openFile.path) return;
1423
- void useEditorStore.getState().loadPage(openFile.path);
1424
- }, [openFile]);
1425
-
1426
- // Keep watcher refs in sync
1427
- useEffect(() => {
1428
- openFileRef.current = openFile;
1429
- }, [openFile]);
1430
- useEffect(() => {
1431
- editingRef.current = editing;
1432
- }, [editing]);
1433
- useEffect(() => {
1434
- refreshViewerRef.current = refreshViewer;
1435
- }, [refreshViewer]);
1436
-
1437
- // File watcher: auto-update tree + open file via SSE
1438
- useEffect(() => {
1439
- if (!rootConfigured) return;
1440
-
1441
- const pendingReloads = new Map<string, ReturnType<typeof setTimeout>>();
1442
-
1443
- function scheduleReload(dir: string) {
1444
- if (pendingReloads.has(dir)) clearTimeout(pendingReloads.get(dir)!);
1445
- pendingReloads.set(
1446
- dir,
1447
- setTimeout(() => {
1448
- pendingReloads.delete(dir);
1449
- void reloadDir(dir);
1450
- }, 300),
1451
- );
1452
- }
1453
-
1454
- const es = new EventSource(withWs("/api/wiki/watch"));
1455
-
1456
- es.onmessage = (event) => {
1457
- let data: { type: string; path: string };
1458
- try {
1459
- data = JSON.parse(event.data as string) as {
1460
- type: string;
1461
- path: string;
1462
- };
1463
- } catch {
1464
- return;
1465
- }
1466
- const { type, path: relPath } = data;
1467
-
1468
- // Reload the affected parent dir in the tree
1469
- const parts = relPath.split("/");
1470
- const parentDir =
1471
- parts.length > 1 ? parts.slice(0, -1).join("/") : "";
1472
- scheduleReload(parentDir);
1473
-
1474
- // If the open file changed externally and we're not editing, refresh it
1475
- if (
1476
- type === "change" &&
1477
- openFileRef.current?.path === relPath &&
1478
- !editingRef.current
1479
- ) {
1480
- const key = `__file__${relPath}`;
1481
- if (pendingReloads.has(key)) clearTimeout(pendingReloads.get(key)!);
1482
- pendingReloads.set(
1483
- key,
1484
- setTimeout(() => {
1485
- pendingReloads.delete(key);
1486
- void refreshViewerRef.current();
1487
- }, 400),
1488
- );
1489
- }
1490
- };
1491
-
1492
- return () => {
1493
- es.close();
1494
- for (const t of pendingReloads.values()) clearTimeout(t);
1495
- };
1496
- }, [rootConfigured, reloadDir]);
1497
-
1498
- async function toggleFolder(node: TreeNode) {
1499
- if (node.type !== "dir" && node.type !== "app" && node.type !== "node-app") return;
1500
- if (!node.expanded) {
1501
- if (node.children === undefined) {
1502
- const prefetched = await takePrefetchedDir(node.path);
1503
- if (prefetched) {
1504
- setRoots((prev) =>
1505
- updateNodes(prev, node.path, (n) => ({
1506
- ...n,
1507
- loading: false,
1508
- children: prefetched,
1509
- expanded: true,
1510
- })),
1511
- );
1512
- return;
1513
- }
1514
- setRoots((prev) =>
1515
- updateNodes(prev, node.path, (n) => ({ ...n, loading: true })),
1516
- );
1517
- const children = await fetchDir(node.path);
1518
- setRoots((prev) =>
1519
- updateNodes(prev, node.path, (n) => ({
1520
- ...n,
1521
- loading: false,
1522
- children,
1523
- expanded: true,
1524
- })),
1525
- );
1526
- } else {
1527
- setRoots((prev) =>
1528
- updateNodes(prev, node.path, (n) => ({ ...n, expanded: true })),
1529
- );
1530
- }
1531
- } else {
1532
- setRoots((prev) =>
1533
- updateNodes(prev, node.path, (n) => ({ ...n, expanded: false })),
1534
- );
1535
- }
1536
- }
1537
-
1538
- useEffect(() => {
1539
- setAppFullscreen(false);
1540
- }, []);
1541
-
1542
- async function openViewer(node: TreeNode) {
1543
- // Push to recent files (only real files, not dirs/apps)
1544
- if (node.type === "file") {
1545
- useRecentStore.getState().push(
1546
- { path: node.path, name: node.name },
1547
- activeWorkspaceId,
1548
- );
1549
- }
1550
- setOpenFile({
1551
- path: node.path,
1552
- name: node.name,
1553
- size: node.size,
1554
- nodeType:
1555
- node.type === "app"
1556
- ? "app"
1557
- : node.type === "node-app"
1558
- ? "node-app"
1559
- : "file",
1560
- });
1561
- setEditing(false);
1562
- setSaveError(null);
1563
- setFileContent(null);
1564
- setFileRevision(0);
1565
- if (node.type === "file" && isMarkdown(node.name)) {
1566
- setFileLoading(true);
1567
- try {
1568
- await useEditorStore.getState().loadPage(node.path);
1569
- } finally {
1570
- setFileLoading(false);
1571
- }
1572
- return;
1573
- }
1574
- const kind = viewerKindFor(node.name, node.type);
1575
- if (!["editor", "text"].includes(kind) && !isText(node.name)) return;
1576
- setFileLoading(true);
1577
- try {
1578
- const res = await wsFetch(
1579
- `/api/wiki/content?path=${encodeURIComponent(node.path)}`,
1580
- );
1581
- if (res.ok) {
1582
- const d: { content: string } = await res.json();
1583
- setFileContent(d.content);
1584
- setFileRevision(Number(res.headers.get("X-Wiki-Revision") ?? 0));
1585
- }
1586
- } catch {
1587
- /* ignore */
1588
- }
1589
- setFileLoading(false);
1590
- }
1591
-
1592
- // Open a file from a search result: reveal it in the tree, then open it.
1593
- const openFromSearch = useCallback(
1594
- (relPath: string) => {
1595
- const name = relPath.split("/").pop() ?? relPath;
1596
- void revealPath(relPath);
1597
- void openViewer({ path: relPath, name, type: "file" } as TreeNode);
1598
- },
1599
- [revealPath],
1600
- );
1601
-
1602
- // biome-ignore lint/correctness/useExhaustiveDependencies: only react to editor path changes
1603
- useEffect(() => {
1604
- if (!editorCurrentPath) return;
1605
- if (openFile && openFile.path === editorCurrentPath) return;
1606
- const name = editorCurrentPath.split("/").pop() ?? editorCurrentPath;
1607
- void openViewer({
1608
- path: editorCurrentPath,
1609
- name,
1610
- type: "file",
1611
- } as TreeNode);
1612
- }, [editorCurrentPath]);
1613
-
1614
- // Persist the open file to the URL (?path=) so reloads restore it.
1615
- // replaceState avoids polluting history on every file switch.
1616
- useEffect(() => {
1617
- if (typeof window === "undefined") return;
1618
- const url = new URL(window.location.href);
1619
- if (openFile) url.searchParams.set("path", openFile.path);
1620
- else url.searchParams.delete("path");
1621
- window.history.replaceState(null, "", url.toString());
1622
- }, [openFile]);
1623
-
1624
- // Restore the open file from the URL once the root tree is loaded.
1625
- // Resolves node type from the parent dir listing (file vs app vs node-app).
1626
- // biome-ignore lint/correctness/useExhaustiveDependencies: openViewer is a hoisted stable fn; restore runs once
1627
- useEffect(() => {
1628
- if (didRestoreRef.current) return;
1629
- if (!rootLoaded) return;
1630
- const target = initialUrlPathRef.current;
1631
- if (!target) {
1632
- didRestoreRef.current = true;
1633
- return;
1634
- }
1635
- didRestoreRef.current = true;
1636
-
1637
- void (async () => {
1638
- const parts = target.split("/");
1639
- const name = parts[parts.length - 1];
1640
- const parentDir = parts.slice(0, -1).join("/");
1641
- const siblings = await fetchDir(parentDir);
1642
- const match = siblings.find((s) => s.path === target);
1643
- if (!match) return; // file no longer exists; leave home view
1644
- await revealPath(target);
1645
- void openViewer({
1646
- path: target,
1647
- name,
1648
- type: match.type,
1649
- modifiedAt: match.modifiedAt,
1650
- } as TreeNode);
1651
- })();
1652
- }, [rootLoaded, revealPath]);
1653
-
1654
- const switchWorkspace = useCallback(async (id: string) => {
1655
- if (id === activeWorkspaceId) return;
1656
- try {
1657
- await fetch(`/api/system/workspaces/${id}/open`, { method: "POST" });
1658
- } catch {
1659
- /* best-effort lastOpened bump */
1660
- }
1661
- const u = new URL(location.href);
1662
- u.searchParams.set("ws", id);
1663
- u.searchParams.delete("path");
1664
- history.pushState(null, "", u.toString());
1665
- // Reset open file + caches; the key={activeWorkspaceId} remount clears the rest.
1666
- setOpenFile(null);
1667
- setFileContent(null);
1668
- setEditing(false);
1669
- setRoots([]);
1670
- setRootLoaded(false);
1671
- rootLoadingRef.current = false;
1672
- useWikiSlugsStore.getState().invalidate();
1673
- const ws = workspaces.find((w) => w.id === id);
1674
- if (ws) setRootPath(ws.rootDir);
1675
- setActiveWorkspaceId(id);
1676
- }, [activeWorkspaceId, workspaces]);
1677
-
1678
- const handleDeleteWorkspace = useCallback(async () => {
1679
- if (!deletingWorkspaceId) return;
1680
- try {
1681
- const res = await fetch(`/api/system/workspaces/${deletingWorkspaceId}`, {
1682
- method: "DELETE",
1683
- });
1684
- if (!res.ok) throw new Error("Failed");
1685
- // If deleting the active workspace, switch to first available or picker
1686
- if (deletingWorkspaceId === activeWorkspaceId) {
1687
- const remaining = workspaces.filter((w) => w.id !== deletingWorkspaceId);
1688
- if (remaining.length > 0) {
1689
- const next = [...remaining].sort(
1690
- (a, b) => (b.lastOpenedAt ?? b.createdAt).localeCompare(a.lastOpenedAt ?? a.createdAt)
1691
- )[0];
1692
- await switchWorkspace(next.id);
1693
- } else {
1694
- setRootConfigured(false);
1695
- }
1696
- }
1697
- await loadWorkspaces();
1698
- } catch {
1699
- /* ignore */
1700
- } finally {
1701
- setDeletingWorkspaceId(null);
1702
- }
1703
- }, [deletingWorkspaceId, activeWorkspaceId, workspaces, switchWorkspace, loadWorkspaces]);
1704
-
1705
- const handleRefreshWorkspace = useCallback(async (id: string) => {
1706
- if (refreshingWsId) return;
1707
- setRefreshingWsId(id);
1708
- try {
1709
- const res = await fetch(`/api/system/workspaces/${id}/refresh`, { method: "POST" });
1710
- if (!res.ok) {
1711
- const e: { error?: string } = await res.json();
1712
- showError(e.error ?? "Refresh failed");
1713
- }
1714
- await loadWorkspaces();
1715
- } catch {
1716
- showError("Refresh failed");
1717
- } finally {
1718
- setRefreshingWsId(null);
1719
- }
1720
- }, [refreshingWsId, loadWorkspaces]);
1721
-
1722
- async function handleSave() {
1723
- if (!openFile) return;
1724
- setSaving(true);
1725
- setSaveError(null);
1726
- const res = await wsFetch("/api/wiki/content", {
1727
- method: "PUT",
1728
- headers: { "Content-Type": "application/json" },
1729
- body: JSON.stringify({
1730
- path: openFile.path,
1731
- content: editContent,
1732
- baseRevision: fileRevision,
1733
- }),
1734
- });
1735
- if (res.ok) {
1736
- const d: { revision?: number } = await res.json();
1737
- if (typeof d.revision === "number") setFileRevision(d.revision);
1738
- setFileContent(editContent);
1739
- setEditing(false);
1740
- } else {
1741
- const e: { error?: string } = await res.json();
1742
- setSaveError(e.error ?? "Save failed");
1743
- }
1744
- setSaving(false);
1745
- }
1746
-
1747
- const doUpload = useCallback(
1748
- async (files: FileList | File[], dir: string) => {
1749
- const list = Array.from(files);
1750
- if (!list.length) return;
1751
- setUploading(true);
1752
- setUploadError(null);
1753
- try {
1754
- for (const file of list) {
1755
- const fd = new FormData();
1756
- fd.append("file", file);
1757
- fd.append("dir", dir);
1758
- const res = await wsFetch("/api/wiki/upload", {
1759
- method: "POST",
1760
- body: fd,
1761
- });
1762
- if (!res.ok) {
1763
- const e: { error?: string } = await res.json();
1764
- setUploadError(e.error ?? "Upload failed");
1765
- showError(e.error ?? "Upload failed");
1766
- break;
1767
- }
1768
- }
1769
- await reloadDir(dir);
1770
- } catch {
1771
- setUploadError("Upload failed.");
1772
- } finally {
1773
- setUploading(false);
1774
- if (fileInputRef.current) fileInputRef.current.value = "";
1775
- }
1776
- },
1777
- [reloadDir],
1778
- );
1779
-
1780
- function triggerUpload(dir: string) {
1781
- uploadDirRef.current = dir;
1782
- fileInputRef.current?.click();
1783
- }
1784
-
1785
- async function handleCreateFolder() {
1786
- const name = newFolderName.trim();
1787
- if (!name || newFolderParent === null) return;
1788
- setFolderError(null);
1789
- const rel = newFolderParent ? `${newFolderParent}/${name}` : name;
1790
- const res = await wsFetch("/api/wiki/folder", {
1791
- method: "POST",
1792
- headers: { "Content-Type": "application/json" },
1793
- body: JSON.stringify({ path: rel }),
1794
- });
1795
- if (res.ok) {
1796
- setNewFolderParent(null);
1797
- setNewFolderName("");
1798
- await reloadDir(newFolderParent);
1799
- if (newFolderParent !== "") {
1800
- setRoots((prev) =>
1801
- updateNodes(prev, newFolderParent, (n) => ({ ...n, expanded: true })),
1802
- );
1803
- }
1804
- } else {
1805
- const e: { error?: string } = await res.json();
1806
- setFolderError(e.error ?? "Failed");
1807
- }
1808
- }
1809
-
1810
- async function handleCreateFile() {
1811
- const raw = newFileName.trim();
1812
- if (!raw || newFileParent === null) return;
1813
- setFileCreateError(null);
1814
- const name = raw.includes(".") ? raw : `${raw}.md`;
1815
- const rel = newFileParent ? `${newFileParent}/${name}` : name;
1816
- const res = await wsFetch("/api/wiki/new-file", {
1817
- method: "POST",
1818
- headers: { "Content-Type": "application/json" },
1819
- body: JSON.stringify({ path: rel }),
1820
- });
1821
- if (res.ok) {
1822
- const parent = newFileParent;
1823
- setNewFileParent(null);
1824
- setNewFileName("");
1825
- await reloadDir(parent);
1826
- if (parent !== "") {
1827
- setRoots((prev) =>
1828
- updateNodes(prev, parent, (n) => ({ ...n, expanded: true })),
1829
- );
1830
- }
1831
- void openViewer({
1832
- path: rel,
1833
- name,
1834
- type: "file",
1835
- modifiedAt: new Date().toISOString(),
1836
- } as TreeNode);
1837
- } else {
1838
- const e: { error?: string } = await res.json();
1839
- setFileCreateError(e.error ?? "Failed");
1840
- }
1841
- }
1842
-
1843
- function handleDownload(node: TreeNode) {
1844
- const url = withWs(`/api/wiki/download?path=${encodeURIComponent(node.path)}`);
1845
- const a = document.createElement("a");
1846
- a.href = url;
1847
- a.download = node.type === "file" ? node.name : `${node.name}.zip`;
1848
- document.body.appendChild(a);
1849
- a.click();
1850
- a.remove();
1851
- }
1852
-
1853
- function copyPath(path: string) {
1854
- void navigator.clipboard.writeText(path);
1855
- showSuccess("Path copied");
1856
- }
1857
-
1858
- function copyWikiLink(name: string) {
1859
- const slug = name.replace(/\.(md|markdown)$/i, "");
1860
- void navigator.clipboard.writeText(`[[${slug}]]`);
1861
- showSuccess("Wiki link copied");
1862
- }
1863
-
1864
- function copyUrl(path: string) {
1865
- const url = new URL(location.href);
1866
- url.searchParams.set("path", path);
1867
- if (activeWorkspaceId) url.searchParams.set("ws", activeWorkspaceId);
1868
- void navigator.clipboard.writeText(url.toString());
1869
- showSuccess("URL copied");
1870
- }
1871
-
1872
- async function getTextContent(path: string) {
1873
- if (openFile?.path === path && fileContent !== null) return fileContent;
1874
- if (useEditorStore.getState().currentPath === path) {
1875
- return useEditorStore.getState().content;
1876
- }
1877
- const res = await wsFetch(`/api/wiki/content?path=${encodeURIComponent(path)}`);
1878
- if (!res.ok) throw new Error("Cannot copy content");
1879
- const data: { content: string } = await res.json();
1880
- return data.content;
1881
- }
1882
-
1883
- async function copyRawContent(path: string) {
1884
- try {
1885
- const content = await getTextContent(path);
1886
- await navigator.clipboard.writeText(content);
1887
- showSuccess("Raw content copied");
1888
- } catch {
1889
- showError("Could not copy file content");
1890
- }
1891
- }
1892
-
1893
- async function copyFormattedContent(path: string, name: string) {
1894
- try {
1895
- const content = await getTextContent(path);
1896
- if (!isMarkdown(name)) {
1897
- await navigator.clipboard.writeText(content);
1898
- showSuccess("Content copied");
1899
- return;
1900
- }
1901
- const html = await markdownToHtml(content, { pagePath: path, sanitize: true });
1902
- if ("ClipboardItem" in window && navigator.clipboard.write) {
1903
- await navigator.clipboard.write([
1904
- new ClipboardItem({
1905
- "text/html": new Blob([html], { type: "text/html" }),
1906
- "text/plain": new Blob([content], { type: "text/plain" }),
1907
- }),
1908
- ]);
1909
- } else {
1910
- await navigator.clipboard.writeText(content);
1911
- }
1912
- showSuccess("Formatted content copied");
1913
- } catch {
1914
- showError("Could not copy formatted content");
1915
- }
1916
- }
1917
-
1918
- function renderCopyMenu(node: { path: string; name: string }) {
1919
- return (
1920
- <DropdownMenu>
1921
- <DropdownMenuTrigger asChild>
1922
- <Button
1923
- size="sm"
1924
- variant="ghost"
1925
- className="h-7 gap-1.5 px-2 text-xs"
1926
- title="Copy path, wiki link, or URL"
1927
- >
1928
- <Copy className="h-3.5 w-3.5" />
1929
- Copy
1930
- <ChevronDown className="h-3 w-3 opacity-60" />
1931
- </Button>
1932
- </DropdownMenuTrigger>
1933
- <DropdownMenuContent align="end" className="w-44">
1934
- <DropdownMenuItem onClick={() => copyPath(node.path)}>
1935
- <Copy className="mr-2 h-3.5 w-3.5" />
1936
- Copy path
1937
- </DropdownMenuItem>
1938
- {isMarkdown(node.name) && (
1939
- <DropdownMenuItem onClick={() => copyWikiLink(node.name)}>
1940
- <FileText className="mr-2 h-3.5 w-3.5" />
1941
- Copy wiki link
1942
- </DropdownMenuItem>
1943
- )}
1944
- <DropdownMenuSeparator />
1945
- <DropdownMenuItem onClick={() => copyUrl(node.path)}>
1946
- <Link className="mr-2 h-3.5 w-3.5" />
1947
- Copy URL
1948
- </DropdownMenuItem>
1949
- {isText(node.name) && (
1950
- <>
1951
- <DropdownMenuSeparator />
1952
- <DropdownMenuItem onClick={() => void copyRawContent(node.path)}>
1953
- <FileText className="mr-2 h-3.5 w-3.5" />
1954
- Copy raw content
1955
- </DropdownMenuItem>
1956
- <DropdownMenuItem onClick={() => void copyFormattedContent(node.path, node.name)}>
1957
- <FileText className="mr-2 h-3.5 w-3.5" />
1958
- Copy formatted content
1959
- </DropdownMenuItem>
1960
- </>
1961
- )}
1962
- </DropdownMenuContent>
1963
- </DropdownMenu>
1964
- );
1965
- }
1966
-
1967
- async function handleDelete() {
1968
- if (!deletingPath) return;
1969
- await wsFetch("/api/wiki", {
1970
- method: "DELETE",
1971
- headers: { "Content-Type": "application/json" },
1972
- body: JSON.stringify({ path: deletingPath }),
1973
- });
1974
- if (
1975
- openFile?.path === deletingPath ||
1976
- openFile?.path.startsWith(`${deletingPath}/`)
1977
- ) {
1978
- setOpenFile(null);
1979
- setFileContent(null);
1980
- }
1981
- setRoots((prev) => removeNode(prev, deletingPath));
1982
- setDeletingPath(null);
1983
- }
1984
-
1985
- function handleDragStart(e: React.DragEvent, node: TreeNode) {
1986
- dragNodeRef.current = node;
1987
- e.dataTransfer.effectAllowed = "move";
1988
- e.dataTransfer.setData("text/plain", node.path);
1989
- }
1990
-
1991
- function handleDragOver(
1992
- e: React.DragEvent,
1993
- targetPath: string,
1994
- targetType: "dir" | "root",
1995
- ) {
1996
- e.preventDefault();
1997
- e.stopPropagation();
1998
- const dragging = dragNodeRef.current;
1999
- if (!dragging) return;
2000
- if (
2001
- dragging.path === targetPath ||
2002
- targetPath.startsWith(`${dragging.path}/`)
2003
- )
2004
- return;
2005
- e.dataTransfer.dropEffect = "move";
2006
- setDragOverPath(targetType === "root" ? "" : targetPath);
2007
- }
2008
-
2009
- async function handleDropOnFolder(e: React.DragEvent, targetDirPath: string) {
2010
- e.preventDefault();
2011
- e.stopPropagation();
2012
- setDragOverPath(null);
2013
- const node = dragNodeRef.current;
2014
- dragNodeRef.current = null;
2015
- if (!node) return;
2016
- if (
2017
- node.path === targetDirPath ||
2018
- targetDirPath.startsWith(`${node.path}/`)
2019
- )
2020
- return;
2021
-
2022
- const newPath = targetDirPath ? `${targetDirPath}/${node.name}` : node.name;
2023
- if (newPath === node.path) return;
2024
-
2025
- const res = await wsFetch("/api/wiki/move", {
2026
- method: "POST",
2027
- headers: { "Content-Type": "application/json" },
2028
- body: JSON.stringify({ from: node.path, to: newPath }),
2029
- });
2030
- if (res.ok) {
2031
- const sourceParent = node.path.includes("/")
2032
- ? node.path.split("/").slice(0, -1).join("/")
2033
- : "";
2034
- await reloadDir(sourceParent);
2035
- if (targetDirPath !== sourceParent) await reloadDir(targetDirPath);
2036
- if (openFile?.path === node.path)
2037
- setOpenFile({
2038
- path: newPath,
2039
- name: node.name,
2040
- nodeType: openFile.nodeType,
2041
- size: openFile.size,
2042
- });
2043
- }
2044
- }
2045
-
2046
- const openFileViewerKind = openFile
2047
- ? viewerKindFor(openFile.name, openFile.nodeType)
2048
- : null;
2049
-
2050
- // Gate everything not explicitly safe. Markdown ("editor") is gated too:
2051
- // TipTap builds a node per block and freezes on big docs.
2052
- const showLargeFileGate =
2053
- !!openFile &&
2054
- openFileViewerKind !== null &&
2055
- !SAFE_VIEWER_KINDS.has(openFileViewerKind) &&
2056
- (openFile.size ?? 0) > LARGE_FILE_GATE_BYTES &&
2057
- gateBypassPath !== openFile.path;
2058
-
2059
- const viewWidth = useViewWidthStore((s) => s.width);
2060
- const viewAlign = useViewWidthStore((s) => s.align);
2061
- // Width toggle only meaningful for text-flow viewers (long prose lines).
2062
- const widthAwareViewer =
2063
- openFileViewerKind === null ||
2064
- openFileViewerKind === "editor" ||
2065
- openFileViewerKind === "text" ||
2066
- openFileViewerKind === "source" ||
2067
- openFileViewerKind === "notebook" ||
2068
- openFileViewerKind === "fallback";
2069
- const contentWidthClass = widthAwareViewer ? VIEW_WIDTH_CLASS[viewWidth] : "";
2070
- const contentAlignClass = widthAwareViewer ? VIEW_ALIGN_CLASS[viewAlign] : "";
2071
-
2072
- // Ref-backed dispatcher: handlers below are recreated each render, but the
2073
- // dispatcher methods are referentially stable, so FileTree's React.memo holds.
2074
- const treeHandlersRef = useRef<TreeCtx | null>(null);
2075
- treeHandlersRef.current = {
2076
- toggleFolder: (n) => void toggleFolder(n),
2077
- openViewer: (n) => void openViewer(n),
2078
- copyPath,
2079
- copyWikiLink,
2080
- copyUrl,
2081
- copyRawContent: (path) => void copyRawContent(path),
2082
- copyFormattedContent: (path, name) => void copyFormattedContent(path, name),
2083
- handleDownload,
2084
- triggerUpload,
2085
- handleCreateFile: () => void handleCreateFile(),
2086
- handleCreateFolder: () => void handleCreateFolder(),
2087
- handleDragStart,
2088
- handleDragOver,
2089
- handleDropOnFolder: (e, p) => void handleDropOnFolder(e, p),
2090
- handleGitPull: (p, d) => void handleGitPull(p, d),
2091
- handleCheckout: (p, b, d) => void handleCheckout(p, b, d),
2092
- loadBranches: (p) => void loadBranches(p),
2093
- prefetch: (node) => {
2094
- if (node.type === "file") { if (isMarkdown(node.name)) prefetchPage(node.path); }
2095
- else prefetchDir(node.path);
2096
- },
2097
- togglePin: (node, wsId) =>
2098
- usePinStore.getState().toggle(
2099
- { path: node.path, name: node.name, type: (node.type === "dir" ? "file" : node.type) as "file" | "app" | "node-app" },
2100
- wsId,
2101
- ),
2102
- setDragOverPath,
2103
- setSidebarCollapsed,
2104
- setBranchDropdownNode,
2105
- setBranchDropdownPos,
2106
- setNewFileParent,
2107
- setNewFileName,
2108
- setFileCreateError,
2109
- setNewFolderParent,
2110
- setNewFolderName,
2111
- setFolderError,
2112
- setDeletingPath,
2113
- setDeletingIsDir,
2114
- };
2115
- const treeCtx = useMemo<TreeCtx>(() => {
2116
- const r = treeHandlersRef;
2117
- return {
2118
- toggleFolder: (n) => r.current!.toggleFolder(n),
2119
- openViewer: (n) => r.current!.openViewer(n),
2120
- copyPath: (p) => r.current!.copyPath(p),
2121
- copyWikiLink: (n) => r.current!.copyWikiLink(n),
2122
- copyUrl: (p) => r.current!.copyUrl(p),
2123
- copyRawContent: (p) => r.current!.copyRawContent(p),
2124
- copyFormattedContent: (p, n) => r.current!.copyFormattedContent(p, n),
2125
- handleDownload: (n) => r.current!.handleDownload(n),
2126
- triggerUpload: (d) => r.current!.triggerUpload(d),
2127
- handleCreateFile: () => r.current!.handleCreateFile(),
2128
- handleCreateFolder: () => r.current!.handleCreateFolder(),
2129
- handleDragStart: (e, n) => r.current!.handleDragStart(e, n),
2130
- handleDragOver: (e, p, t) => r.current!.handleDragOver(e, p, t),
2131
- handleDropOnFolder: (e, p) => r.current!.handleDropOnFolder(e, p),
2132
- handleGitPull: (p, d) => r.current!.handleGitPull(p, d),
2133
- handleCheckout: (p, b, d) => r.current!.handleCheckout(p, b, d),
2134
- loadBranches: (p) => r.current!.loadBranches(p),
2135
- prefetch: (n) => r.current!.prefetch(n),
2136
- togglePin: (n, w) => r.current!.togglePin(n, w),
2137
- setDragOverPath: (p) => r.current!.setDragOverPath(p),
2138
- setSidebarCollapsed: (b) => r.current!.setSidebarCollapsed(b),
2139
- setBranchDropdownNode: (p) => r.current!.setBranchDropdownNode(p),
2140
- setBranchDropdownPos: (p) => r.current!.setBranchDropdownPos(p),
2141
- setNewFileParent: (p) => r.current!.setNewFileParent(p),
2142
- setNewFileName: (s) => r.current!.setNewFileName(s),
2143
- setFileCreateError: (s) => r.current!.setFileCreateError(s),
2144
- setNewFolderParent: (p) => r.current!.setNewFolderParent(p),
2145
- setNewFolderName: (s) => r.current!.setNewFolderName(s),
2146
- setFolderError: (s) => r.current!.setFolderError(s),
2147
- setDeletingPath: (p) => r.current!.setDeletingPath(p),
2148
- setDeletingIsDir: (b) => r.current!.setDeletingIsDir(b),
2149
- };
2150
- }, []);
2151
-
2152
- return (
2153
- <div key={activeWorkspaceId ?? "none"} className="flex h-screen gap-0 overflow-hidden bg-background">
2154
- {rootConfigured === null && (
2155
- <div className="flex-1 flex items-center justify-center">
2156
- <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
2157
- </div>
2158
- )}
2159
- {rootConfigured === false && (
2160
- <DirPicker onSelect={(workspaceId) => {
2161
- const u = new URL(location.href);
2162
- u.searchParams.set("ws", workspaceId);
2163
- u.searchParams.delete("path");
2164
- history.replaceState(null, "", u.toString());
2165
- setRootLoaded(false);
2166
- rootLoadingRef.current = false;
2167
- setActiveWorkspaceId(workspaceId);
2168
- setRootConfigured(true);
2169
- void loadWorkspaces();
2170
- }} />
2171
- )}
2172
- {rootConfigured === true && addingWorkspace && (
2173
- <div className="flex-1 flex flex-col">
2174
- <div className="flex items-center justify-end border-b px-3 py-2 bg-muted shrink-0">
2175
- <Button size="sm" variant="ghost" className="h-7 gap-1.5 text-xs" onClick={() => setAddingWorkspace(false)}>
2176
- <X className="h-3.5 w-3.5" /> Cancel
2177
- </Button>
2178
- </div>
2179
- <DirPicker onSelect={(workspaceId) => {
2180
- setAddingWorkspace(false);
2181
- void loadWorkspaces();
2182
- void switchWorkspace(workspaceId);
2183
- }} />
2184
- </div>
2185
- )}
2186
- {rootConfigured === true && !addingWorkspace && <>
2187
- <SearchCommandDialog
2188
- onOpenFile={openFromSearch}
2189
- onToggleSidebar={() =>
2190
- setSidebarCollapsed((v) => {
2191
- const next = !v;
2192
- if (isMobile && next === false) useAIPanelStore.getState().close();
2193
- return next;
2194
- })
2195
- }
2196
- onNewFile={() => setNewFileParent("")}
2197
- onCopyPath={() => {
2198
- if (openFile) void navigator.clipboard.writeText(openFile.path);
2199
- }}
2200
- />
2201
- {/* Tree sidebar */}
2202
- {!sidebarCollapsed && isMobile && (
2203
- <div
2204
- className="fixed inset-0 z-40 bg-overlay backdrop-blur-[1px] md:hidden"
2205
- onClick={() => setSidebarCollapsed(true)}
2206
- aria-hidden
2207
- />
2208
- )}
2209
- {!sidebarCollapsed && (
2210
- <Card
2211
- style={isMobile ? undefined : { width: sidebarWidth }}
2212
- className="fixed inset-y-0 left-0 z-50 w-[85vw] max-w-[20rem] md:relative md:z-auto md:w-auto md:max-w-none flex flex-col shrink-0 overflow-hidden rounded-none border-r border-l-0 border-t-0 border-b-0">
2213
- {/* Row 1: brand + collapse */}
2214
- <div className="flex items-center justify-between gap-2 px-3 py-2 border-b bg-muted shrink-0">
2215
- <div className="flex min-w-0 items-center gap-1.5">
2216
- <img src="/logo.svg" alt="Wiki Viewer" className="h-5 w-5 shrink-0" />
2217
- <span className="truncate text-xs font-semibold leading-5 tracking-tight translate-y-[0.5px]">
2218
- Wiki Viewer
2219
- </span>
2220
- </div>
2221
- <Button
2222
- size="sm"
2223
- variant="ghost"
2224
- className="h-7 w-7 p-0 shrink-0"
2225
- title="Collapse sidebar"
2226
- onClick={() => setSidebarCollapsed(true)}
2227
- >
2228
- <PanelLeftClose className="h-3.5 w-3.5" />
2229
- </Button>
2230
- </div>
2231
-
2232
- {/* Row 2: actions toolbar */}
2233
- <div className="flex items-center justify-between gap-1 px-3 py-1.5 border-b bg-muted/50 shrink-0">
2234
- <DropdownMenu>
2235
- <DropdownMenuTrigger asChild>
2236
- <Button
2237
- size="sm"
2238
- variant="ghost"
2239
- className="h-7 gap-1 px-2 text-xs"
2240
- disabled={uploading}
2241
- >
2242
- {uploading ? (
2243
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
2244
- ) : (
2245
- <Plus className="h-3.5 w-3.5" />
2246
- )}
2247
- New
2248
- <ChevronDown className="h-3 w-3 opacity-60" />
2249
- </Button>
2250
- </DropdownMenuTrigger>
2251
- <DropdownMenuContent align="start" className="w-44">
2252
- <DropdownMenuItem
2253
- onClick={() => {
2254
- setNewFileParent("");
2255
- setNewFileName("");
2256
- setFileCreateError(null);
2257
- }}
2258
- >
2259
- <FilePlus className="mr-2 h-3.5 w-3.5" />
2260
- New file
2261
- </DropdownMenuItem>
2262
- <DropdownMenuItem
2263
- onClick={() => {
2264
- setNewFolderParent("");
2265
- setNewFolderName("");
2266
- setFolderError(null);
2267
- }}
2268
- >
2269
- <FolderPlus className="mr-2 h-3.5 w-3.5" />
2270
- New folder
2271
- </DropdownMenuItem>
2272
- <DropdownMenuItem
2273
- onClick={() => triggerUpload("")}
2274
- disabled={uploading}
2275
- >
2276
- <Upload className="mr-2 h-3.5 w-3.5" />
2277
- Upload
2278
- </DropdownMenuItem>
2279
- </DropdownMenuContent>
2280
- </DropdownMenu>
2281
-
2282
- <div className="flex items-center gap-0.5">
2283
- <Button
2284
- size="sm"
2285
- variant="ghost"
2286
- className="h-7 w-7 p-0"
2287
- title="Refresh tree"
2288
- onClick={refreshTree}
2289
- disabled={refreshingTree}
2290
- >
2291
- {refreshingTree ? (
2292
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
2293
- ) : (
2294
- <RefreshCw className="h-3.5 w-3.5" />
2295
- )}
2296
- </Button>
2297
- <Button
2298
- size="sm"
2299
- variant="ghost"
2300
- className="h-7 w-7 p-0"
2301
- title={showHidden ? "Hide hidden files" : "Show hidden files"}
2302
- onClick={() => useShowHiddenStore.getState().toggle()}
2303
- >
2304
- {showHidden ? (
2305
- <Eye className="h-3.5 w-3.5" />
2306
- ) : (
2307
- <EyeOff className="h-3.5 w-3.5" />
2308
- )}
2309
- </Button>
2310
- <ThemeToggle />
2311
- <Button
2312
- size="sm"
2313
- variant="ghost"
2314
- className="h-7 w-7 p-0"
2315
- title="AI Agent panel"
2316
- onClick={() => {
2317
- useAIPanelStore.getState().toggle();
2318
- if (isMobile) setSidebarCollapsed(true);
2319
- }}
2320
- >
2321
- <Bot className="h-3.5 w-3.5" />
2322
- </Button>
2323
- <Button
2324
- size="sm"
2325
- variant="ghost"
2326
- className="h-7 w-7 p-0"
2327
- title="Settings"
2328
- onClick={() => setSettingsOpen(true)}
2329
- >
2330
- <Settings className="h-3.5 w-3.5" />
2331
- </Button>
2332
- </div>
2333
- </div>
2334
-
2335
- {uploadError && (
2336
- <div className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-destructive bg-destructive/10 shrink-0">
2337
- <AlertCircle className="h-3.5 w-3.5 shrink-0" />
2338
- {uploadError}
2339
- </div>
2340
- )}
2341
-
2342
- {newFolderParent === "" && (
2343
- <div className="flex items-center gap-1.5 px-2 py-1 border-b shrink-0">
2344
- <Folder className="h-4 w-4 shrink-0 text-warning" />
2345
- <input
2346
- className="flex-1 bg-transparent text-sm outline-none border-b border-border min-w-0"
2347
- placeholder="Folder name"
2348
- value={newFolderName}
2349
- onChange={(e) => setNewFolderName(e.target.value)}
2350
- onKeyDown={(e) => {
2351
- if (e.key === "Enter") handleCreateFolder();
2352
- if (e.key === "Escape") {
2353
- setNewFolderParent(null);
2354
- setNewFolderName("");
2355
- }
2356
- }}
2357
- />
2358
- {folderError && (
2359
- <span className="text-xs text-destructive">{folderError}</span>
2360
- )}
2361
- <Button
2362
- size="sm"
2363
- variant="ghost"
2364
- className="h-6 w-6 p-0"
2365
- onClick={handleCreateFolder}
2366
- >
2367
- <Check className="h-3 w-3" />
2368
- </Button>
2369
- <Button
2370
- size="sm"
2371
- variant="ghost"
2372
- className="h-6 w-6 p-0"
2373
- onClick={() => {
2374
- setNewFolderParent(null);
2375
- setNewFolderName("");
2376
- }}
2377
- >
2378
- <X className="h-3 w-3" />
2379
- </Button>
2380
- </div>
2381
- )}
2382
-
2383
- {newFileParent === "" && (
2384
- <div className="flex items-center gap-1.5 px-2 py-1 border-b shrink-0">
2385
- <FileText className="h-4 w-4 shrink-0 text-accent" />
2386
- <input
2387
- autoFocus
2388
- className="flex-1 bg-transparent text-sm outline-none border-b border-border min-w-0"
2389
- placeholder="filename (default .md)"
2390
- value={newFileName}
2391
- onChange={(e) => setNewFileName(e.target.value)}
2392
- onKeyDown={(e) => {
2393
- if (e.key === "Enter") handleCreateFile();
2394
- if (e.key === "Escape") {
2395
- setNewFileParent(null);
2396
- setNewFileName("");
2397
- }
2398
- }}
2399
- />
2400
- {fileCreateError && (
2401
- <span className="text-xs text-destructive">{fileCreateError}</span>
2402
- )}
2403
- <Button
2404
- size="sm"
2405
- variant="ghost"
2406
- className="h-6 w-6 p-0"
2407
- onClick={handleCreateFile}
2408
- >
2409
- <Check className="h-3 w-3" />
2410
- </Button>
2411
- <Button
2412
- size="sm"
2413
- variant="ghost"
2414
- className="h-6 w-6 p-0"
2415
- onClick={() => {
2416
- setNewFileParent(null);
2417
- setNewFileName("");
2418
- }}
2419
- >
2420
- <X className="h-3 w-3" />
2421
- </Button>
2422
- </div>
2423
- )}
2424
-
2425
- <div className="border-b">
2426
- <SidebarSearchBox onOpenFile={openFromSearch} />
2427
- </div>
2428
-
2429
- <div
2430
- ref={sidebarScrollRef}
2431
- className={cn(
2432
- "flex-1 overflow-auto py-1 editorial-file-tree",
2433
- dragOverPath === "" &&
2434
- "ring-2 ring-inset ring-primary bg-primary/5",
2435
- )}
2436
- onDragOver={(e) => handleDragOver(e, "", "root")}
2437
- onDragLeave={(e) => {
2438
- if (!e.currentTarget.contains(e.relatedTarget as Node))
2439
- setDragOverPath(null);
2440
- }}
2441
- onDrop={(e) => handleDropOnFolder(e, "")}
2442
- >
2443
- {/* Pinned section */}
2444
- {pins.length > 0 && (
2445
- <div className="border-b mb-1">
2446
- <button
2447
- type="button"
2448
- className="flex w-full items-center gap-1 px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
2449
- onClick={() => setPinnedCollapsed((c) => !c)}
2450
- >
2451
- {pinnedCollapsed ? <ChevronRight className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
2452
- <Pin className="h-3 w-3" />
2453
- Pinned
2454
- <span className="ml-auto text-[9px] tabular-nums opacity-60">{pins.length}</span>
2455
- </button>
2456
- {!pinnedCollapsed && pins.map((p) => (
2457
- <div
2458
- key={p.path}
2459
- role="button"
2460
- tabIndex={0}
2461
- className={cn(
2462
- "group flex items-center gap-1.5 rounded-sm px-2 py-1 text-sm cursor-pointer transition-colors select-none",
2463
- openFile?.path === p.path ? "bg-accent-soft text-foreground font-medium" : "hover:bg-muted",
2464
- )}
2465
- onClick={() => { void openViewer({ path: p.path, name: p.name, type: (p.type ?? "file") as TreeNode["type"], modifiedAt: "" } as TreeNode); if (isMobile) setSidebarCollapsed(true); }}
2466
- onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); void openViewer({ path: p.path, name: p.name, type: (p.type ?? "file") as TreeNode["type"], modifiedAt: "" } as TreeNode); } }}
2467
- >
2468
- <Star className="h-3 w-3 shrink-0 fill-current text-amber-400" />
2469
- <span className="min-w-0 flex-1 truncate text-xs">{p.name}</span>
2470
- <span className="max-w-[80px] truncate text-[10px] text-muted-foreground/60">{p.path.split("/").slice(0, -1).join("/")}</span>
2471
- <button
2472
- type="button"
2473
- className="hover-reveal shrink-0 rounded p-0.5 text-muted-foreground/50 opacity-0 transition-colors hover:bg-muted hover:text-amber-400 group-hover:opacity-100 focus:opacity-100"
2474
- title="Remove from pinned"
2475
- onClick={(e) => {
2476
- e.stopPropagation();
2477
- usePinStore
2478
- .getState()
2479
- .toggle({ path: p.path, name: p.name }, activeWorkspaceId);
2480
- }}
2481
- >
2482
- <X className="h-3 w-3" />
2483
- </button>
2484
- </div>
2485
- ))}
2486
- </div>
2487
- )}
2488
- {/* Recent files section */}
2489
- {recents.length > 0 && (
2490
- <div className="border-b mb-1">
2491
- <button
2492
- type="button"
2493
- className="flex w-full items-center gap-1 px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
2494
- onClick={() => setRecentCollapsed((c) => !c)}
2495
- >
2496
- {recentCollapsed ? <ChevronRight className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
2497
- Recent
2498
- <span className="ml-auto text-[9px] tabular-nums opacity-60">{recents.length}</span>
2499
- </button>
2500
- {!recentCollapsed && recents.slice(0, 8).map((r) => (
2501
- <div
2502
- key={r.path}
2503
- role="button"
2504
- tabIndex={0}
2505
- className={cn(
2506
- "flex items-center gap-1.5 rounded-sm px-2 py-1 text-sm cursor-pointer transition-colors select-none",
2507
- openFile?.path === r.path ? "bg-accent-soft text-foreground font-medium" : "hover:bg-muted",
2508
- )}
2509
- onClick={() => { void openViewer({ path: r.path, name: r.name, type: (r.type ?? "file") as TreeNode["type"], modifiedAt: "" } as TreeNode); if (isMobile) setSidebarCollapsed(true); }}
2510
- onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); void openViewer({ path: r.path, name: r.name, type: (r.type ?? "file") as TreeNode["type"], modifiedAt: "" } as TreeNode); } }}
2511
- >
2512
- <FileText className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
2513
- <span className="flex-1 truncate text-xs">{r.name}</span>
2514
- <span className="text-[10px] text-muted-foreground/60 truncate max-w-[80px]">{r.path.split("/").slice(0, -1).join("/")}</span>
2515
- </div>
2516
- ))}
2517
- </div>
2518
- )}
2519
- {rootLoading ? (
2520
- <div className="flex justify-center py-6">
2521
- <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
2522
- </div>
2523
- ) : roots.length === 0 ? (
2524
- <div className="flex flex-col items-center gap-3 px-4 py-8 text-center">
2525
- <div className="rounded-full bg-muted p-3">
2526
- <FileText className="h-6 w-6 text-muted-foreground" />
2527
- </div>
2528
- <div className="space-y-1">
2529
- <p className="text-sm font-medium">No files yet</p>
2530
- <p className="text-xs text-muted-foreground">
2531
- Upload files or add them to the configured directory
2532
- </p>
2533
- </div>
2534
- <Button
2535
- size="sm"
2536
- variant="outline"
2537
- className="w-full gap-1.5 max-w-[180px]"
2538
- onClick={() => triggerUpload("")}
2539
- disabled={uploading}
2540
- >
2541
- {uploading ? (
2542
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
2543
- ) : (
2544
- <Upload className="h-3.5 w-3.5" />
2545
- )}
2546
- Upload Files
2547
- </Button>
2548
- </div>
2549
- ) : (
2550
- <FileTree
2551
- ctx={treeCtx}
2552
- nodes={roots}
2553
- openPath={openFile?.path ?? null}
2554
- dragOverPath={dragOverPath}
2555
- branchDropdownNode={branchDropdownNode}
2556
- branchDropdownPos={branchDropdownPos}
2557
- nodeBranches={nodeBranches}
2558
- branchesLoading={branchesLoading}
2559
- checkingOutBranch={checkingOutBranch}
2560
- pullingRepo={pullingRepo}
2561
- activePaths={activePaths}
2562
- pins={pins}
2563
- isMobile={isMobile}
2564
- activeWorkspaceId={activeWorkspaceId}
2565
- newFileParent={newFileParent}
2566
- newFileName={newFileName}
2567
- fileCreateError={fileCreateError}
2568
- newFolderParent={newFolderParent}
2569
- newFolderName={newFolderName}
2570
- folderError={folderError}
2571
- sidebarScrollRef={sidebarScrollRef}
2572
- />
2573
- )}
2574
- </div>
2575
- <div className="border-t px-2 py-2 bg-muted shrink-0">
2576
- <DropdownMenu>
2577
- <DropdownMenuTrigger asChild>
2578
- <Button
2579
- size="sm"
2580
- variant="ghost"
2581
- className="w-full h-auto justify-between gap-2 px-2 py-1.5 text-left"
2582
- title={rootPath ?? ""}
2583
- >
2584
- <span className="flex flex-col min-w-0">
2585
- <span className="truncate text-xs font-medium">
2586
- {workspaces.find((w) => w.id === activeWorkspaceId)?.name ?? "Workspace"}
2587
- </span>
2588
- <span className="truncate text-[10px] text-muted-foreground font-mono">
2589
- {rootPath ?? ""}
2590
- </span>
2591
- </span>
2592
- <ChevronDown className="h-3.5 w-3.5 shrink-0 opacity-60" />
2593
- </Button>
2594
- </DropdownMenuTrigger>
2595
- <DropdownMenuContent align="start" className="w-60">
2596
- {workspaces.map((w) => (
2597
- <DropdownMenuItem
2598
- key={w.id}
2599
- onClick={() => void switchWorkspace(w.id)}
2600
- className={cn("gap-2", w.id === activeWorkspaceId && "font-medium")}
2601
- >
2602
- {w.id === activeWorkspaceId ? (
2603
- <Check className="h-3.5 w-3.5 shrink-0" />
2604
- ) : (
2605
- <span className="w-3.5 shrink-0" />
2606
- )}
2607
- <span className="flex flex-col min-w-0 flex-1">
2608
- <span className="flex items-center gap-1.5 truncate">
2609
- <span className="truncate">{w.name}</span>
2610
- {w.readOnly && (
2611
- <span className="inline-flex items-center gap-0.5 rounded bg-muted px-1.5 text-[10px] text-muted-foreground font-normal shrink-0">
2612
- <GitBranch className="h-2.5 w-2.5" /> read-only
2613
- </span>
2614
- )}
2615
- </span>
2616
- <span className="truncate text-[10px] text-muted-foreground font-mono">{w.rootDir}</span>
2617
- {w.git?.lastPulledAt && timeAgo(w.git.lastPulledAt) && (
2618
- <span className="text-[10px] text-muted-foreground/70">synced {timeAgo(w.git.lastPulledAt)}</span>
2619
- )}
2620
- </span>
2621
- {isWsAdmin && w.git && (
2622
- <button
2623
- className={cn(
2624
- "shrink-0 rounded p-0.5 hover:bg-accent transition-colors",
2625
- w.git.lastError ? "text-destructive" : "text-muted-foreground hover:text-foreground",
2626
- )}
2627
- title={w.git.lastError ? `Last refresh failed: ${w.git.lastError}` : "Refresh"}
2628
- disabled={refreshingWsId === w.id}
2629
- onClick={(e) => {
2630
- e.stopPropagation();
2631
- e.preventDefault();
2632
- void handleRefreshWorkspace(w.id);
2633
- }}
2634
- >
2635
- <RefreshCw className={cn("h-3.5 w-3.5", refreshingWsId === w.id && "animate-spin")} />
2636
- </button>
2637
- )}
2638
- {isWsAdmin && (
2639
- <button
2640
- className="shrink-0 rounded p-0.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
2641
- title="Delete workspace (does not delete files)"
2642
- onClick={(e) => {
2643
- e.stopPropagation();
2644
- setDeletingWorkspaceId(w.id);
2645
- }}
2646
- >
2647
- <Trash2 className="h-3.5 w-3.5" />
2648
- </button>
2649
- )}
2650
- </DropdownMenuItem>
2651
- ))}
2652
- {isWsAdmin && (
2653
- <>
2654
- <DropdownMenuSeparator />
2655
- <DropdownMenuItem onClick={() => setAddingWorkspace(true)}>
2656
- <FolderPlus className="mr-2 h-3.5 w-3.5" />
2657
- Add workspace…
2658
- </DropdownMenuItem>
2659
- </>
2660
- )}
2661
- </DropdownMenuContent>
2662
- </DropdownMenu>
2663
- </div>
2664
- {/* Resize handle */}
2665
- <div
2666
- role="separator"
2667
- aria-orientation="vertical"
2668
- aria-label="Resize sidebar"
2669
- title="Drag to resize"
2670
- onMouseDown={(e) => {
2671
- e.preventDefault();
2672
- const startX = e.clientX;
2673
- const startW = sidebarWidth;
2674
- setSidebarResizing(true);
2675
- const prevCursor = document.body.style.cursor;
2676
- const prevSelect = document.body.style.userSelect;
2677
- document.body.style.cursor = "col-resize";
2678
- document.body.style.userSelect = "none";
2679
- const onMove = (ev: MouseEvent) => {
2680
- setSidebarWidth(startW + (ev.clientX - startX));
2681
- };
2682
- const onUp = () => {
2683
- setSidebarResizing(false);
2684
- document.body.style.cursor = prevCursor;
2685
- document.body.style.userSelect = prevSelect;
2686
- window.removeEventListener("mousemove", onMove);
2687
- window.removeEventListener("mouseup", onUp);
2688
- };
2689
- window.addEventListener("mousemove", onMove);
2690
- window.addEventListener("mouseup", onUp);
2691
- }}
2692
- onDoubleClick={() => setSidebarWidth(288)}
2693
- onKeyDown={(e) => {
2694
- if (e.key === "ArrowLeft") {
2695
- e.preventDefault();
2696
- setSidebarWidth(sidebarWidth - 16);
2697
- } else if (e.key === "ArrowRight") {
2698
- e.preventDefault();
2699
- setSidebarWidth(sidebarWidth + 16);
2700
- }
2701
- }}
2702
- tabIndex={0}
2703
- aria-valuemin={SIDEBAR_MIN_WIDTH}
2704
- aria-valuemax={SIDEBAR_MAX_WIDTH}
2705
- aria-valuenow={sidebarWidth}
2706
- className={cn(
2707
- "absolute right-0 top-0 z-20 h-full w-1.5 cursor-col-resize -mr-px transition-colors hover:bg-primary/40 focus:bg-primary/40 focus:outline-none hidden md:block",
2708
- sidebarResizing && "bg-primary/60",
2709
- )}
2710
- />
2711
- </Card>
2712
- )}
2713
-
2714
- {/* Right panel */}
2715
- <div className="flex-1 flex flex-col min-w-0 relative">
2716
- {/* Desktop: floating reopen button when sidebar is collapsed */}
2717
- {sidebarCollapsed && (
2718
- <Button
2719
- size="sm"
2720
- variant="ghost"
2721
- className="hidden md:flex absolute left-2 top-2 z-10 h-7 w-7 p-0"
2722
- title="Show sidebar"
2723
- onClick={() => setSidebarCollapsed(false)}
2724
- >
2725
- <PanelLeftOpen className="h-3.5 w-3.5" />
2726
- </Button>
2727
- )}
2728
- {/* Mobile: dedicated top bar hosting the drawer + AI panel toggles */}
2729
- <div className="md:hidden flex h-11 shrink-0 items-center justify-between gap-2 border-b bg-muted px-1">
2730
- <Button
2731
- size="sm"
2732
- variant="ghost"
2733
- className="h-9 w-9 p-0"
2734
- title="Show sidebar"
2735
- onClick={() => {
2736
- setSidebarCollapsed(false);
2737
- useAIPanelStore.getState().close();
2738
- }}
2739
- >
2740
- <PanelLeftOpen className="h-4 w-4" />
2741
- </Button>
2742
- <span
2743
- className="flex min-w-0 items-center gap-1.5 text-xs font-semibold tracking-tight text-muted-foreground"
2744
- title={openFile?.path}
2745
- >
2746
- {openFile && (
2747
- <span className="editorial-tree-typeicon shrink-0">
2748
- {openFileViewerKind === "app" || openFileViewerKind === "html" ? (
2749
- <Globe className="h-4 w-4 text-foreground/70" />
2750
- ) : isImage(openFile.name) ? (
2751
- <ImageIcon className="h-4 w-4 text-sunshine-700" />
2752
- ) : isText(openFile.name) ? (
2753
- <FileText className="h-4 w-4 text-foreground/70" />
2754
- ) : (
2755
- <File className="h-4 w-4 text-foreground/60" />
2756
- )}
2757
- </span>
2758
- )}
2759
- <span className="truncate">
2760
- {openFile ? openFile.name : "Wiki Viewer"}
2761
- </span>
2762
- </span>
2763
- <Button
2764
- size="sm"
2765
- variant="ghost"
2766
- className="h-9 w-9 p-0"
2767
- title="AI Agent panel"
2768
- onClick={() => {
2769
- useAIPanelStore.getState().open();
2770
- setSidebarCollapsed(true);
2771
- }}
2772
- >
2773
- <Bot className="h-4 w-4" />
2774
- </Button>
2775
- </div>
2776
- {openFile ? (
2777
- openFileViewerKind === "node-app" ? (
2778
- <NodeAppViewer path={openFile.path} title={openFile.name} />
2779
- ) : (openFileViewerKind === "app" || openFileViewerKind === "html") ? (
2780
- // html files → direct asset URL; app folders → index.html (default)
2781
- (() => {
2782
- const websiteSrc =
2783
- openFileViewerKind === "html"
2784
- ? `/api/assets/${openFile.path}`
2785
- : undefined;
2786
- return appFullscreen ? (
2787
- <WebsiteViewer
2788
- path={openFile.path}
2789
- title={openFile.name}
2790
- src={websiteSrc}
2791
- fullscreen
2792
- onExit={() => setAppFullscreen(false)}
2793
- />
2794
- ) : (
2795
- <div className="flex-1 flex flex-col overflow-hidden min-w-0">
2796
- <div className={cn("flex items-center justify-between px-4 py-2 border-b bg-muted shrink-0 editorial-doc-header", !isMobile && sidebarCollapsed && "pl-11")}>
2797
- <div className="flex items-center gap-2 min-w-0">
2798
- <span className="hidden md:inline-flex">
2799
- <span className="editorial-tree-typeicon">
2800
- <Globe className="h-4 w-4 shrink-0 text-foreground/70" />
2801
- </span>
2802
- </span>
2803
- <span
2804
- className="hidden md:inline text-sm font-normal truncate"
2805
- title={openFile.path}
2806
- >
2807
- {openFile.path}
2808
- </span>
2809
- </div>
2810
- <div className="flex items-center gap-1 shrink-0">
2811
- {renderCopyMenu(openFile)}
2812
- {openFileViewerKind === "html" &&
2813
- !editing &&
2814
- fileContent !== null && (
2815
- <Button
2816
- size="sm"
2817
- variant="ghost"
2818
- className="h-7 w-7 p-0"
2819
- title="Edit source"
2820
- onClick={() => {
2821
- setEditing(true);
2822
- setEditContent(fileContent);
2823
- setSaveError(null);
2824
- }}
2825
- >
2826
- <Pencil className="h-3.5 w-3.5" />
2827
- </Button>
2828
- )}
2829
- <Button
2830
- size="sm"
2831
- variant="ghost"
2832
- className="h-7 w-7 p-0"
2833
- title="Refresh"
2834
- onClick={() => setAppKey((k) => k + 1)}
2835
- >
2836
- <RefreshCw className="h-3.5 w-3.5" />
2837
- </Button>
2838
- <Button
2839
- size="sm"
2840
- variant="ghost"
2841
- className="h-7 gap-1.5 text-xs"
2842
- onClick={() => setAppFullscreen(true)}
2843
- >
2844
- <Maximize2 className="h-3.5 w-3.5" />
2845
- Open fullscreen
2846
- </Button>
2847
- <Button
2848
- size="sm"
2849
- variant="ghost"
2850
- className="h-7 w-7 p-0"
2851
- onClick={() => {
2852
- setOpenFile(null);
2853
- setEditing(false);
2854
- }}
2855
- >
2856
- <X className="h-3.5 w-3.5" />
2857
- </Button>
2858
- </div>
2859
- </div>
2860
- {editing && openFileViewerKind === "html" ? (
2861
- <div className="flex-1 flex flex-col overflow-hidden min-h-0">
2862
- <textarea
2863
- value={editContent}
2864
- onChange={(e) => setEditContent(e.target.value)}
2865
- spellCheck={false}
2866
- className="flex-1 w-full min-h-0 resize-none bg-background text-foreground px-4 py-3 font-mono text-[13px] leading-relaxed outline-none border-0"
2867
- />
2868
- <div className="border-t px-4 py-2 flex items-center justify-end gap-2 bg-muted shrink-0">
2869
- {saveError && (
2870
- <span className="text-xs text-destructive mr-auto">
2871
- {saveError}
2872
- </span>
2873
- )}
2874
- <Button
2875
- size="sm"
2876
- variant="ghost"
2877
- onClick={() => {
2878
- setEditing(false);
2879
- setSaveError(null);
2880
- }}
2881
- >
2882
- Cancel
2883
- </Button>
2884
- <Button
2885
- size="sm"
2886
- className="gap-1"
2887
- onClick={handleSave}
2888
- disabled={saving}
2889
- >
2890
- {saving && (
2891
- <Loader2 className="h-3 w-3 animate-spin" />
2892
- )}
2893
- Save
2894
- </Button>
2895
- </div>
2896
- </div>
2897
- ) : (
2898
- <WebsiteViewer
2899
- key={appKey}
2900
- path={openFile.path}
2901
- title={openFile.name}
2902
- src={websiteSrc}
2903
- />
2904
- )}
2905
- </div>
2906
- );
2907
- })()
2908
- ) : (
2909
- <div className="flex-1 flex flex-col overflow-hidden min-w-0">
2910
- <div className={cn("flex items-center justify-between px-4 py-2 border-b bg-muted shrink-0 editorial-doc-header", !isMobile && sidebarCollapsed && "pl-11")}>
2911
- <div className="flex items-center gap-2 min-w-0">
2912
- <span className="hidden md:inline-flex"><span className="editorial-tree-typeicon">
2913
- {isImage(openFile.name) ? (
2914
- <ImageIcon className="h-4 w-4 shrink-0 text-sunshine-700" />
2915
- ) : isText(openFile.name) ? (
2916
- <FileText className="h-4 w-4 shrink-0 text-foreground/70" />
2917
- ) : (
2918
- <File className="h-4 w-4 shrink-0 text-foreground/60" />
2919
- )}
2920
- </span></span>
2921
- <span
2922
- className="hidden md:inline text-sm font-normal truncate"
2923
- title={openFile.path}
2924
- >
2925
- {openFile.path}
2926
- </span>
2927
- {gitFileInfo && (
2928
- <span className="hidden md:flex items-center gap-1 text-[11px] text-muted-foreground shrink-0 ml-1">
2929
- <User className="h-3 w-3 shrink-0" />
2930
- <span className="truncate max-w-[100px]">{gitFileInfo.author}</span>
2931
- <span title={new Date(gitFileInfo.date).toLocaleString()} className="shrink-0">{timeAgo(gitFileInfo.date)}</span>
2932
- </span>
2933
- )}
2934
- </div>
2935
- <div className="flex items-center gap-1 shrink-0">
2936
- {renderCopyMenu(openFile)}
2937
- <Button
2938
- size="sm"
2939
- variant="ghost"
2940
- className="h-7 w-7 p-0"
2941
- title="File history"
2942
- onClick={() => { if (showHistory) setShowHistory(false); else void loadHistory(); }}
2943
- >
2944
- <History className="h-3.5 w-3.5" />
2945
- </Button>
2946
- <Button
2947
- size="sm"
2948
- variant="ghost"
2949
- className="h-7 w-7 p-0"
2950
- title="Share"
2951
- onClick={() => setShareDialogOpen(true)}
2952
- >
2953
- <Share className="h-3.5 w-3.5" />
2954
- </Button>
2955
- {isText(openFile.name) &&
2956
- !editing &&
2957
- (fileContent !== null || isMarkdown(openFile.name)) && (
2958
- <Button
2959
- size="sm"
2960
- variant="ghost"
2961
- className="h-7 w-7 p-0"
2962
- onClick={() => {
2963
- setEditing(true);
2964
- setEditContent(fileContent ?? "");
2965
- setSaveError(null);
2966
- if (isMarkdown(openFile.name)) {
2967
- void useEditorStore
2968
- .getState()
2969
- .loadPage(openFile.path);
2970
- }
2971
- }}
2972
- >
2973
- <Pencil className="h-3.5 w-3.5" />
2974
- </Button>
2975
- )}
2976
- {isText(openFile.name) &&
2977
- editing &&
2978
- isMarkdown(openFile.name) && (
2979
- <Button
2980
- size="sm"
2981
- variant="ghost"
2982
- className="h-7 w-7 p-0"
2983
- title="Done editing"
2984
- onClick={() => {
2985
- setEditing(false);
2986
- setSaveError(null);
2987
- }}
2988
- >
2989
- <Eye className="h-3.5 w-3.5" />
2990
- </Button>
2991
- )}
2992
- {isText(openFile.name) &&
2993
- !editing &&
2994
- (fileContent !== null || isMarkdown(openFile.name)) && (
2995
- <Button
2996
- size="sm"
2997
- variant="ghost"
2998
- className="h-7 w-7 p-0"
2999
- title="Refresh"
3000
- onClick={refreshViewer}
3001
- disabled={fileLoading}
3002
- >
3003
- {fileLoading ? (
3004
- <Loader2 className="h-3.5 w-3.5 animate-spin" />
3005
- ) : (
3006
- <RefreshCw className="h-3.5 w-3.5" />
3007
- )}
3008
- </Button>
3009
- )}
3010
- {widthAwareViewer && <ViewWidthToggle />}
3011
- <Button
3012
- size="sm"
3013
- variant="ghost"
3014
- className="h-7 w-7 p-0"
3015
- onClick={() => {
3016
- setOpenFile(null);
3017
- setFileContent(null);
3018
- setEditing(false);
3019
- }}
3020
- >
3021
- <X className="h-3.5 w-3.5" />
3022
- </Button>
3023
- </div>
3024
- </div>
3025
-
3026
- {/* History panel */}
3027
- {showHistory && (
3028
- <div className="border-b bg-muted/30 shrink-0 max-h-[40vh] overflow-auto">
3029
- <div className="flex items-center justify-between px-4 py-1.5 border-b">
3030
- <span className="text-xs font-semibold text-muted-foreground">History</span>
3031
- <button type="button" className="text-muted-foreground hover:text-foreground" onClick={() => setShowHistory(false)}>
3032
- <X className="h-3.5 w-3.5" />
3033
- </button>
3034
- </div>
3035
- {historyLoading ? (
3036
- <div className="flex justify-center py-4">
3037
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
3038
- </div>
3039
- ) : historyCommits.length === 0 ? (
3040
- <p className="px-4 py-3 text-xs text-muted-foreground">No history found.</p>
3041
- ) : (
3042
- <div>
3043
- {historyCommits.map((c) => (
3044
- <div key={c.sha}>
3045
- <button
3046
- type="button"
3047
- className={cn(
3048
- "w-full text-left px-4 py-2 hover:bg-muted transition-colors",
3049
- selectedDiffSha === c.sha && "bg-muted",
3050
- )}
3051
- onClick={() => void selectDiff(c.sha)}
3052
- >
3053
- <div className="flex items-center gap-2">
3054
- <code className="text-[11px] font-mono text-muted-foreground shrink-0">{c.shortSha}</code>
3055
- <span className="flex-1 truncate text-xs">{c.message}</span>
3056
- <span className="shrink-0 text-[11px] text-muted-foreground">{c.author}</span>
3057
- <span className="shrink-0 text-[11px] text-muted-foreground" title={new Date(c.date).toLocaleString()}>{timeAgo(c.date)}</span>
3058
- </div>
3059
- </button>
3060
- {selectedDiffSha === c.sha && (
3061
- <div className="border-t">
3062
- {diffLoading ? (
3063
- <div className="flex justify-center py-3">
3064
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
3065
- </div>
3066
- ) : diffContent !== null ? (
3067
- <pre className="overflow-auto px-4 py-2 text-[11px] font-mono leading-relaxed whitespace-pre text-foreground/80 max-h-60">{diffContent}</pre>
3068
- ) : null}
3069
- </div>
3070
- )}
3071
- </div>
3072
- ))}
3073
- </div>
3074
- )}
3075
- </div>
3076
- )}
3077
-
3078
- {showLargeFileGate ? (
3079
- <LargeFileGate
3080
- path={openFile.path}
3081
- size={openFile.size ?? 0}
3082
- onOpen={() => setGateBypassPath(openFile.path)}
3083
- />
3084
- ) : editing ? (
3085
- <div className="flex-1 flex flex-col overflow-hidden min-h-0">
3086
- {isMarkdown(openFile.name) ? (
3087
- <KBEditor />
3088
- ) : (
3089
- <textarea
3090
- value={editContent}
3091
- onChange={(e) => setEditContent(e.target.value)}
3092
- spellCheck={false}
3093
- className="flex-1 w-full min-h-0 resize-none bg-background text-foreground px-4 py-3 font-mono text-[13px] leading-relaxed outline-none border-0"
3094
- />
3095
- )}
3096
- </div>
3097
- ) : isMarkdown(openFile.name) ? (
3098
- <div className="flex-1 flex flex-col overflow-hidden min-h-0">
3099
- {fileLoading ? (
3100
- <div className="flex justify-center py-8">
3101
- <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
3102
- </div>
3103
- ) : (
3104
- <KBEditor mode="viewing" />
3105
- )}
3106
- </div>
3107
- ) : (
3108
- <div className="flex-1 overflow-auto p-4 min-h-0">
3109
- <div className={cn("w-full", contentAlignClass, contentWidthClass)}>
3110
- {fileLoading ? (
3111
- <div className="flex justify-center py-8">
3112
- <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
3113
- </div>
3114
- ) : openFileViewerKind === "csv" ? (
3115
- <CsvViewer path={openFile.path} title={openFile.name} />
3116
- ) : openFileViewerKind === "pdf" ? (
3117
- <PdfViewer path={openFile.path} title={openFile.name} />
3118
- ) : openFileViewerKind === "mermaid" ? (
3119
- <MermaidViewer
3120
- path={openFile.path}
3121
- title={openFile.name}
3122
- />
3123
- ) : openFileViewerKind === "notebook" ? (
3124
- <NotebookViewer
3125
- path={openFile.path}
3126
- title={openFile.name}
3127
- />
3128
- ) : openFileViewerKind === "image" ? (
3129
- <ImageViewer path={openFile.path} title={openFile.name} />
3130
- ) : openFileViewerKind === "media" ? (
3131
- <MediaViewer
3132
- path={openFile.path}
3133
- title={openFile.name}
3134
- type={
3135
- ["mp4", "webm", "mov", "m4v"].includes(
3136
- ext(openFile.name),
3137
- )
3138
- ? "video"
3139
- : "audio"
3140
- }
3141
- />
3142
- ) : openFileViewerKind === "docx" ? (
3143
- <DocxViewer path={openFile.path} title={openFile.name} />
3144
- ) : openFileViewerKind === "xlsx" ? (
3145
- <XlsxViewer path={openFile.path} title={openFile.name} />
3146
- ) : openFileViewerKind === "pptx" ? (
3147
- <PptxViewer path={openFile.path} title={openFile.name} />
3148
- ) : openFileViewerKind === "source" ? (
3149
- <SourceViewer
3150
- path={openFile.path}
3151
- title={openFile.name}
3152
- />
3153
- ) : openFileViewerKind === "fallback" ? (
3154
- <FileFallbackViewer
3155
- path={openFile.path}
3156
- title={openFile.name}
3157
- />
3158
- ) : fileContent !== null ? (
3159
- <pre className="text-xs font-mono whitespace-pre-wrap break-words leading-relaxed">
3160
- {fileContent}
3161
- </pre>
3162
- ) : isText(openFile.name) ? (
3163
- <p className="text-sm text-muted-foreground">
3164
- Could not load file.
3165
- </p>
3166
- ) : (
3167
- <p className="text-sm text-muted-foreground">
3168
- Preview not available for this file type.
3169
- </p>
3170
- )}
3171
- </div>
3172
- </div>
3173
- )}
3174
-
3175
- {editing && !isMarkdown(openFile.name) && (
3176
- <div className="border-t px-4 py-2 flex items-center justify-end gap-2 bg-muted shrink-0">
3177
- {saveError && (
3178
- <span className="text-xs text-destructive mr-auto">
3179
- {saveError}
3180
- </span>
3181
- )}
3182
- <Button
3183
- size="sm"
3184
- variant="ghost"
3185
- onClick={() => {
3186
- setEditing(false);
3187
- setSaveError(null);
3188
- }}
3189
- >
3190
- Cancel
3191
- </Button>
3192
- <Button
3193
- size="sm"
3194
- className="gap-1"
3195
- onClick={handleSave}
3196
- disabled={saving}
3197
- >
3198
- {saving && <Loader2 className="h-3 w-3 animate-spin" />}
3199
- Save
3200
- </Button>
3201
- </div>
3202
- )}
3203
- </div>
3204
- )
3205
- ) : (
3206
- <div className="flex-1 flex flex-col items-center justify-center">
3207
- <div className="flex flex-col items-center gap-2 text-center px-4">
3208
- <FileText className="h-8 w-8 text-muted-foreground" />
3209
- <p className="text-sm text-muted-foreground">
3210
- Select a file to view or edit
3211
- </p>
3212
- </div>
3213
- </div>
3214
- )}
3215
- </div>
3216
-
3217
- <ShareDialog
3218
- open={shareDialogOpen}
3219
- onOpenChange={setShareDialogOpen}
3220
- filePath={openFile?.path ?? ""}
3221
- />
3222
- <AuthSettingsSheet open={settingsOpen} onOpenChange={setSettingsOpen} />
3223
- <AIPanel currentPath={openFile?.path} />
3224
- <input
3225
- ref={fileInputRef}
3226
- type="file"
3227
- multiple
3228
- className="hidden"
3229
- onChange={(e) => {
3230
- if (e.target.files) doUpload(e.target.files, uploadDirRef.current);
3231
- }}
3232
- />
3233
-
3234
- <ConfirmDialog
3235
- open={!!deletingPath}
3236
- onOpenChange={(open) => {
3237
- if (!open) setDeletingPath(null);
3238
- }}
3239
- title={deletingIsDir ? "Delete folder?" : "Delete file?"}
3240
- description={
3241
- deletingIsDir
3242
- ? `"${deletingPath?.split("/").pop()}" and all its contents will be permanently deleted.`
3243
- : `"${deletingPath?.split("/").pop()}" will be permanently removed.`
3244
- }
3245
- onConfirm={handleDelete}
3246
- />
3247
-
3248
- <ConfirmDialog
3249
- open={!!deletingWorkspaceId}
3250
- onOpenChange={(open) => {
3251
- if (!open) setDeletingWorkspaceId(null);
3252
- }}
3253
- title="Delete workspace?"
3254
- description={`"${workspaces.find((w) => w.id === deletingWorkspaceId)?.name ?? ""}" will be removed from the workspace list. Files on disk are NOT deleted.`}
3255
- onConfirm={handleDeleteWorkspace}
3256
- />
3257
- </>}
3258
- </div>
3259
- );
3260
- }