@xiaobuyu/nodesign 0.0.7 → 0.0.8

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 (353) hide show
  1. package/README.md +266 -79
  2. package/README.zh-CN.md +254 -67
  3. package/package.json +2 -2
  4. package/server/api/admin.js +7 -0
  5. package/server/api/assets/docx-page.js +5 -4
  6. package/server/api/assets/helpers.js +8 -0
  7. package/server/api/assets.js +21 -33
  8. package/server/api/board.js +29 -1
  9. package/server/api/browse.js +3 -2
  10. package/server/api/chatai.js +9 -8
  11. package/server/api/exports/cards.js +7 -6
  12. package/server/api/exports/docx-pdf.js +4 -1
  13. package/server/api/local.js +10 -8
  14. package/server/api/projects.js +19 -3
  15. package/server/api/publish.js +3 -2
  16. package/server/api/sessions-rewind.js +220 -0
  17. package/server/api/sessions.js +58 -194
  18. package/server/api/stage.js +35 -0
  19. package/server/api/turn-model-switch.js +109 -0
  20. package/server/api/turn.js +52 -88
  21. package/server/auth/middleware.js +29 -6
  22. package/server/auth/tier.js +27 -7
  23. package/server/auth/users-store.js +14 -1
  24. package/server/engine/agent/actor-trail.js +110 -0
  25. package/server/engine/agent/agent-shared.js +22 -52
  26. package/server/engine/agent/beat-state.js +61 -0
  27. package/server/engine/agent/cast.js +322 -0
  28. package/server/engine/agent/events.js +9 -2
  29. package/server/engine/agent/hooks/beat-gate.js +73 -0
  30. package/server/engine/agent/hooks/lifecycle.js +6 -2
  31. package/server/engine/agent/hooks/post-trim.js +1 -0
  32. package/server/engine/agent/hooks/pre-board-dirty.js +29 -0
  33. package/server/engine/agent/hooks/pre-defaults.js +206 -2
  34. package/server/engine/agent/hooks/pre-injectors.js +19 -2
  35. package/server/engine/agent/hooks/pre-peer-guard.js +88 -0
  36. package/server/engine/agent/hooks/pre-unknown-params.js +82 -0
  37. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +33 -0
  38. package/server/engine/agent/hooks/resident-role-lifecycle.js +100 -0
  39. package/server/engine/agent/hooks/slot-alias.js +30 -0
  40. package/server/engine/agent/hooks/user-prompt-submit.js +197 -7
  41. package/server/engine/agent/hooks.js +76 -6
  42. package/server/engine/agent/isolation.js +25 -2
  43. package/server/engine/agent/memory-config.js +27 -1
  44. package/server/engine/agent/model-context.js +102 -18
  45. package/server/engine/agent/model-table.js +361 -58
  46. package/server/engine/agent/prompts/nodesign-prelude.md +566 -59
  47. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
  48. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +2 -2
  49. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
  50. package/server/engine/agent/prompts/tools/site-reference.md +2 -1
  51. package/server/engine/agent/role-card.js +175 -0
  52. package/server/engine/agent/rp-mode.js +17 -0
  53. package/server/engine/agent/session-loop.js +26 -14
  54. package/server/engine/agent/stage-status.js +73 -0
  55. package/server/engine/agent/subagent-flight.js +73 -0
  56. package/server/engine/agent/system-prompts.js +41 -0
  57. package/server/engine/agent/tool-input-stream.js +165 -0
  58. package/server/engine/mcp/actor.js +59 -0
  59. package/server/engine/mcp/capability-gate.js +21 -2
  60. package/server/engine/mcp/index.js +116 -24
  61. package/server/engine/mcp/mode-profile.js +139 -0
  62. package/server/engine/mcp/server-name.js +19 -0
  63. package/server/engine/mcp/tools/artifact-session.js +2 -1
  64. package/server/engine/mcp/tools/browse-find-batch.js +76 -14
  65. package/server/engine/mcp/tools/browse.js +1 -1
  66. package/server/engine/mcp/tools/cast-role.js +167 -0
  67. package/server/engine/mcp/tools/draw-trend.js +104 -0
  68. package/server/engine/mcp/tools/edit-board-schema.js +56 -0
  69. package/server/engine/mcp/tools/edit-board.js +569 -0
  70. package/server/engine/mcp/tools/look-at-board.js +4 -3
  71. package/server/engine/mcp/tools/open-sheet.js +242 -0
  72. package/server/engine/mcp/tools/organize-board.js +19 -5
  73. package/server/engine/mcp/tools/paint-still.js +1 -1
  74. package/server/engine/mcp/tools/pin-to-board.js +78 -3
  75. package/server/engine/mcp/tools/read-board.js +109 -16
  76. package/server/engine/mcp/tools/read-tavern-json.js +87 -19
  77. package/server/engine/mcp/tools/report-issue.js +1 -0
  78. package/server/engine/mcp/tools/role-memory.js +83 -0
  79. package/server/engine/mcp/tools/roll-dice.js +82 -0
  80. package/server/engine/mcp/tools/roll-film.js +1 -1
  81. package/server/engine/mcp/tools/screenshot.js +2 -2
  82. package/server/engine/mcp/tools/set-vars.js +150 -0
  83. package/server/engine/mcp/tools/sheet-replan.js +46 -0
  84. package/server/engine/mcp/tools/web-search.js +1 -1
  85. package/server/engine/mcp/tools/write-on-board-flow.js +108 -0
  86. package/server/engine/mcp/tools/write-on-board-place.js +211 -0
  87. package/server/engine/mcp/tools/write-on-board-role-anchor.js +48 -0
  88. package/server/engine/mcp/tools/write-on-board-schema.js +125 -0
  89. package/server/engine/mcp/tools/write-on-board.js +567 -107
  90. package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
  91. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +11 -3
  92. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +34 -4
  93. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +20 -4
  94. package/server/engine/plugins/nodesign/skills/story-craft/SKILL.md +94 -0
  95. package/server/engine/plugins/nodesign/skills/story-import/SKILL.md +112 -0
  96. package/server/engine/plugins/nodesign/skills/story-intimacy/SKILL.md +72 -0
  97. package/server/engine/plugins/nodesign/skills/story-voice/SKILL.md +66 -0
  98. package/server/engine/plugins/nodesign/skills/story-voice/voices//345/206/267/347/241/254.md +33 -0
  99. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/203/205/346/204/237/347/233/264/347/273/231.md +42 -0
  100. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/204/217/350/257/206/346/265/201.md +42 -0
  101. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/255/246/344/276/240/347/231/275/350/257/235.md +34 -0
  102. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/224/237/346/264/273/346/265/201.md +35 -0
  103. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/231/275/346/217/217.md +37 -0
  104. package/server/engine/plugins/nodesign/skills/story-voice/voices//350/210/236/345/217/260/345/211/247.md +32 -0
  105. package/server/engine/plugins/nodesign/skills/story-voice/voices//351/230/266/346/256/265/345/210/207/346/215/242.md +42 -0
  106. package/server/engine/runs/board-seater.js +272 -0
  107. package/server/engine/runs/board-tasklist.js +90 -48
  108. package/server/engine/runs/turn-relay.js +24 -2
  109. package/server/engine/runs/user-chalk-echo.js +44 -0
  110. package/server/index.js +2 -0
  111. package/server/lib/board-anchor.js +49 -0
  112. package/server/lib/board-dirty.js +93 -0
  113. package/server/lib/board-excerpt.js +80 -0
  114. package/server/lib/board-follow.js +111 -0
  115. package/server/lib/board-groups.js +1 -5
  116. package/server/lib/board-hero.js +3 -1
  117. package/server/lib/board-kind-sizes.js +36 -0
  118. package/server/lib/board-lanes.js +58 -0
  119. package/server/lib/board-obstacles.js +80 -0
  120. package/server/lib/board-relations.js +5 -1
  121. package/server/lib/board-sheets.js +467 -0
  122. package/server/lib/board-shelf.js +86 -0
  123. package/server/lib/board-transform.js +62 -0
  124. package/server/lib/canvas-id.js +18 -1
  125. package/server/lib/chalk-flow.js +88 -0
  126. package/server/lib/chalk-rewrite.js +63 -0
  127. package/server/lib/chalk-size-pref.js +48 -0
  128. package/server/lib/chalk.js +42 -3
  129. package/server/lib/ingress/forward-openai-chat.js +16 -4
  130. package/server/lib/ingress/openai-chat.js +23 -12
  131. package/server/lib/ingress/session-routes.js +3 -3
  132. package/server/lib/ingress/stream-watchdog.js +53 -0
  133. package/server/lib/ingress/upstream-billing.js +15 -0
  134. package/server/lib/ingress/upstream-error-hints.js +36 -0
  135. package/server/lib/json-preview.js +58 -0
  136. package/server/lib/model-ingress.js +26 -1
  137. package/server/lib/moderation.js +10 -5
  138. package/server/lib/quota.js +26 -6
  139. package/server/lib/rect.js +50 -0
  140. package/server/lib/screen.js +36 -0
  141. package/server/lib/sheet-state.js +19 -0
  142. package/server/lib/sketch-array.js +118 -0
  143. package/server/lib/sketch-hatch.js +99 -0
  144. package/server/lib/sketch-layout.js +269 -69
  145. package/server/lib/sketch-shapes.js +152 -0
  146. package/server/lib/sketch-stencils.js +170 -0
  147. package/server/lib/state-table.js +237 -0
  148. package/server/lib/state-trend.js +94 -0
  149. package/server/lib/state-triggers.js +199 -0
  150. package/server/lib/tavern-json.js +48 -0
  151. package/server/lib/user-uploads.js +88 -0
  152. package/server/projects/board-limits.js +5 -3
  153. package/server/projects/board-sanitize.js +220 -10
  154. package/server/projects/board-store.js +95 -6
  155. package/server/projects/session-jsonl.js +110 -0
  156. package/server/projects/store.js +56 -3
  157. package/server/projects/viewpoint-store.js +35 -3
  158. package/server/projects/workspace-slots.js +30 -0
  159. package/server/projects/workspace-templates.js +29 -0
  160. package/server/projects/workspace.js +16 -17
  161. package/server/runtime/capabilities.js +6 -2
  162. package/server/shared/locales.js +70 -0
  163. package/server/shared/messages-en.js +69 -0
  164. package/server/shared/messages.js +65 -0
  165. package/server/ws/broker.js +3 -0
  166. package/server/ws/index.js +43 -3
  167. package/web/README.md +1 -1
  168. package/web/dist/assets/{BrowserWindow-BoOBaIyz.js → BrowserWindow-CcaGRWz5.js} +1 -1
  169. package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
  170. package/web/dist/assets/{DocxWindow-B_u6aX9o.js → DocxWindow-B9KZmU5z.js} +2 -2
  171. package/web/dist/assets/{MermaidBlock-BO-46Du5.js → MermaidBlock-DrfRnz8E.js} +43 -43
  172. package/web/dist/assets/{SiteWindow-ConPmzdM.js → SiteWindow-CJOjCF7d.js} +6 -6
  173. package/web/dist/assets/{abnfDiagram-VCTEODGH-FCO9yf-n.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
  174. package/web/dist/assets/{arc-BLcEOSRX.js → arc-C1-HSUny.js} +1 -1
  175. package/web/dist/assets/{architectureDiagram-5GKGNRK7-kxiX4TeN.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
  176. package/web/dist/assets/{blockDiagram-NRAW4CY4-CptFksix.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
  177. package/web/dist/assets/{c4Diagram-UCG6FXSJ-Dxmxj34F.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
  178. package/web/dist/assets/channel-BxpWNOGU.js +1 -0
  179. package/web/dist/assets/{chunk-2Q5K7J3B-D9inx5oo.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
  180. package/web/dist/assets/{chunk-5VM5RSS4-DydfHlEc.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
  181. package/web/dist/assets/{chunk-F27PBJKO-BO7GxhSv.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
  182. package/web/dist/assets/{chunk-G27WJ6UU-BzP6YiQ_.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
  183. package/web/dist/assets/{chunk-JWPE2WC7-CHcgcd0s.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
  184. package/web/dist/assets/{chunk-LCL6LL3I-D2Wg5JtX.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
  185. package/web/dist/assets/{chunk-POPQ4Y6H-B2fCRfpM.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
  186. package/web/dist/assets/{chunk-SVP7TREG-JSLcsRlZ.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
  187. package/web/dist/assets/{chunk-XXDRQBXY-BCjvaeHm.js → chunk-XXDRQBXY-BtXzVF-_.js} +1 -1
  188. package/web/dist/assets/classDiagram-DTDB5LWJ-DUEKmDfo.js +1 -0
  189. package/web/dist/assets/classDiagram-v2-JRS7N3AN-DUEKmDfo.js +1 -0
  190. package/web/dist/assets/{cose-bilkent-JH36ORCC-0uKv6oJ7.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
  191. package/web/dist/assets/{cynefin-OW5HDTMX-Bvt-gEmA.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
  192. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-C9Ht02qC.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
  193. package/web/dist/assets/{dagre-3AP2YEHR-BfxZny4s.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
  194. package/web/dist/assets/{diagram-S7CK7UJ4-CrE0ZETE.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
  195. package/web/dist/assets/{diagram-UQ7AKVKN-BO8VQ4Fx.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
  196. package/web/dist/assets/{diagram-VSXAHHWV-CHUZnR9F.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
  197. package/web/dist/assets/{diagram-VX7I27RA-DYQAz1Vu.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
  198. package/web/dist/assets/{diagram-Z3DM3KII-BDjivgLr.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
  199. package/web/dist/assets/{ebnfDiagram-PWID7BFC-Bpe_QVbH.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
  200. package/web/dist/assets/{erDiagram-SSCWMZ5O-BxhwuAu1.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
  201. package/web/dist/assets/{flowDiagram-A5DVABFB-CCw2H08-.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
  202. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-DtTc7MyI.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
  203. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-BvORxMd2.js → gitGraphDiagram-WWUBYQGX-B5_6JhR3.js} +1 -1
  204. package/web/dist/assets/hand-CuvZZQGW.webp +0 -0
  205. package/web/dist/assets/index-BYKfMnmv.js +2759 -0
  206. package/web/dist/assets/index-CW7VsrVg.css +1 -0
  207. package/web/dist/assets/{infoDiagram-RXCK75RN-DMcrkypD.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
  208. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CZkTIbe-.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
  209. package/web/dist/assets/{journeyDiagram-EYS64GPL-DR0XazdE.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
  210. package/web/dist/assets/{kanban-definition-3QL26DDD-DN8VbwVu.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
  211. package/web/dist/assets/{layout-9_paCbyt.js → layout-Dhgck-Zo.js} +1 -1
  212. package/web/dist/assets/{linear-g6Ct012e.js → linear-DMSQ-aUw.js} +1 -1
  213. package/web/dist/assets/lxgw-nd-app-bold-D1pKA1bR.woff2 +0 -0
  214. package/web/dist/assets/lxgw-nd-app-regular-BDe3d26v.woff2 +0 -0
  215. package/web/dist/assets/lxgw-nd-bold-Bufvlg03.woff2 +0 -0
  216. package/web/dist/assets/lxgw-nd-regular-DQwoI4OP.woff2 +0 -0
  217. package/web/dist/assets/lxgwwenkaiscreen-subset-100-DLPe7tEt.woff2 +0 -0
  218. package/web/dist/assets/lxgwwenkaiscreen-subset-101-d3VsZKWT.woff2 +0 -0
  219. package/web/dist/assets/lxgwwenkaiscreen-subset-102-ClzJuLq6.woff2 +0 -0
  220. package/web/dist/assets/lxgwwenkaiscreen-subset-103-DuDnQ6K-.woff2 +0 -0
  221. package/web/dist/assets/lxgwwenkaiscreen-subset-104-kPGW3Eem.woff2 +0 -0
  222. package/web/dist/assets/lxgwwenkaiscreen-subset-105-BzD54h-0.woff2 +0 -0
  223. package/web/dist/assets/lxgwwenkaiscreen-subset-106-BQ7fIzlx.woff2 +0 -0
  224. package/web/dist/assets/lxgwwenkaiscreen-subset-107-D8tDfFHI.woff2 +0 -0
  225. package/web/dist/assets/lxgwwenkaiscreen-subset-108-tGhkpwep.woff2 +0 -0
  226. package/web/dist/assets/lxgwwenkaiscreen-subset-109-7SMVSrly.woff2 +0 -0
  227. package/web/dist/assets/lxgwwenkaiscreen-subset-110-_uDKrFed.woff2 +0 -0
  228. package/web/dist/assets/lxgwwenkaiscreen-subset-111-Bio825RE.woff2 +0 -0
  229. package/web/dist/assets/lxgwwenkaiscreen-subset-112-D2ee0i-C.woff2 +0 -0
  230. package/web/dist/assets/lxgwwenkaiscreen-subset-113-JPFVDrga.woff2 +0 -0
  231. package/web/dist/assets/lxgwwenkaiscreen-subset-114-BcfxYA4w.woff2 +0 -0
  232. package/web/dist/assets/lxgwwenkaiscreen-subset-115-B1HeeOsd.woff2 +0 -0
  233. package/web/dist/assets/lxgwwenkaiscreen-subset-116-hY12tXpq.woff2 +0 -0
  234. package/web/dist/assets/lxgwwenkaiscreen-subset-117-BvuZwmre.woff2 +0 -0
  235. package/web/dist/assets/lxgwwenkaiscreen-subset-118-tzvs_H7j.woff2 +0 -0
  236. package/web/dist/assets/lxgwwenkaiscreen-subset-119-J2DWuYDN.woff2 +0 -0
  237. package/web/dist/assets/lxgwwenkaiscreen-subset-21-pDubCJV5.woff2 +0 -0
  238. package/web/dist/assets/lxgwwenkaiscreen-subset-22-ChAPMpqp.woff2 +0 -0
  239. package/web/dist/assets/lxgwwenkaiscreen-subset-23-BBx3oNpT.woff2 +0 -0
  240. package/web/dist/assets/lxgwwenkaiscreen-subset-24-CYt-Oagk.woff2 +0 -0
  241. package/web/dist/assets/lxgwwenkaiscreen-subset-25-Cx3j-ZUv.woff2 +0 -0
  242. package/web/dist/assets/lxgwwenkaiscreen-subset-26-B9AfZ3LL.woff2 +0 -0
  243. package/web/dist/assets/lxgwwenkaiscreen-subset-27-Bh2s9MNE.woff2 +0 -0
  244. package/web/dist/assets/lxgwwenkaiscreen-subset-28-C02JPoKP.woff2 +0 -0
  245. package/web/dist/assets/lxgwwenkaiscreen-subset-29-T_tbUGhk.woff2 +0 -0
  246. package/web/dist/assets/lxgwwenkaiscreen-subset-30-DMfyNzhz.woff2 +0 -0
  247. package/web/dist/assets/lxgwwenkaiscreen-subset-31-DFfdzbkg.woff2 +0 -0
  248. package/web/dist/assets/lxgwwenkaiscreen-subset-32-Bs1fz2iW.woff2 +0 -0
  249. package/web/dist/assets/lxgwwenkaiscreen-subset-33-cpB96OkZ.woff2 +0 -0
  250. package/web/dist/assets/lxgwwenkaiscreen-subset-34-BhinPYWX.woff2 +0 -0
  251. package/web/dist/assets/lxgwwenkaiscreen-subset-35-D_njRQqA.woff2 +0 -0
  252. package/web/dist/assets/lxgwwenkaiscreen-subset-36-DqXlLE3t.woff2 +0 -0
  253. package/web/dist/assets/lxgwwenkaiscreen-subset-37-j5LbvMph.woff2 +0 -0
  254. package/web/dist/assets/lxgwwenkaiscreen-subset-38-oO4jWdKm.woff2 +0 -0
  255. package/web/dist/assets/lxgwwenkaiscreen-subset-39-CabsX4rw.woff2 +0 -0
  256. package/web/dist/assets/lxgwwenkaiscreen-subset-4-PatS3YHh.woff2 +0 -0
  257. package/web/dist/assets/lxgwwenkaiscreen-subset-40-yrLg3C3e.woff2 +0 -0
  258. package/web/dist/assets/lxgwwenkaiscreen-subset-41-DBXmCQhc.woff2 +0 -0
  259. package/web/dist/assets/lxgwwenkaiscreen-subset-42-Bqij_UiQ.woff2 +0 -0
  260. package/web/dist/assets/lxgwwenkaiscreen-subset-43-D3kp0Ntc.woff2 +0 -0
  261. package/web/dist/assets/lxgwwenkaiscreen-subset-44-B_BNGvK4.woff2 +0 -0
  262. package/web/dist/assets/lxgwwenkaiscreen-subset-45-CZc0cQWs.woff2 +0 -0
  263. package/web/dist/assets/lxgwwenkaiscreen-subset-46-CHQX5Kd9.woff2 +0 -0
  264. package/web/dist/assets/lxgwwenkaiscreen-subset-47-Ci8gLrHA.woff2 +0 -0
  265. package/web/dist/assets/lxgwwenkaiscreen-subset-48-run3ugD3.woff2 +0 -0
  266. package/web/dist/assets/lxgwwenkaiscreen-subset-49-DgfiUb6u.woff2 +0 -0
  267. package/web/dist/assets/lxgwwenkaiscreen-subset-5-B0CtGbX_.woff2 +0 -0
  268. package/web/dist/assets/lxgwwenkaiscreen-subset-50-CdCG-POr.woff2 +0 -0
  269. package/web/dist/assets/lxgwwenkaiscreen-subset-51-L2okeQZ2.woff2 +0 -0
  270. package/web/dist/assets/lxgwwenkaiscreen-subset-52-BZAqyGJW.woff2 +0 -0
  271. package/web/dist/assets/lxgwwenkaiscreen-subset-53-B0xWqLeU.woff2 +0 -0
  272. package/web/dist/assets/lxgwwenkaiscreen-subset-54-BZVQX-EA.woff2 +0 -0
  273. package/web/dist/assets/lxgwwenkaiscreen-subset-55-093mkIUJ.woff2 +0 -0
  274. package/web/dist/assets/lxgwwenkaiscreen-subset-56-CbhWG8G-.woff2 +0 -0
  275. package/web/dist/assets/lxgwwenkaiscreen-subset-57-pLQuvo6c.woff2 +0 -0
  276. package/web/dist/assets/lxgwwenkaiscreen-subset-58-BHhoY50x.woff2 +0 -0
  277. package/web/dist/assets/lxgwwenkaiscreen-subset-59-D1_6DuIQ.woff2 +0 -0
  278. package/web/dist/assets/lxgwwenkaiscreen-subset-6-BnAelmYU.woff2 +0 -0
  279. package/web/dist/assets/lxgwwenkaiscreen-subset-60-HDBIuLTZ.woff2 +0 -0
  280. package/web/dist/assets/lxgwwenkaiscreen-subset-61-C-c13SeU.woff2 +0 -0
  281. package/web/dist/assets/lxgwwenkaiscreen-subset-62-kHRVOhZ2.woff2 +0 -0
  282. package/web/dist/assets/lxgwwenkaiscreen-subset-63-C0MCsvFv.woff2 +0 -0
  283. package/web/dist/assets/lxgwwenkaiscreen-subset-64-DZekguCj.woff2 +0 -0
  284. package/web/dist/assets/lxgwwenkaiscreen-subset-65-CtP6c3IU.woff2 +0 -0
  285. package/web/dist/assets/lxgwwenkaiscreen-subset-66-DLVWoMSf.woff2 +0 -0
  286. package/web/dist/assets/lxgwwenkaiscreen-subset-67-BVuOnayH.woff2 +0 -0
  287. package/web/dist/assets/lxgwwenkaiscreen-subset-68-DpxQ6Mnx.woff2 +0 -0
  288. package/web/dist/assets/lxgwwenkaiscreen-subset-69-qesBxLmC.woff2 +0 -0
  289. package/web/dist/assets/lxgwwenkaiscreen-subset-70-BBt1xN7h.woff2 +0 -0
  290. package/web/dist/assets/lxgwwenkaiscreen-subset-71-BWqZacwB.woff2 +0 -0
  291. package/web/dist/assets/lxgwwenkaiscreen-subset-72-B1wP_Yst.woff2 +0 -0
  292. package/web/dist/assets/lxgwwenkaiscreen-subset-73-C99T7jNZ.woff2 +0 -0
  293. package/web/dist/assets/lxgwwenkaiscreen-subset-74-DrUTsWBe.woff2 +0 -0
  294. package/web/dist/assets/lxgwwenkaiscreen-subset-75-C4M5XXQe.woff2 +0 -0
  295. package/web/dist/assets/lxgwwenkaiscreen-subset-76-Bq8spVWF.woff2 +0 -0
  296. package/web/dist/assets/lxgwwenkaiscreen-subset-77-D9vNupE2.woff2 +0 -0
  297. package/web/dist/assets/lxgwwenkaiscreen-subset-78-D6th7Gnu.woff2 +0 -0
  298. package/web/dist/assets/lxgwwenkaiscreen-subset-79-bezM85cS.woff2 +0 -0
  299. package/web/dist/assets/lxgwwenkaiscreen-subset-80-DD8pnhG2.woff2 +0 -0
  300. package/web/dist/assets/lxgwwenkaiscreen-subset-81-N6jtSyBs.woff2 +0 -0
  301. package/web/dist/assets/lxgwwenkaiscreen-subset-82-D6c180bZ.woff2 +0 -0
  302. package/web/dist/assets/lxgwwenkaiscreen-subset-83-em17AadM.woff2 +0 -0
  303. package/web/dist/assets/lxgwwenkaiscreen-subset-84-CJ8gdrMK.woff2 +0 -0
  304. package/web/dist/assets/lxgwwenkaiscreen-subset-85-9PPGO1eB.woff2 +0 -0
  305. package/web/dist/assets/lxgwwenkaiscreen-subset-86-CjWPf-u5.woff2 +0 -0
  306. package/web/dist/assets/lxgwwenkaiscreen-subset-87-8D_X_yOC.woff2 +0 -0
  307. package/web/dist/assets/lxgwwenkaiscreen-subset-88-9vtKFPBx.woff2 +0 -0
  308. package/web/dist/assets/lxgwwenkaiscreen-subset-89-BhE5jAKX.woff2 +0 -0
  309. package/web/dist/assets/lxgwwenkaiscreen-subset-90-D2Npfm5E.woff2 +0 -0
  310. package/web/dist/assets/lxgwwenkaiscreen-subset-91-C6Ft1-Eu.woff2 +0 -0
  311. package/web/dist/assets/lxgwwenkaiscreen-subset-97-CaExyQP9.woff2 +0 -0
  312. package/web/dist/assets/lxgwwenkaiscreen-subset-98-CPoVbsUd.woff2 +0 -0
  313. package/web/dist/assets/lxgwwenkaiscreen-subset-99-ZUQZGVgH.woff2 +0 -0
  314. package/web/dist/assets/{mindmap-definition-FBJOCRG2-BGt4omYC.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
  315. package/web/dist/assets/{pegDiagram-XKGWAZYB-uSfS-PBE.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
  316. package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
  317. package/web/dist/assets/{pieDiagram-E7YTZNPT-DpeI935H.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
  318. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-B7IuNvWa.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
  319. package/web/dist/assets/{railroadDiagram-O6MQD6OU-ARzYpPQI.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
  320. package/web/dist/assets/{requirementDiagram-EFPCY7ZU-Cj4upkAo.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
  321. package/web/dist/assets/{rotate-cw-CXJcyHWk.js → rotate-cw-62liz7LX.js} +1 -1
  322. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-OaVwSnLP.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
  323. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-1yrUbq4t.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
  324. package/web/dist/assets/{sizeCapture-X5ZJPWSS-nYH5p9D6.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
  325. package/web/dist/assets/{square-dashed-mouse-pointer-CTtTVKoH.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
  326. package/web/dist/assets/{stateDiagram-HBIQ2CUA-Dj5HvyNq.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
  327. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
  328. package/web/dist/assets/{swimlanes-XN3QIQJK-umeT0HpB.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
  329. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
  330. package/web/dist/assets/{timeline-definition-24CTP7MA-MqnQfHKB.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
  331. package/web/dist/assets/{vennDiagram-4TSXK5OY-b2liyZra.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
  332. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-QMK-zNh1.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
  333. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-D5Mc6tfA.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
  334. package/web/dist/index.html +2 -2
  335. package/server/engine/mcp/tools/arrange-on-board.js +0 -126
  336. package/server/engine/mcp/tools/create-on-board.js +0 -152
  337. package/server/engine/mcp/tools/edit-sketch.js +0 -167
  338. package/server/engine/mcp/tools/relate-on-board.js +0 -150
  339. package/server/engine/mcp/tools/sketch-on-board.js +0 -345
  340. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +0 -235
  341. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +0 -78
  342. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +0 -268
  343. package/web/dist/assets/DeckWindow-Bg3rgkvi.js +0 -11
  344. package/web/dist/assets/channel-DFyiuGL0.js +0 -1
  345. package/web/dist/assets/classDiagram-DTDB5LWJ-Aw1f8z-b.js +0 -1
  346. package/web/dist/assets/classDiagram-v2-JRS7N3AN-Aw1f8z-b.js +0 -1
  347. package/web/dist/assets/index-C0KUf6Wd.css +0 -1
  348. package/web/dist/assets/index-DGG5wIZs.js +0 -2118
  349. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  350. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  351. package/web/dist/assets/pending-edit-apply-uP4uvrOD.js +0 -31
  352. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-kJ1m_zJT.js +0 -1
  353. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-DPSkMbbl.js +0 -8
@@ -0,0 +1,242 @@
1
+ /**
2
+ * mcp/tools/open-sheet.js —— open_sheet 铺纸(2026-08-29 纸范式刀 2)
3
+ *
4
+ * 开工先铺纸:在画布上登记一块 0.75 倍缩放下等于用户一屏的矩形工作区。
5
+ * 铺在哪由机器定(第一张对准用户此刻的视口 —— 「agent 在用户眼皮底下开工」;
6
+ * 之后缺省铺在当前纸正下方,像往桌上接着铺稿纸)。铺完这张纸就是**当前纸**:
7
+ * write_on_board 的 at 坐标以它的版心左上角为原点,没给 at 的写入在纸内自动
8
+ * 往下排,写满自动翻下一张。
9
+ *
10
+ * 纸是分配纪律不是容器,**也不渲染**(用户拍板:纸只是位置范围的概念)——用户
11
+ * 看到的只有内容本身;纸的存在体现在落位秩序、翻纸导航和 read_board 的账本里。
12
+ * 用户可以把东西拖出纸外,组照旧由 tag/线派生。
13
+ */
14
+
15
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
16
+ import { z } from 'zod';
17
+ import { byOf } from '../actor.js';
18
+ import { readBoard, patchBoard } from '../../../projects/board-store.js';
19
+ import { TAG_RE } from '../../../projects/board-sanitize.js';
20
+ import { obstaclesIn } from '../../../lib/board-obstacles.js';
21
+ import { fitFor, capacityOf, DEFAULT_CHALK_W } from '../../../lib/sketch-layout.js';
22
+ import { CARD_MAX_H } from '../../../lib/screen.js';
23
+ import { getViewpoint } from '../../../projects/viewpoint-store.js';
24
+ import {
25
+ sheetSizeFor, allocateSheetRect, nextSheetName, currentSheet, innerRect, SHEET_MARGIN,
26
+ membersInRect, sheetMembers,
27
+ } from '../../../lib/board-sheets.js';
28
+ import { setCurrentSheetId, currentSheetIdOf } from '../../../lib/sheet-state.js';
29
+ import { Events } from '../../agent/events.js';
30
+
31
+ /** 版位的 at:两轴各自可省 —— 省掉的轴由竖排糖补(below/接上一块)。
32
+ * ⛔ 不复用 write_on_board 的 SHEET_PT:它双轴必填,`at:{x:640}, below:"s1"`
33
+ * 这种半坐标会被 zod 整调用拒掉。 */
34
+ const SLOT_AT = z.object({
35
+ x: z.number().min(0).max(12000).optional(),
36
+ y: z.number().min(0).max(12000).optional(),
37
+ });
38
+
39
+ /** 一块地(版位)。坐标/尺寸全是纸内局部像素 —— 跟 write_on_board 的 at 同一套
40
+ *(export 给 edit_board 的 replan 用 —— 版位的合法性只有这一份定义) */
41
+ export const SLOT = z.object({
42
+ slot: z.string().regex(TAG_RE).describe('Name for this block, ASCII like main/aside/notes'),
43
+ at: SLOT_AT.optional().describe("Top-left of this block, pixels from the sheet's writable corner. OMIT it to stack this block right below the previous one in the list (or below the block named in `below`) — carve a column of slots without doing any y arithmetic. In a replan, omitting it on an EXISTING slot means resize in place (position kept)"),
44
+ below: z.string().regex(TAG_RE).optional().describe('Stack this block right under the named slot (24px gap). x follows that slot unless at.x says otherwise'),
45
+ w: z.number().min(48).max(12000).describe('Width in PIXELS (a default note column is 432)'),
46
+ h: z.number().min(24).max(12000).describe('Height in PIXELS (~26px per line of text)'),
47
+ about: z.string().max(60).optional().describe('What goes here (正文 / 人物小传 / 待办) — for your own map'),
48
+ for: z.literal('artifacts').optional()
49
+ .describe("Set to 'artifacts' on ONE block to make it this page's landing spot for generated files (images, docs, sites). Without it, files you produce have nowhere planned to go and the page fills up with them wherever there is room."),
50
+ });
51
+
52
+ /**
53
+ * 版面规划钳制(2026-08-29 刀 E;2026-08-30 抽出来给 replan 共用):
54
+ * 开工先把这一屏切成几块地。坐标跟 at 同一套(纸内局部像素)。
55
+ * 越出版心的钳回来 —— 钳过如实报(规划错了要当场知道)。
56
+ *
57
+ * 竖排糖(2026-08-30 用户拍板「agent 自己定几个空位分段填」):省掉 at(或点名
58
+ * `below`)=接在上一块正下方 24px。**y 累加这道算术归机器**,几个空位怎么切、
59
+ * 每段写什么归 agent —— 分段的语义权不外包,只有几何外包。
60
+ * `prevSlots` 是 replan 时该纸已有的版位(below 可以引用它们;点名已有版位又
61
+ * 没给坐标=原地改尺寸,不走竖排糖)。
62
+ */
63
+ export function clampPlan(plan, inner0, prevSlots = {}) {
64
+ const slots = {};
65
+ const clampedSlots = [];
66
+ let prev = null; // 数组序上一块(本批的)
67
+ const refOf = (name) => slots[name] || prevSlots[name] || null;
68
+ for (const it of Array.isArray(plan) ? plan : []) {
69
+ const w = Math.min(Math.round(it.w), inner0.w);
70
+ const h = Math.min(Math.round(it.h), inner0.h);
71
+ // 点名**已有**版位又没给坐标 = 原地改尺寸(2026-08-30):replan 想把 state 加高、
72
+ // 位置没提,此前走竖排糖落到 (0,0) 跟别的版位叠上 —— glm 真会话为这一下花了
73
+ // 三分钟理版面。竖排糖只对新版位生效。
74
+ const keep = !it.at && !it.below ? prevSlots[it.slot] : null;
75
+ const base = keep ? null : (it.below ? refOf(it.below) : (it.at ? null : prev));
76
+ const wantX = it.at?.x ?? keep?.x ?? base?.x ?? 0;
77
+ const wantY = it.at?.y ?? keep?.y ?? (base ? base.y + base.h + 24 : 0);
78
+ const x = Math.min(Math.max(0, Math.round(wantX)), Math.max(0, inner0.w - w));
79
+ const y = Math.min(Math.max(0, Math.round(wantY)), Math.max(0, inner0.h - h));
80
+ if (x !== Math.round(wantX) || y !== Math.round(wantY)
81
+ || w !== Math.round(it.w) || h !== Math.round(it.h)) clampedSlots.push(it.slot);
82
+ slots[it.slot] = { x, y, w, h, ...(it.about ? { about: it.about } : {}), ...(it.for === 'artifacts' ? { for: 'artifacts' } : {}) };
83
+ prev = slots[it.slot];
84
+ }
85
+ return { slots, clampedSlots };
86
+ }
87
+
88
+ /**
89
+ * 铺一张纸并登记(write_on_board 自动铺纸也走这一份 —— 分配纪律只有一份)。
90
+ * @returns {{ id, x, y, w, h, innerW, innerH, basis, overlapsLoose }}
91
+ */
92
+ export async function openSheetFor(projectId, {
93
+ sessionId = null, by = 'agent', title = null, name = null, where = null, plan = null,
94
+ } = {}) {
95
+ const board = await readBoard(projectId);
96
+ const vp = getViewpoint(projectId);
97
+ const fit = fitFor(vp);
98
+ const size = sheetSizeFor(fit);
99
+ let cur = currentSheet(board, currentSheetIdOf(sessionId));
100
+ // 缺省:还没有纸(或点名 viewport)→ 对准用户视口;有当前纸 → 铺在它正下方
101
+ const mode = where || (cur ? 'next' : 'viewport');
102
+ // 翻页裁纸(刀② 2026-08-30):纸固定一屏高、下一张贴满高矩形正下方,上一张
103
+ // 只用了一半时内容之间就是几百像素的空白(proj_mtfpehm3 实测 268~557px,
104
+ // 设计间隔明明只有 48)。翻页那一刻把上一张裁到内容底 —— 纸是分配纪律不是
105
+ // 画出来的框,裁掉的只是"再也不会写进去的余白";它的版位一并钳到新底。
106
+ let trimmed = null;
107
+ if (mode === 'next' && cur) {
108
+ const members = sheetMembers(board, cur.id);
109
+ const innerC = innerRect(cur);
110
+ const bottom = members.length ? Math.max(...members.map((m) => m.y + m.h)) : innerC.y;
111
+ const newH = Math.max(240, Math.ceil((bottom + SHEET_MARGIN - cur.y) / 24) * 24);
112
+ if (newH < cur.h - 24) {
113
+ const oldEntry = board.sheets[cur.id];
114
+ const newSlots = {};
115
+ for (const [nm, sl] of Object.entries(oldEntry.slots || {})) {
116
+ newSlots[nm] = { ...sl, h: Math.max(24, Math.min(sl.h, newH - SHEET_MARGIN * 2 - sl.y)) };
117
+ }
118
+ const patched = { ...oldEntry, h: newH, ...(Object.keys(newSlots).length ? { slots: newSlots } : {}) };
119
+ await patchBoard(projectId, { sheets: { [cur.id]: patched } });
120
+ trimmed = { id: cur.id, from: cur.h, to: newH };
121
+ board.sheets = { ...board.sheets, [cur.id]: patched };
122
+ cur = { ...cur, h: newH };
123
+ }
124
+ }
125
+ // 铺纸尽量不压散件。⛔ 不含文件夹/卷卡这类常驻家具:纸不渲染,纸矩形盖住文件夹
126
+ // 用户什么也看不见,而纸内落位照样会避开它 —— 算进来只会把第一张纸推离用户视口。
127
+ const obstacles = obstaclesIn(board, '', { furniture: false });
128
+ const rect = allocateSheetRect({
129
+ board, size,
130
+ viewport: mode === 'viewport' && vp?.camera && !vp.layer ? vp.camera : null,
131
+ nearSheet: mode === 'next' && cur ? cur.id : null,
132
+ obstacles,
133
+ });
134
+ const id = (name && TAG_RE.test(name) && !board.sheets?.[name]) ? name : nextSheetName(board);
135
+ const inner0 = innerRect({ x: rect.x, y: rect.y, w: rect.w, h: rect.h });
136
+ const { slots, clampedSlots } = clampPlan(plan, inner0);
137
+ const entry = {
138
+ x: rect.x, y: rect.y, w: rect.w, h: rect.h,
139
+ by, at: new Date().toISOString(), ...(title ? { title } : {}),
140
+ ...(Object.keys(slots).length ? { slots } : {}),
141
+ };
142
+ await patchBoard(projectId, { sheets: { [id]: entry } });
143
+ setCurrentSheetId(sessionId, id);
144
+ const inner = innerRect({ ...entry });
145
+ // 占地者点名(刀③ 2026-08-30):铺纸不避家具(纸不渲染,压着文件夹用户什么也
146
+ // 看不见),但**必须说清这块地上已经住着谁** —— proj_mtfpehm3 首拍就是纸铺在
147
+ // 文件夹排上、agent 规划的版位下半截被占,连吃四发拒收却没人告诉它为什么。
148
+ const occupants = membersInRect({ ...board, sheets: board.sheets }, { x: rect.x, y: rect.y, w: rect.w, h: rect.h })
149
+ .map((m) => ({
150
+ id: m.id, folder: !!m.folder,
151
+ local: { x: m.x - inner.x, y: m.y - inner.y }, w: m.w, h: m.h,
152
+ slots: Object.entries(slots)
153
+ .filter(([, sl]) => m.x < inner.x + sl.x + sl.w && m.x + m.w > inner.x + sl.x
154
+ && m.y < inner.y + sl.y + sl.h && m.y + m.h > inner.y + sl.y)
155
+ .map(([nm]) => nm),
156
+ }));
157
+ return {
158
+ id, ...entry, innerW: inner.w, innerH: inner.h,
159
+ basis: rect.basis, overlapsLoose: rect.overlapsLoose, clampedSlots, occupants, trimmed,
160
+ };
161
+ }
162
+
163
+ const DESCRIPTION = `Lay a fresh SHEET on the board and make it the current one — do this before you
164
+ start writing (like turning to a clean page). A sheet is one screen of the user's
165
+ device at 75% zoom; the machine picks where it goes (the first sheet opens right
166
+ under the user's current view; later ones stack below the current sheet).
167
+ After this, write_on_board's at:{x,y} means PIXELS from this sheet's top-left
168
+ writable corner, and writes without at flow top-to-bottom on it; when a sheet
169
+ fills up a new one is opened for you automatically — call open_sheet yourself when
170
+ you START A NEW TOPIC/CHAPTER, so each sheet reads as one page about one thing.
171
+ The user can drag things off a sheet freely — the sheet constrains you, not them.`;
172
+
173
+ export function makeOpenSheetTool({ projectId, sessionId, ctx }) {
174
+ return tool('open_sheet', DESCRIPTION, {
175
+ title: z.string().max(60).optional().describe('What this sheet is about (for read_board / your own map, e.g. 第二章 — sheets are invisible to the user)'),
176
+ name: z.string().regex(TAG_RE).optional().describe('Sheet name to refer to it later (ASCII like act2; default auto p1/p2/…)'),
177
+ where: z.enum(['next', 'viewport']).optional()
178
+ .describe("next = below the current sheet (default when sheets exist); viewport = right where the user is looking now (default for the first sheet — also use it to bring work back to the user's eyes)"),
179
+ plan: z.array(SLOT).max(24).optional()
180
+ .describe('PLAN THE WHOLE PAGE HERE, before writing anything: carve the sheet into named blocks (slots) and say what goes in each. Then write_on_board{slot:"main"} drops content into that block. A sheet is WIDE (landscape) — a single column down the left wastes most of it; think in columns and rows.'),
181
+ }, async (args, extra) => {
182
+ if (!projectId) return { content: [{ type: 'text', text: 'No project bound.' }], isError: true };
183
+ const by = byOf(extra);
184
+ const s = await openSheetFor(projectId, {
185
+ sessionId, by, title: args.title || null, name: args.name || null, where: args.where || null,
186
+ plan: args.plan || null,
187
+ });
188
+ try {
189
+ ctx?.emit?.({ type: 'board.updated', sessionId: null, summary: `铺了一张纸 ${s.id}` });
190
+ ctx?.emit?.(Events.boardFocus({ x: s.x, y: s.y, w: s.w, h: s.h }, { layer: '', soft: s.basis !== 'viewport', actor: by !== 'agent' ? by : null }));
191
+ } catch { /* fail-soft */ }
192
+ const colCap = capacityOf(DEFAULT_CHALK_W, s.innerH);
193
+ const cols = Math.max(1, Math.floor(s.innerW / (DEFAULT_CHALK_W + SHEET_MARGIN)));
194
+ const lines = [
195
+ `Sheet ${s.id} laid at world (${s.x},${s.y}) ${s.w}x${s.h}${s.title ? ` — “${s.title}”` : ''}; it is now the current sheet.`,
196
+ `Writable area: ${s.innerW}x${s.innerH}px, margin ${SHEET_MARGIN}. at:{x,y} means pixels from its top-left writable corner (x→right, y→down).`,
197
+ // 量纲(刀 D):agent 手里的东西是字,护栏却全是像素 —— 让它自己换算
198
+ // = 每次落笔前做一道做不准的算术,结果是写完才发现装不下。
199
+ `Scale: ~26px per line, ~${colCap.perLine} CJK chars per ${DEFAULT_CHALK_W}px-wide line. This sheet is ${colCap.lines} lines deep and ${cols} columns wide (≈${colCap.cjk * cols} CJK chars if you use all of it).`,
200
+ ];
201
+ const slotList = Object.entries(s.slots || {});
202
+ if (slotList.length) {
203
+ // 覆盖率(刀 E):一张横着的纸只画一条竖栏,大半是空的 —— 把这个数直接报出来
204
+ const used = slotList.reduce((n, [, v]) => n + v.w * v.h, 0);
205
+ const pct = Math.round((used / (s.innerW * s.innerH)) * 100);
206
+ lines.push(`Planned ${slotList.length} slots covering ${pct}% of the sheet:`);
207
+ for (const [nm, v] of slotList) {
208
+ const c = capacityOf(v.w, v.h);
209
+ lines.push(` ${nm}${v.about ? `(${v.about})` : ''}: at (${v.x},${v.y}) ${v.w}x${v.h} — ~${c.lines} lines, ~${c.cjk} CJK chars`);
210
+ }
211
+ lines.push('write_on_board{slot:"<name>"} drops a note into a block (they stack downward inside it). Content that does not fit is REFUSED, not squeezed — re-plan or split it.');
212
+ if (pct < 45) lines.push(`⚠ ${100 - pct}% of this sheet is unplanned. A landscape sheet holds ${cols} columns side by side — carve more blocks rather than leaving it empty.`);
213
+ if (s.clampedSlots?.length) lines.push(`⚠ Clamped into the sheet (they stuck out): ${s.clampedSlots.join(', ')} — check their at/w/h.`);
214
+ } else {
215
+ lines.push(`No slots planned. Plan the page in one go — open_sheet{plan:[{slot,at,w,h,about}…]} — instead of writing one note at a time and hoping it lands well. This sheet takes ${cols} columns side by side; a single column down the left leaves ${Math.round((1 - 1 / cols) * 100)}% of it empty.`);
216
+ }
217
+ lines.push(s.basis === 'viewport' ? 'Opened under the user’s current view.' : (s.basis === 'below-sheet' ? 'Stacked below the current sheet.' : 'Placed below existing content.'));
218
+ if (s.trimmed) {
219
+ lines.push(`Previous sheet ${s.trimmed.id} was trimmed to its content (${s.trimmed.from}→${s.trimmed.to}px tall) so the pages sit close — its leftover blank is gone, not its content.`);
220
+ // 利用率点名(2026-08-30):裁掉近半张纸 = 翻页翻快了。只点名不拦 ——
221
+ // 换场景翻新纸永远合法,这里治的是「每拍一张纸」的过度避险。
222
+ const waste = s.trimmed.from - s.trimmed.to;
223
+ if (waste >= 400) {
224
+ lines.push(`⚠ You left ~${waste}px of that sheet unused — pages are turning faster than they fill. Short beats can keep landing on the current sheet (they flow down; no slot needed). Turn the page for a scene change or a refusal, not for every beat.`);
225
+ }
226
+ }
227
+ // 占地者点名(刀③):谁在纸上、占了哪块版位、该怎么办 —— 三样一次说清
228
+ if (s.occupants?.length) {
229
+ const named = s.occupants.slice(0, 6).map((o) =>
230
+ `${o.folder ? '📁 ' : ''}${o.id} at local (${Math.round(o.local.x)},${Math.round(o.local.y)}) ${o.w}x${o.h}${o.slots.length ? ` — sits in your slot "${o.slots.join('", "')}"` : ''}`);
231
+ lines.push(`⚠ ${s.occupants.length} item(s) ALREADY LIVE on this sheet's ground (they count against your space):`);
232
+ for (const n of named) lines.push(` ${n}`);
233
+ if (s.occupants.length > 6) lines.push(` …and ${s.occupants.length - 6} more (read_board lists them).`);
234
+ lines.push(' Options: plan/re-plan your blocks AROUND them (edit_board replan), move them somewhere'
235
+ + ' deliberate (edit_board move — they may be the user\'s, so keep them visible), or re-open with'
236
+ + ' where:"next" for clean ground. Do NOT just write over them.');
237
+ } else if (s.overlapsLoose) {
238
+ lines.push('⚠ Some loose items already sit in this area — they now read as “on this sheet”; move them (edit_board) if that is wrong.');
239
+ }
240
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
241
+ });
242
+ }
@@ -9,8 +9,8 @@
9
9
  * 实现 = **和用户拖拽「移动到…」同一份核心**(projects/move-entry.js):
10
10
  * 磁盘先行、画布身份同步、转发表记账,一个字不重写。
11
11
  *
12
- * 批量制(同 roll_film / paint_still):≤16 件、串行、**中途失败即停**
13
- * (后面的不动,报告哪件停的)。目标夹不存在就建 —— 归纳常配新夹。
12
+ * 批量制(同 roll_film / paint_still):≤16 件、串行。**重名跳过、其余即停**
13
+ * (停了就报哪件停的,后面的不动)。目标夹不存在就建 —— 归纳常配新夹。
14
14
  */
15
15
 
16
16
  import { tool } from '@anthropic-ai/claude-agent-sdk';
@@ -33,7 +33,7 @@ at the moved items) are rewritten to the new paths automatically — the count o
33
33
  rewritten spots is reported per file so you can verify. Pass rewrite_refs:false
34
34
  to move only.
35
35
 
36
- Batch: up to 16 items, moved in order, stops at first failure.`,
36
+ Batch: up to 16 items, moved in order. A name clash at the destination is skipped (that one is likely already filed) and the rest still move; any other failure stops the batch.`,
37
37
  {
38
38
  items: z.array(z.string().min(1)).min(1).max(16)
39
39
  .describe('Workspace-relative paths to move (files or folders), e.g. ["assets/generated/a.png", "旧稿.html"]'),
@@ -45,7 +45,7 @@ Batch: up to 16 items, moved in order, stops at first failure.`,
45
45
  async ({ items, into, rewrite_refs: rewriteRefs }) => {
46
46
  const lines = [];
47
47
  const moves = [];
48
- let moved = 0;
48
+ let moved = 0; let skipped = 0;
49
49
  for (const item of items) {
50
50
  try {
51
51
  const out = await moveEntry(projectId, item, into, { createFolder: true });
@@ -61,8 +61,22 @@ Batch: up to 16 items, moved in order, stops at first failure.`,
61
61
  }
62
62
  } catch (err) {
63
63
  const why = err instanceof MoveError ? err.message : (err?.message || String(err));
64
+ /**
65
+ * 重名(MoveError 409)**不中断整批**:目标夹里已经有个同名的,说明这件
66
+ * 大概率已经收过一遍了,它挡不住后面十几件。其余错误照旧即停。
67
+ *
68
+ * 真案 proj_mtg61or1 19:37,用户说"把素材都收到一块,别四处丢",agent
69
+ * 一次收 12 件,第 1 件重名 → 后面 11 件全没动,暂存架自始至终没清过。
70
+ * 收纳这个动词本来就不是事务性的(前面搬成的不会回滚),中途停下只是
71
+ * 让人分不清搬到哪儿了。
72
+ */
73
+ if (err instanceof MoveError && err.status === 409) {
74
+ skipped += 1;
75
+ lines.push(`· ${item}:${why}(跳过,接着搬后面的)`);
76
+ continue;
77
+ }
64
78
  lines.push(`✗ ${item}:${why}`);
65
- lines.push(`(后面 ${items.length - moved - 1} 件没动 —— 修正后重调)`);
79
+ lines.push(`(后面 ${items.length - moved - skipped - 1} 件没动 —— 修正后重调)`);
66
80
  break;
67
81
  }
68
82
  }
@@ -398,7 +398,7 @@ breakage; do not burn high-detail on every frame.`,
398
398
  negative: z.string().optional().describe('overrides the per-model default; no-op for krea2'),
399
399
  size: z.string().regex(/^\d{3,4}x\d{3,4}$/).default('1344x768'),
400
400
  seed: z.number().int().optional().describe('omit for fresh random per still; batch uses seed, seed+1, ...'),
401
- name: z.string().regex(/^[\w-]{1,40}$/).default('still'),
401
+ name: z.string().regex(/^[\w一-鿿぀-ヿ-]{1,40}$/).default('still'),
402
402
  batch: z.number().int().min(1).max(8).optional()
403
403
  .describe('variations of this same prompt in one pass (default 1). Use 4-8 to roll for a keeper.'),
404
404
  lora: z.string().optional()
@@ -29,8 +29,11 @@ import { tool } from '@anthropic-ai/claude-agent-sdk';
29
29
  import { z } from 'zod';
30
30
  import path from 'path';
31
31
  import { promises as fs } from 'fs';
32
- import { pinToZone, readBoard } from '../../../projects/board-store.js';
32
+ import { pinToZone, readBoard, patchBoard } from '../../../projects/board-store.js';
33
33
  import { layerOf } from '../../../lib/canvas-id.js';
34
+ import { currentSheet, slotRectOf, nextSpotInSlot, placeAtOnSheet } from '../../../lib/board-sheets.js';
35
+ import { estimateSizeOn } from '../../../lib/board-kind-sizes.js';
36
+ import { currentSheetIdOf } from '../../../lib/sheet-state.js';
34
37
 
35
38
  // (folderOfObjectId 08-24 拆除:它按 dirname 硬算,会给 assets/generated 这类
36
39
  // 前端不当层渲染的路径发"层"标签 —— 改问 layerOf + board.zones,跟渲染同口径)
@@ -41,7 +44,7 @@ import { layerOf } from '../../../lib/canvas-id.js';
41
44
  * @param {string} [deps.projectId]
42
45
  * @param {import('../../agent/context.js').AgentContext} [deps.ctx]
43
46
  */
44
- export function makePinToBoardTool({ sharedRoot, projectId, ctx }) {
47
+ export function makePinToBoardTool({ sharedRoot, projectId, sessionId = null, ctx }) {
45
48
  return tool(
46
49
  'pin_to_board',
47
50
  `Bring an item to the front of the user's canvas, at a free spot in whatever
@@ -49,6 +52,9 @@ folder it lives in. The canvas is their desktop: whatever you write appears
49
52
  there automatically — you do NOT need this tool for your own outputs.
50
53
  Use it only to deliberately surface something:
51
54
 
55
+ - Place a staged arrival: new files land on the staging shelf (the vertical
56
+ strip of unplaced cards the status block names) — pin_to_board{path, slot}
57
+ moves one into a block you planned
52
58
  - Pull a reference (an uploaded asset, a memory note, an older image) into view
53
59
  - Restore something the user dragged off-screen, when they ask for it back
54
60
 
@@ -65,8 +71,14 @@ Paths are workspace-relative, exactly as they are on disk. Accepted forms:
65
71
  .min(1)
66
72
  .max(300)
67
73
  .describe('Item to surface — see accepted forms in the tool description'),
74
+ slot: z.string().max(40).optional()
75
+ .describe('Drop it into a planned block on the current sheet (names from open_sheet / read_board). This is how a produced file lands where you planned it instead of wherever there was room.'),
76
+ at: z.object({ x: z.number(), y: z.number() }).optional()
77
+ .describe('Exact spot on the sheet, pixels from its top-left writable corner. Use slot unless this one file needs a precise place.'),
78
+ sheet: z.string().max(40).optional()
79
+ .describe('Which sheet (default: the one you are working on)'),
68
80
  },
69
- async ({ path: rawPath }) => {
81
+ async ({ path: rawPath, slot, at, sheet }) => {
70
82
  try {
71
83
  if (!projectId) {
72
84
  return { content: [{ type: 'text', text: 'No project bound; cannot pin.' }], isError: true };
@@ -112,6 +124,69 @@ Paths are workspace-relative, exactly as they are on disk. Accepted forms:
112
124
  // dirname 硬算的老写法会把 assets/generated 当层写进 zone,arrange 就此拒摆
113
125
  const boardNow = await readBoard(projectId);
114
126
  const zoneId = layerOf(objectId, null, new Set(Object.keys(boardNow.zones || {})));
127
+
128
+ /**
129
+ * 点名落位(2026-08-30 刀 G):`slot` / `at` 给了就走纸上落位,跟 write_on_board
130
+ * 同一套几何。这是「产物也由 agent 规划位置」的那只手 —— 暂存架上的东西,
131
+ * agent 规划出地方之后就用它请下来(seat 改写成 agent,自然离架)。
132
+ */
133
+ if (slot || at) {
134
+ const sh = (sheet && boardNow.sheets?.[sheet])
135
+ ? { id: sheet, ...boardNow.sheets[sheet] }
136
+ : currentSheet(boardNow, currentSheetIdOf(sessionId)); // 会话正写的那张优先
137
+ if (!sh) {
138
+ return { content: [{ type: 'text', text: 'No sheet yet — open_sheet first (plan the page, then place things into its blocks).' }], isError: true };
139
+ }
140
+ const box = estimateSizeOn(boardNow, objectId, boardNow.objects?.[objectId] || null);
141
+ let spot = null; let where = '';
142
+ if (slot) {
143
+ const rect = slotRectOf(sh, slot);
144
+ if (!rect) {
145
+ const names = Object.keys(sh.slots || {});
146
+ return { content: [{ type: 'text', text: names.length
147
+ ? `Sheet ${sh.id} has no slot "${slot}". It has: ${names.join(', ')}.`
148
+ : `Sheet ${sh.id} has no slots planned. Plan the page first: open_sheet{plan:[{slot,at,w,h,about}…]}.` }], isError: true };
149
+ }
150
+ const p = nextSpotInSlot(boardNow, rect, box);
151
+ if (p.full) {
152
+ return { content: [{ type: 'text', text: `⛔ Slot "${slot}" on sheet ${sh.id} is full — ${objectId} needs ${box.h}px, ~${p.freeH}px left. `
153
+ + 'Nothing moved. Re-plan the page with a taller block, or point it at another slot.' }], isError: true };
154
+ }
155
+ spot = p; where = ` in slot "${slot}"`;
156
+ } else {
157
+ const p = placeAtOnSheet(sh, at, box);
158
+ spot = p; where = p.clamped ? ' (clamped into the sheet)' : '';
159
+ }
160
+ /**
161
+ * ⛔ 摆到纸上 = 摆到**根层**,zone 必须写 ''(不是文件所在的文件夹层)。
162
+ *
163
+ * spot 是 nextSpotInSlot 在根层纸矩形里算出来的**世界坐标**;zone 写成
164
+ * 文件夹层的话,这两个字段当场互相矛盾:前端 dirOf 按 zone 把它渲染进
165
+ * 那个文件夹里(世界坐标在那儿是没意义的局部坐标),根层画布上根本
166
+ * 看不见它 —— 而工具还报了「Placed on sheet p3 in slot refs at (24,2941)」。
167
+ *
168
+ * 更实的后果:rootObjects 只收根层,membersInRect 跟着看不见它,于是
169
+ * **下一件 pin 进同一个版位的东西算出来还是同一个坐标**。真案
170
+ * proj_mtg61or1 19:18 连 pin 五张官方参考图,后四张全落 (24,3405),
171
+ * 工具四次都报 success。对照组(同样五张放根层)落点正常且装不下时
172
+ * 如实报「版位满」。
173
+ *
174
+ * 把文件夹里的文件 pin 到纸上,语义就是把它拎到桌面上摆着 —— 跟用户
175
+ * 从文件夹里拖一张卡出来是同一件事(前端注释:拖出来的写 '')。
176
+ */
177
+ const prev = boardNow.objects?.[objectId] || {};
178
+ const nextPending = (boardNow.pending || []).filter(r => r !== objectId && `deck:${r}` !== objectId && `site:${r}` !== objectId);
179
+ await patchBoard(projectId, {
180
+ objects: { [objectId]: { ...prev, x: Math.round(spot.x), y: Math.round(spot.y), w: Math.round(box.w), h: Math.round(box.h), zone: '', seat: 'agent' } },
181
+ ...(nextPending.length !== (boardNow.pending || []).length ? { pending: nextPending } : {}),
182
+ });
183
+ try {
184
+ ctx?.emit?.({ type: 'board.updated', sessionId: null, objectId, zoneId: '', summary: `已把 ${objectId} 摆到 ${sh.id}${where}` });
185
+ } catch { /* emit fail-safe */ }
186
+ return { content: [{ type: 'text', text: `Placed ${objectId} on sheet ${sh.id}${where} at (${Math.round(spot.x)}, ${Math.round(spot.y)}).`
187
+ + (nextPending.length !== (boardNow.pending || []).length ? ` It is no longer waiting for a spot (${nextPending.length} still are).` : '') }] };
188
+ }
189
+
115
190
  const { zone: placedZone, placed } = await pinToZone(projectId, { objectId, zoneId });
116
191
 
117
192
  try {
@@ -16,47 +16,57 @@ import { tool } from '@anthropic-ai/claude-agent-sdk';
16
16
  import { z } from 'zod';
17
17
  import { readBoard } from '../../../projects/board-store.js';
18
18
  import { estimateSizeOn } from '../../../lib/board-kind-sizes.js';
19
- import { layerOf } from '../../../lib/canvas-id.js';
19
+ import { layerOf, bareTag } from '../../../lib/canvas-id.js';
20
20
  import { relationsDigest, bindingLine } from '../../../lib/board-relations.js';
21
21
  import { groupObjects, asciiMinimap, bboxOfRects, relationOf, columnsOf, viewportRelation } from '../../../lib/board-groups.js';
22
+ import { laneSummaries } from '../../../lib/board-lanes.js';
23
+ import { capacityOf, DEFAULT_CHALK_W } from '../../../lib/sketch-layout.js';
24
+ import { sheetSummaries, rollCardRect, slotRectOf, nextSpotInSlot, freeColumnsInSheet, latestSheetId } from '../../../lib/board-sheets.js';
22
25
  import { getViewpoint } from '../../../projects/viewpoint-store.js';
23
26
  import { chalkExcerpts, CHALK_DIR } from '../../../lib/chalk.js';
24
27
  import { getSharedDir } from '../../../projects/workspace.js';
25
28
  import { promises as fs } from 'node:fs';
29
+ import { byOf, describeBy } from '../actor.js';
30
+ import { listRoleNames } from '../../agent/role-card.js';
26
31
  import path from 'node:path';
27
32
 
28
33
  /** 同一"行"的 y 容差:入座算法一行内顶对齐,40 世界像素内视作同行 */
29
34
  const ROW_TOLERANCE = 40;
30
35
 
31
- function describeEntry(board, id, entry, glyph = null, excerpts = null, staleIds = null) {
36
+ function describeEntry(board, id, entry, glyph = null, excerpts = null, staleIds = null, view = null) {
37
+ // view = { by: 当前读者, names: slug→展示名 }。读 read_board 的可能是主 agent,
38
+ // 也可能是常驻角色 —— 同一句「你写的」对两个读者含义相反,所以称呼要带视角。
39
+ const who = (b) => describeBy(b || 'agent', view?.by || 'agent', view?.names);
40
+ const mine = (b) => (b || 'agent') === (view?.by || 'agent');
32
41
  const sz = estimateSizeOn(board, id, entry);
33
42
  const at = `@(${Math.round(entry.x)},${Math.round(entry.y)}) ${Math.round(sz.w)}x${Math.round(sz.h)}`;
34
43
  const g = glyph ? `[${glyph}] ` : '';
35
44
  const flags = `${entry.staging ? ' 〔草稿〕' : ''}${entry.tag ? ` #${entry.tag}` : ''}`;
36
45
  const ch = excerpts?.get(id);
37
- if (ch) return `- ${g}[板书·${ch.by === 'user' ? '用户' : '你'}写的] 「${ch.first}」 ${at} (path: ${id})${ch.anchor ? ` 关于 ${ch.anchor}` : ''}${ch.replyTo ? ` 回应 ${ch.replyTo}` : ''}${flags}`;
46
+ if (ch) return `- ${g}[板书·${who(ch.by)}写的] 「${ch.first}」 ${at} (path: ${id})${ch.anchor ? ` 关于 ${ch.anchor}` : ''}${ch.replyTo ? ` 回应 ${ch.replyTo}` : ''}${flags}`;
38
47
  if (entry.kind === 'text') {
39
48
  const t = String(entry.data?.t || '').replace(/\s+/g, ' ').slice(0, entry.data?.format === 'md' ? 60 : 24);
40
49
  const md = entry.data?.format === 'md' ? 'md' : '手写';
41
- return `- ${g}[${md}] 「${t}」 ${at} (id: ${id})${entry.by === 'agent' ? ' ·你写的' : ''}${flags}`;
50
+ return `- ${g}[${md}] 「${t}」 ${at} (id: ${id})${entry.by ? ` ·${who(entry.by)}写的` : ''}${flags}`;
42
51
  }
43
- if (entry.kind === 'scribble') return `- ${g}[涂鸦] ${at} (id: ${id})${entry.by === 'agent' ? ' ·你画的' : ''}${flags}`;
52
+ if (entry.kind === 'scribble') return `- ${g}[涂鸦] ${at} (id: ${id})${entry.by ? ` ·${who(entry.by)}画的` : ''}${flags}`;
44
53
  // 过期座位要明说(iss_mt38ucyq:旧路径条目被 agent 当"失效卡"差点建议删素材母版)
45
54
  const stale = staleIds?.has(id) ? ' 〔⚠️磁盘上已无此路径 —— 多半被移动/改名了,以磁盘为准,别据此判失效或建议删除〕' : '';
46
- return `- ${g}${id} ${at}${entry.by === 'agent' ? ' ·你摆的' : ''}${flags}${stale}`;
55
+ return `- ${g}${id} ${at}${entry.by ? ` ·${who(entry.by)}摆的` : ''}${flags}${stale}`;
56
+ // eslint-disable-next-line no-unused-vars -- mine 留给后续按视角过滤用
47
57
  }
48
58
 
49
- export function makeReadBoardTool({ projectId }) {
59
+ export function makeReadBoardTool({ projectId, sharedRoot = null }) {
50
60
  return tool(
51
61
  'read_board',
52
62
  `Read the workbench canvas: an ASCII minimap, then GROUPS (things linked by lines or
53
63
  sharing a #tag), then loose items row by row, then relation lines.
54
64
 
55
- Use this BEFORE arranging (arrange_on_board), writing notes (create_on_board) or
56
- sketching (sketch_on_board) — placement without looking is guessing. Coordinates are
57
- world pixels. Only seated items appear (fresh artifacts get a seat within a second).
65
+ Use this BEFORE moving things (edit_board) or writing/sketching (write_on_board)
66
+ placement without looking is guessing. Coordinates are world pixels. Only seated items
67
+ appear (files you just wrote are seated automatically within a couple of seconds).
58
68
  Items marked 〔草稿〕 are still staging (yours from this turn, half-transparent until
59
- finish_sketch / end of turn). The user's current viewport (if known) is drawn as a box
69
+ edit_board commit / end of turn). The user's current viewport (if known) is drawn as a box
60
70
  on the minimap and listed with what is inside it.`,
61
71
  {
62
72
  layer: z.string().max(300).optional()
@@ -65,7 +75,11 @@ on the minimap and listed with what is inside it.`,
65
75
  .describe('Only list items/lines carrying this #tag (one group, e.g. a sketch you made)'),
66
76
  minimap: z.boolean().optional().describe('Also print an ASCII minimap (off by default — the relative-position summary is usually enough)'),
67
77
  },
68
- async ({ layer, tag, minimap }) => {
78
+ async ({ layer, tag: rawTag, minimap }, extra) => {
79
+ const tag = rawTag ? bareTag(rawTag) : rawTag; // #状态板 也认(查询侧统一剥 #)
80
+ // 视角:谁在读这块板。常驻角色读到自己写的板书才该显示「你写的」,
81
+ // 读到别人的显示那个人的名字(展示名只是渲染,判断一律用 slug)。
82
+ const view = { by: byOf(extra), names: await listRoleNames(sharedRoot) };
69
83
  if (!projectId) {
70
84
  return { content: [{ type: 'text', text: 'No project bound.' }], isError: true };
71
85
  }
@@ -86,6 +100,9 @@ on the minimap and listed with what is inside it.`,
86
100
  const excerpts = await chalkExcerpts(getSharedDir(projectId), (byLayer.get(want) || []).map(it => it.id));
87
101
  const items = (byLayer.get(want) || [])
88
102
  .filter(({ entry }) => !tag || entry.tag === tag)
103
+ // 收卷(2026-08-27 收纳器):收着的组不逐件列 —— 版图里压成一行,这是 agent
104
+ // 上下文的收纳(跟画布收纳同一刀)。显式 tag= 点名看某组时照常展开列。
105
+ .filter(({ entry }) => tag || !entry.tag || !board.rolls?.[entry.tag])
89
106
  // 板书条目但文件已经没了 = 幽灵座位,别列给 agent(删文件那条路会清座位,这是兜底)
90
107
  .filter(({ id }) => !id.startsWith(`${CHALK_DIR}/`) || excerpts.has(id))
91
108
  .sort((a, b) => (a.entry.y - b.entry.y) || (a.entry.x - b.entry.x));
@@ -143,7 +160,7 @@ on the minimap and listed with what is inside it.`,
143
160
  lines.push('', `组 ${i + 1}${tags ? ` ${tags}` : ''}(${g.members.length} 件 ${g.edges.length} 线${staging ? ',草稿' : ''}):`);
144
161
  const sorted = g.members.map(id => ({ id, entry: entryOf.get(id) }))
145
162
  .sort((a, b) => (a.entry.y - b.entry.y) || (a.entry.x - b.entry.x));
146
- for (const { id, entry } of sorted) lines.push(describeEntry(board, id, entry, glyphOf.get(id), excerpts, staleIds));
163
+ for (const { id, entry } of sorted) lines.push(describeEntry(board, id, entry, glyphOf.get(id), excerpts, staleIds, view));
147
164
  for (const bid of g.edges.slice(0, 12)) lines.push(` ${bindingLine(board.bindings[bid], board)} (line id: ${bid})`);
148
165
  if (g.edges.length > 12) lines.push(` …还有 ${g.edges.length - 12} 条线`);
149
166
  });
@@ -161,7 +178,7 @@ on the minimap and listed with what is inside it.`,
161
178
  rowY = entry.y;
162
179
  lines.push(`— 行 y≈${Math.round(rowY)} —`);
163
180
  }
164
- lines.push(describeEntry(board, id, entry, glyphOf.get(id), excerpts, staleIds));
181
+ lines.push(describeEntry(board, id, entry, glyphOf.get(id), excerpts, staleIds, view));
165
182
  }
166
183
  }
167
184
  }
@@ -174,7 +191,82 @@ on the minimap and listed with what is inside it.`,
174
191
  }).join('、')}`);
175
192
  }
176
193
  }
177
- if (board.hero && !tag) lines.push('', `★ 显式主角:${board.hero}(arrange_on_board 的 feature/unfeature 管它)`);
194
+ if (board.hero && !tag) lines.push('', `★ 显式主角:${board.hero}(edit_board 的 feature/unfeature 管它)`);
195
+
196
+ // 纸的清单(2026-08-29 纸范式):agent 的空间账本 —— 每张纸的名字/位置/件数/
197
+ // 剩余空地。at 坐标以当前纸版心左上为原点,这一节就是坐标系的地图。
198
+ if (!want && !tag) {
199
+ try {
200
+ const ss = sheetSummaries(board);
201
+ if (ss.length) {
202
+ const latest = latestSheetId(board);
203
+ lines.push('', '纸(sheet):at:{x,y} 写的是纸内像素(版心左上为原点)。'
204
+ + '**写满不会自动翻页** —— 排不下时写入被拒收,下一页由你自己 open_sheet 规划:');
205
+ for (const s of ss) {
206
+ // 剩多少地方按**字**报(08-29 刀 D):agent 手里的东西是字,
207
+ // 只给像素等于让它每次落笔前做一道做不准的算术
208
+ const cap = capacityOf(DEFAULT_CHALK_W, s.freeH);
209
+ lines.push(` ${s.id}${s.id === latest ? '(当前)' : ''}${s.title ? `(${s.title})` : ''}:世界 (${s.x},${s.y}) ${s.w}x${s.h},${s.count} 件,剩 ~${s.freeH}px 高(≈${cap.lines} 行 / ${cap.cjk} 字)${s.lastId ? `,最新 ${s.lastId}` : ''}`);
210
+ // 规划过的块各剩多少(08-29 刀 E):填到一半要知道还有哪块地是空的,
211
+ // 不然只能靠猜 —— 猜错的下场是写入被拒收,白跑一趟
212
+ // 这张纸还剩哪些空地(08-29 刀 F):只报"最后一件下面剩多少"是不够的 ——
213
+ // 那一列到底了不等于这张纸满了,右边可能整片空着,而 agent 要靠这个
214
+ // 判断"还放不放得下、要不要开新一页"
215
+ const free = freeColumnsInSheet(board, s.id);
216
+ if (free.length > 1) {
217
+ const cells = free.map((f) => `x=${f.x} 剩 ${capacityOf(DEFAULT_CHALK_W, f.freeH).lines} 行`);
218
+ lines.push(` 空地:${cells.join(' / ')}`);
219
+ }
220
+ const sheet = board.sheets?.[s.id];
221
+ for (const [nm, sl] of Object.entries(sheet?.slots || {})) {
222
+ const r = slotRectOf({ ...sheet, id: s.id }, nm);
223
+ const spot = nextSpotInSlot(board, r, { w: 1, h: 1 });
224
+ const freeH = spot.full ? 0 : Math.max(0, r.y + r.h - spot.y);
225
+ const c = capacityOf(r.w, freeH);
226
+ lines.push(` · ${nm}${sl.about ? `(${sl.about})` : ''}${sl.for === 'artifacts' ? '【收产物】' : ''}:块内 (${sl.x},${sl.y}) ${sl.w}x${sl.h},剩 ≈${c.lines} 行 / ${c.cjk} 字`);
227
+ }
228
+ }
229
+ }
230
+ // 暂存架(2026-08-30):机器到货的默认座 —— 上了墙但还没进版面,等 agent 安置
231
+ const shelf = Object.entries(board.objects || {})
232
+ .filter(([, e]) => e?.seat === 'shelf' && !e.zone && Number.isFinite(e?.x))
233
+ .map(([id]) => id);
234
+ const pend = Array.isArray(board.pending) ? board.pending : [];
235
+ const unplaced = [...shelf, ...pend];
236
+ if (unplaced.length) {
237
+ lines.push('', `📦 暂存架上 ${unplaced.length} 件等安置(机器只码在架上,版面归你):`);
238
+ for (const r of unplaced.slice(0, 8)) lines.push(` ${r}`);
239
+ if (unplaced.length > 8) lines.push(` …还有 ${unplaced.length - 8} 件`);
240
+ lines.push(' 安置的手:open_sheet{plan:[{slot:"图",at:{…},w,h,for:"artifacts"}…]} 规划产物地,'
241
+ + '或逐件 pin_to_board{path,slot} / edit_board{ops:[{op:"move",…}]}。');
242
+ }
243
+ } catch { /* 纸读不出不挡座次 */ }
244
+ }
245
+
246
+ // 版图(2026-08-27 空间规划):线 = 同 tag 的纵列。这是 agent 的符号地图 ——
247
+ // 摆放按关系(续哪条线/岔自哪条)声明,几何机器排,别按坐标猜。
248
+ if (!tag) {
249
+ const laneList = laneSummaries(board);
250
+ if (laneList.length) {
251
+ lines.push('', '线的清单(一条线 = 同一个 tag 的一纵列;{tag,chain:true} 接着写,open_lane 开新的一条):');
252
+ for (const l of laneList) {
253
+ // 收着的线一行带过:细节不进上下文(要看就 read_board tag= 点名,或 unroll)
254
+ const roll = board.rolls?.[l.tag];
255
+ if (roll) {
256
+ const rc = rollCardRect(board, l.tag);
257
+ lines.push(` #${l.tag}:已收卷${roll.label ? `(「${roll.label}」)` : ''},${l.count} 件收在卷里`
258
+ + `${rc ? `,卷卡占位约 @(${rc.x},${rc.y}) ${rc.w}x${rc.h}` : ''}`
259
+ + ` —— 座位和文件都在(Read 照常),edit_board unroll 展开;别往收着的线里接新话`);
260
+ continue;
261
+ }
262
+ const dirTxt = '';
263
+ lines.push(l.registered
264
+ ? ` #${l.tag}:${l.count} 节${l.parent ? `,岔自 ${l.parent}` : ''},列头 (${l.x},${l.y})`
265
+ + `${l.frontier ? `,接着写会落 (${l.frontier.x},${l.frontier.y}) 附近` : ''}${l.lastId ? `,最新 ${l.lastId}` : ''}${dirTxt}`
266
+ : ` #${l.tag}:${l.count} 件(未登记的野线 —— chain:true 照样能续)${dirTxt}`);
267
+ }
268
+ }
269
+ }
178
270
 
179
271
  // 用户视点(有上报才有)
180
272
  if (vp && !tag) {
@@ -197,7 +289,8 @@ on the minimap and listed with what is inside it.`,
197
289
  } catch { /* 关系读不到不挡座次 */ }
198
290
  }
199
291
 
200
- lines.push('', '(口径:稀疏表只列摆过的;层归属为服务端近似;尺寸为形态估算;带⚠️的条目=座位与磁盘对不上账)');
292
+ lines.push('', '(口径:稀疏表只列摆过的;层归属为服务端近似;尺寸=存档真值优先、缺了按形态估;'
293
+ + '角色精灵贴着该角色最新一条板书(那条四周留了 60px 身位);带⚠️的条目=座位与磁盘对不上账)');
201
294
  return { content: [{ type: 'text', text: lines.join('\n') }] };
202
295
  },
203
296
  );