kennel 0.2.2 → 0.2.5

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 (237) hide show
  1. package/README.md +23 -13
  2. package/dist/cli/dispatch.js +16 -3
  3. package/dist/cli/src/cli/dispatch.js +16 -3
  4. package/dist/cli/src/cli/usage.js +1 -1
  5. package/dist/cli/src/core/command.js +1 -0
  6. package/dist/cli/src/core/init.js +5 -5
  7. package/dist/cli/src/plugins/external.d.ts +29 -3
  8. package/dist/cli/src/plugins/external.js +78 -24
  9. package/dist/cli/src/site/server-plugins.d.ts +2 -7
  10. package/dist/cli/src/site/viewer.js +1 -0
  11. package/dist/cli/usage.js +1 -1
  12. package/dist/core/command.js +1 -0
  13. package/dist/core/init.js +5 -5
  14. package/dist/core/runtime.js +357 -3207
  15. package/dist/package/preview.js +1232 -5113
  16. package/dist/plugins/external.d.ts +29 -3
  17. package/dist/plugins/external.js +78 -24
  18. package/dist/site/server-plugins.d.ts +2 -7
  19. package/dist/site/server-plugins.js +552 -10447
  20. package/dist/site/viewer.js +1 -0
  21. package/kernel/apps/web/package.json +1 -5
  22. package/kernel/apps/web/scripts/dev.mjs +53 -0
  23. package/kernel/apps/web/src/app.css +0 -3
  24. package/kernel/apps/web/src/main.tsx +0 -1
  25. package/kernel/apps/web/src/plugins.ts +0 -30
  26. package/kernel/apps/web/tsconfig.json +1 -2
  27. package/kernel/apps/web/vite.config.ts +31 -13
  28. package/kernel/packages/api/package.json +0 -4
  29. package/kernel/packages/api/src/server/folder-metadata-plugin.ts +13 -38
  30. package/kernel/packages/api/src/server/index.ts +0 -1
  31. package/kernel/packages/api/src/server/plugins.ts +49 -16
  32. package/kernel/packages/api/test/core.test.ts +13 -25
  33. package/kernel/packages/markdown/README.md +1 -1
  34. package/package.json +1 -1
  35. package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
  36. package/web/assets/app-CX20nNIa.js +2 -0
  37. package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
  38. package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
  39. package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
  40. package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
  41. package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
  42. package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
  43. package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
  44. package/web/assets/channel-X7WHyxxT.js +1 -0
  45. package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
  46. package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
  47. package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
  48. package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
  49. package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
  50. package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
  51. package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
  52. package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
  53. package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
  54. package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
  55. package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
  56. package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
  57. package/web/assets/client-7OWw5kW2.js +6 -0
  58. package/web/assets/client-DYj_td4Q.js +17 -0
  59. package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
  60. package/web/assets/command-zkzH4hVz.js +38 -0
  61. package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
  62. package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
  63. package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
  64. package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
  65. package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
  66. package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
  67. package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
  68. package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
  69. package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
  70. package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
  71. package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
  72. package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
  73. package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
  74. package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
  75. package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
  76. package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
  77. package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
  78. package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
  79. package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
  80. package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
  81. package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
  82. package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
  83. package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
  84. package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
  85. package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
  86. package/web/assets/index-CegvVQ1C.js +1 -0
  87. package/web/assets/index-Dwhf8F_U.css +1 -0
  88. package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
  89. package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
  90. package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
  91. package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
  92. package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
  93. package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
  94. package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
  95. package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
  96. package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
  97. package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
  98. package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
  99. package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
  100. package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
  101. package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
  102. package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
  103. package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
  104. package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
  105. package/web/assets/select-CtXC_PLC.js +30 -0
  106. package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
  107. package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
  108. package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
  109. package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
  110. package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
  111. package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
  112. package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
  113. package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
  114. package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
  115. package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
  116. package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
  117. package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
  118. package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
  119. package/web/index.html +2 -2
  120. package/kernel/packages/api/test/partial-loading.test.ts +0 -43
  121. package/kernel/plugins/calendar/SKILL.md +0 -173
  122. package/kernel/plugins/calendar/eslint.config.js +0 -10
  123. package/kernel/plugins/calendar/package.json +0 -73
  124. package/kernel/plugins/calendar/schema.json +0 -137
  125. package/kernel/plugins/calendar/src/calendar.tsx +0 -328
  126. package/kernel/plugins/calendar/src/client.tsx +0 -139
  127. package/kernel/plugins/calendar/src/context.ts +0 -48
  128. package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
  129. package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
  130. package/kernel/plugins/calendar/src/index.ts +0 -4
  131. package/kernel/plugins/calendar/src/manifest.ts +0 -7
  132. package/kernel/plugins/calendar/src/parse.ts +0 -610
  133. package/kernel/plugins/calendar/src/server.ts +0 -160
  134. package/kernel/plugins/calendar/src/utils.ts +0 -80
  135. package/kernel/plugins/calendar/src/views.tsx +0 -418
  136. package/kernel/plugins/calendar/test/context.test.mjs +0 -60
  137. package/kernel/plugins/calendar/test/package.test.mjs +0 -43
  138. package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
  139. package/kernel/plugins/calendar/test/server.test.mjs +0 -126
  140. package/kernel/plugins/calendar/tsconfig.json +0 -17
  141. package/kernel/plugins/mail/SKILL.md +0 -97
  142. package/kernel/plugins/mail/eslint.config.js +0 -10
  143. package/kernel/plugins/mail/package.json +0 -79
  144. package/kernel/plugins/mail/src/api.ts +0 -359
  145. package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
  146. package/kernel/plugins/mail/src/client.tsx +0 -14
  147. package/kernel/plugins/mail/src/flock.ts +0 -137
  148. package/kernel/plugins/mail/src/folders.ts +0 -37
  149. package/kernel/plugins/mail/src/history.ts +0 -485
  150. package/kernel/plugins/mail/src/index.ts +0 -3
  151. package/kernel/plugins/mail/src/mail-card.tsx +0 -123
  152. package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
  153. package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
  154. package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
  155. package/kernel/plugins/mail/src/mail-page.tsx +0 -109
  156. package/kernel/plugins/mail/src/mail-request.ts +0 -78
  157. package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
  158. package/kernel/plugins/mail/src/mailos.ts +0 -723
  159. package/kernel/plugins/mail/src/manifest.ts +0 -7
  160. package/kernel/plugins/mail/src/node.ts +0 -171
  161. package/kernel/plugins/mail/src/parse.ts +0 -139
  162. package/kernel/plugins/mail/src/server.ts +0 -125
  163. package/kernel/plugins/mail/src/types.ts +0 -10
  164. package/kernel/plugins/mail/src/viewer.tsx +0 -147
  165. package/kernel/plugins/mail/test/flock.test.mjs +0 -156
  166. package/kernel/plugins/mail/test/folders.test.mjs +0 -138
  167. package/kernel/plugins/mail/test/history.test.mjs +0 -460
  168. package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
  169. package/kernel/plugins/mail/test/node.test.mjs +0 -207
  170. package/kernel/plugins/mail/test/package.test.mjs +0 -54
  171. package/kernel/plugins/mail/test/parse.test.mjs +0 -32
  172. package/kernel/plugins/mail/test/server.test.mjs +0 -116
  173. package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
  174. package/kernel/plugins/mail/tsconfig.json +0 -17
  175. package/kernel/plugins/projects/SKILL.md +0 -73
  176. package/kernel/plugins/projects/eslint.config.js +0 -10
  177. package/kernel/plugins/projects/package.json +0 -70
  178. package/kernel/plugins/projects/src/client.tsx +0 -17
  179. package/kernel/plugins/projects/src/data/model.ts +0 -129
  180. package/kernel/plugins/projects/src/index.ts +0 -17
  181. package/kernel/plugins/projects/src/manifest.ts +0 -7
  182. package/kernel/plugins/projects/src/project-context.ts +0 -41
  183. package/kernel/plugins/projects/src/server.ts +0 -276
  184. package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
  185. package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
  186. package/kernel/plugins/projects/test/package.test.mjs +0 -51
  187. package/kernel/plugins/projects/tsconfig.json +0 -17
  188. package/kernel/plugins/tasks/SKILL.md +0 -341
  189. package/kernel/plugins/tasks/eslint.config.js +0 -10
  190. package/kernel/plugins/tasks/package.json +0 -74
  191. package/kernel/plugins/tasks/schema.json +0 -939
  192. package/kernel/plugins/tasks/src/api.ts +0 -212
  193. package/kernel/plugins/tasks/src/client.tsx +0 -23
  194. package/kernel/plugins/tasks/src/data/parse.ts +0 -544
  195. package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
  196. package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
  197. package/kernel/plugins/tasks/src/index.ts +0 -34
  198. package/kernel/plugins/tasks/src/manifest.ts +0 -7
  199. package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
  200. package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
  201. package/kernel/plugins/tasks/src/server.ts +0 -447
  202. package/kernel/plugins/tasks/src/styles.css +0 -9
  203. package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
  204. package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
  205. package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
  206. package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
  207. package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
  208. package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
  209. package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
  210. package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
  211. package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
  212. package/kernel/plugins/tasks/test/api.test.mjs +0 -99
  213. package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
  214. package/kernel/plugins/tasks/test/package.test.mjs +0 -54
  215. package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
  216. package/kernel/plugins/tasks/test/server.test.mjs +0 -386
  217. package/kernel/plugins/tasks/tsconfig.json +0 -17
  218. package/web/assets/app-DoGr2Zi7.js +0 -2
  219. package/web/assets/channel-uMEjV8qc.js +0 -1
  220. package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
  221. package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
  222. package/web/assets/client-C6Nxdfif.js +0 -2
  223. package/web/assets/client-CXYfBko5.js +0 -17
  224. package/web/assets/client-CoKtOq-X.js +0 -7
  225. package/web/assets/client-DyRQz5RU.js +0 -53
  226. package/web/assets/client-OY6HUzuD.js +0 -98
  227. package/web/assets/client-YOYWZIFi.js +0 -34
  228. package/web/assets/command-BugZBGXE.js +0 -38
  229. package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
  230. package/web/assets/index-BaRnbYRw.js +0 -1
  231. package/web/assets/index-BexmLxIP.css +0 -1
  232. package/web/assets/index-DDirZMgx.js +0 -1
  233. package/web/assets/note-card-DBfX6gW1.js +0 -6
  234. package/web/assets/select-D8cqNgcp.js +0 -30
  235. package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
  236. package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
  237. package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
