agent-whiteboard 1.0.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +61 -5
  3. package/dist/client/assets/abnfDiagram-VCTEODGH-Dj8sd_5g.js +1 -0
  4. package/dist/client/assets/{arc-4691b008.js → arc-Aq92EeUK.js} +1 -1
  5. package/dist/client/assets/architectureDiagram-5GKGNRK7-B1QyVL0F.js +36 -0
  6. package/dist/client/assets/blockDiagram-I7D4REHJ-CmU2SSFe.js +129 -0
  7. package/dist/client/assets/{bootstrap.min-21652813.js → bootstrap.min-UtnB-TxT.js} +1 -2
  8. package/dist/client/assets/c4Diagram-7LVT6UL2-DWmhp77c.js +38 -0
  9. package/dist/client/assets/channel-C9ni1Via.js +1 -0
  10. package/dist/client/assets/{chunk-QZHKN3VN-0f554985.js → chunk-2Q5K7J3B-CQLo8Sbv.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-23d48b24.js → chunk-5VM5RSS4-fJTWmTTt.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-1104c326.js → chunk-F27PBJKO-DqzEf5gi.js} +1 -1
  13. package/dist/client/assets/chunk-IMKFNOWR-CMamLTz3.js +231 -0
  14. package/dist/client/assets/{chunk-4BX2VUAB-3c93511d.js → chunk-JWPE2WC7-tyYPyD43.js} +1 -1
  15. package/dist/client/assets/chunk-POPQ4Y6H-XdY4rO_V.js +1 -0
  16. package/dist/client/assets/chunk-SVP7TREG-COvqzqaO.js +88 -0
  17. package/dist/client/assets/chunk-TICWLB2K-BsJSFuPC.js +206 -0
  18. package/dist/client/assets/{chunk-55IACEB6-0243aefd.js → chunk-XXDRQBXY-HfVrYBJb.js} +1 -1
  19. package/dist/client/assets/classDiagram-ZZMXUADV-DktNEiGC.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-VYDZK3BY-DktNEiGC.js +1 -0
  21. package/dist/client/assets/cose-bilkent-JH36ORCC-UXGdwSjS.js +1 -0
  22. package/dist/client/assets/cynefin-OW5HDTMX-PWlPYnpn.js +178 -0
  23. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CYE5H1wR.js +62 -0
  24. package/dist/client/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  25. package/dist/client/assets/dagre-GXQ25YYZ-DaknBOKq.js +4 -0
  26. package/dist/client/assets/diagram-S7CK7UJ4-Dz7rZHxF.js +30 -0
  27. package/dist/client/assets/diagram-UQ7AKVKN-DzCi6tiS.js +41 -0
  28. package/dist/client/assets/{diagram-KO2AKTUF-81f42eeb.js → diagram-VSXAHHWV-TlUzhXgv.js} +3 -3
  29. package/dist/client/assets/diagram-VX7I27RA-Ds7jdzK7.js +24 -0
  30. package/dist/client/assets/diagram-Z3DM3KII-C--JD7cf.js +24 -0
  31. package/dist/client/assets/ebnfDiagram-PWID7BFC-Dw5OM-dL.js +1 -0
  32. package/dist/client/assets/embed-DcNeILcO.js +211 -0
  33. package/dist/client/assets/erDiagram-RLTQ6QDP-DXalBHjA.js +99 -0
  34. package/dist/client/assets/flowDiagram-HODETNUW-DcI3UqyE.js +168 -0
  35. package/dist/client/assets/ganttDiagram-EL5Y4UJY-DyrkIo6c.js +292 -0
  36. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-fJKfGRcg.js +106 -0
  37. package/dist/client/assets/index-CIhkXT_z.js +19 -0
  38. package/dist/client/assets/index-DEiH2AE7.css +1 -0
  39. package/dist/client/assets/infoDiagram-27XIBGKW-Cijiqswb.js +2 -0
  40. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-50b43d03.js → ishikawaDiagram-5VMMS53U-C1WqWU2l.js} +2 -2
  41. package/dist/client/assets/{journeyDiagram-JHISSGLW-dce41bdf.js → journeyDiagram-3NMN7TZE-7j05mbLR.js} +4 -4
  42. package/dist/client/assets/{kanban-definition-UN3LZRKU-92401a86.js → kanban-definition-UXKFOSKX-D5mGDfs_.js} +1 -1
  43. package/dist/client/assets/{katex-4c9ea27f.js → katex-BTI-AXqd.js} +3 -3
  44. package/dist/client/assets/{katex-0cba7652.js → katex-C5jXJg4s.js} +3 -3
  45. package/dist/client/assets/layout-KEin06-H.js +1 -0
  46. package/dist/client/assets/linear-D1EL1Azg.js +1 -0
  47. package/dist/client/assets/mermaid.core-CLvSDmrA.js +310 -0
  48. package/dist/client/assets/{mindmap-definition-RKZ34NQL-66fe4544.js → mindmap-definition-YA3MSWOX-BsCqKd_d.js} +12 -12
  49. package/dist/client/assets/{ordinal-db88ae6f.js → ordinal-BeghXfj9.js} +1 -1
  50. package/dist/client/assets/pegDiagram-XKGWAZYB-Du5lDU3W.js +1 -0
  51. package/dist/client/assets/pieDiagram-E7YTZNPT-hjv5Ao6B.js +39 -0
  52. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-f64a7ed7.js → quadrantDiagram-AXDQQJYC-DNon8LWK.js} +4 -4
  53. package/dist/client/assets/railroadDiagram-O6MQD6OU-yNZz2zxo.js +1 -0
  54. package/dist/client/assets/requirementDiagram-BXWQKSXE-CZiiEUHW.js +84 -0
  55. package/dist/client/assets/sankeyDiagram-P5KCCOFB-D-u8ZQUV.js +40 -0
  56. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BxjTf3q9.js +162 -0
  57. package/dist/client/assets/sizeCapture-INFHLROL-Dt4Wg8C5.js +1 -0
  58. package/dist/client/assets/stateDiagram-D77RDMKH-B8BM6dln.js +1 -0
  59. package/dist/client/assets/stateDiagram-v2-MP3YSRHH-MJauX84T.js +1 -0
  60. package/dist/client/assets/step-BEQzeS8V.js +1 -0
  61. package/dist/client/assets/swimlanes-42K2YHIH-eGWrsWAA.js +1 -0
  62. package/dist/client/assets/swimlanesDiagram-VR7AAH4N-BYFu8tyL.js +8 -0
  63. package/dist/client/assets/{time-043b3464.js → time-BaoZouO5.js} +1 -1
  64. package/dist/client/assets/{timeline-definition-PNZ67QCA-b0c81243.js → timeline-definition-24CTP7MA-CAM-3Qj9.js} +10 -10
  65. package/dist/client/assets/vennDiagram-4TSXK5OY-CuFEwwD0.js +34 -0
  66. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-8d4d7626.js → wardleyDiagram-VM6X3IG4-DI5yCVh0.js} +3 -3
  67. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BI0Bn-OQ.js +7 -0
  68. package/dist/client/index.html +2 -3
  69. package/dist/server/app.js +28 -396
  70. package/dist/server/channel.js +5 -3
  71. package/dist/server/export-html.js +30 -60
  72. package/dist/server/export-zip.js +83 -0
  73. package/dist/server/import-zip.js +196 -0
  74. package/dist/server/index.js +2 -1
  75. package/dist/server/mcp.js +28 -13
  76. package/dist/server/port.js +18 -0
  77. package/dist/server/quiz-submission.js +116 -0
  78. package/dist/server/quiz.js +110 -0
  79. package/dist/server/render-core.js +8 -7
  80. package/dist/server/routes/export.js +95 -0
  81. package/dist/server/routes/import.js +24 -0
  82. package/dist/server/routes/quiz.js +12 -0
  83. package/dist/server/routes/render.js +184 -0
  84. package/dist/server/routes/slideshow.js +52 -0
  85. package/dist/server/routes/snapshots.js +119 -0
  86. package/dist/server/snapshot-reader.js +58 -20
  87. package/dist/server/snapshot-writer.js +18 -16
  88. package/dist/server/validate.js +14 -0
  89. package/dist/server/viewport-cache.js +97 -25
  90. package/package.json +13 -7
  91. package/dist/client/assets/architecture-7EHR7CIX-1221aa61.js +0 -1
  92. package/dist/client/assets/architectureDiagram-3BPJPVTR-1eef4d3c.js +0 -36
  93. package/dist/client/assets/blockDiagram-GPEHLZMM-313fb06c.js +0 -132
  94. package/dist/client/assets/c4Diagram-AAUBKEIU-92b023ad.js +0 -10
  95. package/dist/client/assets/channel-5ee4a8a7.js +0 -1
  96. package/dist/client/assets/chunk-2J33WTMH-fb021fdc.js +0 -1
  97. package/dist/client/assets/chunk-727SXJPM-8e0e0228.js +0 -206
  98. package/dist/client/assets/chunk-AQP2D5EJ-2ccc13c2.js +0 -231
  99. package/dist/client/assets/classDiagram-4FO5ZUOK-42ffecb6.js +0 -1
  100. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-42ffecb6.js +0 -1
  101. package/dist/client/assets/cose-bilkent-S5V4N54A-45ec2e52.js +0 -1
  102. package/dist/client/assets/cytoscape.esm-1491e259.js +0 -331
  103. package/dist/client/assets/dagre-BM42HDAG-ed748885.js +0 -4
  104. package/dist/client/assets/diagram-2AECGRRQ-ccc0ddec.js +0 -43
  105. package/dist/client/assets/diagram-5GNKFQAL-342efdea.js +0 -10
  106. package/dist/client/assets/diagram-LMA3HP47-39e66739.js +0 -24
  107. package/dist/client/assets/diagram-OG6HWLK6-b3e61903.js +0 -24
  108. package/dist/client/assets/embed-2b4adf34.js +0 -201
  109. package/dist/client/assets/erDiagram-TEJ5UH35-281f5cc7.js +0 -85
  110. package/dist/client/assets/eventmodeling-FCH6USID-6508120e.js +0 -1
  111. package/dist/client/assets/flowDiagram-I6XJVG4X-c3a4269e.js +0 -162
  112. package/dist/client/assets/ganttDiagram-6RSMTGT7-013b07ec.js +0 -292
  113. package/dist/client/assets/gitGraph-WXDBUCRP-02827359.js +0 -1
  114. package/dist/client/assets/gitGraphDiagram-PVQCEYII-de184acb.js +0 -106
  115. package/dist/client/assets/graph-f3dfaf53.js +0 -1
  116. package/dist/client/assets/index-d674a6a4.css +0 -1
  117. package/dist/client/assets/index-ffc5af4b.js +0 -5
  118. package/dist/client/assets/info-J43DQDTF-81e32294.js +0 -1
  119. package/dist/client/assets/infoDiagram-5YYISTIA-196101ee.js +0 -2
  120. package/dist/client/assets/layout-3a4eff80.js +0 -1
  121. package/dist/client/assets/linear-fed00341.js +0 -1
  122. package/dist/client/assets/mermaid-parser.core-52f9cd92.js +0 -173
  123. package/dist/client/assets/mermaid.core-7dfc221a.js +0 -306
  124. package/dist/client/assets/packet-YPE3B663-278a272a.js +0 -1
  125. package/dist/client/assets/pie-LRSECV5Y-2e55d9ae.js +0 -1
  126. package/dist/client/assets/pieDiagram-4H26LBE5-65b1bcbd.js +0 -30
  127. package/dist/client/assets/radar-GUYGQ44K-608e459f.js +0 -1
  128. package/dist/client/assets/requirementDiagram-4Y6WPE33-90ebf889.js +0 -84
  129. package/dist/client/assets/sankeyDiagram-5OEKKPKP-2d1a83b3.js +0 -40
  130. package/dist/client/assets/sequenceDiagram-3UESZ5HK-aece3633.js +0 -162
  131. package/dist/client/assets/stateDiagram-AJRCARHV-9bd5b5f4.js +0 -1
  132. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-7c16c251.js +0 -1
  133. package/dist/client/assets/step-171ad54a.js +0 -1
  134. package/dist/client/assets/treeView-BLDUP644-e54b9ddb.js +0 -1
  135. package/dist/client/assets/treemap-LRROVOQU-6a81775b.js +0 -1
  136. package/dist/client/assets/vennDiagram-CIIHVFJN-8eaf0464.js +0 -34
  137. package/dist/client/assets/wardley-L42UT6IY-7abd921f.js +0 -1
  138. package/dist/client/assets/xychartDiagram-2RQKCTM6-450f333c.js +0 -7
  139. /package/dist/client/assets/{KaTeX_AMS-Regular-0cdd387c.woff2 → KaTeX_AMS-Regular-BQhdFMY1.woff2} +0 -0
  140. /package/dist/client/assets/{KaTeX_AMS-Regular-30da91e8.woff → KaTeX_AMS-Regular-DMm9YOAa.woff} +0 -0
  141. /package/dist/client/assets/{KaTeX_AMS-Regular-68534840.ttf → KaTeX_AMS-Regular-DRggAlZN.ttf} +0 -0
  142. /package/dist/client/assets/{KaTeX_Caligraphic-Bold-07d8e303.ttf → KaTeX_Caligraphic-Bold-ATXxdsX0.ttf} +0 -0
  143. /package/dist/client/assets/{KaTeX_Caligraphic-Bold-1ae6bd74.woff → KaTeX_Caligraphic-Bold-BEiXGLvX.woff} +0 -0
  144. /package/dist/client/assets/{KaTeX_Caligraphic-Bold-de7701e4.woff2 → KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2} +0 -0
  145. /package/dist/client/assets/{KaTeX_Caligraphic-Regular-3398dd02.woff → KaTeX_Caligraphic-Regular-CTRA-rTL.woff} +0 -0
  146. /package/dist/client/assets/{KaTeX_Caligraphic-Regular-5d53e70a.woff2 → KaTeX_Caligraphic-Regular-Di6jR-x-.woff2} +0 -0
  147. /package/dist/client/assets/{KaTeX_Caligraphic-Regular-ed0b7437.ttf → KaTeX_Caligraphic-Regular-wX97UBjC.ttf} +0 -0
  148. /package/dist/client/assets/{KaTeX_Fraktur-Bold-9163df9c.ttf → KaTeX_Fraktur-Bold-BdnERNNW.ttf} +0 -0
  149. /package/dist/client/assets/{KaTeX_Fraktur-Bold-9be7ceb8.woff → KaTeX_Fraktur-Bold-BsDP51OF.woff} +0 -0
  150. /package/dist/client/assets/{KaTeX_Fraktur-Bold-74444efd.woff2 → KaTeX_Fraktur-Bold-CL6g_b3V.woff2} +0 -0
  151. /package/dist/client/assets/{KaTeX_Fraktur-Regular-1e6f9579.ttf → KaTeX_Fraktur-Regular-CB_wures.ttf} +0 -0
  152. /package/dist/client/assets/{KaTeX_Fraktur-Regular-51814d27.woff2 → KaTeX_Fraktur-Regular-CTYiF6lA.woff2} +0 -0
  153. /package/dist/client/assets/{KaTeX_Fraktur-Regular-5e28753b.woff → KaTeX_Fraktur-Regular-Dxdc4cR9.woff} +0 -0
  154. /package/dist/client/assets/{KaTeX_Main-Bold-0f60d1b8.woff2 → KaTeX_Main-Bold-Cx986IdX.woff2} +0 -0
  155. /package/dist/client/assets/{KaTeX_Main-Bold-c76c5d69.woff → KaTeX_Main-Bold-Jm3AIy58.woff} +0 -0
  156. /package/dist/client/assets/{KaTeX_Main-Bold-138ac28d.ttf → KaTeX_Main-Bold-waoOVXN0.ttf} +0 -0
  157. /package/dist/client/assets/{KaTeX_Main-BoldItalic-99cd42a3.woff2 → KaTeX_Main-BoldItalic-DxDJ3AOS.woff2} +0 -0
  158. /package/dist/client/assets/{KaTeX_Main-BoldItalic-70ee1f64.ttf → KaTeX_Main-BoldItalic-DzxPMmG6.ttf} +0 -0
  159. /package/dist/client/assets/{KaTeX_Main-BoldItalic-a6f7ec0d.woff → KaTeX_Main-BoldItalic-SpSLRI95.woff} +0 -0
  160. /package/dist/client/assets/{KaTeX_Main-Italic-0d85ae7c.ttf → KaTeX_Main-Italic-3WenGoN9.ttf} +0 -0
  161. /package/dist/client/assets/{KaTeX_Main-Italic-f1d6ef86.woff → KaTeX_Main-Italic-BMLOBm91.woff} +0 -0
  162. /package/dist/client/assets/{KaTeX_Main-Italic-97479ca6.woff2 → KaTeX_Main-Italic-NWA7e6Wa.woff2} +0 -0
  163. /package/dist/client/assets/{KaTeX_Main-Regular-c2342cd8.woff2 → KaTeX_Main-Regular-B22Nviop.woff2} +0 -0
  164. /package/dist/client/assets/{KaTeX_Main-Regular-c6368d87.woff → KaTeX_Main-Regular-Dr94JaBh.woff} +0 -0
  165. /package/dist/client/assets/{KaTeX_Main-Regular-d0332f52.ttf → KaTeX_Main-Regular-ypZvNtVU.ttf} +0 -0
  166. /package/dist/client/assets/{KaTeX_Math-BoldItalic-f9377ab0.ttf → KaTeX_Math-BoldItalic-B3XSjfu4.ttf} +0 -0
  167. /package/dist/client/assets/{KaTeX_Math-BoldItalic-dc47344d.woff2 → KaTeX_Math-BoldItalic-CZnvNsCZ.woff2} +0 -0
  168. /package/dist/client/assets/{KaTeX_Math-BoldItalic-850c0af5.woff → KaTeX_Math-BoldItalic-iY-2wyZ7.woff} +0 -0
  169. /package/dist/client/assets/{KaTeX_Math-Italic-8a8d2445.woff → KaTeX_Math-Italic-DA0__PXp.woff} +0 -0
  170. /package/dist/client/assets/{KaTeX_Math-Italic-08ce98e5.ttf → KaTeX_Math-Italic-flOr_0UB.ttf} +0 -0
  171. /package/dist/client/assets/{KaTeX_Math-Italic-7af58c5e.woff2 → KaTeX_Math-Italic-t53AETM-.woff2} +0 -0
  172. /package/dist/client/assets/{KaTeX_SansSerif-Bold-1ece03f7.ttf → KaTeX_SansSerif-Bold-CFMepnvq.ttf} +0 -0
  173. /package/dist/client/assets/{KaTeX_SansSerif-Bold-e99ae511.woff2 → KaTeX_SansSerif-Bold-D1sUS0GD.woff2} +0 -0
  174. /package/dist/client/assets/{KaTeX_SansSerif-Bold-ece03cfd.woff → KaTeX_SansSerif-Bold-DbIhKOiC.woff} +0 -0
  175. /package/dist/client/assets/{KaTeX_SansSerif-Italic-00b26ac8.woff2 → KaTeX_SansSerif-Italic-C3H0VqGB.woff2} +0 -0
  176. /package/dist/client/assets/{KaTeX_SansSerif-Italic-91ee6750.woff → KaTeX_SansSerif-Italic-DN2j7dab.woff} +0 -0
  177. /package/dist/client/assets/{KaTeX_SansSerif-Italic-3931dd81.ttf → KaTeX_SansSerif-Italic-YYjJ1zSn.ttf} +0 -0
  178. /package/dist/client/assets/{KaTeX_SansSerif-Regular-f36ea897.ttf → KaTeX_SansSerif-Regular-BNo7hRIc.ttf} +0 -0
  179. /package/dist/client/assets/{KaTeX_SansSerif-Regular-11e4dc8a.woff → KaTeX_SansSerif-Regular-CS6fqUqJ.woff} +0 -0
  180. /package/dist/client/assets/{KaTeX_SansSerif-Regular-68e8c73e.woff2 → KaTeX_SansSerif-Regular-DDBCnlJ7.woff2} +0 -0
  181. /package/dist/client/assets/{KaTeX_Script-Regular-1c67f068.ttf → KaTeX_Script-Regular-C5JkGWo-.ttf} +0 -0
  182. /package/dist/client/assets/{KaTeX_Script-Regular-036d4e95.woff2 → KaTeX_Script-Regular-D3wIWfF6.woff2} +0 -0
  183. /package/dist/client/assets/{KaTeX_Script-Regular-d96cdf2b.woff → KaTeX_Script-Regular-D5yQViql.woff} +0 -0
  184. /package/dist/client/assets/{KaTeX_Size1-Regular-c943cc98.woff → KaTeX_Size1-Regular-C195tn64.woff} +0 -0
  185. /package/dist/client/assets/{KaTeX_Size1-Regular-95b6d2f1.ttf → KaTeX_Size1-Regular-Dbsnue_I.ttf} +0 -0
  186. /package/dist/client/assets/{KaTeX_Size1-Regular-6b47c401.woff2 → KaTeX_Size1-Regular-mCD8mA8B.woff2} +0 -0
  187. /package/dist/client/assets/{KaTeX_Size2-Regular-a6b2099f.ttf → KaTeX_Size2-Regular-B7gKUWhC.ttf} +0 -0
  188. /package/dist/client/assets/{KaTeX_Size2-Regular-d04c5421.woff2 → KaTeX_Size2-Regular-Dy4dx90m.woff2} +0 -0
  189. /package/dist/client/assets/{KaTeX_Size2-Regular-2014c523.woff → KaTeX_Size2-Regular-oD1tc_U0.woff} +0 -0
  190. /package/dist/client/assets/{KaTeX_Size3-Regular-6ab6b62e.woff → KaTeX_Size3-Regular-CTq5MqoE.woff} +0 -0
  191. /package/dist/client/assets/{KaTeX_Size3-Regular-500e04d5.ttf → KaTeX_Size3-Regular-DgpXs0kz.ttf} +0 -0
  192. /package/dist/client/assets/{KaTeX_Size4-Regular-99f9c675.woff → KaTeX_Size4-Regular-BF-4gkZK.woff} +0 -0
  193. /package/dist/client/assets/{KaTeX_Size4-Regular-c647367d.ttf → KaTeX_Size4-Regular-DWFBv043.ttf} +0 -0
  194. /package/dist/client/assets/{KaTeX_Size4-Regular-a4af7d41.woff2 → KaTeX_Size4-Regular-Dl5lxZxV.woff2} +0 -0
  195. /package/dist/client/assets/{KaTeX_Typewriter-Regular-e14fed02.woff → KaTeX_Typewriter-Regular-C0xS9mPB.woff} +0 -0
  196. /package/dist/client/assets/{KaTeX_Typewriter-Regular-71d517d6.woff2 → KaTeX_Typewriter-Regular-CO6r4hn1.woff2} +0 -0
  197. /package/dist/client/assets/{KaTeX_Typewriter-Regular-f01f3e87.ttf → KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf} +0 -0
  198. /package/dist/client/assets/{colors-98f238a0.js → colors-Cc3OSVma.js} +0 -0
  199. /package/dist/client/assets/{defaultLocale-a8476f12.js → defaultLocale-CzDG__Ur.js} +0 -0
  200. /package/dist/client/assets/{index-a1656aec.js → index-Bi7T_FA4.js} +0 -0
  201. /package/dist/client/assets/{init-a5b10ee5.js → init-Dmth1JHB.js} +0 -0
  202. /package/dist/client/assets/{range-6bc2a9a0.js → range-OtVwhkKS.js} +0 -0
