wiki-viewer 1.14.1 → 1.14.2

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 (384) 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.html +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/share/[token]/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/share/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/refresh/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/system/workspaces/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/wiki/backlinks/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/wiki/download/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/wiki/file/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/wiki/git-branches/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/wiki/git-checkout/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/wiki/git-diff/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/wiki/git-file-info/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/wiki/git-history/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/wiki/git-pull/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/wiki/outlinks/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/wiki/search/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/index.html +1 -1
  61. package/.next/standalone/.next/server/app/index.rsc +1 -1
  62. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  63. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  64. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  65. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  66. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  67. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  68. package/.next/standalone/.next/server/middleware-manifest.json +1 -1
  69. package/.next/standalone/.next/server/pages/404.html +1 -1
  70. package/.next/standalone/.next/server/pages/500.html +1 -1
  71. package/.next/standalone/package.json +1 -1
  72. package/bin/postbuild.js +21 -0
  73. package/package.json +1 -1
  74. package/.next/standalone/.github/workflows/publish.yml +0 -37
  75. package/.next/standalone/.handoffs/archive/handoff-20260612-113332.md +0 -98
  76. package/.next/standalone/.handoffs/archive/handoff-20260612-141032.md +0 -73
  77. package/.next/standalone/.pi/ownership.json +0 -3
  78. package/.next/standalone/AGENTS.md +0 -114
  79. package/.next/standalone/DESIGN.md +0 -521
  80. package/.next/standalone/README.md +0 -821
  81. package/.next/standalone/TODO.md +0 -20
  82. package/.next/standalone/agents/bootstrap-prompt.md +0 -14
  83. package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +0 -456
  84. package/.next/standalone/bin/postbuild.js +0 -78
  85. package/.next/standalone/bin/wiki-viewer.js +0 -922
  86. package/.next/standalone/certificates/localhost-key.pem +0 -28
  87. package/.next/standalone/certificates/localhost.pem +0 -24
  88. package/.next/standalone/docs/agent-collab-plan.md +0 -1615
  89. package/.next/standalone/docs/agent-collab-v2-plan.md +0 -771
  90. package/.next/standalone/docs/agent-fs-plan.md +0 -232
  91. package/.next/standalone/docs/editorial-skin-spec.md +0 -242
  92. package/.next/standalone/docs/file-vs-collab-authority.md +0 -124
  93. package/.next/standalone/docs/mobile-readiness-spec.md +0 -229
  94. package/.next/standalone/docs/workspacing-plan.md +0 -364
  95. package/.next/standalone/packages/wiki-viewer-mcp/README.md +0 -195
  96. package/.next/standalone/packages/wiki-viewer-mcp/bench-e2e.mjs +0 -166
  97. package/.next/standalone/packages/wiki-viewer-mcp/bench-latency.mjs +0 -87
  98. package/.next/standalone/packages/wiki-viewer-mcp/bench-payload.mjs +0 -45
  99. package/.next/standalone/packages/wiki-viewer-mcp/dist/http-client.js +0 -331
  100. package/.next/standalone/packages/wiki-viewer-mcp/dist/index.js +0 -531
  101. package/.next/standalone/packages/wiki-viewer-mcp/dist/register.js +0 -94
  102. package/.next/standalone/packages/wiki-viewer-mcp/dist/state-cache.js +0 -28
  103. package/.next/standalone/packages/wiki-viewer-mcp/package-lock.json +0 -1716
  104. package/.next/standalone/packages/wiki-viewer-mcp/package.json +0 -41
  105. package/.next/standalone/packages/wiki-viewer-mcp/src/http-client.ts +0 -459
  106. package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +0 -638
  107. package/.next/standalone/packages/wiki-viewer-mcp/src/register.ts +0 -144
  108. package/.next/standalone/packages/wiki-viewer-mcp/src/state-cache.ts +0 -55
  109. package/.next/standalone/packages/wiki-viewer-mcp/tsconfig.json +0 -19
  110. package/.next/standalone/pnpm-lock.yaml +0 -6697
  111. package/.next/standalone/pnpm-workspace.yaml +0 -8
  112. package/.next/standalone/postcss.config.mjs +0 -2
  113. package/.next/standalone/src/app/api/agent/activity/route.ts +0 -59
  114. package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +0 -40
  115. package/.next/standalone/src/app/api/agent/admin/agents/route.ts +0 -33
  116. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +0 -83
  117. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +0 -36
  118. package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +0 -27
  119. package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +0 -148
  120. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +0 -219
  121. package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +0 -446
  122. package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +0 -152
  123. package/.next/standalone/src/app/api/agent/fs/move/route.ts +0 -130
  124. package/.next/standalone/src/app/api/agent/fs/search/route.ts +0 -272
  125. package/.next/standalone/src/app/api/agent/internal/span/route.ts +0 -121
  126. package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +0 -50
  127. package/.next/standalone/src/app/api/agent/register/route.ts +0 -118
  128. package/.next/standalone/src/app/api/agent/settings/route.ts +0 -34
  129. package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +0 -20
  130. package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +0 -53
  131. package/.next/standalone/src/app/api/agents/install/route.ts +0 -143
  132. package/.next/standalone/src/app/api/agents/skill/route.ts +0 -20
  133. package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +0 -87
  134. package/.next/standalone/src/app/api/app-proxy/[...path]/route.ts +0 -247
  135. package/.next/standalone/src/app/api/assets/[...path]/route.ts +0 -75
  136. package/.next/standalone/src/app/api/auth/[...all]/route.ts +0 -7
  137. package/.next/standalone/src/app/api/owner/init/route.ts +0 -14
  138. package/.next/standalone/src/app/api/share/[token]/route.ts +0 -269
  139. package/.next/standalone/src/app/api/share/route.ts +0 -127
  140. package/.next/standalone/src/app/api/system/admins/route.ts +0 -103
  141. package/.next/standalone/src/app/api/system/auth-config/route.ts +0 -22
  142. package/.next/standalone/src/app/api/system/auth-settings/route.ts +0 -86
  143. package/.next/standalone/src/app/api/system/browse/route.ts +0 -82
  144. package/.next/standalone/src/app/api/system/clear-root/route.ts +0 -16
  145. package/.next/standalone/src/app/api/system/config/route.ts +0 -14
  146. package/.next/standalone/src/app/api/system/pins/route.ts +0 -38
  147. package/.next/standalone/src/app/api/system/reveal/route.ts +0 -36
  148. package/.next/standalone/src/app/api/system/root-status/route.ts +0 -13
  149. package/.next/standalone/src/app/api/system/set-root/route.ts +0 -53
  150. package/.next/standalone/src/app/api/system/users/route.ts +0 -120
  151. package/.next/standalone/src/app/api/system/workspaces/[id]/open/route.ts +0 -34
  152. package/.next/standalone/src/app/api/system/workspaces/[id]/refresh/route.ts +0 -32
  153. package/.next/standalone/src/app/api/system/workspaces/[id]/route.ts +0 -78
  154. package/.next/standalone/src/app/api/system/workspaces/route.ts +0 -128
  155. package/.next/standalone/src/app/api/upload/[...path]/route.ts +0 -85
  156. package/.next/standalone/src/app/api/wiki/app/route.ts +0 -60
  157. package/.next/standalone/src/app/api/wiki/backlinks/route.ts +0 -29
  158. package/.next/standalone/src/app/api/wiki/content/route.ts +0 -159
  159. package/.next/standalone/src/app/api/wiki/download/route.ts +0 -100
  160. package/.next/standalone/src/app/api/wiki/file/route.ts +0 -57
  161. package/.next/standalone/src/app/api/wiki/folder/route.ts +0 -34
  162. package/.next/standalone/src/app/api/wiki/git-branches/route.ts +0 -32
  163. package/.next/standalone/src/app/api/wiki/git-checkout/route.ts +0 -45
  164. package/.next/standalone/src/app/api/wiki/git-diff/route.ts +0 -33
  165. package/.next/standalone/src/app/api/wiki/git-file-info/route.ts +0 -27
  166. package/.next/standalone/src/app/api/wiki/git-history/route.ts +0 -27
  167. package/.next/standalone/src/app/api/wiki/git-pull/route.ts +0 -48
  168. package/.next/standalone/src/app/api/wiki/move/route.ts +0 -61
  169. package/.next/standalone/src/app/api/wiki/new-file/route.ts +0 -56
  170. package/.next/standalone/src/app/api/wiki/outlinks/route.ts +0 -76
  171. package/.next/standalone/src/app/api/wiki/page/route.ts +0 -92
  172. package/.next/standalone/src/app/api/wiki/presence/route.ts +0 -56
  173. package/.next/standalone/src/app/api/wiki/route.ts +0 -116
  174. package/.next/standalone/src/app/api/wiki/search/route.ts +0 -44
  175. package/.next/standalone/src/app/api/wiki/slugs/route.ts +0 -54
  176. package/.next/standalone/src/app/api/wiki/upload/route.ts +0 -95
  177. package/.next/standalone/src/app/api/wiki/watch/route.ts +0 -67
  178. package/.next/standalone/src/app/globals.css +0 -1249
  179. package/.next/standalone/src/app/icon.svg +0 -11
  180. package/.next/standalone/src/app/layout.tsx +0 -78
  181. package/.next/standalone/src/app/manifest.ts +0 -30
  182. package/.next/standalone/src/app/page.tsx +0 -3260
  183. package/.next/standalone/src/app/s/[token]/page.tsx +0 -357
  184. package/.next/standalone/src/app/signin/page.tsx +0 -16
  185. package/.next/standalone/src/app/signin/signin-form.tsx +0 -238
  186. package/.next/standalone/src/components/ai-panel/activity-row.tsx +0 -284
  187. package/.next/standalone/src/components/ai-panel/ai-panel.tsx +0 -311
  188. package/.next/standalone/src/components/ai-panel/token-section.tsx +0 -313
  189. package/.next/standalone/src/components/auth-settings-sheet.tsx +0 -474
  190. package/.next/standalone/src/components/confirm-dialog.tsx +0 -56
  191. package/.next/standalone/src/components/dir-picker.tsx +0 -606
  192. package/.next/standalone/src/components/editor/backlinks-panel.tsx +0 -139
  193. package/.next/standalone/src/components/editor/bubble-menu.tsx +0 -346
  194. package/.next/standalone/src/components/editor/callout-extension.ts +0 -36
  195. package/.next/standalone/src/components/editor/comment-pip.tsx +0 -56
  196. package/.next/standalone/src/components/editor/comment-thread.tsx +0 -254
  197. package/.next/standalone/src/components/editor/csv-viewer.tsx +0 -379
  198. package/.next/standalone/src/components/editor/document-outline.tsx +0 -230
  199. package/.next/standalone/src/components/editor/editor-toolbar.tsx +0 -564
  200. package/.next/standalone/src/components/editor/editor.tsx +0 -1327
  201. package/.next/standalone/src/components/editor/extensions/drag-handle.ts +0 -325
  202. package/.next/standalone/src/components/editor/extensions/heading-anchors.ts +0 -55
  203. package/.next/standalone/src/components/editor/extensions/proof-span.ts +0 -60
  204. package/.next/standalone/src/components/editor/extensions/resizable-image.tsx +0 -159
  205. package/.next/standalone/src/components/editor/extensions.ts +0 -150
  206. package/.next/standalone/src/components/editor/file-fallback-viewer.tsx +0 -75
  207. package/.next/standalone/src/components/editor/folder-index.tsx +0 -236
  208. package/.next/standalone/src/components/editor/google-doc-viewer.tsx +0 -73
  209. package/.next/standalone/src/components/editor/image-viewer.tsx +0 -57
  210. package/.next/standalone/src/components/editor/large-file-gate.tsx +0 -108
  211. package/.next/standalone/src/components/editor/link-popover.tsx +0 -111
  212. package/.next/standalone/src/components/editor/media-popover.tsx +0 -212
  213. package/.next/standalone/src/components/editor/media-viewer.tsx +0 -70
  214. package/.next/standalone/src/components/editor/mermaid-viewer.tsx +0 -286
  215. package/.next/standalone/src/components/editor/node-app-viewer.tsx +0 -328
  216. package/.next/standalone/src/components/editor/notebook-viewer.tsx +0 -371
  217. package/.next/standalone/src/components/editor/office/docx-viewer.tsx +0 -93
  218. package/.next/standalone/src/components/editor/office/office-chrome.tsx +0 -83
  219. package/.next/standalone/src/components/editor/office/pptx-viewer.tsx +0 -104
  220. package/.next/standalone/src/components/editor/office/xlsx-viewer.tsx +0 -192
  221. package/.next/standalone/src/components/editor/pdf-viewer.tsx +0 -32
  222. package/.next/standalone/src/components/editor/proof-span-popover.tsx +0 -162
  223. package/.next/standalone/src/components/editor/slash-commands.tsx +0 -629
  224. package/.next/standalone/src/components/editor/source-viewer.tsx +0 -289
  225. package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +0 -229
  226. package/.next/standalone/src/components/editor/suggestion-card.tsx +0 -206
  227. package/.next/standalone/src/components/editor/table-menu.tsx +0 -232
  228. package/.next/standalone/src/components/editor/view-mode-comment-button.tsx +0 -77
  229. package/.next/standalone/src/components/editor/website-viewer.tsx +0 -75
  230. package/.next/standalone/src/components/editor/wiki-link-create-dialog.tsx +0 -189
  231. package/.next/standalone/src/components/editor/wiki-link-decorator.ts +0 -79
  232. package/.next/standalone/src/components/editor/wiki-link-extension.ts +0 -120
  233. package/.next/standalone/src/components/editor/wiki-link-picker.tsx +0 -250
  234. package/.next/standalone/src/components/layout/viewer-toolbar.tsx +0 -54
  235. package/.next/standalone/src/components/search/search-command-dialog.tsx +0 -246
  236. package/.next/standalone/src/components/search/sidebar-search-box.tsx +0 -121
  237. package/.next/standalone/src/components/search/snippet-text.tsx +0 -48
  238. package/.next/standalone/src/components/share-dialog.tsx +0 -307
  239. package/.next/standalone/src/components/theme-provider.tsx +0 -16
  240. package/.next/standalone/src/components/theme-toggle.tsx +0 -24
  241. package/.next/standalone/src/components/ui/badge.tsx +0 -35
  242. package/.next/standalone/src/components/ui/button.tsx +0 -56
  243. package/.next/standalone/src/components/ui/card.tsx +0 -63
  244. package/.next/standalone/src/components/ui/collapsible.tsx +0 -9
  245. package/.next/standalone/src/components/ui/command.tsx +0 -151
  246. package/.next/standalone/src/components/ui/context-menu.tsx +0 -191
  247. package/.next/standalone/src/components/ui/dialog.tsx +0 -119
  248. package/.next/standalone/src/components/ui/dropdown-menu.tsx +0 -194
  249. package/.next/standalone/src/components/ui/input.tsx +0 -22
  250. package/.next/standalone/src/components/ui/label.tsx +0 -25
  251. package/.next/standalone/src/components/ui/popover.tsx +0 -33
  252. package/.next/standalone/src/components/ui/scroll-area.tsx +0 -47
  253. package/.next/standalone/src/components/ui/select.tsx +0 -156
  254. package/.next/standalone/src/components/ui/separator.tsx +0 -30
  255. package/.next/standalone/src/components/ui/sheet.tsx +0 -140
  256. package/.next/standalone/src/components/ui/skeleton.tsx +0 -15
  257. package/.next/standalone/src/components/ui/switch.tsx +0 -28
  258. package/.next/standalone/src/components/ui/tabs.tsx +0 -54
  259. package/.next/standalone/src/components/ui/textarea.tsx +0 -21
  260. package/.next/standalone/src/components/ui/tip.tsx +0 -23
  261. package/.next/standalone/src/components/ui/toggle-group.tsx +0 -41
  262. package/.next/standalone/src/components/ui/tooltip.tsx +0 -27
  263. package/.next/standalone/src/components/view-width-toggle.tsx +0 -70
  264. package/.next/standalone/src/components/wiki/frontmatter-header.tsx +0 -164
  265. package/.next/standalone/src/hooks/use-is-mobile.ts +0 -14
  266. package/.next/standalone/src/lib/app-runner.ts +0 -272
  267. package/.next/standalone/src/lib/auth/admin.ts +0 -120
  268. package/.next/standalone/src/lib/auth/allowlist.ts +0 -50
  269. package/.next/standalone/src/lib/auth/client.ts +0 -4
  270. package/.next/standalone/src/lib/auth/csrf.ts +0 -68
  271. package/.next/standalone/src/lib/auth/server.ts +0 -215
  272. package/.next/standalone/src/lib/cabinets/tree.ts +0 -75
  273. package/.next/standalone/src/lib/config.ts +0 -114
  274. package/.next/standalone/src/lib/embeds/detect.ts +0 -142
  275. package/.next/standalone/src/lib/git-secrets.ts +0 -65
  276. package/.next/standalone/src/lib/git.ts +0 -503
  277. package/.next/standalone/src/lib/google/detect.ts +0 -107
  278. package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +0 -104
  279. package/.next/standalone/src/lib/markdown/remark-wikilinks.ts +0 -100
  280. package/.next/standalone/src/lib/markdown/sanitize-schema.ts +0 -106
  281. package/.next/standalone/src/lib/markdown/to-html.ts +0 -282
  282. package/.next/standalone/src/lib/markdown/to-markdown.ts +0 -252
  283. package/.next/standalone/src/lib/proof/activity-shared.ts +0 -31
  284. package/.next/standalone/src/lib/proof/activity.ts +0 -74
  285. package/.next/standalone/src/lib/proof/audit.ts +0 -88
  286. package/.next/standalone/src/lib/proof/auth.ts +0 -143
  287. package/.next/standalone/src/lib/proof/block-refs.ts +0 -133
  288. package/.next/standalone/src/lib/proof/blocks.ts +0 -73
  289. package/.next/standalone/src/lib/proof/client-auth.ts +0 -23
  290. package/.next/standalone/src/lib/proof/collab-state.ts +0 -81
  291. package/.next/standalone/src/lib/proof/event-bus.ts +0 -47
  292. package/.next/standalone/src/lib/proof/file-lock.ts +0 -43
  293. package/.next/standalone/src/lib/proof/glob.ts +0 -51
  294. package/.next/standalone/src/lib/proof/idempotency.ts +0 -37
  295. package/.next/standalone/src/lib/proof/lease.ts +0 -117
  296. package/.next/standalone/src/lib/proof/mutex.ts +0 -36
  297. package/.next/standalone/src/lib/proof/ops-applier.ts +0 -727
  298. package/.next/standalone/src/lib/proof/owner-auth.ts +0 -2
  299. package/.next/standalone/src/lib/proof/pending.ts +0 -97
  300. package/.next/standalone/src/lib/proof/proof-span.ts +0 -141
  301. package/.next/standalone/src/lib/proof/rate-limit.ts +0 -53
  302. package/.next/standalone/src/lib/proof/raw-fs.ts +0 -197
  303. package/.next/standalone/src/lib/proof/register-rate-limit.ts +0 -53
  304. package/.next/standalone/src/lib/proof/registry.ts +0 -195
  305. package/.next/standalone/src/lib/proof/sidecar.ts +0 -103
  306. package/.next/standalone/src/lib/proof/suggest-capture.ts +0 -69
  307. package/.next/standalone/src/lib/proof/types.ts +0 -182
  308. package/.next/standalone/src/lib/proof-config.ts +0 -14
  309. package/.next/standalone/src/lib/root-dir.ts +0 -35
  310. package/.next/standalone/src/lib/search/indexable-exts.ts +0 -25
  311. package/.next/standalone/src/lib/search/indexer.ts +0 -634
  312. package/.next/standalone/src/lib/search/sanitize.ts +0 -35
  313. package/.next/standalone/src/lib/search/search-db.ts +0 -57
  314. package/.next/standalone/src/lib/search/watcher-pool.ts +0 -79
  315. package/.next/standalone/src/lib/shared-docs/db.ts +0 -177
  316. package/.next/standalone/src/lib/toast.ts +0 -9
  317. package/.next/standalone/src/lib/utils.ts +0 -6
  318. package/.next/standalone/src/lib/wiki-helpers.ts +0 -36
  319. package/.next/standalone/src/lib/workspace-client.ts +0 -82
  320. package/.next/standalone/src/lib/workspace-context.ts +0 -173
  321. package/.next/standalone/src/lib/workspaces.ts +0 -400
  322. package/.next/standalone/src/middleware.ts +0 -44
  323. package/.next/standalone/src/stores/ai-panel-store.ts +0 -79
  324. package/.next/standalone/src/stores/editor-store.ts +0 -439
  325. package/.next/standalone/src/stores/pin-store.ts +0 -56
  326. package/.next/standalone/src/stores/proof-store.ts +0 -124
  327. package/.next/standalone/src/stores/recent-store.ts +0 -51
  328. package/.next/standalone/src/stores/search-store.ts +0 -104
  329. package/.next/standalone/src/stores/show-hidden-store.ts +0 -26
  330. package/.next/standalone/src/stores/sidebar-width-store.ts +0 -39
  331. package/.next/standalone/src/stores/skin-store.ts +0 -42
  332. package/.next/standalone/src/stores/tree-store.ts +0 -52
  333. package/.next/standalone/src/stores/view-width-store.ts +0 -101
  334. package/.next/standalone/src/stores/wiki-slugs-store.ts +0 -75
  335. package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +0 -146
  336. package/.next/standalone/src/tests/proof/admin-create-user.test.ts +0 -43
  337. package/.next/standalone/src/tests/proof/agent-fs.test.ts +0 -973
  338. package/.next/standalone/src/tests/proof/agent-ownership.test.ts +0 -140
  339. package/.next/standalone/src/tests/proof/agent-workspace-scope.test.ts +0 -134
  340. package/.next/standalone/src/tests/proof/agents-install.test.ts +0 -110
  341. package/.next/standalone/src/tests/proof/bench-write.ts +0 -179
  342. package/.next/standalone/src/tests/proof/better-auth.test.ts +0 -68
  343. package/.next/standalone/src/tests/proof/block-refs.test.ts +0 -108
  344. package/.next/standalone/src/tests/proof/blocks.test.ts +0 -92
  345. package/.next/standalone/src/tests/proof/collab-state.test.ts +0 -501
  346. package/.next/standalone/src/tests/proof/comments-ops.test.ts +0 -177
  347. package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +0 -85
  348. package/.next/standalone/src/tests/proof/external-edit.test.ts +0 -58
  349. package/.next/standalone/src/tests/proof/file-lock.test.ts +0 -80
  350. package/.next/standalone/src/tests/proof/git-branches-checkout.test.ts +0 -136
  351. package/.next/standalone/src/tests/proof/git-read-features.test.ts +0 -473
  352. package/.next/standalone/src/tests/proof/git-secrets.test.ts +0 -138
  353. package/.next/standalone/src/tests/proof/git-sparse.test.ts +0 -257
  354. package/.next/standalone/src/tests/proof/git-workspace.test.ts +0 -135
  355. package/.next/standalone/src/tests/proof/git.test.ts +0 -243
  356. package/.next/standalone/src/tests/proof/glob.test.ts +0 -82
  357. package/.next/standalone/src/tests/proof/helpers/auth-session.ts +0 -27
  358. package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +0 -46
  359. package/.next/standalone/src/tests/proof/ops-applier.test.ts +0 -368
  360. package/.next/standalone/src/tests/proof/owner-init.test.ts +0 -2
  361. package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +0 -60
  362. package/.next/standalone/src/tests/proof/preload.ts +0 -21
  363. package/.next/standalone/src/tests/proof/proof-span.test.ts +0 -98
  364. package/.next/standalone/src/tests/proof/rate-limit.test.ts +0 -54
  365. package/.next/standalone/src/tests/proof/readonly-enforcement.test.ts +0 -161
  366. package/.next/standalone/src/tests/proof/reconcile-sidecar.test.ts +0 -195
  367. package/.next/standalone/src/tests/proof/registration-flow.test.ts +0 -330
  368. package/.next/standalone/src/tests/proof/registry.test.ts +0 -169
  369. package/.next/standalone/src/tests/proof/routes.test.ts +0 -516
  370. package/.next/standalone/src/tests/proof/search-index.test.ts +0 -376
  371. package/.next/standalone/src/tests/proof/sidecar-lifecycle.test.ts +0 -177
  372. package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +0 -58
  373. package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +0 -280
  374. package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +0 -140
  375. package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +0 -208
  376. package/.next/standalone/src/tests/proof/workspace-isolation.test.ts +0 -87
  377. package/.next/standalone/src/tests/proof/workspaces-api.test.ts +0 -227
  378. package/.next/standalone/src/types/index.ts +0 -47
  379. package/.next/standalone/tailwind.config.ts +0 -191
  380. package/.next/standalone/tsconfig.json +0 -41
  381. package/.next/standalone/tsconfig.tsbuildinfo +0 -1
  382. /package/.next/standalone/.next/static/{WMOTgfV82apmdovxzgbMx → rRyvFORvJoc5OWbCR2A_P}/_buildManifest.js +0 -0
  383. /package/.next/standalone/.next/static/{WMOTgfV82apmdovxzgbMx → rRyvFORvJoc5OWbCR2A_P}/_clientMiddlewareManifest.js +0 -0
  384. /package/.next/standalone/.next/static/{WMOTgfV82apmdovxzgbMx → rRyvFORvJoc5OWbCR2A_P}/_ssgManifest.js +0 -0