@@ -1,467 +0,0 @@
1
- import type { ReactNode } from "react"
2
- import {
3
- Archive,
4
- ArrowLeft,
5
- ExternalLink,
6
- RefreshCw,
7
- Trash2,
8
- X,
9
- } from "lucide-react"
10
-
11
- import type { FolderPluginViewProps } from "@raggle-ai/folder-plugin/client"
12
- import { BarkdownMarkdown } from "@raggle-ai/markdown"
13
- import { CopyContextButton } from "@raggle-ai/ui/components/copy-context"
14
- import { FilePreview } from "@raggle-ai/ui/components/file-preview"
15
- import { toast } from "@raggle-ai/ui/components/sonner"
16
-
17
- import type { MailPreview } from "./types.js"
18
- import type { MailDetail } from "./viewer.js"
19
- import { MailConversationPanel } from "./mail-conversation.js"
20
- import { useMailAction, useMailDetail, useMailSender } from "./mail-hooks.js"
21
- import { mailErrorText } from "./mail-request.js"
22
- import { MailSendBox } from "./mail-send-box.js"
23
- import { mailItemContext } from "./viewer.js"
24
-
25
- function displayTimestamp(value: string | null) {
26
- if (!value) return "—"
27
- const parsed = Date.parse(value)
28
- if (Number.isNaN(parsed)) return value
29
- return new Date(parsed).toLocaleString(undefined, {
30
- dateStyle: "medium",
31
- timeStyle: "short",
32
- })
33
- }
34
-
35
- function MailMetaRow({ label, value }: { label: string; value: ReactNode }) {
36
- return (
37
- <div className="grid grid-cols-[88px_1fr] items-start gap-x-3 gap-y-1">
38
- <span
39
- className="pt-0.5 text-[0.75rem] font-medium tracking-wide
40
- text-[#6c7a6d] uppercase"
41
- >
42
- {label}
43
- </span>
44
- <span className="min-w-0 text-sm break-words">{value}</span>
45
- </div>
46
- )
47
- }
48
-
49
- function RecipientItems({ items, max }: { items: string[]; max?: number }) {
50
- if (!items.length) {
51
- return <span className="text-[#6c7a6d]">—</span>
52
- }
53
-
54
- const visible = max ? items.slice(0, max) : items
55
- const extra = max && items.length > max ? items.length - max : 0
56
- const pillClassName =
57
- "rounded-md bg-[#f3f5f2] px-2 py-1 text-[0.85rem] text-[#2f382f]"
58
-
59
- return (
60
- <span className="flex min-w-0 flex-wrap gap-1">
61
- {visible.map((recipient) => (
62
- <span key={recipient} className={pillClassName} title={recipient}>
63
- <span className="break-all">{recipient}</span>
64
- </span>
65
- ))}
66
- {extra ? (
67
- <span className={pillClassName} title={`${extra} more recipients`}>
68
- +{extra}
69
- </span>
70
- ) : null}
71
- </span>
72
- )
73
- }
74
-
75
- /**
76
- * Attachment cards with an optional per-card remove control. The remove
77
- * button only unlinks the file from the draft's frontmatter `file:` list;
78
- * the file stays on disk.
79
- */
80
- function AttachmentList({
81
- paths,
82
- pending,
83
- onRemove,
84
- }: {
85
- paths: string[]
86
- pending?: boolean
87
- onRemove?: (attachment: string) => void
88
- }) {
89
- return (
90
- <span className="flex min-w-0 flex-wrap gap-2">
91
- {paths.map((path) => {
92
- const name = path.slice(path.lastIndexOf("/") + 1)
93
- return (
94
- <span key={path} className="relative">
95
- <FilePreview path={path} />
96
- {onRemove ? (
97
- <button
98
- type="button"
99
- aria-label={`Remove ${name} from this draft`}
100
- title={`Remove ${name} from this draft}`}
101
- className="absolute top-1.5 right-1.5 z-10 flex size-6
102
- cursor-pointer items-center justify-center rounded-full border
103
- border-[#e5c4c4] bg-white text-[#9c3a3a] shadow-xs
104
- hover:bg-[#fbf3f3] disabled:pointer-events-none
105
- disabled:opacity-50"
106
- onClick={() => onRemove(path)}
107
- disabled={pending}
108
- >
109
- <X className="size-3.5" />
110
- </button>
111
- ) : null}
112
- </span>
113
- )
114
- })}
115
- </span>
116
- )
117
- }
118
-
119
- function MailDetailView({
120
- mail,
121
- error,
122
- preview,
123
- refreshing,
124
- onRefresh,
125
- onArchive,
126
- onDelete,
127
- actionPending,
128
- onOpenPath,
129
- onRemoveAttachment,
130
- }: {
131
- mail: MailDetail
132
- error?: string
133
- preview?: MailPreview
134
- refreshing: boolean
135
- onRefresh: () => void
136
- /** Archive this draft; omit to hide the Archive button. */
137
- onArchive?: () => void
138
- /** Delete this draft; omit to hide the Delete button. */
139
- onDelete?: () => void
140
- /** True while an archive or delete request is running. */
141
- actionPending?: boolean
142
- /** Open a physical file in the host's default app; omit to hide the button. */
143
- onOpenPath?: (path: string) => Promise<unknown>
144
- /** Unlink one attachment from this draft; omit to hide the remove button. */
145
- onRemoveAttachment?: (attachment: string) => void
146
- }) {
147
- return (
148
- <div className="grid min-w-0 grid-cols-1 gap-4">
149
- <header className="flex flex-wrap items-start justify-between gap-3">
150
- <div className="min-w-0">
151
- <h1 className="m-0 text-2xl font-semibold break-words text-[#233025]">
152
- {mail.subject ?? "(no subject)"}
153
- </h1>
154
- <p className="m-0 mt-1 text-sm text-[#6c7a6d]">
155
- Updated:{" "}
156
- {(mail.updatedAt ?? mail.createdAt) ? (
157
- <time
158
- dateTime={mail.updatedAt ?? mail.createdAt ?? undefined}
159
- title={mail.updatedAt ?? mail.createdAt ?? undefined}
160
- >
161
- {displayTimestamp(mail.updatedAt ?? mail.createdAt)}
162
- </time>
163
- ) : (
164
- "—"
165
- )}
166
- </p>
167
- </div>
168
-
169
- <div className="flex shrink-0 flex-wrap items-center gap-2">
170
- <CopyContextButton
171
- className="border bg-white text-[#4a554b] shadow-xs
172
- hover:bg-[#f3f5f2]"
173
- context={() => mailItemContext(mail, preview?.body ?? mail.body)}
174
- label={`Copy full context for ${mail.subject ?? "this draft"}`}
175
- size="sm"
176
- variant="ghost"
177
- />
178
- <CopyContextButton
179
- className="border bg-white text-[#4a554b] shadow-xs
180
- hover:bg-[#f3f5f2]"
181
- context={mail.filePath}
182
- label="Copy draft path"
183
- size="sm"
184
- variant="ghost"
185
- />
186
- <button
187
- type="button"
188
- className="inline-flex items-center gap-2 rounded-md border px-3
189
- py-1.5 text-sm"
190
- onClick={onRefresh}
191
- disabled={refreshing}
192
- >
193
- <RefreshCw
194
- className={refreshing ? "size-4 animate-spin" : "size-4"}
195
- />
196
- Refresh
197
- </button>
198
- {onOpenPath ? (
199
- <button
200
- type="button"
201
- className="inline-flex cursor-pointer items-center gap-2
202
- rounded-md border px-3 py-1.5 text-sm"
203
- onClick={() => void onOpenPath(mail.filePath)}
204
- >
205
- <ExternalLink className="size-4" />
206
- Open
207
- </button>
208
- ) : null}
209
- {onArchive && mail.status !== "sent" ? (
210
- <button
211
- type="button"
212
- className="inline-flex items-center gap-2 rounded-md border px-3
213
- py-1.5 text-sm disabled:pointer-events-none disabled:opacity-50"
214
- onClick={onArchive}
215
- disabled={actionPending}
216
- >
217
- <Archive className="size-4" />
218
- Archive
219
- </button>
220
- ) : null}
221
- {onDelete ? (
222
- <button
223
- type="button"
224
- className="inline-flex items-center gap-2 rounded-md border
225
- border-[#e5c4c4] px-3 py-1.5 text-sm text-[#9c3a3a]
226
- hover:bg-[#fbf3f3] disabled:pointer-events-none
227
- disabled:opacity-50"
228
- onClick={onDelete}
229
- disabled={actionPending}
230
- >
231
- <Trash2 className="size-4" />
232
- Delete
233
- </button>
234
- ) : null}
235
- </div>
236
- </header>
237
-
238
- <div
239
- className="grid grid-cols-1 gap-3 rounded-xl border border-[#e7ebe7]
240
- bg-white px-4 py-4 shadow-[0_1px_2px_rgb(31_41_55/0.05)]"
241
- >
242
- {mail.from ? <MailMetaRow label="From" value={mail.from} /> : null}
243
- {mail.subject ? (
244
- <MailMetaRow label="Subject" value={mail.subject} />
245
- ) : null}
246
- <MailMetaRow label="To" value={<RecipientItems items={mail.to} />} />
247
- <MailMetaRow label="CC" value={<RecipientItems items={mail.cc} />} />
248
- <MailMetaRow label="BCC" value={<RecipientItems items={mail.bcc} />} />
249
- {mail.noSignature ? (
250
- <MailMetaRow label="Signature" value="none" />
251
- ) : null}
252
- {mail.attachments.length ? (
253
- <MailMetaRow
254
- label="Attachments"
255
- value={
256
- <AttachmentList
257
- paths={mail.attachments}
258
- pending={actionPending}
259
- onRemove={onRemoveAttachment}
260
- />
261
- }
262
- />
263
- ) : null}
264
- </div>
265
-
266
- {error ? <p className="m-0 text-sm text-[#9c3a3a]">{error}</p> : null}
267
-
268
- {preview ? (
269
- <article
270
- className="overflow-hidden rounded-xl border border-[#e7ebe7] bg-white
271
- shadow-[0_1px_2px_rgb(31_41_55/0.05)]"
272
- >
273
- <section className="px-4 py-4">
274
- {!preview.body.trim() && !preview.signature ? (
275
- <p className="m-0 text-sm text-[#6c7a6d]">(empty body)</p>
276
- ) : (
277
- <div className="prose prose-neutral max-w-none">
278
- <BarkdownMarkdown
279
- value={
280
- [preview.body, preview.signature]
281
- .filter(Boolean)
282
- .join("\n\n") as string
283
- }
284
- />
285
- </div>
286
- )}
287
- </section>
288
- </article>
289
- ) : null}
290
- </div>
291
- )
292
- }
293
-
294
- export function MailDetailPage({
295
- folder,
296
- subpath: mailId,
297
- listHref,
298
- hostActions,
299
- }: Pick<
300
- FolderPluginViewProps,
301
- "folder" | "subpath" | "listHref" | "hostActions"
302
- >) {
303
- const query = useMailDetail(folder, mailId)
304
- const action = useMailAction(folder)
305
-
306
- async function handleArchive() {
307
- if (!mail) return
308
- try {
309
- await action.mutateAsync({
310
- action: "archive",
311
- id: mailId,
312
- path: folder ?? "",
313
- revision: mail.revision,
314
- })
315
- void query.refetch()
316
- } catch (error) {
317
- toast.error(mailErrorText(error))
318
- }
319
- }
320
-
321
- async function handleRemoveAttachment(attachment: string) {
322
- if (!mail) return
323
- if (
324
- !window.confirm(
325
- `Remove "${attachment}" from this draft? The file is kept on disk.`,
326
- )
327
- )
328
- return
329
- try {
330
- await action.mutateAsync({
331
- action: "remove-attachment",
332
- id: mailId,
333
- path: folder ?? "",
334
- revision: mail.revision,
335
- attachment,
336
- })
337
- void query.refetch()
338
- } catch (error) {
339
- toast.error(mailErrorText(error))
340
- }
341
- }
342
-
343
- async function handleDelete() {
344
- if (!mail) return
345
- if (
346
- !window.confirm(
347
- "Delete this draft? It will be moved to draft history first.",
348
- )
349
- )
350
- return
351
- try {
352
- await action.mutateAsync({
353
- action: "delete",
354
- id: mailId,
355
- path: folder ?? "",
356
- revision: mail.revision,
357
- })
358
- window.location.assign(listHref)
359
- } catch (error) {
360
- toast.error(mailErrorText(error))
361
- }
362
- }
363
-
364
- const senderNeeded = Boolean(query.data?.mail && !query.data.mail.from)
365
- const fromQuery = useMailSender(folder, mailId, senderNeeded)
366
-
367
- const resolvedFrom =
368
- query.data?.mail?.from ??
369
- (fromQuery.data?.name && fromQuery.data?.from
370
- ? `${fromQuery.data.name} <${fromQuery.data.from}>`
371
- : (fromQuery.data?.from ?? null))
372
- const mail = query.data?.mail
373
- ? { ...query.data.mail, from: resolvedFrom }
374
- : undefined
375
-
376
- const conversationEnabled = Boolean(
377
- query.data?.mail &&
378
- (query.data.mail.inReplyTo ?? query.data.mail.references.length > 0),
379
- )
380
-
381
- if (!folder || !mailId) {
382
- return (
383
- <div
384
- className="mx-auto grid w-full max-w-3xl min-w-0 grid-cols-1 gap-4 px-4
385
- py-5"
386
- >
387
- <p className="m-0 text-sm text-[#6c7a6d]">
388
- Select a mail from the list.
389
- </p>
390
- <a
391
- className="w-fit cursor-pointer rounded-md border border-[#dde1dc]
392
- bg-white px-3 py-1.5 text-[0.85rem] text-[#4a554b]
393
- hover:bg-[#f3f5f2]"
394
- href={listHref}
395
- >
396
- Back
397
- </a>
398
- </div>
399
- )
400
- }
401
-
402
- return (
403
- <div
404
- className="mx-auto grid w-full max-w-3xl min-w-0 grid-cols-1 gap-4 px-4
405
- py-5"
406
- >
407
- <a
408
- className="inline-flex w-fit items-center gap-2 rounded-md border
409
- border-[#dde1dc] bg-white px-3 py-1.5 text-sm text-[#4a554b]
410
- no-underline hover:bg-[#f3f5f2]"
411
- href={listHref}
412
- >
413
- <ArrowLeft className="size-4" />
414
- Back to Mail
415
- </a>
416
-
417
- {query.isPending ? (
418
- <p className="m-0 text-sm text-[#6c7a6d]">Loading…</p>
419
- ) : null}
420
-
421
- {query.error && !query.data?.mail ? (
422
- <p className="m-0 text-sm text-[#9c3a3a]">
423
- Could not load mail preview.
424
- </p>
425
- ) : null}
426
-
427
- {mail ? (
428
- <MailDetailView
429
- mail={mail}
430
- preview={query.error ? undefined : query.data?.preview}
431
- error={
432
- query.error
433
- ? "Could not load mail preview."
434
- : query.data?.preview
435
- ? undefined
436
- : "Could not load mail preview."
437
- }
438
- refreshing={query.isFetching}
439
- onRefresh={() => void query.refetch()}
440
- onArchive={
441
- mail.status === "sent" ? undefined : () => void handleArchive()
442
- }
443
- onDelete={() => void handleDelete()}
444
- actionPending={action.isPending}
445
- onOpenPath={hostActions?.openPath}
446
- onRemoveAttachment={(attachment) =>
447
- void handleRemoveAttachment(attachment)
448
- }
449
- />
450
- ) : null}
451
-
452
- <MailConversationPanel
453
- enabled={conversationEnabled}
454
- folder={folder}
455
- mailId={mailId}
456
- />
457
-
458
- {mail ? (
459
- <MailSendBox folder={folder} mailId={mailId} status={mail.status} />
460
- ) : null}
461
-
462
- {!query.isPending && !query.error && !query.data?.mail ? (
463
- <p className="m-0 text-sm text-[#6c7a6d]">Mail not found.</p>
464
- ) : null}
465
- </div>
466
- )
467
- }
@@ -1,252 +0,0 @@
1
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
2
-
3
- import { folderPathId } from "@raggle-ai/folder-plugin/client"
4
-
5
- import { mailRequest } from "./mail-request.js"
6
-
7
- export type MailListResponse = {
8
- folder: string
9
- items: MailListItemPayload[]
10
- }
11
-
12
- export type MailListItemPayload = {
13
- attachments: string[]
14
- bcc: string[]
15
- cc: string[]
16
- createdAt: string | null
17
- excerpt: string
18
- filePath: string
19
- folder: string | null
20
- from: string | null
21
- id: string
22
- inReplyTo: string | null
23
- references: string[]
24
- revision: string
25
- status: string | null
26
- subject: string | null
27
- tags: string[]
28
- to: string[]
29
- updatedAt: string | null
30
- noSignature: boolean
31
- }
32
-
33
- export type MailDetailPayload = Omit<MailListItemPayload, "excerpt"> & {
34
- body: string
35
- }
36
-
37
- export type MailSenderPayload = {
38
- from: string | null
39
- name: string | null
40
- ok: boolean
41
- }
42
-
43
- export type ConversationMessagePayload = {
44
- body: string
45
- date: string | null
46
- from: string | null
47
- id: string
48
- subject: string | null
49
- to: string | null
50
- }
51
-
52
- export type ConversationOutcome =
53
- "empty" | "ok" | "partial" | "not-found" | "timeout"
54
-
55
- export type ConversationPayload = {
56
- items: ConversationMessagePayload[]
57
- missing: string[]
58
- outcome: ConversationOutcome
59
- }
60
-
61
- export type MailSendPreviewPayload = {
62
- dryRun: true
63
- output: string
64
- revision: string
65
- }
66
-
67
- export type MailSendResultPayload = {
68
- dryRun: false
69
- output: string
70
- }
71
-
72
- function mailPath(folder: string | null | undefined) {
73
- return { path: folder ?? "" }
74
- }
75
-
76
- function folderCacheKey(folder: string | null | undefined) {
77
- return ["folder", folder ? folderPathId(folder) : "none", "mail"] as const
78
- }
79
-
80
- export function useMailList(folder: string | null | undefined) {
81
- return useQuery({
82
- enabled: Boolean(folder),
83
- gcTime: 5 * 60_000,
84
- queryFn: ({ signal }) =>
85
- mailRequest<MailListResponse>("/api/mail", {
86
- search: mailPath(folder),
87
- signal,
88
- }),
89
- queryKey: [...folderCacheKey(folder)],
90
- staleTime: 15_000,
91
- })
92
- }
93
-
94
- export type MailPreviewPayload = {
95
- body: string
96
- signature: string | null
97
- }
98
-
99
- export type MailDetailResponsePayload = {
100
- mail: MailDetailPayload
101
- preview?: MailPreviewPayload
102
- }
103
-
104
- export function useMailDetail(
105
- folder: string | null | undefined,
106
- mailId: string | null | undefined,
107
- ) {
108
- return useQuery<MailDetailResponsePayload>({
109
- enabled: Boolean(folder && mailId),
110
- gcTime: 5 * 60_000,
111
- queryFn: ({ signal }) =>
112
- mailRequest<MailDetailResponsePayload>("/api/mail/item", {
113
- search: { ...mailPath(folder), id: mailId ?? "" },
114
- signal,
115
- }),
116
- queryKey: [...folderCacheKey(folder), mailId ?? ""],
117
- retry: false,
118
- staleTime: 15_000,
119
- })
120
- }
121
-
122
- /** Lazy sender resolution: runs only when the draft has no recorded From. */
123
- export function useMailSender(
124
- folder: string | null | undefined,
125
- mailId: string | null | undefined,
126
- enabled: boolean,
127
- ) {
128
- return useQuery({
129
- enabled: Boolean(enabled && mailId),
130
- gcTime: 5 * 60_000,
131
- queryFn: ({ signal }) =>
132
- mailRequest<MailSenderPayload>("/api/mail/from", {
133
- search: { ...mailPath(folder), id: mailId ?? "" },
134
- signal,
135
- }),
136
- queryKey: [...folderCacheKey(folder), mailId ?? "", "from"],
137
- retry: false,
138
- staleTime: 15_000,
139
- })
140
- }
141
-
142
- export function useMailConversation(
143
- folder: string | null | undefined,
144
- mailId: string | null | undefined,
145
- enabled: boolean,
146
- ) {
147
- return useQuery<ConversationPayload>({
148
- enabled: Boolean(enabled && mailId),
149
- gcTime: 5 * 60_000,
150
- queryFn: ({ signal }) =>
151
- mailRequest<ConversationPayload>("/api/mail/conversation", {
152
- search: { ...mailPath(folder), id: mailId ?? "" },
153
- signal,
154
- }),
155
- queryKey: [...folderCacheKey(folder), mailId ?? "", "conversation"],
156
- retry: false,
157
- staleTime: 5 * 60_000,
158
- })
159
- }
160
-
161
- export type MailActionInput = {
162
- action: "archive" | "delete" | "remove-attachment"
163
- id: string
164
- path: string
165
- revision: string
166
- /** Absolute attachment path; used only with the remove-attachment action. */
167
- attachment?: string
168
- }
169
-
170
- export function useMailAction(folder: string | null | undefined) {
171
- const queryClient = useQueryClient()
172
- return useMutation({
173
- mutationFn: (input: MailActionInput) =>
174
- mailRequest<{ action: string; id: string; ok: true }>(
175
- "/api/mail/action",
176
- {
177
- body: input,
178
- method: "POST",
179
- },
180
- ),
181
- onSuccess: () => {
182
- void queryClient.invalidateQueries({
183
- queryKey: [...folderCacheKey(folder)],
184
- })
185
- },
186
- })
187
- }
188
-
189
- export function useMailSend(folder: string | null | undefined) {
190
- const queryClient = useQueryClient()
191
- return useMutation({
192
- mutationFn: (input: { dryRun: boolean; id: string; revision?: string }) =>
193
- mailRequest<MailSendPreviewPayload | MailSendResultPayload>(
194
- "/api/mail/send",
195
- {
196
- body: { path: folder ?? "", ...input },
197
- method: "POST",
198
- },
199
- ),
200
- onSuccess: () => {
201
- void queryClient.invalidateQueries({
202
- queryKey: [...folderCacheKey(folder)],
203
- })
204
- },
205
- })
206
- }
207
-
208
- export type ConversationPanelState =
209
- | { variant: "hidden" }
210
- | { variant: "loading" }
211
- | { variant: "error"; message: string }
212
- | { variant: "empty" }
213
- | { variant: "ok" }
214
- | { variant: "partial"; missing: number }
215
- | { variant: "not-found" }
216
- | { variant: "timeout" }
217
-
218
- /**
219
- * Pure mapping from the conversation query to a UI state. A disabled query
220
- * (no references, or the detail still loading) is "hidden" — never an
221
- * endless loading spinner.
222
- */
223
- export function conversationPanelState(input: {
224
- data?: ConversationPayload
225
- enabled: boolean
226
- error: unknown
227
- isPending: boolean
228
- }): ConversationPanelState {
229
- if (!input.enabled) return { variant: "hidden" }
230
- if (input.isPending && !input.data) return { variant: "loading" }
231
- if (input.error && !input.data) {
232
- const message =
233
- input.error instanceof Error
234
- ? input.error.message
235
- : "Could not load the conversation."
236
- return { variant: "error", message }
237
- }
238
- switch (input.data?.outcome) {
239
- case "empty":
240
- return { variant: "empty" }
241
- case "ok":
242
- return input.data.items.length ? { variant: "ok" } : { variant: "empty" }
243
- case "partial":
244
- return { variant: "partial", missing: input.data.missing.length }
245
- case "not-found":
246
- return { variant: "not-found" }
247
- case "timeout":
248
- return { variant: "timeout" }
249
- default:
250
- return { variant: "hidden" }
251
- }
252
- }