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.
- package/CHANGELOG.md +76 -0
- package/README.md +61 -5
- package/dist/client/assets/abnfDiagram-VCTEODGH-Dj8sd_5g.js +1 -0
- package/dist/client/assets/{arc-4691b008.js → arc-Aq92EeUK.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-B1QyVL0F.js +36 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-CmU2SSFe.js +129 -0
- package/dist/client/assets/{bootstrap.min-21652813.js → bootstrap.min-UtnB-TxT.js} +1 -2
- package/dist/client/assets/c4Diagram-7LVT6UL2-DWmhp77c.js +38 -0
- package/dist/client/assets/channel-C9ni1Via.js +1 -0
- package/dist/client/assets/{chunk-QZHKN3VN-0f554985.js → chunk-2Q5K7J3B-CQLo8Sbv.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-23d48b24.js → chunk-5VM5RSS4-fJTWmTTt.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-1104c326.js → chunk-F27PBJKO-DqzEf5gi.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-CMamLTz3.js +231 -0
- package/dist/client/assets/{chunk-4BX2VUAB-3c93511d.js → chunk-JWPE2WC7-tyYPyD43.js} +1 -1
- package/dist/client/assets/chunk-POPQ4Y6H-XdY4rO_V.js +1 -0
- package/dist/client/assets/chunk-SVP7TREG-COvqzqaO.js +88 -0
- package/dist/client/assets/chunk-TICWLB2K-BsJSFuPC.js +206 -0
- package/dist/client/assets/{chunk-55IACEB6-0243aefd.js → chunk-XXDRQBXY-HfVrYBJb.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-DktNEiGC.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-DktNEiGC.js +1 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-UXGdwSjS.js +1 -0
- package/dist/client/assets/cynefin-OW5HDTMX-PWlPYnpn.js +178 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CYE5H1wR.js +62 -0
- package/dist/client/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/dist/client/assets/dagre-GXQ25YYZ-DaknBOKq.js +4 -0
- package/dist/client/assets/diagram-S7CK7UJ4-Dz7rZHxF.js +30 -0
- package/dist/client/assets/diagram-UQ7AKVKN-DzCi6tiS.js +41 -0
- package/dist/client/assets/{diagram-KO2AKTUF-81f42eeb.js → diagram-VSXAHHWV-TlUzhXgv.js} +3 -3
- package/dist/client/assets/diagram-VX7I27RA-Ds7jdzK7.js +24 -0
- package/dist/client/assets/diagram-Z3DM3KII-C--JD7cf.js +24 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-Dw5OM-dL.js +1 -0
- package/dist/client/assets/embed-DcNeILcO.js +211 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DXalBHjA.js +99 -0
- package/dist/client/assets/flowDiagram-HODETNUW-DcI3UqyE.js +168 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DyrkIo6c.js +292 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-fJKfGRcg.js +106 -0
- package/dist/client/assets/index-CIhkXT_z.js +19 -0
- package/dist/client/assets/index-DEiH2AE7.css +1 -0
- package/dist/client/assets/infoDiagram-27XIBGKW-Cijiqswb.js +2 -0
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-50b43d03.js → ishikawaDiagram-5VMMS53U-C1WqWU2l.js} +2 -2
- package/dist/client/assets/{journeyDiagram-JHISSGLW-dce41bdf.js → journeyDiagram-3NMN7TZE-7j05mbLR.js} +4 -4
- package/dist/client/assets/{kanban-definition-UN3LZRKU-92401a86.js → kanban-definition-UXKFOSKX-D5mGDfs_.js} +1 -1
- package/dist/client/assets/{katex-4c9ea27f.js → katex-BTI-AXqd.js} +3 -3
- package/dist/client/assets/{katex-0cba7652.js → katex-C5jXJg4s.js} +3 -3
- package/dist/client/assets/layout-KEin06-H.js +1 -0
- package/dist/client/assets/linear-D1EL1Azg.js +1 -0
- package/dist/client/assets/mermaid.core-CLvSDmrA.js +310 -0
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-66fe4544.js → mindmap-definition-YA3MSWOX-BsCqKd_d.js} +12 -12
- package/dist/client/assets/{ordinal-db88ae6f.js → ordinal-BeghXfj9.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-Du5lDU3W.js +1 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-hjv5Ao6B.js +39 -0
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-f64a7ed7.js → quadrantDiagram-AXDQQJYC-DNon8LWK.js} +4 -4
- package/dist/client/assets/railroadDiagram-O6MQD6OU-yNZz2zxo.js +1 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CZiiEUHW.js +84 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-D-u8ZQUV.js +40 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BxjTf3q9.js +162 -0
- package/dist/client/assets/sizeCapture-INFHLROL-Dt4Wg8C5.js +1 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-B8BM6dln.js +1 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-MJauX84T.js +1 -0
- package/dist/client/assets/step-BEQzeS8V.js +1 -0
- package/dist/client/assets/swimlanes-42K2YHIH-eGWrsWAA.js +1 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-BYFu8tyL.js +8 -0
- package/dist/client/assets/{time-043b3464.js → time-BaoZouO5.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-b0c81243.js → timeline-definition-24CTP7MA-CAM-3Qj9.js} +10 -10
- package/dist/client/assets/vennDiagram-4TSXK5OY-CuFEwwD0.js +34 -0
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-8d4d7626.js → wardleyDiagram-VM6X3IG4-DI5yCVh0.js} +3 -3
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BI0Bn-OQ.js +7 -0
- package/dist/client/index.html +2 -3
- package/dist/server/app.js +28 -396
- package/dist/server/channel.js +5 -3
- package/dist/server/export-html.js +30 -60
- package/dist/server/export-zip.js +83 -0
- package/dist/server/import-zip.js +196 -0
- package/dist/server/index.js +2 -1
- package/dist/server/mcp.js +28 -13
- package/dist/server/port.js +18 -0
- package/dist/server/quiz-submission.js +116 -0
- package/dist/server/quiz.js +110 -0
- package/dist/server/render-core.js +8 -7
- package/dist/server/routes/export.js +95 -0
- package/dist/server/routes/import.js +24 -0
- package/dist/server/routes/quiz.js +12 -0
- package/dist/server/routes/render.js +184 -0
- package/dist/server/routes/slideshow.js +52 -0
- package/dist/server/routes/snapshots.js +119 -0
- package/dist/server/snapshot-reader.js +58 -20
- package/dist/server/snapshot-writer.js +18 -16
- package/dist/server/validate.js +14 -0
- package/dist/server/viewport-cache.js +97 -25
- package/package.json +13 -7
- package/dist/client/assets/architecture-7EHR7CIX-1221aa61.js +0 -1
- package/dist/client/assets/architectureDiagram-3BPJPVTR-1eef4d3c.js +0 -36
- package/dist/client/assets/blockDiagram-GPEHLZMM-313fb06c.js +0 -132
- package/dist/client/assets/c4Diagram-AAUBKEIU-92b023ad.js +0 -10
- package/dist/client/assets/channel-5ee4a8a7.js +0 -1
- package/dist/client/assets/chunk-2J33WTMH-fb021fdc.js +0 -1
- package/dist/client/assets/chunk-727SXJPM-8e0e0228.js +0 -206
- package/dist/client/assets/chunk-AQP2D5EJ-2ccc13c2.js +0 -231
- package/dist/client/assets/classDiagram-4FO5ZUOK-42ffecb6.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-42ffecb6.js +0 -1
- package/dist/client/assets/cose-bilkent-S5V4N54A-45ec2e52.js +0 -1
- package/dist/client/assets/cytoscape.esm-1491e259.js +0 -331
- package/dist/client/assets/dagre-BM42HDAG-ed748885.js +0 -4
- package/dist/client/assets/diagram-2AECGRRQ-ccc0ddec.js +0 -43
- package/dist/client/assets/diagram-5GNKFQAL-342efdea.js +0 -10
- package/dist/client/assets/diagram-LMA3HP47-39e66739.js +0 -24
- package/dist/client/assets/diagram-OG6HWLK6-b3e61903.js +0 -24
- package/dist/client/assets/embed-2b4adf34.js +0 -201
- package/dist/client/assets/erDiagram-TEJ5UH35-281f5cc7.js +0 -85
- package/dist/client/assets/eventmodeling-FCH6USID-6508120e.js +0 -1
- package/dist/client/assets/flowDiagram-I6XJVG4X-c3a4269e.js +0 -162
- package/dist/client/assets/ganttDiagram-6RSMTGT7-013b07ec.js +0 -292
- package/dist/client/assets/gitGraph-WXDBUCRP-02827359.js +0 -1
- package/dist/client/assets/gitGraphDiagram-PVQCEYII-de184acb.js +0 -106
- package/dist/client/assets/graph-f3dfaf53.js +0 -1
- package/dist/client/assets/index-d674a6a4.css +0 -1
- package/dist/client/assets/index-ffc5af4b.js +0 -5
- package/dist/client/assets/info-J43DQDTF-81e32294.js +0 -1
- package/dist/client/assets/infoDiagram-5YYISTIA-196101ee.js +0 -2
- package/dist/client/assets/layout-3a4eff80.js +0 -1
- package/dist/client/assets/linear-fed00341.js +0 -1
- package/dist/client/assets/mermaid-parser.core-52f9cd92.js +0 -173
- package/dist/client/assets/mermaid.core-7dfc221a.js +0 -306
- package/dist/client/assets/packet-YPE3B663-278a272a.js +0 -1
- package/dist/client/assets/pie-LRSECV5Y-2e55d9ae.js +0 -1
- package/dist/client/assets/pieDiagram-4H26LBE5-65b1bcbd.js +0 -30
- package/dist/client/assets/radar-GUYGQ44K-608e459f.js +0 -1
- package/dist/client/assets/requirementDiagram-4Y6WPE33-90ebf889.js +0 -84
- package/dist/client/assets/sankeyDiagram-5OEKKPKP-2d1a83b3.js +0 -40
- package/dist/client/assets/sequenceDiagram-3UESZ5HK-aece3633.js +0 -162
- package/dist/client/assets/stateDiagram-AJRCARHV-9bd5b5f4.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-7c16c251.js +0 -1
- package/dist/client/assets/step-171ad54a.js +0 -1
- package/dist/client/assets/treeView-BLDUP644-e54b9ddb.js +0 -1
- package/dist/client/assets/treemap-LRROVOQU-6a81775b.js +0 -1
- package/dist/client/assets/vennDiagram-CIIHVFJN-8eaf0464.js +0 -34
- package/dist/client/assets/wardley-L42UT6IY-7abd921f.js +0 -1
- package/dist/client/assets/xychartDiagram-2RQKCTM6-450f333c.js +0 -7
- /package/dist/client/assets/{KaTeX_AMS-Regular-0cdd387c.woff2 → KaTeX_AMS-Regular-BQhdFMY1.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_AMS-Regular-30da91e8.woff → KaTeX_AMS-Regular-DMm9YOAa.woff} +0 -0
- /package/dist/client/assets/{KaTeX_AMS-Regular-68534840.ttf → KaTeX_AMS-Regular-DRggAlZN.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Bold-07d8e303.ttf → KaTeX_Caligraphic-Bold-ATXxdsX0.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Bold-1ae6bd74.woff → KaTeX_Caligraphic-Bold-BEiXGLvX.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Bold-de7701e4.woff2 → KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Regular-3398dd02.woff → KaTeX_Caligraphic-Regular-CTRA-rTL.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Regular-5d53e70a.woff2 → KaTeX_Caligraphic-Regular-Di6jR-x-.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Caligraphic-Regular-ed0b7437.ttf → KaTeX_Caligraphic-Regular-wX97UBjC.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Bold-9163df9c.ttf → KaTeX_Fraktur-Bold-BdnERNNW.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Bold-9be7ceb8.woff → KaTeX_Fraktur-Bold-BsDP51OF.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Bold-74444efd.woff2 → KaTeX_Fraktur-Bold-CL6g_b3V.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Regular-1e6f9579.ttf → KaTeX_Fraktur-Regular-CB_wures.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Regular-51814d27.woff2 → KaTeX_Fraktur-Regular-CTYiF6lA.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Fraktur-Regular-5e28753b.woff → KaTeX_Fraktur-Regular-Dxdc4cR9.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Bold-0f60d1b8.woff2 → KaTeX_Main-Bold-Cx986IdX.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Bold-c76c5d69.woff → KaTeX_Main-Bold-Jm3AIy58.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Bold-138ac28d.ttf → KaTeX_Main-Bold-waoOVXN0.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Main-BoldItalic-99cd42a3.woff2 → KaTeX_Main-BoldItalic-DxDJ3AOS.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Main-BoldItalic-70ee1f64.ttf → KaTeX_Main-BoldItalic-DzxPMmG6.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Main-BoldItalic-a6f7ec0d.woff → KaTeX_Main-BoldItalic-SpSLRI95.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Italic-0d85ae7c.ttf → KaTeX_Main-Italic-3WenGoN9.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Italic-f1d6ef86.woff → KaTeX_Main-Italic-BMLOBm91.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Italic-97479ca6.woff2 → KaTeX_Main-Italic-NWA7e6Wa.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Regular-c2342cd8.woff2 → KaTeX_Main-Regular-B22Nviop.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Regular-c6368d87.woff → KaTeX_Main-Regular-Dr94JaBh.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Main-Regular-d0332f52.ttf → KaTeX_Main-Regular-ypZvNtVU.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Math-BoldItalic-f9377ab0.ttf → KaTeX_Math-BoldItalic-B3XSjfu4.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Math-BoldItalic-dc47344d.woff2 → KaTeX_Math-BoldItalic-CZnvNsCZ.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Math-BoldItalic-850c0af5.woff → KaTeX_Math-BoldItalic-iY-2wyZ7.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Math-Italic-8a8d2445.woff → KaTeX_Math-Italic-DA0__PXp.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Math-Italic-08ce98e5.ttf → KaTeX_Math-Italic-flOr_0UB.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Math-Italic-7af58c5e.woff2 → KaTeX_Math-Italic-t53AETM-.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Bold-1ece03f7.ttf → KaTeX_SansSerif-Bold-CFMepnvq.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Bold-e99ae511.woff2 → KaTeX_SansSerif-Bold-D1sUS0GD.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Bold-ece03cfd.woff → KaTeX_SansSerif-Bold-DbIhKOiC.woff} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Italic-00b26ac8.woff2 → KaTeX_SansSerif-Italic-C3H0VqGB.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Italic-91ee6750.woff → KaTeX_SansSerif-Italic-DN2j7dab.woff} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Italic-3931dd81.ttf → KaTeX_SansSerif-Italic-YYjJ1zSn.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Regular-f36ea897.ttf → KaTeX_SansSerif-Regular-BNo7hRIc.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Regular-11e4dc8a.woff → KaTeX_SansSerif-Regular-CS6fqUqJ.woff} +0 -0
- /package/dist/client/assets/{KaTeX_SansSerif-Regular-68e8c73e.woff2 → KaTeX_SansSerif-Regular-DDBCnlJ7.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Script-Regular-1c67f068.ttf → KaTeX_Script-Regular-C5JkGWo-.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Script-Regular-036d4e95.woff2 → KaTeX_Script-Regular-D3wIWfF6.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Script-Regular-d96cdf2b.woff → KaTeX_Script-Regular-D5yQViql.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size1-Regular-c943cc98.woff → KaTeX_Size1-Regular-C195tn64.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size1-Regular-95b6d2f1.ttf → KaTeX_Size1-Regular-Dbsnue_I.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Size1-Regular-6b47c401.woff2 → KaTeX_Size1-Regular-mCD8mA8B.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Size2-Regular-a6b2099f.ttf → KaTeX_Size2-Regular-B7gKUWhC.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Size2-Regular-d04c5421.woff2 → KaTeX_Size2-Regular-Dy4dx90m.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Size2-Regular-2014c523.woff → KaTeX_Size2-Regular-oD1tc_U0.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size3-Regular-6ab6b62e.woff → KaTeX_Size3-Regular-CTq5MqoE.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size3-Regular-500e04d5.ttf → KaTeX_Size3-Regular-DgpXs0kz.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Size4-Regular-99f9c675.woff → KaTeX_Size4-Regular-BF-4gkZK.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Size4-Regular-c647367d.ttf → KaTeX_Size4-Regular-DWFBv043.ttf} +0 -0
- /package/dist/client/assets/{KaTeX_Size4-Regular-a4af7d41.woff2 → KaTeX_Size4-Regular-Dl5lxZxV.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Typewriter-Regular-e14fed02.woff → KaTeX_Typewriter-Regular-C0xS9mPB.woff} +0 -0
- /package/dist/client/assets/{KaTeX_Typewriter-Regular-71d517d6.woff2 → KaTeX_Typewriter-Regular-CO6r4hn1.woff2} +0 -0
- /package/dist/client/assets/{KaTeX_Typewriter-Regular-f01f3e87.ttf → KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf} +0 -0
- /package/dist/client/assets/{colors-98f238a0.js → colors-Cc3OSVma.js} +0 -0
- /package/dist/client/assets/{defaultLocale-a8476f12.js → defaultLocale-CzDG__Ur.js} +0 -0
- /package/dist/client/assets/{index-a1656aec.js → index-Bi7T_FA4.js} +0 -0
- /package/dist/client/assets/{init-a5b10ee5.js → init-Dmth1JHB.js} +0 -0
- /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 {
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
180
|
-
|
|
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 {
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
116
|
-
deleteViewports(idsToClean);
|
|
117
|
+
await rm(workspaceDir, { recursive: true, force: true });
|
|
118
|
+
await deleteViewports(idsToClean);
|
|
117
119
|
}
|
package/dist/server/validate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
33
|
+
const raw = await readFile(path, "utf-8");
|
|
16
34
|
const parsed = JSON.parse(raw);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
46
|
+
cache = {};
|
|
29
47
|
}
|
|
48
|
+
cachedForPath = path;
|
|
49
|
+
return cache;
|
|
30
50
|
}
|
|
31
|
-
function
|
|
51
|
+
async function flushCache() {
|
|
52
|
+
if (cache === undefined)
|
|
53
|
+
return;
|
|
32
54
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
44
|
-
|
|
89
|
+
export async function setViewport(id, frameIndex, viewport) {
|
|
90
|
+
const c = await loadCache();
|
|
91
|
+
c[cacheKey(id, frameIndex)] = viewport;
|
|
92
|
+
scheduleFlush();
|
|
45
93
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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(
|
|
133
|
+
for (const key of Object.keys(c)) {
|
|
62
134
|
if (prefixes.some((prefix) => key.startsWith(prefix))) {
|
|
63
|
-
delete
|
|
135
|
+
delete c[key];
|
|
64
136
|
changed = true;
|
|
65
137
|
}
|
|
66
138
|
}
|
|
67
139
|
if (changed)
|
|
68
|
-
|
|
140
|
+
scheduleFlush();
|
|
69
141
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-whiteboard",
|
|
3
|
-
"version": "1.0
|
|
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.
|
|
59
|
-
"
|
|
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": "^
|
|
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.
|
|
89
|
-
"vitest": "^
|
|
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};
|