@@ -1,1327 +0,0 @@
1
- "use client";
2
-
3
- import { cellAround, isInTable } from "@tiptap/pm/tables";
4
- import { EditorContent, useEditor } from "@tiptap/react";
5
- import { AlertCircle, Check, Code2, FilePlus, Loader2, Sparkles } from "lucide-react";
6
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
- import { findNodeByPath } from "@/lib/cabinets/tree";
8
- import { markdownToHtml } from "@/lib/markdown/to-html";
9
- import { htmlToMarkdown } from "@/lib/markdown/to-markdown";
10
- import { parseFrontmatter } from "@/lib/markdown/parse-frontmatter";
11
- import { useAIPanelStore } from "@/stores/ai-panel-store";
12
- import { useEditorStore } from "@/stores/editor-store";
13
- import { useTreeStore } from "@/stores/tree-store";
14
- import {
15
- useViewWidthStore,
16
- VIEW_WIDTH_CSS,
17
- VIEW_ALIGN_ML,
18
- } from "@/stores/view-width-store";
19
- import { useWikiSlugsStore } from "@/stores/wiki-slugs-store";
20
- import type { TreeNode } from "@/types";
21
- import { useProofStore } from "@/stores/proof-store";
22
- import { captureSuggestion } from "@/lib/proof/suggest-capture";
23
- import { wsFetch, withWs } from "@/lib/workspace-client";
24
- import { showError } from "@/lib/toast";
25
- import { EditorBubbleMenu } from "./bubble-menu";
26
- import { EditorToolbar } from "./editor-toolbar";
27
- import { editorExtensions } from "./extensions";
28
- import { FolderIndex } from "./folder-index";
29
- import { CommentPip } from "./comment-pip";
30
- import { CommentThread } from "./comment-thread";
31
- import { ProofSpanPopover } from "./proof-span-popover";
32
- import { SuggestionCard } from "./suggestion-card";
33
- import { SuggestEditPopover } from "./suggest-edit-popover";
34
- import { SlashCommands } from "./slash-commands";
35
- import { DocumentOutline } from "./document-outline";
36
- import { BacklinksPanel } from "./backlinks-panel";
37
- import { TableMenu } from "./table-menu";
38
- import {
39
- useWikiLinkCreate,
40
- type WikiCreateResult,
41
- } from "./wiki-link-create-dialog";
42
- import { WikiLinkPicker } from "./wiki-link-picker";
43
- import { FrontmatterHeader } from "@/components/wiki/frontmatter-header";
44
- import { ViewModeCommentButton } from "./view-mode-comment-button";
45
-
46
- async function uploadFile(
47
- pagePath: string,
48
- file: File,
49
- ): Promise<string | null> {
50
- const formData = new FormData();
51
- formData.append("file", file);
52
- try {
53
- const res = await wsFetch(`/api/upload/${pagePath}`, {
54
- method: "POST",
55
- body: formData,
56
- });
57
- if (!res.ok) {
58
- showError(`Upload failed: ${file.name}`);
59
- return null;
60
- }
61
- const data = await res.json();
62
- return data.url;
63
- } catch {
64
- showError(`Upload failed: ${file.name}`);
65
- return null;
66
- }
67
- }
68
-
69
- function flattenTree(nodes: TreeNode[]): { path: string; name: string }[] {
70
- const result: { path: string; name: string }[] = [];
71
- for (const node of nodes) {
72
- result.push({ path: node.path, name: node.name });
73
- if (node.children) result.push(...flattenTree(node.children));
74
- }
75
- return result;
76
- }
77
-
78
- function findPageBySlug(
79
- slug: string,
80
- currentPath: string | null,
81
- nodes: TreeNode[],
82
- ): string | null {
83
- const allPages = flattenTree(nodes);
84
- // The slug matches the last segment of the path
85
- const matches = allPages.filter(
86
- (p) => p.name === slug || p.path.endsWith(`/${slug}`),
87
- );
88
- if (matches.length === 0) return null;
89
- if (matches.length === 1) return matches[0].path;
90
-
91
- // Prefer sibling pages (same parent directory as current page)
92
- if (currentPath) {
93
- const parentDir = currentPath.includes("/")
94
- ? currentPath.substring(0, currentPath.lastIndexOf("/"))
95
- : "";
96
- const sibling = matches.find(
97
- (m) => m.path === (parentDir ? `${parentDir}/${slug}` : slug),
98
- );
99
- if (sibling) return sibling.path;
100
- }
101
- return matches[0].path;
102
- }
103
-
104
- function navigateToPage(
105
- targetPath: string,
106
- selectPage: (path: string) => void,
107
- expandPath: (path: string) => void,
108
- ) {
109
- const parts = targetPath.split("/");
110
- for (let i = 1; i < parts.length; i++) {
111
- expandPath(parts.slice(0, i).join("/"));
112
- }
113
- selectPage(targetPath);
114
- useEditorStore.getState().loadPage(targetPath);
115
- // Scroll editor container to top
116
- setTimeout(() => {
117
- document.querySelector("[data-editor-scroll]")?.scrollTo(0, 0);
118
- }, 0);
119
- }
120
-
121
- function resolveInternalLink(
122
- href: string,
123
- currentPath: string | null,
124
- nodes: TreeNode[],
125
- ): string | null {
126
- const allPages = flattenTree(nodes);
127
-
128
- // Clean up the href: strip .md extension, leading ./ or /
129
- const linkPath = href
130
- .replace(/\.md$/, "")
131
- .replace(/^\.\//, "")
132
- .replace(/^\//, "");
133
-
134
- // 1. Try as absolute path (exact match in tree)
135
- const exactMatch = allPages.find((p) => p.path === linkPath);
136
- if (exactMatch) return exactMatch.path;
137
-
138
- // 2. Try relative to current page's directory
139
- if (currentPath) {
140
- const parentDir = currentPath.includes("/")
141
- ? currentPath.substring(0, currentPath.lastIndexOf("/"))
142
- : "";
143
- const relativePath = parentDir ? `${parentDir}/${linkPath}` : linkPath;
144
- const relMatch = allPages.find((p) => p.path === relativePath);
145
- if (relMatch) return relMatch.path;
146
- }
147
-
148
- // 3. Try matching by last segment (slug-style lookup)
149
- const slug = linkPath.includes("/") ? linkPath.split("/").pop()! : linkPath;
150
- return findPageBySlug(slug, currentPath, nodes);
151
- }
152
-
153
- type KBEditorMode = "viewing" | "editing" | "suggesting";
154
-
155
- interface KBEditorProps {
156
- mode?: KBEditorMode;
157
- }
158
-
159
- export function KBEditor({ mode }: KBEditorProps = {}) {
160
- const {
161
- currentPath,
162
- content,
163
- saveStatus,
164
- frontmatter,
165
- isLoading,
166
- loadStatus,
167
- createMissingPage,
168
- editMode,
169
- setEditMode,
170
- } = useEditorStore();
171
- const effectiveMode = mode ?? editMode;
172
- const isViewing = effectiveMode === "viewing";
173
- const isSuggesting = effectiveMode === "suggesting";
174
- const parsedViewingContent = useMemo(
175
- () => (isViewing ? parseFrontmatter(content) : { data: {}, body: content }),
176
- [content, isViewing],
177
- );
178
- const nodes = useTreeStore((s) => s.nodes);
179
- const editorMaxW = useViewWidthStore((s) => VIEW_WIDTH_CSS[s.width]);
180
- const editorMl = useViewWidthStore((s) => VIEW_ALIGN_ML[s.align]);
181
- const isRtl = isViewing
182
- ? parsedViewingContent.data.dir === "rtl"
183
- : frontmatter?.dir === "rtl";
184
- const { open: openAI, clearMessages } = useAIPanelStore();
185
- const { open: openWikiCreate, Dialog: WikiCreateDialog } =
186
- useWikiLinkCreate();
187
- // Keep a stable ref so the click handler closure can call the latest version
188
- // without being re-created on every render.
189
- const openWikiCreateRef =
190
- useRef<(slug: string) => Promise<WikiCreateResult>>(openWikiCreate);
191
- openWikiCreateRef.current = openWikiCreate;
192
-
193
- const isLoadingRef = useRef(false);
194
- const isViewingRef = useRef(isViewing);
195
- isViewingRef.current = isViewing;
196
- const [sourceMode, setSourceMode] = useState(false);
197
- const [sourceText, setSourceText] = useState("");
198
- // Reset the tab to "page" whenever the path changes — opening a new folder
199
- // shouldn't skip its index.md if the previous folder was on Files. Has to
200
- // be an effect (not state-during-render) because Tiptap's EditorContent
201
- // calls flushSync internally; setState during the parent render explodes
202
- // when EditorContent renders in the same pass.
203
- const [folderTab, setFolderTab] = useState<"page" | "files">("page");
204
- useEffect(() => {
205
- setFolderTab("page");
206
- }, []);
207
-
208
- // Prime the slug index once on mount so wiki-link broken-state and
209
- // the autocomplete picker both have data immediately.
210
- useEffect(() => {
211
- void useWikiSlugsStore.getState().load();
212
- }, []);
213
-
214
- // Load sidecar when the current path changes. Debounced: rapid navigation
215
- // otherwise fires a sidecar fetch per pass-through file, flooding the
216
- // connection pool. The cleanup clears the timer, so only the settled path loads.
217
- useEffect(() => {
218
- if (!currentPath) return;
219
- const id = setTimeout(() => {
220
- void useProofStore.getState().loadSidecar(currentPath);
221
- }, 200);
222
- return () => clearTimeout(id);
223
- }, [currentPath]);
224
-
225
- // Human edit-lease heartbeat: tell the server a human has this markdown doc
226
- // open so computeCollabState() reports "active" even before the first
227
- // suggestion/comment. This is what makes raw-fs writes from agents defer to
228
- // the collaborative (Tier-2) path while a human is editing. Only markdown
229
- // files participate in the collab-state machine.
230
- useEffect(() => {
231
- if (typeof window === "undefined") return;
232
- if (!currentPath) return;
233
- if (isViewing) return;
234
- if (!/\.(md|markdown)$/i.test(currentPath)) return;
235
-
236
- const path = currentPath;
237
- const ping = (action: "open" | "heartbeat" | "close") => {
238
- // keepalive lets the "close" beacon survive page unload.
239
- void wsFetch("/api/wiki/presence", {
240
- method: "POST",
241
- headers: { "Content-Type": "application/json" },
242
- body: JSON.stringify({ path, action }),
243
- keepalive: action === "close",
244
- }).catch(() => {
245
- /* presence is best-effort; ignore failures */
246
- });
247
- };
248
-
249
- // Debounce the "open" ping: rapid navigation through files otherwise emits
250
- // an open+close pair per pass-through. Only claim the lease once the user
251
- // settles on a file for >200ms; if we never opened, we never close.
252
- let opened = false;
253
- let id: ReturnType<typeof setInterval> | null = null;
254
- const onHidden = () => {
255
- if (opened && document.visibilityState === "hidden") ping("heartbeat");
256
- };
257
- const openTimer = setTimeout(() => {
258
- opened = true;
259
- ping("open");
260
- // Refresh well within the 90s server lease TTL.
261
- id = setInterval(() => ping("heartbeat"), 30_000);
262
- document.addEventListener("visibilitychange", onHidden);
263
- }, 200);
264
-
265
- return () => {
266
- clearTimeout(openTimer);
267
- if (id) clearInterval(id);
268
- document.removeEventListener("visibilitychange", onHidden);
269
- if (opened) ping("close");
270
- };
271
- }, [currentPath, isViewing]);
272
-
273
- // Subscribe to chokidar SSE: when current file changes on disk, reload sidecar.
274
- useEffect(() => {
275
- if (typeof window === "undefined") return;
276
- const es = new EventSource(withWs("/api/wiki/watch"));
277
- es.onmessage = (evt: MessageEvent<string>) => {
278
- try {
279
- const data = JSON.parse(evt.data) as { type: string; path: string };
280
- const activePath = useEditorStore.getState().currentPath;
281
- if (
282
- (data.type === "change" || data.type === "add") &&
283
- activePath &&
284
- data.path === activePath
285
- ) {
286
- // loadSnapshot first so server-side readSnapshot detects
287
- // fingerprint mismatch, emits file.externallyEdited, and persists
288
- // the sidecar. Then loadSidecar to refresh comments/suggestions.
289
- void useProofStore
290
- .getState()
291
- .loadSnapshot(activePath)
292
- .then(() => useProofStore.getState().loadSidecar(activePath));
293
- if (isViewingRef.current) {
294
- void useEditorStore.getState().loadPage(activePath);
295
- }
296
- }
297
- } catch {
298
- // ignore malformed events
299
- }
300
- };
301
- return () => {
302
- es.close();
303
- };
304
- }, []);
305
-
306
- // Proof-span popover state.
307
- const [proofTarget, setProofTarget] = useState<HTMLElement | null>(null);
308
-
309
- /**
310
- * Ref to the editor scroll container. Used to compute block positions
311
- * relative to the scrollable area for suggestion cards and comment pips.
312
- *
313
- * Phase D coordination: comment-pip positioning uses this same ref and the
314
- * same blockRefPositions map computed below.
315
- */
316
- const scrollContainerRef = useRef<HTMLDivElement>(null);
317
-
318
- /** Map of block ref → position relative to scroll container */
319
- const [blockRefPositions, setBlockRefPositions] = useState<
320
- Map<string, { top: number; left: number; width: number; bottom: number }>
321
- >(new Map());
322
-
323
- // Subscribe to snapshot data for suggestion cards.
324
- // NOTE: select the RAW stored references here — returning a freshly built
325
- // array (e.g. `?? []` or `.filter(...)`) on every call makes
326
- // useSyncExternalStore think the snapshot changed each render, which spins
327
- // into a "Maximum update depth exceeded" loop. Derive defaults/filters below.
328
- const snapshotBlocksRaw = useProofStore((s) =>
329
- currentPath ? s.byPath[currentPath]?.snapshotBlocks : undefined
330
- );
331
- const suggestionsRaw = useProofStore((s) =>
332
- currentPath ? s.byPath[currentPath]?.sidecar?.suggestions : undefined
333
- );
334
- const snapshotRevision = useProofStore((s) =>
335
- currentPath ? (s.byPath[currentPath]?.snapshotRevision ?? 0) : 0
336
- );
337
- const commentsRaw = useProofStore((s) =>
338
- currentPath ? s.byPath[currentPath]?.sidecar?.comments : undefined
339
- );
340
-
341
- const snapshotBlocks = useMemo(() => snapshotBlocksRaw ?? [], [snapshotBlocksRaw]);
342
- const comments = useMemo(() => commentsRaw ?? [], [commentsRaw]);
343
- const pendingSuggestions = useMemo(
344
- () => suggestionsRaw?.filter((sg) => sg.status === "pending") ?? [],
345
- [suggestionsRaw],
346
- );
347
-
348
- /** Group comments by block ref for pip rendering. */
349
- const commentsByRef = useMemo(() => {
350
- const map: Record<string, typeof comments> = {};
351
- for (const c of comments) {
352
- (map[c.ref] ??= []).push(c);
353
- }
354
- return map;
355
- }, [comments]);
356
-
357
- /** Tracks which block's comment thread is open and its anchor element. */
358
- const [threadTarget, setThreadTarget] = useState<{ blockRef: string; el: HTMLElement } | null>(null);
359
-
360
- /** Tracks the open human "suggest edit" popover (block + anchor + content). */
361
- const [suggestTarget, setSuggestTarget] = useState<
362
- { blockRef: string; markdown: string; anchor: { top: number; left: number } } | null
363
- >(null);
364
-
365
- /**
366
- * Resolve the current editor selection to a top-level block.
367
- *
368
- * Primary strategy: map the selection to its top-level ProseMirror child
369
- * INDEX, then look up snapshotBlocks[index] — the same index-based mapping
370
- * used by the position-tracker effect. This is robust even when the DOM
371
- * `data-block-ref` annotation has not been applied yet (e.g. snapshot still
372
- * loading), which previously made the suggest/comment buttons silently
373
- * no-op. Falls back to walking the DOM for an existing [data-block-ref].
374
- */
375
- const resolveSelectionBlock = useCallback((): {
376
- blockRef: string;
377
- blockEl: HTMLElement;
378
- markdown: string;
379
- } | null => {
380
- if (!editorRef.current) return null;
381
- const view = editorRef.current.view;
382
- const { from } = view.state.selection;
383
- const path = useEditorStore.getState().currentPath ?? "";
384
- const blocks = useProofStore.getState().byPath[path]?.snapshotBlocks ?? [];
385
-
386
- // Find the top-level child index containing the selection head.
387
- const $pos = view.state.doc.resolve(from);
388
- const topIndex = $pos.depth > 0 ? $pos.index(0) : 0;
389
-
390
- const proseMirror = scrollContainerRef.current?.querySelector(".ProseMirror");
391
- const children = proseMirror
392
- ? (Array.from(proseMirror.children) as HTMLElement[])
393
- : [];
394
- const blockEl = children[topIndex] ?? null;
395
-
396
- // Prefer the index-aligned snapshot block; fall back to the DOM attr.
397
- const block = blocks[topIndex];
398
- let blockRef: string | null =
399
- block?.ref ?? blockEl?.getAttribute("data-block-ref") ?? null;
400
- const markdown = block?.markdown ?? "";
401
-
402
- if (!blockRef && blockEl) {
403
- // Last-resort: DOM walk from selection anchor.
404
- const domAt = view.domAtPos(from);
405
- const node: HTMLElement | null =
406
- domAt.node.nodeType === Node.ELEMENT_NODE
407
- ? (domAt.node as HTMLElement)
408
- : domAt.node.parentElement;
409
- const found = node?.closest<HTMLElement>("[data-block-ref]") ?? null;
410
- blockRef = found?.getAttribute("data-block-ref") ?? null;
411
- }
412
-
413
- if (!blockRef || !blockEl) return null;
414
- return { blockRef, blockEl, markdown };
415
- }, []);
416
-
417
- const openSuggestForSelection = useCallback(() => {
418
- const resolved = resolveSelectionBlock();
419
- if (!resolved) return;
420
- const rect = resolved.blockEl.getBoundingClientRect();
421
- setSuggestTarget({
422
- blockRef: resolved.blockRef,
423
- markdown: resolved.markdown,
424
- anchor: { top: rect.bottom + 4, left: rect.left },
425
- });
426
- }, [resolveSelectionBlock]);
427
-
428
- const openCommentForSelection = useCallback(() => {
429
- const resolved = resolveSelectionBlock();
430
- if (!resolved) return;
431
- setThreadTarget({ blockRef: resolved.blockRef, el: resolved.blockEl });
432
- }, [resolveSelectionBlock]);
433
-
434
- // ── Suggesting mode: capture human block edits as suggestions ──────────────
435
- //
436
- // In suggesting mode the editor stays editable but edits never touch the
437
- // file. On flush (leaving a block or blurring the editor) we diff each
438
- // top-level block against the snapshot, emit a human `suggestion.add` for
439
- // every changed/added/removed block, then revert the editor to the snapshot
440
- // so the pending suggestion cards render over the original content.
441
-
442
- /** Set true whenever the user edits while in suggesting mode. */
443
- const suggestDirtyRef = useRef(false);
444
- /** Guards against re-entrant flushes (capture is async). */
445
- const flushingRef = useRef(false);
446
- /** Top-level block index that currently holds the selection. */
447
- const activeBlockIndexRef = useRef<number | null>(null);
448
-
449
- const normalizeMd = (s: string): string => s.replace(/\s+$/g, "").trimStart();
450
-
451
- const flushSuggestions = useCallback(async () => {
452
- if (flushingRef.current) return;
453
- if (isViewingRef.current) return;
454
- if (useEditorStore.getState().editMode !== "suggesting") return;
455
- if (!suggestDirtyRef.current) return;
456
- const ed = editorRef.current;
457
- const path = useEditorStore.getState().currentPath;
458
- if (!ed || !path) return;
459
-
460
- const proseMirror = scrollContainerRef.current?.querySelector(".ProseMirror");
461
- if (!proseMirror) return;
462
- const children = Array.from(proseMirror.children) as HTMLElement[];
463
- const snapBlocks =
464
- useProofStore.getState().byPath[path]?.snapshotBlocks ?? [];
465
- if (snapBlocks.length === 0) return;
466
-
467
- flushingRef.current = true;
468
- suggestDirtyRef.current = false;
469
- try {
470
- const getRevision = () =>
471
- useProofStore.getState().byPath[path]?.snapshotRevision ?? 0;
472
- const refresh = async () => {
473
- await useProofStore.getState().loadSnapshot(path);
474
- await useProofStore.getState().loadSidecar(path);
475
- };
476
-
477
- const count = Math.max(children.length, snapBlocks.length);
478
- let captured = false;
479
- for (let i = 0; i < count; i++) {
480
- const el = children[i];
481
- const snap = snapBlocks[i];
482
- const curMd = el ? htmlToMarkdown(el.outerHTML).trim() : null;
483
-
484
- if (snap && curMd !== null) {
485
- if (normalizeMd(curMd) !== normalizeMd(snap.markdown)) {
486
- const ok = await captureSuggestion({
487
- path,
488
- ref: snap.ref,
489
- kind: "replace",
490
- markdown: curMd,
491
- getRevision,
492
- refresh,
493
- });
494
- captured = captured || ok;
495
- }
496
- } else if (snap && curMd === null) {
497
- const ok = await captureSuggestion({
498
- path,
499
- ref: snap.ref,
500
- kind: "delete",
501
- getRevision,
502
- refresh,
503
- });
504
- captured = captured || ok;
505
- } else if (!snap && curMd !== null && curMd.length > 0) {
506
- // New trailing block: suggest inserting after the last known block.
507
- const lastRef = snapBlocks[snapBlocks.length - 1]?.ref;
508
- if (lastRef) {
509
- const ok = await captureSuggestion({
510
- path,
511
- ref: lastRef,
512
- kind: "insertAfter",
513
- markdown: curMd,
514
- getRevision,
515
- refresh,
516
- });
517
- captured = captured || ok;
518
- }
519
- }
520
- }
521
-
522
- if (captured) {
523
- // Reload sidecar so the new pending suggestion cards appear, then
524
- // revert the editor to the snapshot (file unchanged).
525
- await refresh();
526
- const freshSnap =
527
- useProofStore.getState().byPath[path]?.snapshotBlocks ?? snapBlocks;
528
- const snapshotMarkdown = freshSnap.map((b) => b.markdown).join("\n\n");
529
- isLoadingRef.current = true;
530
- const html = await markdownToHtml(snapshotMarkdown, path);
531
- ed.commands.setContent(html);
532
- setTimeout(() => {
533
- isLoadingRef.current = false;
534
- }, 50);
535
- }
536
- } finally {
537
- flushingRef.current = false;
538
- }
539
- }, []);
540
-
541
- // Load snapshot (ordered block list) when path changes so suggestion cards
542
- // can look up block content by ref.
543
- useEffect(() => {
544
- if (!currentPath) return;
545
- const id = setTimeout(() => {
546
- void useProofStore.getState().loadSnapshot(currentPath);
547
- }, 200);
548
- return () => clearTimeout(id);
549
- }, [currentPath]);
550
-
551
- /**
552
- * After content renders, walk `.ProseMirror > *` to build ref→position map.
553
- * Matches by index: the i-th ProseMirror child = snapshotBlocks[i].
554
- *
555
- * Phase D coordination: this effect also annotates each child element with
556
- * data-block-ref for any consumer that needs CSS/query-based lookup.
557
- */
558
- useEffect(() => {
559
- if (!currentPath || snapshotBlocks.length === 0 || !scrollContainerRef.current) return;
560
- const container = scrollContainerRef.current;
561
- const proseMirror = container.querySelector(".ProseMirror");
562
- if (!proseMirror) return;
563
- const children = Array.from(proseMirror.children) as HTMLElement[];
564
- const containerRect = container.getBoundingClientRect();
565
- const next = new Map<string, { top: number; left: number; width: number; bottom: number }>();
566
- for (let i = 0; i < Math.min(children.length, snapshotBlocks.length); i++) {
567
- const el = children[i];
568
- const block = snapshotBlocks[i];
569
- // Annotate DOM element — Phase D comment-pip and other consumers read this
570
- el.setAttribute("data-block-ref", block.ref);
571
- const rect = el.getBoundingClientRect();
572
- next.set(block.ref, {
573
- top: rect.top - containerRect.top + container.scrollTop,
574
- left: rect.left - containerRect.left,
575
- width: rect.width,
576
- bottom: rect.bottom - containerRect.top + container.scrollTop,
577
- });
578
- }
579
- setBlockRefPositions(next);
580
- }, [currentPath, snapshotBlocks]);
581
-
582
- const handleUpdate = useCallback(
583
- ({ editor }: { editor: ReturnType<typeof useEditor> }) => {
584
- if (isLoadingRef.current || isViewingRef.current || !editor) return;
585
- // In suggesting mode, mark the edit dirty so the next block-change or
586
- // blur flushes it into suggestions. Still push content to the store so
587
- // the store guard (no autosave in suggesting mode) keeps it in sync.
588
- if (useEditorStore.getState().editMode === "suggesting") {
589
- suggestDirtyRef.current = true;
590
- }
591
- const html = editor.getHTML();
592
- const md = htmlToMarkdown(html);
593
- useEditorStore.getState().updateContent(md);
594
- },
595
- [],
596
- );
597
-
598
- const editor = useEditor({
599
- extensions: editorExtensions,
600
- content: "",
601
- editable: !isViewing,
602
- onUpdate: handleUpdate,
603
- onBlur: () => {
604
- void flushSuggestions();
605
- },
606
- onSelectionUpdate: ({ editor: ed }) => {
607
- if (useEditorStore.getState().editMode !== "suggesting") return;
608
- const { from } = ed.state.selection;
609
- const $pos = ed.state.doc.resolve(from);
610
- const idx = $pos.depth > 0 ? $pos.index(0) : 0;
611
- const prev = activeBlockIndexRef.current;
612
- activeBlockIndexRef.current = idx;
613
- // Moved to a different top-level block — flush edits to the prior one.
614
- if (prev !== null && prev !== idx && suggestDirtyRef.current) {
615
- void flushSuggestions();
616
- }
617
- },
618
- editorProps: {
619
- attributes: {
620
- class:
621
- "focus:outline-none min-h-[calc(100vh-12rem)] px-4 sm:px-8 py-6 max-w-[var(--editor-max-w,48rem)] ml-[var(--editor-ml,auto)] mr-auto",
622
- },
623
- handleKeyDown: (view, event) => {
624
- if (
625
- (event.metaKey || event.ctrlKey) &&
626
- event.key.toLowerCase() === "a" &&
627
- isInTable(view.state)
628
- ) {
629
- const $cell = cellAround(view.state.selection.$from);
630
- const cell = $cell?.nodeAfter;
631
- if (!$cell || !cell) return false;
632
-
633
- const from = $cell.pos + 1;
634
- const to = $cell.pos + cell.nodeSize - 1;
635
- if (
636
- view.state.selection.from === from &&
637
- view.state.selection.to === to
638
- ) {
639
- return false;
640
- }
641
-
642
- event.preventDefault();
643
- editor?.chain().focus().setTextSelection({ from, to }).run();
644
- return true;
645
- }
646
-
647
- return false;
648
- },
649
- handleClick: (_view, _pos, event) => {
650
- const target = event.target as HTMLElement;
651
- const link = target.closest("a") as HTMLAnchorElement | null;
652
- if (!link) return false;
653
-
654
- const href = link.getAttribute("href");
655
- if (!href) return false;
656
-
657
- // Wiki-links inserted by the WikiLink mark
658
- if (link.dataset.wikiLink === "true") {
659
- event.preventDefault();
660
- event.stopPropagation();
661
- const slug = link.dataset.slug ?? "";
662
- const anchor = link.dataset.anchor ?? null;
663
- if (!slug) return true;
664
-
665
- const slugStore = useWikiSlugsStore.getState();
666
- if (slugStore.has(slug)) {
667
- const dir = slugStore.getDir(slug);
668
- const pagePath =
669
- dir === null || dir === "root"
670
- ? `${slug}.md`
671
- : `${dir}/${slug}.md`;
672
- void useEditorStore.getState().loadPage(pagePath);
673
- if (anchor) {
674
- setTimeout(() => {
675
- document
676
- .querySelector(`[id="${anchor}"]`)
677
- ?.scrollIntoView({ behavior: "smooth" });
678
- }, 200);
679
- }
680
- } else if (isViewingRef.current) {
681
- return true;
682
- } else {
683
- void openWikiCreateRef.current(slug).then((result) => {
684
- if (result.ok) {
685
- const dir = result.dir ?? "entities";
686
- const pagePath = `${dir}/${result.slug}.md`;
687
- void useEditorStore.getState().loadPage(pagePath);
688
- }
689
- });
690
- }
691
- return true;
692
- }
693
-
694
- // Wiki-links: #page:slug
695
- if (href.startsWith("#page:")) {
696
- event.preventDefault();
697
- event.stopPropagation();
698
- const slug = href.replace("#page:", "");
699
- const { nodes, selectPage, expandPath } = useTreeStore.getState();
700
- const activePath = useEditorStore.getState().currentPath;
701
- const targetPath = findPageBySlug(slug, activePath, nodes);
702
- if (targetPath) {
703
- navigateToPage(targetPath, selectPage, expandPath);
704
- }
705
- return true;
706
- }
707
-
708
- // Internal links: relative paths to .md files or other KB pages
709
- // Skip external URLs and API asset links (PDFs, images)
710
- if (/^https?:\/\//.test(href) || href.startsWith("/api/")) return false;
711
- if (href.startsWith("mailto:") || href.startsWith("tel:")) return false;
712
-
713
- event.preventDefault();
714
- event.stopPropagation();
715
-
716
- const { nodes, selectPage, expandPath } = useTreeStore.getState();
717
- const activePath = useEditorStore.getState().currentPath;
718
-
719
- // Resolve the link target to a KB page path
720
- const targetPath = resolveInternalLink(href, activePath, nodes);
721
- if (targetPath) {
722
- navigateToPage(targetPath, selectPage, expandPath);
723
- }
724
- return true;
725
- },
726
- handlePaste: (_view, event) => {
727
- if (isViewingRef.current) return false;
728
- const files = event.clipboardData?.files;
729
- const pagePath = useEditorStore.getState().currentPath;
730
-
731
- // 1. File paste → upload then insert appropriate node
732
- if (files && files.length > 0 && pagePath) {
733
- for (const file of Array.from(files)) {
734
- uploadFile(pagePath, file).then((url) => {
735
- if (!url || !editor) return;
736
- if (file.type.startsWith("image/")) {
737
- editor
738
- .chain()
739
- .focus()
740
- .setImage({ src: url, alt: file.name })
741
- .run();
742
- } else {
743
- editor
744
- .chain()
745
- .focus()
746
- .insertContent(`<a href="${url}">${file.name}</a>`)
747
- .run();
748
- }
749
- });
750
- }
751
- return true;
752
- }
753
-
754
- return false;
755
- },
756
- handleDrop: (_view, event) => {
757
- if (isViewingRef.current) return false;
758
- const files = event.dataTransfer?.files;
759
- if (!files || files.length === 0) return false;
760
-
761
- const pagePath = useEditorStore.getState().currentPath;
762
- if (!pagePath) return false;
763
-
764
- event.preventDefault();
765
- for (const file of Array.from(files)) {
766
- uploadFile(pagePath, file).then((url) => {
767
- if (!url || !editor) return;
768
- if (file.type.startsWith("image/")) {
769
- editor
770
- .chain()
771
- .focus()
772
- .setImage({ src: url, alt: file.name })
773
- .run();
774
- } else {
775
- editor
776
- .chain()
777
- .focus()
778
- .insertContent(`<a href="${url}">${file.name}</a>`)
779
- .run();
780
- }
781
- });
782
- }
783
- return true;
784
- },
785
- },
786
- immediatelyRender: false,
787
- });
788
-
789
- // Stable ref to the editor so callbacks with empty deps reach the live instance.
790
- const editorRef = useRef<typeof editor>(editor);
791
- editorRef.current = editor;
792
-
793
- useEffect(() => {
794
- editor?.setEditable(!isViewing);
795
- if (isViewing) setSourceMode(false);
796
- }, [editor, isViewing]);
797
-
798
- // When content updates from store (after loadPage), set it in editor
799
- const prevPathRef = useRef<string | null>(null);
800
- const renderedKeyRef = useRef<string | null>(null);
801
- const [renderedPath, setRenderedPath] = useState<string | null>(null);
802
- useEffect(() => {
803
- if (!editor || currentPath === null) return;
804
- // Skip if content hasn't actually changed (same path, dirty edit)
805
- if (
806
- useEditorStore.getState().isDirty &&
807
- currentPath === prevPathRef.current
808
- )
809
- return;
810
- // During page navigation the store briefly holds content="" while the
811
- // fetch is in flight. Rendering that empty string into ProseMirror is
812
- // pure waste — every extension runs a full schema pass twice per
813
- // navigation. Skip until the real content arrives.
814
- if (isLoading && content === "") return;
815
- // Dedupe identical (path, content) renders — e.g. cached paint followed
816
- // by a fresh fetch that returned the same markdown.
817
- const renderMarkdown = parsedViewingContent.body;
818
- const key = `${currentPath} ${renderMarkdown}`;
819
- if (renderedKeyRef.current === key) {
820
- if (renderedPath !== currentPath) setRenderedPath(currentPath);
821
- return;
822
- }
823
- prevPathRef.current = currentPath;
824
-
825
- let cancelled = false;
826
- const setContent = async () => {
827
- isLoadingRef.current = true;
828
- const html = await markdownToHtml(
829
- renderMarkdown,
830
- isViewing ? { pagePath: currentPath, sanitize: true } : currentPath,
831
- );
832
- // Rapid navigation: a newer page may have superseded this render while
833
- // markdownToHtml was awaiting. Don't stamp stale HTML into the editor.
834
- if (cancelled || useEditorStore.getState().currentPath !== currentPath) {
835
- isLoadingRef.current = false;
836
- return;
837
- }
838
- editor.commands.setContent(html);
839
- renderedKeyRef.current = key;
840
- setRenderedPath(currentPath);
841
- setTimeout(() => {
842
- isLoadingRef.current = false;
843
- }, 50);
844
- };
845
-
846
- setContent();
847
- return () => {
848
- cancelled = true;
849
- };
850
- }, [editor, content, currentPath, isLoading, renderedPath, parsedViewingContent.body, isViewing]);
851
-
852
- useEffect(() => {
853
- if (!isViewing || !renderedPath) return;
854
- const container = scrollContainerRef.current;
855
- if (!container) return;
856
- container
857
- .querySelectorAll<HTMLInputElement>('input[type="checkbox"]')
858
- .forEach((input) => {
859
- input.disabled = true;
860
- });
861
- }, [isViewing, renderedPath, parsedViewingContent.body]);
862
-
863
- const isLoadingState =
864
- currentPath !== null && (isLoading || renderedPath !== currentPath);
865
- // Don't flash a spinner for fast/cached opens: only reveal the overlay if the
866
- // load is still pending after a grace period. Instant (prefetched/cached) opens
867
- // resolve well within it and never show a loader — the world-class default.
868
- const [showLoadingOverlay, setShowLoadingOverlay] = useState(false);
869
- useEffect(() => {
870
- if (!isLoadingState) {
871
- setShowLoadingOverlay(false);
872
- return;
873
- }
874
- const id = setTimeout(() => setShowLoadingOverlay(true), 150);
875
- return () => clearTimeout(id);
876
- }, [isLoadingState]);
877
-
878
- const handleOpenAI = () => {
879
- clearMessages();
880
- openAI();
881
- };
882
-
883
- if (currentPath === null) {
884
- return (
885
- <div className="flex-1 flex items-center justify-center text-muted-foreground">
886
- <div className="text-center space-y-3">
887
- <p className="text-lg font-medium tracking-[-0.02em]">
888
- No page selected
889
- </p>
890
- <p className="text-sm text-muted-foreground/70">
891
- Select a page from the sidebar or create a new one
892
- </p>
893
- </div>
894
- </div>
895
- );
896
- }
897
-
898
- // Path resolved to a folder (or otherwise-missing target) without an
899
- // index.md. Render an explicit placeholder + Create CTA instead of
900
- // dropping the user into an empty editor that pretends to be the page.
901
- if (loadStatus === "missing") {
902
- const slug = currentPath.split("/").pop() || currentPath;
903
- const inferredTitle = slug
904
- .replace(/[-_]+/g, " ")
905
- .replace(/\b\w/g, (c) => c.toUpperCase());
906
- const folderNode = findNodeByPath(nodes, currentPath);
907
- const folderChildren = folderNode?.children ?? [];
908
- const hasChildren = folderChildren.length > 0;
909
- return (
910
- <div className="flex-1 overflow-y-auto">
911
- <div className="max-w-3xl mx-auto px-6 py-10 space-y-6">
912
- <div className="space-y-3">
913
- <p className="text-lg font-medium tracking-[-0.02em] text-foreground">
914
- {inferredTitle}
915
- </p>
916
- <p className="text-sm text-muted-foreground/80">
917
- This folder doesn&apos;t have an{" "}
918
- <code className="px-1 py-0.5 rounded bg-muted text-[12px]">
919
- index.md
920
- </code>
921
- {hasChildren ? " yet — its contents are listed below." : " yet."}
922
- </p>
923
- <button
924
- onClick={() => void createMissingPage(inferredTitle)}
925
- className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-[13px] font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
926
- >
927
- <FilePlus className="h-3.5 w-3.5" />
928
- Create page
929
- </button>
930
- </div>
931
- {hasChildren && (
932
- <FolderIndex
933
- key={currentPath}
934
- folderPath={currentPath}
935
- entries={folderChildren}
936
- />
937
- )}
938
- </div>
939
- </div>
940
- );
941
- }
942
-
943
- const toggleSourceMode = async () => {
944
- if (!sourceMode) {
945
- // Switching TO source mode — grab current markdown
946
- setSourceText(useEditorStore.getState().content);
947
- setSourceMode(true);
948
- } else {
949
- // Switching FROM source mode — apply changes
950
- useEditorStore.getState().updateContent(sourceText);
951
- if (editor) {
952
- isLoadingRef.current = true;
953
- const html = await markdownToHtml(sourceText, currentPath ?? undefined);
954
- editor.commands.setContent(html);
955
- setTimeout(() => {
956
- isLoadingRef.current = false;
957
- }, 50);
958
- }
959
- setSourceMode(false);
960
- }
961
- };
962
-
963
- // Folder pages with both an index.md (loadStatus === "ok") AND children
964
- // get a Page / Files tab strip so users can switch between the page body
965
- // and the directory listing without leaving the route.
966
- const renderedFolderNode = findNodeByPath(nodes, currentPath);
967
- const renderedFolderChildren =
968
- renderedFolderNode?.type === "directory" ||
969
- renderedFolderNode?.type === "cabinet"
970
- ? (renderedFolderNode.children ?? [])
971
- : [];
972
- const showFolderTabs = renderedFolderChildren.length > 0;
973
- const onFilesTab = showFolderTabs && folderTab === "files";
974
-
975
- return (
976
- <>
977
- <div className="flex-1 flex flex-col overflow-hidden">
978
- {showFolderTabs && (
979
- <div className="flex items-center gap-1 px-3 pt-2 border-b border-border">
980
- <button
981
- onClick={() => setFolderTab("page")}
982
- className={`px-3 py-1.5 text-[12px] rounded-t-md border-b-2 -mb-px transition-colors ${
983
- folderTab === "page"
984
- ? "border-primary text-foreground"
985
- : "border-transparent text-muted-foreground hover:text-foreground"
986
- }`}
987
- aria-pressed={folderTab === "page"}
988
- >
989
- Page
990
- </button>
991
- <button
992
- onClick={() => setFolderTab("files")}
993
- className={`px-3 py-1.5 text-[12px] rounded-t-md border-b-2 -mb-px transition-colors ${
994
- folderTab === "files"
995
- ? "border-primary text-foreground"
996
- : "border-transparent text-muted-foreground hover:text-foreground"
997
- }`}
998
- aria-pressed={folderTab === "files"}
999
- >
1000
- Files
1001
- <span className="ml-1.5 text-muted-foreground/60">
1002
- {renderedFolderChildren.length}
1003
- </span>
1004
- </button>
1005
- </div>
1006
- )}
1007
- {onFilesTab ? (
1008
- <div className="flex-1 overflow-y-auto">
1009
- <div className="max-w-3xl mx-auto px-6 py-6">
1010
- <FolderIndex
1011
- key={currentPath}
1012
- folderPath={currentPath}
1013
- entries={renderedFolderChildren}
1014
- />
1015
- </div>
1016
- </div>
1017
- ) : (
1018
- <>
1019
- {!isViewing && (
1020
- <div className="flex items-center min-w-0">
1021
- <div className="flex-1 min-w-0">
1022
- {!sourceMode && <EditorToolbar editor={editor} />}
1023
- </div>
1024
- <button
1025
- onClick={toggleSourceMode}
1026
- className={`flex items-center gap-1.5 px-3 py-1 mr-2 text-[11px] rounded-md transition-colors border border-border ${
1027
- sourceMode
1028
- ? "bg-primary text-primary-foreground"
1029
- : "text-muted-foreground hover:bg-accent"
1030
- }`}
1031
- >
1032
- <Code2 className="h-3 w-3" />
1033
- {sourceMode ? "Preview" : "Markdown"}
1034
- </button>
1035
- </div>
1036
- )}
1037
-
1038
- {sourceMode ? (
1039
- <div
1040
- className="flex-1 overflow-y-auto p-4"
1041
- dir={isRtl ? "rtl" : undefined}
1042
- >
1043
- <textarea
1044
- value={sourceText}
1045
- onChange={(e) => setSourceText(e.target.value)}
1046
- className="w-full h-full min-h-[calc(100vh-12rem)] bg-transparent font-mono text-[13px] leading-relaxed resize-none focus:outline-none"
1047
- spellCheck={false}
1048
- />
1049
- </div>
1050
- ) : (
1051
- <div className="flex-1 relative" dir={isRtl ? "rtl" : undefined}>
1052
- {isSuggesting && (
1053
- <div className="absolute top-0 inset-x-0 z-20 flex items-center justify-center gap-2 px-3 py-1 bg-primary/10 border-b border-primary/20 text-[11px] text-primary pointer-events-none">
1054
- Suggesting mode · your edits become suggestions for review
1055
- </div>
1056
- )}
1057
- <DocumentOutline editor={editor} scrollContainerRef={scrollContainerRef} />
1058
- <div
1059
- ref={scrollContainerRef}
1060
- className={`absolute inset-0 overflow-y-auto ${
1061
- isSuggesting ? "pt-7" : ""
1062
- }`}
1063
- style={{
1064
- ["--editor-max-w" as string]: editorMaxW,
1065
- ["--editor-ml" as string]: editorMl,
1066
- }}
1067
- data-editor-scroll
1068
- >
1069
- {/* Absolutely-positioned overlay for comment pips and suggestion cards.
1070
- height:0 so it doesn't push content; children overflow freely.
1071
- Positions from blockRefPositions are relative to scroll container top. */}
1072
- <div
1073
- aria-hidden="true"
1074
- className="relative pointer-events-none"
1075
- style={{ height: 0 }}
1076
- >
1077
- {/* Comment pips — one per block with at least one comment */}
1078
- {Object.entries(commentsByRef).map(([blockRef, blockComments]) => {
1079
- const pos = blockRefPositions.get(blockRef);
1080
- if (!pos) return null;
1081
- return (
1082
- <div key={`pip-${blockRef}`} style={{ pointerEvents: "auto" }}>
1083
- <CommentPip
1084
- blockRef={blockRef}
1085
- comments={blockComments}
1086
- top={pos.top + 4}
1087
- left={Math.max(0, pos.left - 20)}
1088
- onClick={() => {
1089
- const el = scrollContainerRef.current?.querySelector(
1090
- `[data-block-ref="${blockRef}"]`,
1091
- ) as HTMLElement | null;
1092
- if (el) setThreadTarget({ blockRef, el });
1093
- }}
1094
- />
1095
- </div>
1096
- );
1097
- })}
1098
-
1099
- {/* Suggestion cards — one per pending suggestion */}
1100
- {currentPath && pendingSuggestions.map((sg) => {
1101
- const pos = blockRefPositions.get(sg.ref);
1102
- const currentBlock = snapshotBlocks.find((b) => b.ref === sg.ref);
1103
- if (!pos || !currentBlock) return null;
1104
- const cardTop =
1105
- sg.kind === "insertAfter" ? pos.bottom + 4 :
1106
- sg.kind === "insertBefore" ? Math.max(0, pos.top - 80) :
1107
- pos.top;
1108
- return (
1109
- <div key={`sug-${sg.id}`} style={{ pointerEvents: "auto" }}>
1110
- <SuggestionCard
1111
- path={currentPath}
1112
- suggestion={sg}
1113
- currentMarkdown={currentBlock.markdown}
1114
- baseRevision={snapshotRevision}
1115
- getLatestRevision={() =>
1116
- useProofStore.getState().byPath[currentPath]?.snapshotRevision ?? 0
1117
- }
1118
- top={cardTop}
1119
- left={pos.left}
1120
- width={pos.width}
1121
- onSettled={() => {
1122
- void useProofStore.getState().loadSidecar(currentPath);
1123
- void useProofStore.getState().loadSnapshot(currentPath);
1124
- }}
1125
- readOnly={isViewing}
1126
- />
1127
- </div>
1128
- );
1129
- })}
1130
- </div>
1131
-
1132
- {/* Comment thread — Portal-rendered, driven by threadTarget */}
1133
- {threadTarget && currentPath && (
1134
- <CommentThread
1135
- path={currentPath}
1136
- blockRef={threadTarget.blockRef}
1137
- comments={commentsByRef[threadTarget.blockRef] ?? []}
1138
- anchorEl={threadTarget.el}
1139
- onClose={() => setThreadTarget(null)}
1140
- readOnly={isViewing}
1141
- />
1142
- )}
1143
-
1144
- {/* Human suggest-edit popover — driven by suggestTarget */}
1145
- {suggestTarget && currentPath && (
1146
- <SuggestEditPopover
1147
- path={currentPath}
1148
- blockRef={suggestTarget.blockRef}
1149
- currentMarkdown={suggestTarget.markdown}
1150
- anchor={suggestTarget.anchor}
1151
- onClose={() => setSuggestTarget(null)}
1152
- />
1153
- )}
1154
-
1155
- {isViewing && Object.keys(parsedViewingContent.data).length > 0 && (
1156
- <div className="max-w-[var(--editor-max-w,48rem)] ml-[var(--editor-ml,auto)] mr-auto px-4 sm:px-8 pt-6">
1157
- <FrontmatterHeader
1158
- data={parsedViewingContent.data as Record<string, never>}
1159
- />
1160
- </div>
1161
- )}
1162
- <EditorContent editor={editor} />
1163
- {currentPath && /\.(md|markdown)$/i.test(currentPath) && (
1164
- <BacklinksPanel currentPath={currentPath} />
1165
- )}
1166
- {/* Proof-span hover delegation */}
1167
- <div
1168
- aria-hidden="true"
1169
- className="contents"
1170
- onMouseOver={(e) => {
1171
- const span = (e.target as HTMLElement).closest<HTMLElement>(".proof-span");
1172
- if (span && span !== proofTarget) setProofTarget(span);
1173
- }}
1174
- onMouseOut={(e) => {
1175
- const related = e.relatedTarget as HTMLElement | null;
1176
- if (!related?.closest(".proof-span")) setProofTarget(null);
1177
- }}
1178
- />
1179
- {currentPath && (
1180
- <ProofSpanPopover
1181
- targetEl={proofTarget}
1182
- path={currentPath}
1183
- onClose={() => setProofTarget(null)}
1184
- onComment={() => {
1185
- if (!proofTarget) return;
1186
- const blockEl = proofTarget.closest<HTMLElement>("[data-block-ref]");
1187
- if (!blockEl) return;
1188
- const blockRef = blockEl.getAttribute("data-block-ref");
1189
- if (blockRef) setThreadTarget({ blockRef, el: blockEl });
1190
- }}
1191
- />
1192
- )}
1193
- {!isViewing && (
1194
- <>
1195
- <EditorBubbleMenu
1196
- editor={editor}
1197
- onSuggestEdit={openSuggestForSelection}
1198
- onComment={openCommentForSelection}
1199
- />
1200
- <TableMenu editor={editor} />
1201
- <SlashCommands editor={editor} />
1202
- <WikiLinkPicker
1203
- editor={editor}
1204
- onCreateRequest={openWikiCreateRef.current}
1205
- />
1206
- </>
1207
- )}
1208
- {isViewing && (
1209
- <ViewModeCommentButton
1210
- containerRef={scrollContainerRef}
1211
- onComment={openCommentForSelection}
1212
- />
1213
- )}
1214
- {/* AI Edit Prompt + slash hint */}
1215
- <div className="max-w-[var(--editor-max-w,48rem)] ml-[var(--editor-ml,auto)] mr-auto px-8 pb-8 flex items-center gap-4">
1216
- <button
1217
- onClick={handleOpenAI}
1218
- className="group flex items-center gap-2 text-[13px] text-muted-foreground/50 hover:text-muted-foreground transition-colors cursor-pointer"
1219
- >
1220
- <Sparkles className="h-3.5 w-3.5 group-hover:text-primary transition-colors" />
1221
- <span>Edit with AI</span>
1222
- </button>
1223
- {!isViewing && (
1224
- <span className="text-[11px] text-muted-foreground/30 select-none">
1225
- <kbd className="rounded px-1 py-0.5 font-mono text-[10px] ring-1 ring-foreground/10">
1226
- /
1227
- </kbd>{" "}
1228
- for commands
1229
- </span>
1230
- )}
1231
- </div>
1232
- </div>
1233
-
1234
- {showLoadingOverlay && (
1235
- <div
1236
- className="absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-md z-20 pointer-events-none"
1237
- aria-hidden="true"
1238
- >
1239
- <Loader2 className="h-5 w-5 animate-spin text-muted-foreground/70" />
1240
- </div>
1241
- )}
1242
- </div>
1243
- )}
1244
-
1245
- {!isViewing && (
1246
- /* Status bar */
1247
- <div className="flex items-center justify-between px-4 py-1 border-t border-border text-xs text-muted-foreground/60">
1248
- <span className="text-[10.5px] text-muted-foreground/30 select-none hidden sm:block">
1249
- <kbd className="rounded px-1 font-mono text-[9.5px] ring-1 ring-foreground/10">
1250
- ⌘S
1251
- </kbd>{" "}
1252
- save
1253
- <span className="mx-1.5 opacity-40">·</span>
1254
- <kbd className="rounded px-1 font-mono text-[9.5px] ring-1 ring-foreground/10">
1255
- /
1256
- </kbd>{" "}
1257
- commands
1258
- </span>
1259
- <div className="flex items-center gap-3">
1260
- {/* Mode toggle */}
1261
- <div
1262
- className="flex items-center rounded-md border border-border overflow-hidden text-[10.5px]"
1263
- role="radiogroup"
1264
- aria-label="Edit mode"
1265
- >
1266
- <button
1267
- type="button"
1268
- role="radio"
1269
- aria-checked={editMode === "editing"}
1270
- onClick={() => setEditMode("editing")}
1271
- className={`px-2 py-0.5 transition-colors ${
1272
- editMode === "editing"
1273
- ? "bg-primary text-primary-foreground"
1274
- : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
1275
- }`}
1276
- >
1277
- Editing
1278
- </button>
1279
- <button
1280
- type="button"
1281
- role="radio"
1282
- aria-checked={editMode === "suggesting"}
1283
- onClick={() => setEditMode("suggesting")}
1284
- className={`px-2 py-0.5 transition-colors ${
1285
- editMode === "suggesting"
1286
- ? "bg-primary text-primary-foreground"
1287
- : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
1288
- }`}
1289
- >
1290
- Suggesting
1291
- </button>
1292
- </div>
1293
- <span
1294
- className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10.5px] transition-all duration-300 ${
1295
- saveStatus === "idle"
1296
- ? "opacity-0 pointer-events-none"
1297
- : "opacity-100"
1298
- } ${
1299
- saveStatus === "saving"
1300
- ? "bg-muted text-muted-foreground"
1301
- : saveStatus === "saved"
1302
- ? "bg-success/10 text-success"
1303
- : saveStatus === "error"
1304
- ? "bg-destructive/10 text-destructive"
1305
- : ""
1306
- }`}
1307
- >
1308
- {saveStatus === "saving" && (
1309
- <><Loader2 className="h-2.5 w-2.5 animate-spin" />Saving…</>
1310
- )}
1311
- {saveStatus === "saved" && (
1312
- <><Check className="h-2.5 w-2.5" />Saved</>
1313
- )}
1314
- {saveStatus === "error" && (
1315
- <><AlertCircle className="h-2.5 w-2.5" />Save failed</>
1316
- )}
1317
- </span>
1318
- </div>
1319
- </div>
1320
- )}
1321
- </>
1322
- )}
1323
- </div>
1324
- {WikiCreateDialog}
1325
- </>
1326
- );
1327
- }