@@ -0,0 +1,119 @@
1
+ import { getLastWorkspace, setLastWorkspace } from "../session.js";
2
+ import { isValidSnapshotFilename, nodeToFrameSchema } from "../validate.js";
3
+ import { isFrameArray, listAllSnapshots, listSnapshots, loadSnapshotContent } from "../snapshot-reader.js";
4
+ import { deleteSnapshotFiles, deleteWorkspace, validateWorkspaceForDelete } from "../snapshot-writer.js";
5
+ import { applyLoadedSnapshotResult, validateWorkspaceInput } from "../render-core.js";
6
+ import { getSnapshotsRoot } from "../paths.js";
7
+ export function registerSnapshotRoutes(app) {
8
+ // ── History navigator (v0.4 — Sprint 17) ──────────────────────────────────────
9
+ app.get("/snapshots", async (c) => {
10
+ // Workspace is mandatory, no lastWorkspace fallback — matches MCP's
11
+ // list_snapshots exactly (F3/NF20; same validateWorkspaceInput() both
12
+ // transports already use for render()/slideshow()).
13
+ const workspaceResult = validateWorkspaceInput(c.req.query("workspace"));
14
+ if (!workspaceResult.ok) {
15
+ return c.json({ ok: false, error: workspaceResult.error }, 400);
16
+ }
17
+ const { workspace } = workspaceResult;
18
+ const root = getSnapshotsRoot();
19
+ const snapshots = await listSnapshots(workspace, root);
20
+ return c.json({ ok: true, snapshots });
21
+ });
22
+ // ── Sprint 18 — GET /snapshots/all (v0.5) ────────────────────────────────────
23
+ app.get("/snapshots/all", async (c) => {
24
+ const workspace = getLastWorkspace();
25
+ const root = getSnapshotsRoot();
26
+ const workspaces = await listAllSnapshots(root, workspace);
27
+ return c.json({ ok: true, workspaces });
28
+ });
29
+ app.post("/snapshots/load", async (c) => {
30
+ const body = await c.req.json();
31
+ const filename = body.filename;
32
+ if (typeof filename !== "string") {
33
+ return c.json({ ok: false, error: "filename must be a string" }, 400);
34
+ }
35
+ // Path safety: filename must end with _screen.json and contain no / or ..
36
+ if (!isValidSnapshotFilename(filename)) {
37
+ return c.json({ ok: false, error: "invalid filename: path traversal not allowed" });
38
+ }
39
+ const currentWorkspace = getLastWorkspace();
40
+ const root = getSnapshotsRoot();
41
+ // Optional workspace override (v0.5 cross-workspace load).
42
+ let workspace;
43
+ if (body.workspace !== undefined) {
44
+ const validated = validateWorkspaceInput(body.workspace);
45
+ if (!validated.ok) {
46
+ return c.json({ ok: false, error: "invalid workspace: path traversal not allowed" }, 400);
47
+ }
48
+ workspace = validated.workspace;
49
+ }
50
+ else {
51
+ workspace = currentWorkspace;
52
+ }
53
+ const raw = await loadSnapshotContent(workspace, root, filename);
54
+ if (raw === null) {
55
+ return c.json({ ok: false, error: `snapshot not found: ${filename}` });
56
+ }
57
+ let snapshot;
58
+ try {
59
+ snapshot = JSON.parse(raw);
60
+ }
61
+ catch {
62
+ return c.json({ ok: false, error: "snapshot file is malformed JSON" });
63
+ }
64
+ if (!isFrameArray(snapshot.frames)) {
65
+ return c.json({ ok: false, error: "snapshot file is missing required fields" });
66
+ }
67
+ const frames = snapshot.frames;
68
+ const title = typeof snapshot.title === "string" ? snapshot.title : undefined;
69
+ const parsedNodeToFrame = nodeToFrameSchema.safeParse(snapshot.nodeToFrame);
70
+ const nodeToFrame = parsedNodeToFrame.success ? parsedNodeToFrame.data : undefined;
71
+ // Pre-v0.11 snapshots may lack an id (J1, `02`) — not addressable by the
72
+ // viewport cache; the browser falls back to treating it as unseen (auto-fit).
73
+ const snapshotId = typeof snapshot.id === "string" ? snapshot.id : undefined;
74
+ const rawPayload = typeof snapshot.rawPayload === "string" ? snapshot.rawPayload : undefined;
75
+ const result = await applyLoadedSnapshotResult(frames, workspace, title, nodeToFrame, snapshotId, rawPayload);
76
+ if (!result.ok) {
77
+ return c.json({ ok: false, error: result.error });
78
+ }
79
+ return c.json({ ok: true });
80
+ });
81
+ // ── Snapshot delete endpoints (v0.12) ─────────────────────────────────────
82
+ app.post("/snapshots/delete-files", async (c) => {
83
+ const body = await c.req.json();
84
+ const root = getSnapshotsRoot();
85
+ const validated = await validateWorkspaceForDelete(body.workspace, root);
86
+ if (!validated.ok) {
87
+ return c.json({ ok: false, error: validated.error }, validated.status);
88
+ }
89
+ const { workspace } = validated;
90
+ if (!Array.isArray(body.filenames) || body.filenames.length === 0) {
91
+ return c.json({ ok: false, error: "filenames must be a non-empty array" }, 400);
92
+ }
93
+ const filenames = body.filenames;
94
+ for (const f of filenames) {
95
+ if (typeof f !== "string") {
96
+ return c.json({ ok: false, error: "each filename must be a string" }, 400);
97
+ }
98
+ }
99
+ const result = await deleteSnapshotFiles(workspace, root, filenames);
100
+ if (!result.ok) {
101
+ return c.json({ ok: false, error: result.error }, 400);
102
+ }
103
+ return c.json({ ok: true, deleted: result.deleted });
104
+ });
105
+ app.post("/snapshots/delete-workspace", async (c) => {
106
+ const body = await c.req.json();
107
+ const root = getSnapshotsRoot();
108
+ const validated = await validateWorkspaceForDelete(body.workspace, root);
109
+ if (!validated.ok) {
110
+ return c.json({ ok: false, error: validated.error }, validated.status);
111
+ }
112
+ const { workspace } = validated;
113
+ await deleteWorkspace(workspace, root);
114
+ if (getLastWorkspace() === workspace) {
115
+ setLastWorkspace("");
116
+ }
117
+ return c.json({ ok: true });
118
+ });
119
+ }
@@ -1,5 +1,6 @@
1
- import { readdirSync, readFileSync } from "fs";
1
+ import { readdir, readFile } from "fs/promises";
2
2
  import { join } from "path";
3
+ import { nodeToFrameSchema } from "./validate.js";
3
4
  /** Shared "is this a valid Frame[]" predicate (F6/NF23) — the one implementation checking non-empty array + per-element shape. */
4
5
  export function isFrameArray(value) {
5
6
  return (Array.isArray(value) &&
@@ -21,11 +22,17 @@ export function isFrameArray(value) {
21
22
  export function badgeType(frames) {
22
23
  return frames.length > 1 ? "step-frames" : frames[0].type;
23
24
  }
24
- export function listSnapshots(workspace, dir) {
25
+ /**
26
+ * NF49: async fs/promises I/O throughout this file — `listSnapshots()` and
27
+ * `listAllSnapshots()` (below) read every snapshot file in a workspace (or
28
+ * every workspace), so a synchronous implementation blocks the event loop
29
+ * for the whole scan; the async equivalents yield between file reads.
30
+ */
31
+ export async function listSnapshots(workspace, dir) {
25
32
  const workspaceDir = join(dir, workspace);
26
33
  let files;
27
34
  try {
28
- files = readdirSync(workspaceDir).filter((f) => f.endsWith("_screen.json"));
35
+ files = (await readdir(workspaceDir)).filter((f) => f.endsWith("_screen.json"));
29
36
  }
30
37
  catch {
31
38
  return [];
@@ -33,7 +40,7 @@ export function listSnapshots(workspace, dir) {
33
40
  const entries = [];
34
41
  for (const filename of files) {
35
42
  try {
36
- const raw = readFileSync(join(workspaceDir, filename), "utf-8");
43
+ const raw = await readFile(join(workspaceDir, filename), "utf-8");
37
44
  const parsed = JSON.parse(raw);
38
45
  if (typeof parsed.timestamp !== "string" || !isFrameArray(parsed.frames)) {
39
46
  console.error(`[agent-whiteboard] snapshot-reader: skipping malformed file: ${filename}`);
@@ -64,10 +71,10 @@ export function listSnapshots(workspace, dir) {
64
71
  * Scan every workspace subdirectory under `dir` and return their snapshots grouped.
65
72
  * Workspaces with no readable snapshots are omitted from the result.
66
73
  */
67
- export function listAllSnapshots(dir, currentWorkspace) {
74
+ export async function listAllSnapshots(dir, currentWorkspace) {
68
75
  let entries;
69
76
  try {
70
- entries = readdirSync(dir, { withFileTypes: true })
77
+ entries = (await readdir(dir, { withFileTypes: true }))
71
78
  .filter((d) => d.isDirectory())
72
79
  .map((d) => d.name);
73
80
  }
@@ -76,7 +83,7 @@ export function listAllSnapshots(dir, currentWorkspace) {
76
83
  }
77
84
  const groups = [];
78
85
  for (const name of entries) {
79
- const snapshots = listSnapshots(name, dir);
86
+ const snapshots = await listSnapshots(name, dir);
80
87
  if (snapshots.length === 0)
81
88
  continue;
82
89
  groups.push({ name, isCurrent: name === currentWorkspace, snapshots });
@@ -88,18 +95,18 @@ export function listAllSnapshots(dir, currentWorkspace) {
88
95
  * Read the raw content of a single snapshot file.
89
96
  * Returns null if the file does not exist or cannot be read.
90
97
  */
91
- export function loadSnapshotContent(workspace, dir, filename) {
98
+ export async function loadSnapshotContent(workspace, dir, filename) {
92
99
  try {
93
- return readFileSync(join(dir, workspace, filename), "utf-8");
100
+ return await readFile(join(dir, workspace, filename), "utf-8");
94
101
  }
95
102
  catch {
96
103
  return null;
97
104
  }
98
105
  }
99
106
  /** Best-effort read of a snapshot file's `id` field, for viewport-cache cleanup on delete. */
100
- export function readSnapshotIdSafe(fullPath) {
107
+ export async function readSnapshotIdSafe(fullPath) {
101
108
  try {
102
- const raw = readFileSync(fullPath, "utf-8");
109
+ const raw = await readFile(fullPath, "utf-8");
103
110
  const parsed = JSON.parse(raw);
104
111
  return typeof parsed.id === "string" ? parsed.id : undefined;
105
112
  }
@@ -114,10 +121,10 @@ export function readSnapshotIdSafe(fullPath) {
114
121
  * mirrors `session.ts`'s `exportCanvas()`: `rawPayload ?? frames[0].payload`.
115
122
  * Old snapshots without an `id` field are silently skipped.
116
123
  */
117
- export function findSnapshotById(id, dir) {
124
+ export async function findSnapshotById(id, dir) {
118
125
  let workspaceDirs;
119
126
  try {
120
- workspaceDirs = readdirSync(dir, { withFileTypes: true })
127
+ workspaceDirs = (await readdir(dir, { withFileTypes: true }))
121
128
  .filter((d) => d.isDirectory())
122
129
  .map((d) => d.name);
123
130
  }
@@ -128,14 +135,14 @@ export function findSnapshotById(id, dir) {
128
135
  const workspaceDir = join(dir, workspace);
129
136
  let files;
130
137
  try {
131
- files = readdirSync(workspaceDir).filter((f) => f.endsWith("_screen.json"));
138
+ files = (await readdir(workspaceDir)).filter((f) => f.endsWith("_screen.json"));
132
139
  }
133
140
  catch {
134
141
  continue;
135
142
  }
136
143
  for (const filename of files) {
137
144
  try {
138
- const raw = readFileSync(join(workspaceDir, filename), "utf-8");
145
+ const raw = await readFile(join(workspaceDir, filename), "utf-8");
139
146
  const parsed = JSON.parse(raw);
140
147
  if (parsed.id === id && isFrameArray(parsed.frames)) {
141
148
  return typeof parsed.rawPayload === "string" ? parsed.rawPayload : parsed.frames[0].payload;
@@ -148,6 +155,36 @@ export function findSnapshotById(id, dir) {
148
155
  }
149
156
  return null;
150
157
  }
158
+ /**
159
+ * Scan a single workspace directory for a snapshot whose `id` field matches,
160
+ * returning its filename and verbatim file contents (not the parsed
161
+ * record) — the zip export pipeline (F36) adds files byte-for-byte rather
162
+ * than re-rendering them. Old snapshots without an `id` field are never
163
+ * matched. Returns null if no match or the workspace directory is absent.
164
+ */
165
+ export async function findSnapshotFileByIdInWorkspace(workspace, id, dir) {
166
+ const workspaceDir = join(dir, workspace);
167
+ let files;
168
+ try {
169
+ files = (await readdir(workspaceDir)).filter((f) => f.endsWith("_screen.json"));
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ for (const filename of files) {
175
+ try {
176
+ const raw = await readFile(join(workspaceDir, filename), "utf-8");
177
+ const parsed = JSON.parse(raw);
178
+ if (parsed.id === id) {
179
+ return { filename, raw };
180
+ }
181
+ }
182
+ catch (err) {
183
+ console.error(`[agent-whiteboard] snapshot-reader: skipping unreadable file ${filename}:`, err instanceof Error ? err.message : String(err));
184
+ }
185
+ }
186
+ return null;
187
+ }
151
188
  /**
152
189
  * Scan a single workspace directory for a snapshot whose `id` field matches.
153
190
  * Returns the full parsed record (frames, timestamp, title?, nodeToFrame?)
@@ -155,18 +192,18 @@ export function findSnapshotById(id, dir) {
155
192
  * directory is absent). Old snapshots without an `id` field are silently
156
193
  * skipped.
157
194
  */
158
- export function findSnapshotByIdInWorkspace(workspace, id, dir) {
195
+ export async function findSnapshotByIdInWorkspace(workspace, id, dir) {
159
196
  const workspaceDir = join(dir, workspace);
160
197
  let files;
161
198
  try {
162
- files = readdirSync(workspaceDir).filter((f) => f.endsWith("_screen.json"));
199
+ files = (await readdir(workspaceDir)).filter((f) => f.endsWith("_screen.json"));
163
200
  }
164
201
  catch {
165
202
  return null;
166
203
  }
167
204
  for (const filename of files) {
168
205
  try {
169
- const raw = readFileSync(join(workspaceDir, filename), "utf-8");
206
+ const raw = await readFile(join(workspaceDir, filename), "utf-8");
170
207
  const parsed = JSON.parse(raw);
171
208
  if (parsed.id === id && typeof parsed.timestamp === "string" && isFrameArray(parsed.frames)) {
172
209
  const record = {
@@ -176,8 +213,9 @@ export function findSnapshotByIdInWorkspace(workspace, id, dir) {
176
213
  if (typeof parsed.title === "string") {
177
214
  record.title = parsed.title;
178
215
  }
179
- if (parsed.nodeToFrame !== null && typeof parsed.nodeToFrame === "object") {
180
- record.nodeToFrame = parsed.nodeToFrame;
216
+ const parsedNodeToFrame = nodeToFrameSchema.safeParse(parsed.nodeToFrame);
217
+ if (parsedNodeToFrame.success) {
218
+ record.nodeToFrame = parsedNodeToFrame.data;
181
219
  }
182
220
  return record;
183
221
  }
@@ -1,4 +1,6 @@
1
- import { existsSync, mkdirSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "fs";
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, writeFileSync } from "fs";
3
+ import { readdir, rm, stat, unlink } from "fs/promises";
2
4
  import { join, resolve, sep } from "path";
3
5
  import { getSnapshotsRoot } from "./paths.js";
4
6
  import { readSnapshotIdSafe } from "./snapshot-reader.js";
@@ -8,7 +10,7 @@ import { deleteViewports } from "./viewport-cache.js";
8
10
  * broadcast the id to the browser (e.g. for viewport-cache keying) without
9
11
  * waiting on the (synchronous, but logically separate) disk write. */
10
12
  export function generateSnapshotId() {
11
- return crypto.randomUUID();
13
+ return randomUUID();
12
14
  }
13
15
  export function saveSnapshot(frames, options, rawPayload, id) {
14
16
  try {
@@ -17,7 +19,7 @@ export function saveSnapshot(frames, options, rawPayload, id) {
17
19
  const dir = join(root, workspace);
18
20
  mkdirSync(dir, { recursive: true });
19
21
  const now = new Date();
20
- const usedId = id ?? crypto.randomUUID();
22
+ const usedId = id ?? randomUUID();
21
23
  // Include the (already-unique) id so two writes in the same second never collide.
22
24
  const filename = `${formatTimestamp(now)}_${usedId}_screen.json`;
23
25
  const content = {
@@ -51,7 +53,7 @@ function formatTimestamp(d) {
51
53
  * e.g. workspace ".", B6) and existence (404 if the workspace was never
52
54
  * created or was already deleted).
53
55
  */
54
- export function validateWorkspaceForDelete(workspace, root) {
56
+ export async function validateWorkspaceForDelete(workspace, root) {
55
57
  const validated = validateWorkspaceInput(workspace);
56
58
  if (!validated.ok) {
57
59
  return { ok: false, error: validated.error, status: 400 };
@@ -60,7 +62,8 @@ export function validateWorkspaceForDelete(workspace, root) {
60
62
  if (!resolve(dir).startsWith(resolve(root) + sep)) {
61
63
  return { ok: false, error: "invalid workspace: path traversal not allowed", status: 400 };
62
64
  }
63
- if (!existsSync(dir)) {
65
+ const dirExists = await stat(dir).then(() => true, () => false);
66
+ if (!dirExists) {
64
67
  return { ok: false, error: "workspace not found", status: 404 };
65
68
  }
66
69
  return { ok: true, workspace: validated.workspace };
@@ -70,7 +73,7 @@ export function validateWorkspaceForDelete(workspace, root) {
70
73
  * viewport-cache entries (C3, `02`). Missing files are silently skipped.
71
74
  * Shared by POST /snapshots/delete-files (NF26, v0.28 Sprint 59).
72
75
  */
73
- export function deleteSnapshotFiles(workspace, root, filenames) {
76
+ export async function deleteSnapshotFiles(workspace, root, filenames) {
74
77
  for (const f of filenames) {
75
78
  if (!isValidSnapshotFilename(f)) {
76
79
  return { ok: false, error: `invalid filename: ${f}` };
@@ -81,9 +84,9 @@ export function deleteSnapshotFiles(workspace, root, filenames) {
81
84
  const deletedIds = [];
82
85
  for (const f of filenames) {
83
86
  const fullPath = join(workspaceDir, f);
84
- const id = readSnapshotIdSafe(fullPath);
87
+ const id = await readSnapshotIdSafe(fullPath);
85
88
  try {
86
- unlinkSync(fullPath);
89
+ await unlink(fullPath);
87
90
  deleted++;
88
91
  if (id !== undefined)
89
92
  deletedIds.push(id);
@@ -92,7 +95,7 @@ export function deleteSnapshotFiles(workspace, root, filenames) {
92
95
  // Missing files are silently skipped.
93
96
  }
94
97
  }
95
- deleteViewports(deletedIds);
98
+ await deleteViewports(deletedIds);
96
99
  return { ok: true, deleted };
97
100
  }
98
101
  /**
@@ -100,18 +103,17 @@ export function deleteSnapshotFiles(workspace, root, filenames) {
100
103
  * for every snapshot that lived in it (C3, `02`). Shared by
101
104
  * POST /snapshots/delete-workspace (NF26, v0.28 Sprint 59).
102
105
  */
103
- export function deleteWorkspace(workspace, root) {
106
+ export async function deleteWorkspace(workspace, root) {
104
107
  const workspaceDir = join(root, workspace);
105
108
  let idsToClean = [];
106
109
  try {
107
- idsToClean = readdirSync(workspaceDir)
108
- .filter((f) => f.endsWith("_screen.json"))
109
- .map((f) => readSnapshotIdSafe(join(workspaceDir, f)))
110
- .filter((id) => id !== undefined);
110
+ const filenames = (await readdir(workspaceDir)).filter((f) => f.endsWith("_screen.json"));
111
+ const ids = await Promise.all(filenames.map((f) => readSnapshotIdSafe(join(workspaceDir, f))));
112
+ idsToClean = ids.filter((id) => id !== undefined);
111
113
  }
112
114
  catch {
113
115
  // Workspace directory unreadable/absent — nothing to clean up.
114
116
  }
115
- rmSync(workspaceDir, { recursive: true, force: true });
116
- deleteViewports(idsToClean);
117
+ await rm(workspaceDir, { recursive: true, force: true });
118
+ await deleteViewports(idsToClean);
117
119
  }
@@ -1,6 +1,7 @@
1
1
  // Server-side payload validation helpers.
2
2
  import mermaid from "mermaid";
3
3
  import { z } from "zod";
4
+ import { validateQuizPayload } from "./quiz.js";
4
5
  /**
5
6
  * Shared shape schemas for `node_actions` (wait_click) and `node_to_frame`
6
7
  * (commit_step_frames) — the single implementation both MCP's zod
@@ -138,3 +139,16 @@ export async function validateFrame(frame) {
138
139
  }
139
140
  return null;
140
141
  }
142
+ /**
143
+ * Types accepted by render() — the frame types plus "quiz" (F44). Quiz is
144
+ * deliberately NOT a FRAME_TYPE: it is a single interactive page, not
145
+ * something that makes sense inside a slideshow or step-frames sequence
146
+ * (wait_quiz hands back one submission), so validateFrame() keeps rejecting it.
147
+ */
148
+ export const RENDER_TYPES = [...FRAME_TYPES, "quiz"];
149
+ /** The one validator for render() payloads, shared by POST /render and the MCP `render` tool. */
150
+ export async function validateRenderPayload(type, payload) {
151
+ if (type === "quiz")
152
+ return validateQuizPayload(payload);
153
+ return validateFrame({ type, payload });
154
+ }
@@ -4,19 +4,37 @@
4
4
  // step-frames sequence persists its own manual viewport independently, consistent
5
5
  // with each frame now getting its own auto-fit trigger (was a bare `id` key,
6
6
  // shared across a whole sequence, pre-v0.26.1).
7
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ //
8
+ // NF49 (v1.4): reads/writes go through fs/promises instead of blocking the event
9
+ // loop, and the on-disk file is treated as a write-back cache — kept in memory
10
+ // after the first load and flushed to disk on a coalesced delay (see
11
+ // `scheduleFlush()`) instead of a full synchronous read-modify-write per
12
+ // zoom/pan event.
13
+ import { mkdir, readFile, writeFile } from "fs/promises";
8
14
  import { join } from "path";
9
15
  import { getSnapshotsRoot } from "./paths.js";
10
16
  function cachePath() {
11
17
  return join(getSnapshotsRoot(), "viewport-cache.json");
12
18
  }
13
- function readCache() {
19
+ function cacheKey(id, frameIndex) {
20
+ return `${id}:${frameIndex}`;
21
+ }
22
+ // In-memory cache, lazily loaded on first access. `cachedForPath` guards
23
+ // against a stale in-memory cache surviving a change to the underlying
24
+ // snapshots root (WHITEBOARD_SNAPSHOTS_DIR) — reloads transparently if the
25
+ // resolved cache path differs from the one currently held in memory.
26
+ let cache;
27
+ let cachedForPath;
28
+ async function loadCache() {
29
+ const path = cachePath();
30
+ if (cache !== undefined && cachedForPath === path)
31
+ return cache;
14
32
  try {
15
- const raw = readFileSync(cachePath(), "utf-8");
33
+ const raw = await readFile(path, "utf-8");
16
34
  const parsed = JSON.parse(raw);
17
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
18
- return {};
19
- return parsed;
35
+ cache = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
36
+ ? parsed
37
+ : {};
20
38
  }
21
39
  catch (err) {
22
40
  // ENOENT (no cache file yet, e.g. first run) is expected and not logged.
@@ -25,45 +43,99 @@ function readCache() {
25
43
  if (err?.code !== "ENOENT") {
26
44
  console.error("[agent-whiteboard] viewport-cache read failed, resetting cache:", err instanceof Error ? err.message : String(err));
27
45
  }
28
- return {};
46
+ cache = {};
29
47
  }
48
+ cachedForPath = path;
49
+ return cache;
30
50
  }
31
- function writeCache(cache) {
51
+ async function flushCache() {
52
+ if (cache === undefined)
53
+ return;
32
54
  try {
33
- mkdirSync(getSnapshotsRoot(), { recursive: true });
34
- writeFileSync(cachePath(), JSON.stringify(cache, null, 2), "utf-8");
55
+ await mkdir(getSnapshotsRoot(), { recursive: true });
56
+ await writeFile(cachePath(), JSON.stringify(cache, null, 2), "utf-8");
35
57
  }
36
58
  catch (err) {
37
59
  console.error("[agent-whiteboard] viewport-cache write failed:", err instanceof Error ? err.message : String(err));
38
60
  }
39
61
  }
40
- function cacheKey(id, frameIndex) {
41
- return `${id}:${frameIndex}`;
62
+ /**
63
+ * Delay before a scheduled flush fires. Exported so tests can advance fake
64
+ * timers by exactly this amount instead of hardcoding a duplicate constant.
65
+ */
66
+ export const VIEWPORT_CACHE_FLUSH_DELAY_MS = 250;
67
+ let flushTimer;
68
+ /**
69
+ * Coalesces bursts of writes (continuous zoom/pan produces many `setViewport`
70
+ * calls in quick succession) into a single disk write roughly
71
+ * `VIEWPORT_CACHE_FLUSH_DELAY_MS` after the first update in a burst, instead
72
+ * of a full synchronous rewrite per update. A timer already pending absorbs
73
+ * every subsequent call in the same burst — it is not reset per call, so a
74
+ * sustained stream of updates still flushes periodically rather than being
75
+ * postponed indefinitely.
76
+ */
77
+ function scheduleFlush() {
78
+ if (flushTimer !== undefined)
79
+ return;
80
+ flushTimer = setTimeout(() => {
81
+ flushTimer = undefined;
82
+ void flushCache();
83
+ }, VIEWPORT_CACHE_FLUSH_DELAY_MS);
84
+ }
85
+ export async function getViewport(id, frameIndex) {
86
+ const c = await loadCache();
87
+ return c[cacheKey(id, frameIndex)];
42
88
  }
43
- export function getViewport(id, frameIndex) {
44
- return readCache()[cacheKey(id, frameIndex)];
89
+ export async function setViewport(id, frameIndex, viewport) {
90
+ const c = await loadCache();
91
+ c[cacheKey(id, frameIndex)] = viewport;
92
+ scheduleFlush();
45
93
  }
46
- export function setViewport(id, frameIndex, viewport) {
47
- const cache = readCache();
48
- cache[cacheKey(id, frameIndex)] = viewport;
49
- writeCache(cache);
94
+ /**
95
+ * Immediately flushes the in-memory cache to disk, bypassing (and cancelling)
96
+ * any pending debounced flush. Not needed in production — `setViewport`'s
97
+ * own scheduled flush is sufficient — but lets tests assert on-disk state
98
+ * deterministically without waiting on the real debounce delay or racing
99
+ * fake timers against real fs/promises I/O.
100
+ */
101
+ export async function flushViewportCacheNow() {
102
+ if (flushTimer !== undefined) {
103
+ clearTimeout(flushTimer);
104
+ flushTimer = undefined;
105
+ }
106
+ await flushCache();
107
+ }
108
+ /**
109
+ * Drops the in-memory cache and cancels any pending flush, forcing the next
110
+ * call to reload from disk. Not needed in production (the snapshots root
111
+ * doesn't change at runtime); exists for tests whose fixtures recreate the
112
+ * snapshots directory on the same path between cases (`cachedForPath`'s
113
+ * path-based invalidation can't detect that on its own).
114
+ */
115
+ export function resetViewportCacheForTest() {
116
+ cache = undefined;
117
+ cachedForPath = undefined;
118
+ if (flushTimer !== undefined) {
119
+ clearTimeout(flushTimer);
120
+ flushTimer = undefined;
121
+ }
50
122
  }
51
- export function deleteViewport(id) {
52
- deleteViewports([id]);
123
+ export async function deleteViewport(id) {
124
+ await deleteViewports([id]);
53
125
  }
54
126
  /** Removes every per-frame entry for each given snapshot id (prefix match on "<id>:"). */
55
- export function deleteViewports(ids) {
127
+ export async function deleteViewports(ids) {
56
128
  if (ids.length === 0)
57
129
  return;
58
- const cache = readCache();
130
+ const c = await loadCache();
59
131
  const prefixes = ids.map((id) => `${id}:`);
60
132
  let changed = false;
61
- for (const key of Object.keys(cache)) {
133
+ for (const key of Object.keys(c)) {
62
134
  if (prefixes.some((prefix) => key.startsWith(prefix))) {
63
- delete cache[key];
135
+ delete c[key];
64
136
  changed = true;
65
137
  }
66
138
  }
67
139
  if (changed)
68
- writeCache(cache);
140
+ scheduleFlush();
69
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-whiteboard",
3
- "version": "1.0.4",
3
+ "version": "1.2.0",
4
4
  "description": "A domain-agnostic interactive whiteboard for AI teacher agents — render diagrams, math, charts, and HTML to a local browser tab via MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,16 +48,21 @@
48
48
  "test": "vitest run",
49
49
  "test:e2e": "playwright test",
50
50
  "lint": "eslint .",
51
- "lint:fix": "eslint . --fix"
51
+ "lint:fix": "eslint . --fix",
52
+ "prepublishOnly": "npm run build"
52
53
  },
53
54
  "dependencies": {
54
55
  "@hono/node-server": "^1.13.7",
55
56
  "@modelcontextprotocol/sdk": "^1.12.1",
57
+ "@types/archiver": "^7.0.0",
56
58
  "@types/dompurify": "^3.2.0",
59
+ "archiver": "^7.0.1",
57
60
  "bootstrap": "^5.3.8",
58
- "dompurify": "^3.4.7",
59
- "happy-dom": "^20.10.6",
61
+ "dompurify": "^3.4.15",
62
+ "extract-zip": "^2.0.1",
60
63
  "hono": "^4.7.11",
64
+ "jsdom": "^26.1.0",
65
+ "jszip": "^3.10.1",
61
66
  "katex": "^0.17.0",
62
67
  "mermaid": "^11.15.0",
63
68
  "open": "^10.1.2",
@@ -69,10 +74,11 @@
69
74
  "devDependencies": {
70
75
  "@eslint/js": "^10.0.1",
71
76
  "@playwright/test": "^1.60.0",
72
- "@sveltejs/vite-plugin-svelte": "^2.4.6",
77
+ "@sveltejs/vite-plugin-svelte": "^3.1.2",
73
78
  "@testing-library/jest-dom": "^6.9.1",
74
79
  "@testing-library/svelte": "^4.2.3",
75
80
  "@tsconfig/svelte": "^5.0.4",
81
+ "@types/jsdom": "^30.0.0",
76
82
  "@types/node": "^20.17.57",
77
83
  "@types/ws": "^8.18.1",
78
84
  "concurrently": "^9.1.2",
@@ -85,8 +91,8 @@
85
91
  "tsx": "^3.14.0",
86
92
  "typescript": "^5.8.3",
87
93
  "typescript-eslint": "^8.62.1",
88
- "vite": "^4.5.10",
89
- "vitest": "^0.34.6",
94
+ "vite": "^5.4.21",
95
+ "vitest": "^3.2.7",
90
96
  "wait-on": "^8.0.3"
91
97
  }
92
98
  }
@@ -1 +0,0 @@
1
- import{A as c,f as i}from"./mermaid-parser.core-52f9cd92.js";import"./index-ffc5af4b.js";export{c as ArchitectureModule,i as createArchitectureServices};